From e6dc196ffd07ec9537943a43b2bd9750aa959026 Mon Sep 17 00:00:00 2001 From: nitrocode <7775707+nitrocode@users.noreply.github.com> Date: Sat, 19 Nov 2022 23:40:52 -0600 Subject: [PATCH 001/501] Use cloudposse/template for arm support (#510) Co-authored-by: Andriy Knysh Co-authored-by: cloudpossebot Co-authored-by: Matt Gowie --- deprecated/aws/opsgenie/versions.tf | 2 +- modules/aws-waf-acl/README.md | 2 +- modules/aws-waf-acl/versions.tf | 4 ++-- modules/eks-iam/README.md | 2 +- modules/eks-iam/versions.tf | 4 ++-- modules/mq-broker/README.md | 2 +- modules/mq-broker/versions.tf | 4 ++-- modules/s3-bucket/README.md | 2 +- modules/vpc-flow-logs-bucket/README.md | 2 +- modules/vpc-flow-logs-bucket/versions.tf | 4 ++-- modules/zscaler/README.md | 2 +- modules/zscaler/versions.tf | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/deprecated/aws/opsgenie/versions.tf b/deprecated/aws/opsgenie/versions.tf index 93d96f5e0..369383021 100755 --- a/deprecated/aws/opsgenie/versions.tf +++ b/deprecated/aws/opsgenie/versions.tf @@ -7,7 +7,7 @@ terraform { version = ">= 2.0" } template = { - source = "hashicorp/template" + source = "cloudposse/template" version = ">= 2.0" } local = { diff --git a/modules/aws-waf-acl/README.md b/modules/aws-waf-acl/README.md index 607db25a0..781d4730f 100644 --- a/modules/aws-waf-acl/README.md +++ b/modules/aws-waf-acl/README.md @@ -43,7 +43,7 @@ components: | [aws](#requirement\_aws) | ~> 3.36 | | [external](#requirement\_external) | ~> 2.1 | | [local](#requirement\_local) | ~> 2.1 | -| [template](#requirement\_template) | ~> 2.2 | +| [template](#requirement\_template) | >= 2.2 | | [utils](#requirement\_utils) | ~> 0.3 | ## Providers diff --git a/modules/aws-waf-acl/versions.tf b/modules/aws-waf-acl/versions.tf index 838c22d35..fe0bd210b 100644 --- a/modules/aws-waf-acl/versions.tf +++ b/modules/aws-waf-acl/versions.tf @@ -11,8 +11,8 @@ terraform { version = "~> 2.1" } template = { - source = "hashicorp/template" - version = "~> 2.2" + source = "cloudposse/template" + version = ">= 2.2" } local = { source = "hashicorp/local" diff --git a/modules/eks-iam/README.md b/modules/eks-iam/README.md index 41e912331..a3d00ade4 100644 --- a/modules/eks-iam/README.md +++ b/modules/eks-iam/README.md @@ -32,7 +32,7 @@ components: | [terraform](#requirement\_terraform) | >= 0.13.0 | | [aws](#requirement\_aws) | >= 3.0 | | [local](#requirement\_local) | >= 1.3 | -| [template](#requirement\_template) | >= 2.0 | +| [template](#requirement\_template) | >= 2.2 | ## Providers diff --git a/modules/eks-iam/versions.tf b/modules/eks-iam/versions.tf index 4076d48ca..720538dd8 100755 --- a/modules/eks-iam/versions.tf +++ b/modules/eks-iam/versions.tf @@ -7,8 +7,8 @@ terraform { version = ">= 3.0" } template = { - source = "hashicorp/template" - version = ">= 2.0" + source = "cloudposse/template" + version = ">= 2.2" } local = { source = "hashicorp/local" diff --git a/modules/mq-broker/README.md b/modules/mq-broker/README.md index fea5ff62d..cc7b92a12 100644 --- a/modules/mq-broker/README.md +++ b/modules/mq-broker/README.md @@ -35,7 +35,7 @@ components: | [terraform](#requirement\_terraform) | >= 0.13.0 | | [aws](#requirement\_aws) | >= 3.0 | | [local](#requirement\_local) | >= 1.3 | -| [template](#requirement\_template) | >= 2.0 | +| [template](#requirement\_template) | >= 2.2 | | [utils](#requirement\_utils) | >= 0.3.0 | ## Providers diff --git a/modules/mq-broker/versions.tf b/modules/mq-broker/versions.tf index 42b56538d..1527b5e19 100755 --- a/modules/mq-broker/versions.tf +++ b/modules/mq-broker/versions.tf @@ -7,8 +7,8 @@ terraform { version = ">= 3.0" } template = { - source = "hashicorp/template" - version = ">= 2.0" + source = "cloudposse/template" + version = ">= 2.2" } local = { source = "hashicorp/local" diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index a684ed2ec..a1a003dcf 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -105,7 +105,7 @@ components: |------|------| | [aws_iam_policy_document.custom_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | -| [template_file.bucket_policy](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file) | data source | +| [template_file.bucket_policy](https://registry.terraform.io/providers/cloudposse/template/latest/docs/data-sources/file) | data source | ## Inputs diff --git a/modules/vpc-flow-logs-bucket/README.md b/modules/vpc-flow-logs-bucket/README.md index 9c4e91fc1..23d415431 100644 --- a/modules/vpc-flow-logs-bucket/README.md +++ b/modules/vpc-flow-logs-bucket/README.md @@ -31,7 +31,7 @@ components: | [terraform](#requirement\_terraform) | >= 0.13.0 | | [aws](#requirement\_aws) | >= 3.0 | | [local](#requirement\_local) | >= 1.3 | -| [template](#requirement\_template) | >= 2.0 | +| [template](#requirement\_template) | >= 2.2 | ## Providers diff --git a/modules/vpc-flow-logs-bucket/versions.tf b/modules/vpc-flow-logs-bucket/versions.tf index 4076d48ca..720538dd8 100644 --- a/modules/vpc-flow-logs-bucket/versions.tf +++ b/modules/vpc-flow-logs-bucket/versions.tf @@ -7,8 +7,8 @@ terraform { version = ">= 3.0" } template = { - source = "hashicorp/template" - version = ">= 2.0" + source = "cloudposse/template" + version = ">= 2.2" } local = { source = "hashicorp/local" diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index f0df84185..cfae9f0db 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -68,7 +68,7 @@ import: | [aws_iam_role_policy_attachment.ssm_core](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_ami.amazon_linux_2](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | | [aws_ssm_parameter.zscaler_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [template_file.userdata](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file) | data source | +| [template_file.userdata](https://registry.terraform.io/providers/cloudposse/template/latest/docs/data-sources/file) | data source | ## Inputs diff --git a/modules/zscaler/versions.tf b/modules/zscaler/versions.tf index 007db2651..b3adb7a76 100644 --- a/modules/zscaler/versions.tf +++ b/modules/zscaler/versions.tf @@ -7,7 +7,7 @@ terraform { version = ">= 3.0" } template = { - source = "hashicorp/template" + source = "cloudposse/template" version = ">= 2.2" } null = { From 73a30fce52da39740057735c5efd577353c133b4 Mon Sep 17 00:00:00 2001 From: nitrocode <7775707+nitrocode@users.noreply.github.com> Date: Tue, 29 Nov 2022 09:17:35 -0800 Subject: [PATCH 002/501] CPLIVE-320: Set VPC to use region-less AZs (#524) --- modules/vpc/README.md | 46 +++++++++++++++++++++++++++++++------ modules/vpc/main.tf | 33 ++++++++++++++++++++++---- modules/vpc/remote-state.tf | 4 +--- modules/vpc/variables.tf | 12 +++++----- 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 1e397ab68..4ae449aa0 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -9,16 +9,47 @@ This component is responsible for provisioning a VPC and corresponding Subnets. Here's an example snippet for how to use this component. ```yaml +# catalog/vpc/defaults or catalog/vpc components: terraform: - vpc: + vpc/defaults: + metadata: + type: abstract + component: vpc + settings: + spacelift: + workspace_enabled: true vars: enabled: true - subnet_type_tag_key: "example.net/subnet/type" + name: vpc + eks_tags_enabled: true + availability_zones: + - "a" + - "b" + - "c" + nat_gateway_enabled: true + nat_instance_enabled: false + max_subnet_count: 3 vpc_flow_logs_enabled: true vpc_flow_logs_bucket_environment_name: - vpc_flow_logs_bucket_stage_name: "audit" + vpc_flow_logs_bucket_stage_name: audit vpc_flow_logs_traffic_type: "ALL" + subnet_type_tag_key: "example.net/subnet/type" + assign_generated_ipv6_cidr_block: true +``` + +```yaml +import: + - catalog/vpc + +components: + terraform: + vpc: + metadata: + component: vpc + inherits: + - vpc/defaults + vars: ipv4_primary_cidr_block: "10.111.0.0/18" ``` @@ -44,9 +75,10 @@ components: | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [subnets](#module\_subnets) | cloudposse/dynamic-subnets/aws | 2.0.4 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/vpc/aws | 2.0.0-rc1 | -| [vpc\_endpoints](#module\_vpc\_endpoints) | cloudposse/vpc/aws//modules/vpc-endpoints | 2.0.0-rc1 | -| [vpc\_flow\_logs\_bucket](#module\_vpc\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.0.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.1.0 | +| [vpc](#module\_vpc) | cloudposse/vpc/aws | 2.0.0 | +| [vpc\_endpoints](#module\_vpc\_endpoints) | cloudposse/vpc/aws//modules/vpc-endpoints | 2.0.0 | +| [vpc\_flow\_logs\_bucket](#module\_vpc\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | ## Resources @@ -62,13 +94,13 @@ components: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [assign\_generated\_ipv6\_cidr\_block](#input\_assign\_generated\_ipv6\_cidr\_block) | When `true`, assign AWS generated IPv6 CIDR block to the VPC. Conflicts with `ipv6_ipam_pool_id`. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [availability\_zone\_ids](#input\_availability\_zone\_ids) | List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`.
Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. | `list(string)` | `[]` | no | | [availability\_zones](#input\_availability\_zones) | List of Availability Zones (AZs) where subnets will be created. Ignored when `availability_zone_ids` is set.
The order of zones in the list ***must be stable*** or else Terraform will continually make changes.
If no AZs are specified, then `max_subnet_count` AZs will be selected in alphabetical order.
If `max_subnet_count > 0` and `length(var.availability_zones) > max_subnet_count`, the list
will be truncated. We recommend setting `availability_zones` and `max_subnet_count` explicitly as constant
(not computed) values for predictability, consistency, and stability. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [eks\_tags\_enabled](#input\_eks\_tags\_enabled) | Whether or not to apply EKS-releated tags to resources | `bool` | `false` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [gateway\_vpc\_endpoints](#input\_gateway\_vpc\_endpoints) | A list of Gateway VPC Endpoints to provision into the VPC. Only valid values are "dynamodb" and "s3". | `set(string)` | `[]` | no | diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index 69153d214..1125b6063 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -13,6 +13,24 @@ locals { ) ) + availability_zones = length(var.availability_zones) > 0 ? ( + (substr( + var.availability_zones[0], + 0, + length(var.region) + ) == var.region) ? var.availability_zones : formatlist("${var.region}%s", var.availability_zones) + ) : var.availability_zones + + short_region = module.utils.region_az_alt_code_maps["to_short"][var.region] + + availability_zone_ids = length(var.availability_zone_ids) > 0 ? ( + (substr( + var.availability_zone_ids[0], + 0, + length(local.short_region) + ) == local.short_region) ? var.availability_zone_ids : formatlist("${local.short_region}%s", var.availability_zone_ids) + ) : var.availability_zone_ids + # required tags to make ALB ingress work https://docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html # https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html public_subnets_additional_tags = { @@ -47,13 +65,18 @@ locals { } } } +module "utils" { + source = "cloudposse/utils/aws" + version = "1.1.0" +} + module "vpc" { source = "cloudposse/vpc/aws" - version = "2.0.0-rc1" + version = "2.0.0" ipv4_primary_cidr_block = var.ipv4_primary_cidr_block internet_gateway_enabled = var.public_subnets_enabled - assign_generated_ipv6_cidr_block = false # disable IPv6 + assign_generated_ipv6_cidr_block = var.assign_generated_ipv6_cidr_block # Required for DNS resolution of VPC Endpoint interfaces, and generally harmless # See https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html#vpc-dns-support @@ -100,7 +123,7 @@ module "endpoint_security_groups" { module "vpc_endpoints" { source = "cloudposse/vpc/aws//modules/vpc-endpoints" - version = "2.0.0-rc1" + version = "2.0.0" enabled = (length(var.interface_vpc_endpoints) + length(var.gateway_vpc_endpoints)) > 0 @@ -115,8 +138,8 @@ module "subnets" { source = "cloudposse/dynamic-subnets/aws" version = "2.0.4" - availability_zones = var.availability_zones - availability_zone_ids = var.availability_zone_ids + availability_zones = local.availability_zones + availability_zone_ids = local.availability_zone_ids ipv4_cidr_block = [module.vpc.vpc_cidr_block] ipv4_cidrs = var.ipv4_cidrs ipv6_enabled = false diff --git a/modules/vpc/remote-state.tf b/modules/vpc/remote-state.tf index 14ea76b3e..ba5b81928 100644 --- a/modules/vpc/remote-state.tf +++ b/modules/vpc/remote-state.tf @@ -1,10 +1,8 @@ - - module "vpc_flow_logs_bucket" { count = var.vpc_flow_logs_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.0.0" + version = "1.3.1" component = "vpc-flow-logs-bucket" environment = var.vpc_flow_logs_bucket_environment_name diff --git a/modules/vpc/variables.tf b/modules/vpc/variables.tf index d63f6c29d..99133e44e 100644 --- a/modules/vpc/variables.tf +++ b/modules/vpc/variables.tf @@ -50,6 +50,12 @@ variable "ipv4_cidrs" { } } +variable "assign_generated_ipv6_cidr_block" { + type = bool + description = "When `true`, assign AWS generated IPv6 CIDR block to the VPC. Conflicts with `ipv6_ipam_pool_id`." + default = false +} + variable "public_subnets_enabled" { type = bool description = <<-EOT @@ -140,12 +146,6 @@ variable "nat_eip_aws_shield_protection_enabled" { default = false } -variable "eks_tags_enabled" { - type = bool - description = "Whether or not to apply EKS-releated tags to resources" - default = false -} - variable "gateway_vpc_endpoints" { type = set(string) description = "A list of Gateway VPC Endpoints to provision into the VPC. Only valid values are \"dynamodb\" and \"s3\"." From 50ba4715da917172bd82f8704b9b76035820be91 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 21 Dec 2022 10:36:07 -0800 Subject: [PATCH 003/501] Upstream Datadog (#525) --- modules/datadog-agent/README.md | 7 +- modules/datadog-agent/asm.tf | 19 -- modules/datadog-agent/main.tf | 18 +- modules/datadog-agent/ssm.tf | 11 - modules/datadog-configuration/README.md | 121 ++++++++ .../asm.tf | 0 modules/datadog-configuration/context.tf | 279 ++++++++++++++++++ modules/datadog-configuration/main.tf | 11 + .../modules/datadog_keys/README.md | 21 ++ .../modules/datadog_keys/context.tf | 279 ++++++++++++++++++ .../modules/datadog_keys/main.tf | 50 ++++ .../modules/datadog_keys/outputs.tf | 20 ++ .../modules/datadog_keys/variables.tf | 16 + modules/datadog-configuration/outputs.tf | 24 ++ modules/datadog-configuration/providers.tf | 48 +++ .../ssm.tf | 4 +- modules/datadog-configuration/variables.tf | 86 ++++++ modules/datadog-configuration/versions.tf | 10 + modules/datadog-integration/README.md | 35 +-- .../datadog-integration/default.auto.tfvars | 5 - modules/datadog-integration/main.tf | 12 +- modules/datadog-integration/outputs.tf | 6 +- .../datadog-integration/provider-datadog.tf | 12 + modules/datadog-integration/providers.tf | 27 +- modules/datadog-integration/variables.tf | 54 ---- modules/datadog-integration/versions.tf | 2 +- modules/datadog-lambda-forwarder/README.md | 28 +- modules/datadog-lambda-forwarder/main.tf | 36 ++- .../provider-datadog.tf | 12 + modules/datadog-lambda-forwarder/providers.tf | 1 + .../datadog-lambda-forwarder/remote-state.tf | 9 + modules/datadog-lambda-forwarder/variables.tf | 48 ++- modules/datadog-lambda-forwarder/versions.tf | 6 +- modules/datadog-monitor/README.md | 31 +- modules/datadog-monitor/asm.tf | 19 -- modules/datadog-monitor/default.auto.tfvars | 3 - modules/datadog-monitor/main.tf | 16 +- modules/datadog-monitor/provider-datadog.tf | 12 + modules/datadog-monitor/providers.tf | 6 +- modules/datadog-monitor/ssm.tf | 11 - modules/datadog-monitor/variables.tf | 11 + modules/datadog-monitor/versions.tf | 2 +- .../datadog-private-location-ecs/README.md | 141 +++++++++ .../datadog-private-location-ecs/context.tf | 279 ++++++++++++++++++ modules/datadog-private-location-ecs/main.tf | 118 ++++++++ .../datadog-private-location-ecs/outputs.tf | 34 +++ .../provider-datadog.tf | 12 + .../datadog-private-location-ecs/providers.tf | 29 ++ .../remote-state.tf | 28 ++ .../datadog-private-location-ecs/variables.tf | 28 ++ .../datadog-private-location-ecs/versions.tf | 14 + modules/ecs/README.md | 4 +- modules/ecs/versions.tf | 2 +- 53 files changed, 1849 insertions(+), 268 deletions(-) delete mode 100644 modules/datadog-agent/asm.tf delete mode 100644 modules/datadog-agent/ssm.tf create mode 100644 modules/datadog-configuration/README.md rename modules/{datadog-integration => datadog-configuration}/asm.tf (100%) create mode 100644 modules/datadog-configuration/context.tf create mode 100644 modules/datadog-configuration/main.tf create mode 100644 modules/datadog-configuration/modules/datadog_keys/README.md create mode 100644 modules/datadog-configuration/modules/datadog_keys/context.tf create mode 100644 modules/datadog-configuration/modules/datadog_keys/main.tf create mode 100644 modules/datadog-configuration/modules/datadog_keys/outputs.tf create mode 100644 modules/datadog-configuration/modules/datadog_keys/variables.tf create mode 100644 modules/datadog-configuration/outputs.tf create mode 100644 modules/datadog-configuration/providers.tf rename modules/{datadog-integration => datadog-configuration}/ssm.tf (92%) create mode 100644 modules/datadog-configuration/variables.tf create mode 100644 modules/datadog-configuration/versions.tf delete mode 100644 modules/datadog-integration/default.auto.tfvars create mode 100644 modules/datadog-integration/provider-datadog.tf create mode 100644 modules/datadog-lambda-forwarder/provider-datadog.tf create mode 100644 modules/datadog-lambda-forwarder/remote-state.tf delete mode 100644 modules/datadog-monitor/asm.tf delete mode 100644 modules/datadog-monitor/default.auto.tfvars create mode 100644 modules/datadog-monitor/provider-datadog.tf mode change 100755 => 100644 modules/datadog-monitor/providers.tf delete mode 100644 modules/datadog-monitor/ssm.tf create mode 100644 modules/datadog-private-location-ecs/README.md create mode 100644 modules/datadog-private-location-ecs/context.tf create mode 100644 modules/datadog-private-location-ecs/main.tf create mode 100644 modules/datadog-private-location-ecs/outputs.tf create mode 100644 modules/datadog-private-location-ecs/provider-datadog.tf create mode 100644 modules/datadog-private-location-ecs/providers.tf create mode 100644 modules/datadog-private-location-ecs/remote-state.tf create mode 100644 modules/datadog-private-location-ecs/variables.tf create mode 100644 modules/datadog-private-location-ecs/versions.tf diff --git a/modules/datadog-agent/README.md b/modules/datadog-agent/README.md index 24c18d9af..3ee8f8770 100644 --- a/modules/datadog-agent/README.md +++ b/modules/datadog-agent/README.md @@ -142,6 +142,7 @@ https-checks: |------|--------|---------| | [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.6.0 | | [datadog\_cluster\_check\_yaml\_config](#module\_datadog\_cluster\_check\_yaml\_config) | cloudposse/config/yaml | 1.0.1 | +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -154,12 +155,6 @@ https-checks: | [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | | [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_secretsmanager_secret.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | -| [aws_secretsmanager_secret.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | -| [aws_secretsmanager_secret_version.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | -| [aws_secretsmanager_secret_version.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | -| [aws_ssm_parameter.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [aws_ssm_parameter.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs diff --git a/modules/datadog-agent/asm.tf b/modules/datadog-agent/asm.tf deleted file mode 100644 index a4e64b0b6..000000000 --- a/modules/datadog-agent/asm.tf +++ /dev/null @@ -1,19 +0,0 @@ -data "aws_secretsmanager_secret" "datadog_api_key" { - count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 - name = var.datadog_api_secret_key -} - -data "aws_secretsmanager_secret_version" "datadog_api_key" { - count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 - secret_id = data.aws_secretsmanager_secret.datadog_api_key[0].id -} - -data "aws_secretsmanager_secret" "datadog_app_key" { - count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 - name = var.datadog_app_secret_key -} - -data "aws_secretsmanager_secret_version" "datadog_app_key" { - count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 - secret_id = data.aws_secretsmanager_secret.datadog_app_key[0].id -} diff --git a/modules/datadog-agent/main.tf b/modules/datadog-agent/main.tf index 876e0e809..ffb43b84f 100644 --- a/modules/datadog-agent/main.tf +++ b/modules/datadog-agent/main.tf @@ -1,17 +1,17 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + + locals { enabled = module.this.enabled tags = module.this.tags - datadog_api_key = local.enabled ? (var.secrets_store_type == "ASM" ? ( - data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string) : - data.aws_ssm_parameter.datadog_api_key[0].value - ) : null - - datadog_app_key = local.enabled ? (var.secrets_store_type == "ASM" ? ( - data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string) : - data.aws_ssm_parameter.datadog_app_key[0].value - ) : null + datadog_api_key = module.datadog_configuration.datadog_api_key + datadog_app_key = module.datadog_configuration.datadog_app_key # combine context tags with passed in datadog_tags # skip name since that won't be relevant for each metric diff --git a/modules/datadog-agent/ssm.tf b/modules/datadog-agent/ssm.tf deleted file mode 100644 index a78029cb9..000000000 --- a/modules/datadog-agent/ssm.tf +++ /dev/null @@ -1,11 +0,0 @@ -data "aws_ssm_parameter" "datadog_api_key" { - count = local.enabled && var.secrets_store_type == "SSM" ? 1 : 0 - name = format("/%s", var.datadog_api_secret_key) - with_decryption = true -} - -data "aws_ssm_parameter" "datadog_app_key" { - count = local.enabled && var.secrets_store_type == "SSM" ? 1 : 0 - name = format("/%s", var.datadog_app_secret_key) - with_decryption = true -} diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md new file mode 100644 index 000000000..24017ed95 --- /dev/null +++ b/modules/datadog-configuration/README.md @@ -0,0 +1,121 @@ +# Component: `datadog-integration` + +This component is responsible for provisioning SSM or ASM entries for datadog api keys. + +It's required that the DataDog API and APP secret keys are available in the `var.datadog_secrets_source_store_account` account +in AWS SSM Parameter Store at the `/datadog/%v/datadog_app_key` paths (where `%v` are the corresponding account names). + +See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. + +## Usage + +**Stack Level**: Regional + +This component should be deployed to every region where you want to provision datadog resources. + +Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts which you want to track AWS metrics with DataDog. + +```yaml +components: + terraform: + datadog-configuration: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + datadog_secrets_store_type: SSM + datadog_secrets_source_store_account_stage: auto + datadog_secrets_source_store_account_region: "us-west-2" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws.api\_keys](#provider\_aws.api\_keys) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [iam\_roles\_datadog\_secrets](#module\_iam\_roles\_datadog\_secrets) | ../account-map/modules/iam-roles | n/a | +| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_secretsmanager_secret.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | +| [aws_secretsmanager_secret.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | +| [aws_secretsmanager_secret_version.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | +| [aws_secretsmanager_secret_version.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | +| [aws_ssm_parameter.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [datadog\_api\_secret\_key](#input\_datadog\_api\_secret\_key) | The name of the Datadog API secret | `string` | `"default"` | no | +| [datadog\_api\_secret\_key\_source\_pattern](#input\_datadog\_api\_secret\_key\_source\_pattern) | The format string (%v will be replaced by the var.datadog\_api\_secret\_key) for the key of the Datadog API secret in the source account | `string` | `"/datadog/%v/datadog_api_key"` | no | +| [datadog\_api\_secret\_key\_target\_pattern](#input\_datadog\_api\_secret\_key\_target\_pattern) | The format string (%v will be replaced by the var.datadog\_api\_secret\_key) for the key of the Datadog API secret in the target account | `string` | `"/datadog/datadog_api_key"` | no | +| [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The name of the Datadog APP secret | `string` | `"default"` | no | +| [datadog\_app\_secret\_key\_source\_pattern](#input\_datadog\_app\_secret\_key\_source\_pattern) | The format string (%v will be replaced by the var.datadog\_app\_secret\_key) for the key of the Datadog APP secret in the source account | `string` | `"/datadog/%v/datadog_app_key"` | no | +| [datadog\_app\_secret\_key\_target\_pattern](#input\_datadog\_app\_secret\_key\_target\_pattern) | The format string (%v will be replaced by the var.datadog\_api\_secret\_key) for the key of the Datadog APP secret in the target account | `string` | `"/datadog/datadog_app_key"` | no | +| [datadog\_aws\_account\_id](#input\_datadog\_aws\_account\_id) | The AWS account ID Datadog's integration servers use for all integrations | `string` | `"464622532012"` | no | +| [datadog\_secrets\_source\_store\_account\_region](#input\_datadog\_secrets\_source\_store\_account\_region) | Region for holding Secret Store Datadog Keys, leave as null to use the same region as the stack | `string` | `null` | no | +| [datadog\_secrets\_source\_store\_account\_stage](#input\_datadog\_secrets\_source\_store\_account\_stage) | Stage holding Secret Store for Datadog API and app keys. | `string` | `"auto"` | no | +| [datadog\_secrets\_source\_store\_account\_tenant](#input\_datadog\_secrets\_source\_store\_account\_tenant) | Tenant holding Secret Store for Datadog API and app keys. | `string` | `"core"` | no | +| [datadog\_secrets\_store\_type](#input\_datadog\_secrets\_store\_type) | Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM` | `string` | `"SSM"` | no | +| [datadog\_site\_url](#input\_datadog\_site\_url) | The Datadog Site URL, https://docs.datadoghq.com/getting_started/site/ | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [datadog\_api\_key\_location](#output\_datadog\_api\_key\_location) | The Datadog API key in the secrets store | +| [datadog\_api\_url](#output\_datadog\_api\_url) | The URL of the Datadog API | +| [datadog\_app\_key\_location](#output\_datadog\_app\_key\_location) | The Datadog APP key location in the secrets store | +| [datadog\_secrets\_store\_type](#output\_datadog\_secrets\_store\_type) | The type of the secrets store to use for Datadog API and APP keys | +| [datadog\_site](#output\_datadog\_site) | The Datadog site to use | + + + +## References +* Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-integration) - Cloud Posse's upstream component + + +[](https://cpco.io/component) diff --git a/modules/datadog-integration/asm.tf b/modules/datadog-configuration/asm.tf similarity index 100% rename from modules/datadog-integration/asm.tf rename to modules/datadog-configuration/asm.tf diff --git a/modules/datadog-configuration/context.tf b/modules/datadog-configuration/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/datadog-configuration/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/datadog-configuration/main.tf b/modules/datadog-configuration/main.tf new file mode 100644 index 000000000..aa90c5aa0 --- /dev/null +++ b/modules/datadog-configuration/main.tf @@ -0,0 +1,11 @@ +locals { + enabled = module.this.enabled + asm_enabled = local.enabled && var.datadog_secrets_store_type == "ASM" + ssm_enabled = local.enabled && var.datadog_secrets_store_type == "SSM" + + # https://docs.datadoghq.com/account_management/api-app-keys/ + datadog_api_key = var.datadog_secrets_store_type == "ASM" ? data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string : data.aws_ssm_parameter.datadog_api_key[0].value + datadog_app_key = var.datadog_secrets_store_type == "ASM" ? data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string : data.aws_ssm_parameter.datadog_app_key[0].value + + datadog_api_url = format("https://api.%s", var.datadog_site_url) +} diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md new file mode 100644 index 000000000..5ad172ec1 --- /dev/null +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -0,0 +1,21 @@ +# Submodule `datadog_keys` + +Useful submodule for other modules to quickly configure the datadog provider + +## Usage + +```hcl +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +provider "datadog" { + api_url = module.datadog_configuration.datadog_api_url + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + validate = local.enabled +} +``` + diff --git a/modules/datadog-configuration/modules/datadog_keys/context.tf b/modules/datadog-configuration/modules/datadog_keys/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/datadog-configuration/modules/datadog_keys/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/datadog-configuration/modules/datadog_keys/main.tf b/modules/datadog-configuration/modules/datadog_keys/main.tf new file mode 100644 index 000000000..496e57a48 --- /dev/null +++ b/modules/datadog-configuration/modules/datadog_keys/main.tf @@ -0,0 +1,50 @@ +module "always" { + source = "cloudposse/label/null" + version = "0.25.0" + + # datadog configuration must always be enabled, even for components that are disabled + # this allows datadog provider to be configured correctly and properly delete resources. + enabled = true + + context = module.this.context +} + +module "utils_example_complete" { + source = "cloudposse/utils/aws" + version = "1.1.0" +} + +locals { + # if we are in the global region, use the + environment = module.this.environment == var.global_environment_name ? module.utils_example_complete.region_az_alt_code_maps[var.region_abbreviation_type][var.region] : var.environment + + context_tags = { + for k, v in module.this.tags : + lower(k) => v + } + dd_tags = [ + for k, v in local.context_tags : + v != null ? format("%s:%s", k, v) : k + ] +} +module "datadog_configuration" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.3.1" + + component = "datadog-configuration" + + environment = local.environment + context = module.always.context +} + +data "aws_ssm_parameter" "datadog_api_key" { + count = module.this.enabled ? 1 : 0 + + name = module.datadog_configuration.outputs.datadog_api_key_location +} + +data "aws_ssm_parameter" "datadog_app_key" { + count = module.this.enabled ? 1 : 0 + + name = module.datadog_configuration.outputs.datadog_app_key_location +} diff --git a/modules/datadog-configuration/modules/datadog_keys/outputs.tf b/modules/datadog-configuration/modules/datadog_keys/outputs.tf new file mode 100644 index 000000000..00d2bd5c2 --- /dev/null +++ b/modules/datadog-configuration/modules/datadog_keys/outputs.tf @@ -0,0 +1,20 @@ +output "datadog_api_key" { + value = one(data.aws_ssm_parameter.datadog_api_key[*].value) +} + +output "datadog_app_key" { + value = one(data.aws_ssm_parameter.datadog_app_key[*].value) +} + +output "datadog_api_url" { + value = module.datadog_configuration.outputs.datadog_api_url +} + +output "datadog_site" { + value = module.datadog_configuration.outputs.datadog_site +} + +output "datadog_tags" { + value = local.dd_tags + description = "The Context Tags in datadog tag format (list of strings formated as 'key:value')" +} diff --git a/modules/datadog-configuration/modules/datadog_keys/variables.tf b/modules/datadog-configuration/modules/datadog_keys/variables.tf new file mode 100644 index 000000000..f6865f7a8 --- /dev/null +++ b/modules/datadog-configuration/modules/datadog_keys/variables.tf @@ -0,0 +1,16 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "global_environment_name" { + type = string + description = "Global environment name" + default = "gbl" +} + +variable "region_abbreviation_type" { + type = string + description = "Region abbreviation type, must be `to_fixed`, `to_short`, or `identity`" + default = "to_short" +} diff --git a/modules/datadog-configuration/outputs.tf b/modules/datadog-configuration/outputs.tf new file mode 100644 index 000000000..958ecd811 --- /dev/null +++ b/modules/datadog-configuration/outputs.tf @@ -0,0 +1,24 @@ +output "datadog_secrets_store_type" { + value = var.datadog_secrets_store_type + description = "The type of the secrets store to use for Datadog API and APP keys" +} + +output "datadog_api_url" { + value = local.datadog_api_url + description = "The URL of the Datadog API" +} + +output "datadog_app_key_location" { + value = local.datadog_app_key_name + description = "The Datadog APP key location in the secrets store" +} + +output "datadog_api_key_location" { + value = local.datadog_api_key_name + description = "The Datadog API key in the secrets store" +} + +output "datadog_site" { + value = var.datadog_site_url + description = "The Datadog site to use" +} diff --git a/modules/datadog-configuration/providers.tf b/modules/datadog-configuration/providers.tf new file mode 100644 index 000000000..177c5fc6c --- /dev/null +++ b/modules/datadog-configuration/providers.tf @@ -0,0 +1,48 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} + +provider "aws" { + alias = "api_keys" + region = coalesce(var.datadog_secrets_source_store_account_region, var.region) + + profile = module.iam_roles_datadog_secrets.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles_datadog_secrets.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles_datadog_secrets.terraform_role_arn) + } + } +} + +module "iam_roles_datadog_secrets" { + source = "../account-map/modules/iam-roles" + stage = var.datadog_secrets_source_store_account_stage + tenant = var.datadog_secrets_source_store_account_tenant + context = module.this.context +} diff --git a/modules/datadog-integration/ssm.tf b/modules/datadog-configuration/ssm.tf similarity index 92% rename from modules/datadog-integration/ssm.tf rename to modules/datadog-configuration/ssm.tf index eff496f6e..edbf9360d 100644 --- a/modules/datadog-integration/ssm.tf +++ b/modules/datadog-configuration/ssm.tf @@ -11,7 +11,7 @@ locals { } data "aws_ssm_parameter" "datadog_api_key" { - count = local.ssm_enabled ? 1 : 0 + count = var.datadog_secrets_store_type == "SSM" ? 1 : 0 name = format(var.datadog_api_secret_key_source_pattern, var.datadog_api_secret_key) with_decryption = true @@ -19,7 +19,7 @@ data "aws_ssm_parameter" "datadog_api_key" { } data "aws_ssm_parameter" "datadog_app_key" { - count = local.ssm_enabled ? 1 : 0 + count = var.datadog_secrets_store_type == "SSM" ? 1 : 0 name = format(var.datadog_app_secret_key_source_pattern, var.datadog_app_secret_key) with_decryption = true provider = aws.api_keys diff --git a/modules/datadog-configuration/variables.tf b/modules/datadog-configuration/variables.tf new file mode 100644 index 000000000..c4ad89d85 --- /dev/null +++ b/modules/datadog-configuration/variables.tf @@ -0,0 +1,86 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "datadog_aws_account_id" { + type = string + description = "The AWS account ID Datadog's integration servers use for all integrations" + default = "464622532012" +} + +variable "datadog_site_url" { + type = string + description = "The Datadog Site URL, https://docs.datadoghq.com/getting_started/site/" + default = null + + validation { + condition = var.datadog_site_url == null ? true : contains([ + "datadoghq.com", + "us3.datadoghq.com", + "us5.datadoghq.com", + "datadoghq.eu", + "ddog-gov.com" + ], var.datadog_site_url) + error_message = "Allowed values: null, `datadoghq.com`, `us3.datadoghq.com`, `us5.datadoghq.com`, `datadoghq.eu`, `ddog-gov.com`." + } +} +variable "datadog_secrets_store_type" { + type = string + description = "Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" + default = "SSM" +} + +variable "datadog_secrets_source_store_account_region" { + type = string + description = "Region for holding Secret Store Datadog Keys, leave as null to use the same region as the stack" + default = null +} + +variable "datadog_secrets_source_store_account_stage" { + type = string + description = "Stage holding Secret Store for Datadog API and app keys." + default = "auto" +} + +variable "datadog_secrets_source_store_account_tenant" { + type = string + description = "Tenant holding Secret Store for Datadog API and app keys." + default = "core" +} + +variable "datadog_api_secret_key_source_pattern" { + type = string + description = "The format string (%v will be replaced by the var.datadog_api_secret_key) for the key of the Datadog API secret in the source account" + default = "/datadog/%v/datadog_api_key" +} + +variable "datadog_app_secret_key_source_pattern" { + type = string + description = "The format string (%v will be replaced by the var.datadog_app_secret_key) for the key of the Datadog APP secret in the source account" + default = "/datadog/%v/datadog_app_key" +} + +variable "datadog_api_secret_key_target_pattern" { + type = string + description = "The format string (%v will be replaced by the var.datadog_api_secret_key) for the key of the Datadog API secret in the target account" + default = "/datadog/datadog_api_key" +} + +variable "datadog_app_secret_key_target_pattern" { + type = string + description = "The format string (%v will be replaced by the var.datadog_api_secret_key) for the key of the Datadog APP secret in the target account" + default = "/datadog/datadog_app_key" +} + +variable "datadog_api_secret_key" { + type = string + description = "The name of the Datadog API secret" + default = "default" +} + +variable "datadog_app_secret_key" { + type = string + description = "The name of the Datadog APP secret" + default = "default" +} diff --git a/modules/datadog-configuration/versions.tf b/modules/datadog-configuration/versions.tf new file mode 100644 index 000000000..cc73ffd35 --- /dev/null +++ b/modules/datadog-configuration/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index b47064d9b..9f6815aeb 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -21,11 +21,7 @@ components: spacelift: workspace_enabled: true vars: - datadog_secrets_store_type: SSM - datadog_secrets_source_store_account: "tools" - datadog_secrets_source_store_region: "us-west-2" - datadog_api_secret_key: "dev" - datadog_app_secret_key: "dev" + enabled: true ``` @@ -34,35 +30,25 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | | [datadog](#requirement\_datadog) | >= 3.3.0 | ## Providers -| Name | Version | -|------|---------| -| [aws.api\_keys](#provider\_aws.api\_keys) | ~> 4.0 | +No providers. ## Modules | Name | Source | Version | |------|--------|---------| -| [datadog\_integration](#module\_datadog\_integration) | cloudposse/datadog-integration/aws | 0.18.0 | +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [datadog\_integration](#module\_datadog\_integration) | cloudposse/datadog-integration/aws | 1.0.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [iam\_roles\_datadog\_secrets](#module\_iam\_roles\_datadog\_secrets) | ../account-map/modules/iam-roles | n/a | -| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources -| Name | Type | -|------|------| -| [aws_secretsmanager_secret.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | -| [aws_secretsmanager_secret.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | -| [aws_secretsmanager_secret_version.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | -| [aws_secretsmanager_secret_version.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | -| [aws_ssm_parameter.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [aws_ssm_parameter.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +No resources. ## Inputs @@ -73,16 +59,7 @@ components: | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [context\_host\_and\_filter\_tags](#input\_context\_host\_and\_filter\_tags) | Automatically add host and filter tags for these context keys | `list(string)` |
[
"namespace",
"tenant",
"stage"
]
| no | -| [datadog\_api\_secret\_key](#input\_datadog\_api\_secret\_key) | The name of the Datadog API secret | `string` | `"default"` | no | -| [datadog\_api\_secret\_key\_source\_pattern](#input\_datadog\_api\_secret\_key\_source\_pattern) | The format string (%v will be replaced by the var.datadog\_api\_secret\_key) for the key of the Datadog API secret in the source account | `string` | `"/datadog/%v/datadog_api_key"` | no | -| [datadog\_api\_secret\_key\_target\_pattern](#input\_datadog\_api\_secret\_key\_target\_pattern) | The format string (%v will be replaced by the var.datadog\_api\_secret\_key) for the key of the Datadog API secret in the target account | `string` | `"/datadog/datadog_api_key"` | no | -| [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The name of the Datadog APP secret | `string` | `"default"` | no | -| [datadog\_app\_secret\_key\_source\_pattern](#input\_datadog\_app\_secret\_key\_source\_pattern) | The format string (%v will be replaced by the var.datadog\_app\_secret\_key) for the key of the Datadog APP secret in the source account | `string` | `"/datadog/%v/datadog_app_key"` | no | -| [datadog\_app\_secret\_key\_target\_pattern](#input\_datadog\_app\_secret\_key\_target\_pattern) | The format string (%v will be replaced by the var.datadog\_api\_secret\_key) for the key of the Datadog APP secret in the target account | `string` | `"/datadog/datadog_app_key"` | no | | [datadog\_aws\_account\_id](#input\_datadog\_aws\_account\_id) | The AWS account ID Datadog's integration servers use for all integrations | `string` | `"464622532012"` | no | -| [datadog\_secrets\_source\_store\_account\_stage](#input\_datadog\_secrets\_source\_store\_account\_stage) | Stage holding Secret Store for Datadog API and app keys. | `string` | `"tools"` | no | -| [datadog\_secrets\_source\_store\_account\_tenant](#input\_datadog\_secrets\_source\_store\_account\_tenant) | Tenant holding Secret Store for Datadog API and app keys. | `string` | `"core"` | no | -| [datadog\_secrets\_store\_type](#input\_datadog\_secrets\_store\_type) | Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM` | `string` | `"SSM"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | diff --git a/modules/datadog-integration/default.auto.tfvars b/modules/datadog-integration/default.auto.tfvars deleted file mode 100644 index 131af3ce0..000000000 --- a/modules/datadog-integration/default.auto.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - - diff --git a/modules/datadog-integration/main.tf b/modules/datadog-integration/main.tf index 3d5f0ddb9..160deceb9 100644 --- a/modules/datadog-integration/main.tf +++ b/modules/datadog-integration/main.tf @@ -1,6 +1,8 @@ module "datadog_integration" { source = "cloudposse/datadog-integration/aws" - version = "0.18.0" + version = "1.0.0" + + count = module.this.enabled && length(var.integrations) > 0 ? 1 : 0 datadog_aws_account_id = var.datadog_aws_account_id integrations = var.integrations @@ -13,13 +15,7 @@ module "datadog_integration" { } locals { - enabled = module.this.enabled - asm_enabled = local.enabled && var.datadog_secrets_store_type == "ASM" - ssm_enabled = local.enabled && var.datadog_secrets_store_type == "SSM" - - # https://docs.datadoghq.com/account_management/api-app-keys/ - datadog_api_key = local.enabled ? (local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string : data.aws_ssm_parameter.datadog_api_key[0].value) : null - datadog_app_key = local.enabled ? (local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string : data.aws_ssm_parameter.datadog_app_key[0].value) : null + enabled = module.this.enabled # Get the context tags and skip tags that we don't want applied to every resource. # i.e. we don't want name since each metric would be called something other than this component's name. diff --git a/modules/datadog-integration/outputs.tf b/modules/datadog-integration/outputs.tf index 96cf1426d..2a801eee8 100644 --- a/modules/datadog-integration/outputs.tf +++ b/modules/datadog-integration/outputs.tf @@ -1,14 +1,14 @@ output "aws_account_id" { - value = module.datadog_integration.aws_account_id + value = one(module.datadog_integration[*].aws_account_id) description = "AWS Account ID of the IAM Role for the Datadog integration" } output "aws_role_name" { - value = module.datadog_integration.aws_role_name + value = one(module.datadog_integration[*].aws_role_name) description = "Name of the AWS IAM Role for the Datadog integration" } output "datadog_external_id" { - value = module.datadog_integration.datadog_external_id + value = one(module.datadog_integration[*].datadog_external_id) description = "Datadog integration external ID" } diff --git a/modules/datadog-integration/provider-datadog.tf b/modules/datadog-integration/provider-datadog.tf new file mode 100644 index 000000000..8db220f1f --- /dev/null +++ b/modules/datadog-integration/provider-datadog.tf @@ -0,0 +1,12 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} diff --git a/modules/datadog-integration/providers.tf b/modules/datadog-integration/providers.tf index 613af8191..08ee01b2a 100644 --- a/modules/datadog-integration/providers.tf +++ b/modules/datadog-integration/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { @@ -26,29 +27,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - -provider "aws" { - alias = "api_keys" - region = var.region - - profile = module.iam_roles_datadog_secrets.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles_datadog_secrets.terraform_profile_name) : null - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles_datadog_secrets.terraform_role_arn) - } - } -} - -module "iam_roles_datadog_secrets" { - source = "../account-map/modules/iam-roles" - stage = var.datadog_secrets_source_store_account_stage - tenant = var.datadog_secrets_source_store_account_tenant - context = module.this.context -} - -provider "datadog" { - api_key = local.datadog_api_key - app_key = local.datadog_app_key - validate = local.enabled -} diff --git a/modules/datadog-integration/variables.tf b/modules/datadog-integration/variables.tf index e32abcfb8..4783b255c 100644 --- a/modules/datadog-integration/variables.tf +++ b/modules/datadog-integration/variables.tf @@ -39,60 +39,6 @@ variable "account_specific_namespace_rules" { default = {} } -variable "datadog_secrets_store_type" { - type = string - description = "Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" - default = "SSM" -} - -variable "datadog_secrets_source_store_account_stage" { - type = string - description = "Stage holding Secret Store for Datadog API and app keys." - default = "tools" -} - -variable "datadog_secrets_source_store_account_tenant" { - type = string - description = "Tenant holding Secret Store for Datadog API and app keys." - default = "core" -} - -variable "datadog_api_secret_key_source_pattern" { - type = string - description = "The format string (%v will be replaced by the var.datadog_api_secret_key) for the key of the Datadog API secret in the source account" - default = "/datadog/%v/datadog_api_key" -} - -variable "datadog_app_secret_key_source_pattern" { - type = string - description = "The format string (%v will be replaced by the var.datadog_app_secret_key) for the key of the Datadog APP secret in the source account" - default = "/datadog/%v/datadog_app_key" -} - -variable "datadog_api_secret_key_target_pattern" { - type = string - description = "The format string (%v will be replaced by the var.datadog_api_secret_key) for the key of the Datadog API secret in the target account" - default = "/datadog/datadog_api_key" -} - -variable "datadog_app_secret_key_target_pattern" { - type = string - description = "The format string (%v will be replaced by the var.datadog_api_secret_key) for the key of the Datadog APP secret in the target account" - default = "/datadog/datadog_app_key" -} - -variable "datadog_api_secret_key" { - type = string - description = "The name of the Datadog API secret" - default = "default" -} - -variable "datadog_app_secret_key" { - type = string - description = "The name of the Datadog APP secret" - default = "default" -} -# variable "context_host_and_filter_tags" { type = list(string) description = "Automatically add host and filter tags for these context keys" diff --git a/modules/datadog-integration/versions.tf b/modules/datadog-integration/versions.tf index 9b8e48942..20f566652 100644 --- a/modules/datadog-integration/versions.tf +++ b/modules/datadog-integration/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.9.0" } datadog = { source = "datadog/datadog" diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 33a2e75db..bd2e5281d 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -26,7 +26,7 @@ components: # 3. CloudWatch Log Group `RDSOSMetrics` exists (it will be created by AWS automatically when RDS Enhanced Monitoring is enabled) forwarder_rds_enabled: true forwarder_log_enabled: true - forwarder_vpc_enabled: true + forwarder_vpc_logs_enabled: true cloudwatch_forwarder_log_groups: rds-enhanced-monitoring: name: "RDSOSMetrics" @@ -52,24 +52,34 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [datadog](#requirement\_datadog) | >= 3.3.0 | ## Providers -No providers. +| Name | Version | +|------|---------| +| [datadog](#provider\_datadog) | >= 3.3.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 0.12.0 | +| [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.0.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [log\_group\_prefix](#module\_log\_group\_prefix) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources -No resources. +| Name | Type | +|------|------| +| [datadog_integration_aws_lambda_arn.log_collector](https://registry.terraform.io/providers/datadog/datadog/latest/docs/resources/integration_aws_lambda_arn) | resource | +| [datadog_integration_aws_lambda_arn.rds_collector](https://registry.terraform.io/providers/datadog/datadog/latest/docs/resources/integration_aws_lambda_arn) | resource | +| [datadog_integration_aws_lambda_arn.vpc_logs_collector](https://registry.terraform.io/providers/datadog/datadog/latest/docs/resources/integration_aws_lambda_arn) | resource | +| [datadog_integration_aws_log_collection.main](https://registry.terraform.io/providers/datadog/datadog/latest/docs/resources/integration_aws_log_collection) | resource | ## Inputs @@ -81,17 +91,17 @@ No resources. | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [context\_tags](#input\_context\_tags) | List of context tags to add to each monitor | `set(string)` |
[
"namespace",
"tenant",
"environment",
"stage"
]
| no | | [context\_tags\_enabled](#input\_context\_tags\_enabled) | Whether to add context tags to add to each monitor | `bool` | `true` | no | +| [datadog\_forwarder\_lambda\_environment\_variables](#input\_datadog\_forwarder\_lambda\_environment\_variables) | Map of environment variables to pass to the Lambda Function | `map(string)` | `{}` | no | | [dd\_api\_key\_kms\_ciphertext\_blob](#input\_dd\_api\_key\_kms\_ciphertext\_blob) | CiphertextBlob stored in environment variable DD\_KMS\_API\_KEY used by the lambda function, along with the KMS key, to decrypt Datadog API key | `string` | `""` | no | | [dd\_api\_key\_source](#input\_dd\_api\_key\_source) | One of: ARN for AWS Secrets Manager (asm) to retrieve the Datadog (DD) api key, ARN for the KMS (kms) key used to decrypt the ciphertext\_blob of the api key, or the name of the SSM (ssm) parameter used to retrieve the Datadog API key |
object({
resource = string
identifier = string
})
|
{
"identifier": "",
"resource": ""
}
| no | | [dd\_artifact\_filename](#input\_dd\_artifact\_filename) | The Datadog artifact filename minus extension | `string` | `"aws-dd-forwarder"` | no | -| [dd\_forwarder\_version](#input\_dd\_forwarder\_version) | Version tag of Datadog lambdas to use. https://github.com/DataDog/datadog-serverless-functions/releases | `string` | `"3.40.0"` | no | +| [dd\_forwarder\_version](#input\_dd\_forwarder\_version) | Version tag of Datadog lambdas to use. https://github.com/DataDog/datadog-serverless-functions/releases | `string` | `"3.61.0"` | no | | [dd\_module\_name](#input\_dd\_module\_name) | The Datadog GitHub repository name | `string` | `"datadog-serverless-functions"` | no | | [dd\_tags\_map](#input\_dd\_tags\_map) | A map of Datadog tags to apply to all logs forwarded to Datadog | `map(string)` | `{}` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [forwarder\_lambda\_datadog\_host](#input\_forwarder\_lambda\_datadog\_host) | Datadog Site to send data to. Possible values are `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com` and `ddog-gov.com` | `string` | `"datadoghq.com"` | no | | [forwarder\_lambda\_debug\_enabled](#input\_forwarder\_lambda\_debug\_enabled) | Whether to enable or disable debug for the Lambda forwarder | `bool` | `false` | no | | [forwarder\_log\_artifact\_url](#input\_forwarder\_log\_artifact\_url) | The URL for the code of the Datadog forwarder for Logs. It can be a local file, URL or git repo | `string` | `null` | no | | [forwarder\_log\_enabled](#input\_forwarder\_log\_enabled) | Flag to enable or disable Datadog log forwarder | `bool` | `false` | no | @@ -113,9 +123,11 @@ No resources. | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lambda\_arn\_enabled](#input\_lambda\_arn\_enabled) | Enable adding the Lambda Arn to this account integration | `bool` | `true` | no | | [lambda\_policy\_source\_json](#input\_lambda\_policy\_source\_json) | Additional IAM policy document that can optionally be passed and merged with the created policy document | `string` | `""` | no | | [lambda\_reserved\_concurrent\_executions](#input\_lambda\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the lambda function. A value of 0 disables Lambda from being triggered and -1 removes any concurrency limitations. Defaults to Unreserved Concurrency Limits -1 | `number` | `-1` | no | -| [lambda\_runtime](#input\_lambda\_runtime) | Runtime environment for Datadog Lambda | `string` | `"python3.7"` | no | +| [lambda\_runtime](#input\_lambda\_runtime) | Runtime environment for Datadog Lambda | `string` | `"python3.8"` | no | +| [log\_collection\_services](#input\_log\_collection\_services) | List of log collection services to enable | `list(string)` |
[
"apigw-access-logs",
"apigw-execution-logs",
"elbv2",
"elb",
"cloudfront",
"lambda",
"redshift",
"s3"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | diff --git a/modules/datadog-lambda-forwarder/main.tf b/modules/datadog-lambda-forwarder/main.tf index dd0157fb0..3f25f4442 100644 --- a/modules/datadog-lambda-forwarder/main.tf +++ b/modules/datadog-lambda-forwarder/main.tf @@ -1,4 +1,6 @@ locals { + enabled = module.this.enabled + # If any keys contain name_suffix, then use a null label to get the label prefix, and create # the appropriate input for the upstream module. cloudwatch_forwarder_log_groups = { @@ -38,7 +40,7 @@ module "log_group_prefix" { module "datadog_lambda_forwarder" { source = "cloudposse/datadog-lambda-forwarder/aws" - version = "0.12.0" + version = "1.0.0" cloudwatch_forwarder_log_groups = local.cloudwatch_forwarder_log_groups dd_api_key_kms_ciphertext_blob = var.dd_api_key_kms_ciphertext_blob @@ -47,7 +49,7 @@ module "datadog_lambda_forwarder" { dd_forwarder_version = var.dd_forwarder_version dd_module_name = var.dd_module_name dd_tags_map = local.dd_tags_map - forwarder_lambda_datadog_host = var.forwarder_lambda_datadog_host + forwarder_lambda_datadog_host = module.datadog_configuration.datadog_site forwarder_lambda_debug_enabled = var.forwarder_lambda_debug_enabled forwarder_log_artifact_url = var.forwarder_log_artifact_url forwarder_log_enabled = var.forwarder_log_enabled @@ -72,5 +74,35 @@ module "datadog_lambda_forwarder" { tracing_config_mode = var.tracing_config_mode vpclogs_cloudwatch_log_group = var.vpclogs_cloudwatch_log_group + datadog_forwarder_lambda_environment_variables = var.datadog_forwarder_lambda_environment_variables + context = module.this.context } + +# Create a new Datadog - Amazon Web Services integration Lambda ARN +resource "datadog_integration_aws_lambda_arn" "rds_collector" { + count = var.lambda_arn_enabled && var.forwarder_rds_enabled ? 1 : 0 + + account_id = module.datadog-integration.outputs.aws_account_id + lambda_arn = module.datadog_lambda_forwarder.lambda_forwarder_rds_function_arn +} + +resource "datadog_integration_aws_lambda_arn" "vpc_logs_collector" { + count = var.lambda_arn_enabled && var.forwarder_vpc_logs_enabled ? 1 : 0 + + account_id = module.datadog-integration.outputs.aws_account_id + lambda_arn = module.datadog_lambda_forwarder.lambda_forwarder_vpc_log_function_arn +} + +resource "datadog_integration_aws_lambda_arn" "log_collector" { + count = var.lambda_arn_enabled && var.forwarder_log_enabled ? 1 : 0 + + account_id = module.datadog-integration.outputs.aws_account_id + lambda_arn = module.datadog_lambda_forwarder.lambda_forwarder_log_function_arn +} + +resource "datadog_integration_aws_log_collection" "main" { + count = var.lambda_arn_enabled ? 1 : 0 + account_id = module.datadog-integration.outputs.aws_account_id + services = var.log_collection_services +} diff --git a/modules/datadog-lambda-forwarder/provider-datadog.tf b/modules/datadog-lambda-forwarder/provider-datadog.tf new file mode 100644 index 000000000..8db220f1f --- /dev/null +++ b/modules/datadog-lambda-forwarder/provider-datadog.tf @@ -0,0 +1,12 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} diff --git a/modules/datadog-lambda-forwarder/providers.tf b/modules/datadog-lambda-forwarder/providers.tf index efa9ede5d..08ee01b2a 100644 --- a/modules/datadog-lambda-forwarder/providers.tf +++ b/modules/datadog-lambda-forwarder/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { diff --git a/modules/datadog-lambda-forwarder/remote-state.tf b/modules/datadog-lambda-forwarder/remote-state.tf new file mode 100644 index 000000000..341a4fc52 --- /dev/null +++ b/modules/datadog-lambda-forwarder/remote-state.tf @@ -0,0 +1,9 @@ +module "datadog-integration" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.3.1" + + component = "datadog-integration" + + environment = "gbl" + context = module.this.context +} diff --git a/modules/datadog-lambda-forwarder/variables.tf b/modules/datadog-lambda-forwarder/variables.tf index cb4748f28..95b0764f5 100644 --- a/modules/datadog-lambda-forwarder/variables.tf +++ b/modules/datadog-lambda-forwarder/variables.tf @@ -25,7 +25,7 @@ variable "lambda_reserved_concurrent_executions" { variable "lambda_runtime" { type = string description = "Runtime environment for Datadog Lambda" - default = "python3.7" + default = "python3.8" } variable "tracing_config_mode" { @@ -92,7 +92,7 @@ variable "dd_module_name" { variable "dd_forwarder_version" { type = string description = "Version tag of Datadog lambdas to use. https://github.com/DataDog/datadog-serverless-functions/releases" - default = "3.40.0" + default = "3.61.0" } variable "forwarder_log_enabled" { @@ -181,16 +181,6 @@ variable "lambda_policy_source_json" { default = "" } -variable "forwarder_lambda_datadog_host" { - type = string - description = "Datadog Site to send data to. Possible values are `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com` and `ddog-gov.com`" - default = "datadoghq.com" - validation { - condition = contains(["datadoghq.com", "datadoghq.eu", "us3.datadoghq.com", "ddog-gov.com"], var.forwarder_lambda_datadog_host) - error_message = "Invalid host: possible values are `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com` and `ddog-gov.com`." - } -} - variable "forwarder_log_layers" { type = list(string) description = "List of Lambda Layer Version ARNs (maximum of 5) to attach to Datadog log forwarder lambda function" @@ -238,3 +228,37 @@ variable "context_tags" { description = "List of context tags to add to each monitor" default = ["namespace", "tenant", "environment", "stage"] } + +variable "lambda_arn_enabled" { + type = bool + description = "Enable adding the Lambda Arn to this account integration" + default = true +} + +# No Datasource for this (yet?) +/** +curl -X GET "${DD_API_URL}/api/v1/integration/aws/logs/services" \ +-H "Accept: application/json" \ +-H "DD-API-KEY: ${DD_API_KEY}" \ +-H "DD-APPLICATION-KEY: ${DD_APP_KEY}" +**/ +variable "log_collection_services" { + type = list(string) + description = "List of log collection services to enable" + default = [ + "apigw-access-logs", + "apigw-execution-logs", + "elbv2", + "elb", + "cloudfront", + "lambda", + "redshift", + "s3" + ] +} + +variable "datadog_forwarder_lambda_environment_variables" { + type = map(string) + default = {} + description = "Map of environment variables to pass to the Lambda Function" +} diff --git a/modules/datadog-lambda-forwarder/versions.tf b/modules/datadog-lambda-forwarder/versions.tf index d5cde7755..f636a1364 100644 --- a/modules/datadog-lambda-forwarder/versions.tf +++ b/modules/datadog-lambda-forwarder/versions.tf @@ -4,7 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.0" + version = ">= 4.0" + } + datadog = { + source = "datadog/datadog" + version = ">= 3.3.0" } } } diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 171d003a5..784cdb977 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -1,8 +1,8 @@ -# Component: `datadog-monitor` +x# Component: `datadog-monitor` -This component is responsible for provisioning Datadog monitors and assigning Datadog roles to the monitors. +This component is responsible for provisioning Datadog monitors and assigning Datadog roles to the monitors. -It's required that the DataDog API and APP secret keys are available in the consuming account at the `var.datadog_api_secret_key` +It's required that the DataDog API and APP secret keys are available in the consuming account at the `var.datadog_api_secret_key` and `var.datadog_app_secret_key` paths in the AWS SSM Parameter Store. ## Usage @@ -41,36 +41,28 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | | [datadog](#requirement\_datadog) | >= 3.3.0 | ## Providers -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +No providers. ## Modules | Name | Source | Version | |------|--------|---------| -| [datadog\_monitors](#module\_datadog\_monitors) | cloudposse/platform/datadog//modules/monitors | 1.0.0 | -| [datadog\_monitors\_merge](#module\_datadog\_monitors\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.1 | +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [datadog\_monitors](#module\_datadog\_monitors) | cloudposse/platform/datadog//modules/monitors | 1.0.1 | +| [datadog\_monitors\_merge](#module\_datadog\_monitors\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [local\_datadog\_monitors\_yaml\_config](#module\_local\_datadog\_monitors\_yaml\_config) | cloudposse/config/yaml | 1.0.1 | -| [remote\_datadog\_monitors\_yaml\_config](#module\_remote\_datadog\_monitors\_yaml\_config) | cloudposse/config/yaml | 1.0.1 | +| [local\_datadog\_monitors\_yaml\_config](#module\_local\_datadog\_monitors\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | +| [remote\_datadog\_monitors\_yaml\_config](#module\_remote\_datadog\_monitors\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources -| Name | Type | -|------|------| -| [aws_secretsmanager_secret.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | -| [aws_secretsmanager_secret.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | -| [aws_secretsmanager_secret_version.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | -| [aws_secretsmanager_secret_version.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | -| [aws_ssm_parameter.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [aws_ssm_parameter.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +No resources. ## Inputs @@ -82,6 +74,7 @@ components: | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [datadog\_api\_secret\_key](#input\_datadog\_api\_secret\_key) | The key of the Datadog API secret | `string` | `"datadog/datadog_api_key"` | no | +| [datadog\_api\_url](#input\_datadog\_api\_url) | The Datadog API URL | `string` | `null` | no | | [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The key of the Datadog Application secret | `string` | `"datadog/datadog_app_key"` | no | | [datadog\_monitor\_context\_tags](#input\_datadog\_monitor\_context\_tags) | List of context tags to add to each monitor | `set(string)` |
[
"namespace",
"tenant",
"environment",
"stage"
]
| no | | [datadog\_monitor\_context\_tags\_enabled](#input\_datadog\_monitor\_context\_tags\_enabled) | Whether to add context tags to each monitor | `bool` | `true` | no | diff --git a/modules/datadog-monitor/asm.tf b/modules/datadog-monitor/asm.tf deleted file mode 100644 index d3ba132c8..000000000 --- a/modules/datadog-monitor/asm.tf +++ /dev/null @@ -1,19 +0,0 @@ -data "aws_secretsmanager_secret" "datadog_api_key" { - count = local.asm_enabled ? 1 : 0 - name = var.datadog_api_secret_key -} - -data "aws_secretsmanager_secret_version" "datadog_api_key" { - count = local.asm_enabled ? 1 : 0 - secret_id = data.aws_secretsmanager_secret.datadog_api_key[0].id -} - -data "aws_secretsmanager_secret" "datadog_app_key" { - count = local.asm_enabled ? 1 : 0 - name = var.datadog_app_secret_key -} - -data "aws_secretsmanager_secret_version" "datadog_app_key" { - count = local.asm_enabled ? 1 : 0 - secret_id = data.aws_secretsmanager_secret.datadog_app_key[0].id -} diff --git a/modules/datadog-monitor/default.auto.tfvars b/modules/datadog-monitor/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/datadog-monitor/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/datadog-monitor/main.tf b/modules/datadog-monitor/main.tf index 78a192940..d47548f08 100644 --- a/modules/datadog-monitor/main.tf +++ b/modules/datadog-monitor/main.tf @@ -1,11 +1,5 @@ locals { - enabled = module.this.enabled - asm_enabled = var.secrets_store_type == "ASM" - ssm_enabled = var.secrets_store_type == "SSM" - - # https://docs.datadoghq.com/account_management/api-app-keys/ - datadog_api_key = local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string : data.aws_ssm_parameter.datadog_api_key[0].value - datadog_app_key = local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string : data.aws_ssm_parameter.datadog_app_key[0].value + enabled = module.this.enabled local_datadog_monitors_enabled = length(var.local_datadog_monitors_config_paths) > 0 remote_datadog_monitors_enabled = length(var.remote_datadog_monitors_config_paths) > 0 @@ -53,7 +47,7 @@ locals { # Convert all Datadog Monitors from YAML config to Terraform map with token replacement using `parameters` module "remote_datadog_monitors_yaml_config" { source = "cloudposse/config/yaml" - version = "1.0.1" + version = "1.0.2" map_config_remote_base_path = var.remote_datadog_monitors_base_path map_config_paths = var.remote_datadog_monitors_config_paths @@ -69,7 +63,7 @@ module "remote_datadog_monitors_yaml_config" { module "local_datadog_monitors_yaml_config" { source = "cloudposse/config/yaml" - version = "1.0.1" + version = "1.0.2" map_config_local_base_path = abspath(path.module) map_config_paths = var.local_datadog_monitors_config_paths @@ -85,7 +79,7 @@ module "local_datadog_monitors_yaml_config" { module "datadog_monitors_merge" { source = "cloudposse/config/yaml//modules/deepmerge" - version = "1.0.1" + version = "1.0.2" # for_each = { for k, v in local.datadog_monitors_yaml_config_map_configs : k => v if local.datadog_monitors_enabled } for_each = { for k, v in merge( @@ -105,7 +99,7 @@ module "datadog_monitors" { count = local.datadog_monitors_enabled ? 1 : 0 source = "cloudposse/platform/datadog//modules/monitors" - version = "1.0.0" + version = "1.0.1" datadog_monitors = local.datadog_monitors diff --git a/modules/datadog-monitor/provider-datadog.tf b/modules/datadog-monitor/provider-datadog.tf new file mode 100644 index 000000000..8db220f1f --- /dev/null +++ b/modules/datadog-monitor/provider-datadog.tf @@ -0,0 +1,12 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} diff --git a/modules/datadog-monitor/providers.tf b/modules/datadog-monitor/providers.tf old mode 100755 new mode 100644 index f95d03446..08ee01b2a --- a/modules/datadog-monitor/providers.tf +++ b/modules/datadog-monitor/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { @@ -26,8 +27,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - -provider "datadog" { - api_key = local.datadog_api_key - app_key = local.datadog_app_key -} diff --git a/modules/datadog-monitor/ssm.tf b/modules/datadog-monitor/ssm.tf deleted file mode 100644 index 28a23dbae..000000000 --- a/modules/datadog-monitor/ssm.tf +++ /dev/null @@ -1,11 +0,0 @@ -data "aws_ssm_parameter" "datadog_api_key" { - count = local.ssm_enabled ? 1 : 0 - name = format("/%s", var.datadog_api_secret_key) - with_decryption = true -} - -data "aws_ssm_parameter" "datadog_app_key" { - count = local.ssm_enabled ? 1 : 0 - name = format("/%s", var.datadog_app_secret_key) - with_decryption = true -} diff --git a/modules/datadog-monitor/variables.tf b/modules/datadog-monitor/variables.tf index afbdad7cc..2efba3fff 100644 --- a/modules/datadog-monitor/variables.tf +++ b/modules/datadog-monitor/variables.tf @@ -104,3 +104,14 @@ variable "message_postfix" { description = "Additional information to put after each monitor message" default = "" } + +variable "datadog_api_url" { + type = string + description = "The Datadog API URL" + default = null + + validation { + condition = var.datadog_api_url == null ? true : contains(["https://api.datadoghq.com/", "https://api.us3.datadoghq.com/", "https://api.us5.datadoghq.com/", "https://api.datadoghq.eu/", "https://api.ddog-gov.com/"], var.datadog_api_url) + error_message = "Allowed values: null, `https://api.datadoghq.com/`, `https://api.us3.datadoghq.com/`, `https://api.us5.datadoghq.com/`, `https://api.datadoghq.eu/`, `https://api.ddog-gov.com/`." + } +} diff --git a/modules/datadog-monitor/versions.tf b/modules/datadog-monitor/versions.tf index 9b8e48942..20f566652 100755 --- a/modules/datadog-monitor/versions.tf +++ b/modules/datadog-monitor/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.9.0" } datadog = { source = "datadog/datadog" diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md new file mode 100644 index 000000000..0216b816f --- /dev/null +++ b/modules/datadog-private-location-ecs/README.md @@ -0,0 +1,141 @@ +# Component: `ecs-service` + +This component is responsible for creating a datadog private location and deploying it to ECS (EC2 / Fargate) + +## Usage + +**Note** The app key required for this component requires admin level permissions if you are using the default roles. +Admin's have permissions to Write to private locations, which is needed for this component. + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +# stacks/catalog/datadog/private-location.yaml +components: + terraform: + datadog-private-location: + metadata: + component: datadog-private-location-ecs + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: datadog-private-location + task: + task_memory: 512 + task_cpu: 256 + launch_type: FARGATE + # capacity_provider_strategies takes precedence over launch_type + capacity_provider_strategies: + - capacity_provider: FARGATE_SPOT + weight: 100 + base: null + network_mode: awsvpc + desired_count: 1 + ignore_changes_desired_count: true + ignore_changes_task_definition: false + use_alb_security_group: false + assign_public_ip: false + propagate_tags: SERVICE + wait_for_steady_state: true + circuit_breaker_deployment_enabled: true + circuit_breaker_rollback_enabled: true + containers: + datadog: + name: datadog-private-location + image: public.ecr.aws/datadog/synthetics-private-location-worker:latest + compatibilities: + - EC2 + - FARGATE + - FARGATE_SPOT + log_configuration: + logDriver: awslogs + options: {} + port_mappings: [] + +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | = 4.0 | +| [datadog](#requirement\_datadog) | >= 3.3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [datadog](#provider\_datadog) | >= 3.3.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [container\_definition](#module\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.66.2 | +| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [roles\_to\_principals](#module\_roles\_to\_principals) | ../account-map/modules/roles-to-principals | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | + +## Resources + +| Name | Type | +|------|------| +| [datadog_synthetics_private_location.private_location](https://registry.terraform.io/providers/datadog/datadog/latest/docs/resources/synthetics_private_location) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_configuration](#input\_alb\_configuration) | The configuration to use for the ALB, specifying which cluster alb configuration to use | `string` | `"default"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [containers](#input\_containers) | Feed inputs into container definition module | `any` | `{}` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [private\_location\_description](#input\_private\_location\_description) | The description of the private location. | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module | `any` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [ecs\_cluster\_arn](#output\_ecs\_cluster\_arn) | Selected ECS cluster ARN | +| [lb\_arn](#output\_lb\_arn) | Selected LB ARN | +| [lb\_listener\_https](#output\_lb\_listener\_https) | Selected LB HTTPS Listener | +| [lb\_sg\_id](#output\_lb\_sg\_id) | Selected LB SG ID | +| [subnet\_ids](#output\_subnet\_ids) | Selected subnet IDs | +| [vpc\_id](#output\_vpc\_id) | Selected VPC ID | +| [vpc\_sg\_id](#output\_vpc\_sg\_id) | Selected VPC SG ID | + + +## References +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecs-service) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/datadog-private-location-ecs/context.tf b/modules/datadog-private-location-ecs/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/datadog-private-location-ecs/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/datadog-private-location-ecs/main.tf b/modules/datadog-private-location-ecs/main.tf new file mode 100644 index 000000000..ea0f491c6 --- /dev/null +++ b/modules/datadog-private-location-ecs/main.tf @@ -0,0 +1,118 @@ +locals { + enabled = module.this.enabled + + container_definition = concat([ + for container in module.container_definition : + container.json_map_object + ], + ) + datadog_location_config = jsondecode(datadog_synthetics_private_location.private_location.config) + +} + +module "roles_to_principals" { + source = "../account-map/modules/roles-to-principals" + context = module.this.context + role_map = {} +} + +resource "datadog_synthetics_private_location" "private_location" { + name = module.this.id + description = coalesce(var.private_location_description, format("Private location for %s", module.this.id)) + tags = module.datadog_configuration.datadog_tags +} + +module "container_definition" { + source = "cloudposse/ecs-container-definition/aws" + version = "0.58.1" + + depends_on = [datadog_synthetics_private_location.private_location] + + for_each = var.containers + + container_name = lookup(each.value, "name") + + container_image = lookup(each.value, "image") + + container_memory = lookup(each.value, "memory", null) + container_memory_reservation = lookup(each.value, "memory_reservation", null) + container_cpu = lookup(each.value, "cpu", null) + essential = lookup(each.value, "essential", true) + readonly_root_filesystem = lookup(each.value, "readonly_root_filesystem", null) + + map_environment = merge( + lookup(each.value, "map_environment", {}), + { "APP_ENV" = format("%s-%s-%s-%s", var.namespace, var.tenant, var.environment, var.stage) }, + { "RUNTIME_ENV" = format("%s-%s-%s", var.namespace, var.tenant, var.stage) }, + { "CLUSTER_NAME" = module.ecs_cluster.outputs.cluster_name }, + { "DATADOG_SITE" = module.datadog_configuration.datadog_site }, + { "DATADOG_API_KEY" = module.datadog_configuration.datadog_api_key }, + { "DATADOG_ACCESS_KEY" = local.datadog_location_config.accessKey }, + { "DATADOG_SECRET_ACCESS_KEY" = local.datadog_location_config.secretAccessKey }, + { "DATADOG_PUBLIC_KEY_PEM" = local.datadog_location_config.publicKey.pem }, + { "DATADOG_PUBLIC_KEY_FINGERPRINT" = local.datadog_location_config.publicKey.fingerprint }, + { "DATADOG_PRIVATE_KEY" = local.datadog_location_config.privateKey }, + { "DATADOG_LOCATION_ID" = local.datadog_location_config.id }, + ) + + map_secrets = lookup(each.value, "map_secrets", null) != null ? zipmap( + keys(lookup(each.value, "map_secrets", null)), + formatlist("%s/%s", format("arn:aws:ssm:%s:%s:parameter", + var.region, module.roles_to_principals.full_account_map[format("%s-%s", var.tenant, var.stage)]), + values(lookup(each.value, "map_secrets", null))) + ) : null + port_mappings = lookup(each.value, "port_mappings", []) + command = lookup(each.value, "command", null) + entrypoint = lookup(each.value, "entrypoint", null) + healthcheck = lookup(each.value, "healthcheck", null) + ulimits = lookup(each.value, "ulimits", null) + volumes_from = lookup(each.value, "volumes_from", null) + docker_labels = lookup(each.value, "docker_labels", null) + + firelens_configuration = lookup(each.value, "firelens_configuration", null) + + # escape hatch for anything not specifically described above or unsupported by the upstream module + container_definition = lookup(each.value, "container_definition", {}) +} + +module "ecs_alb_service_task" { + source = "cloudposse/ecs-alb-service-task/aws" + version = "0.66.2" + + count = var.enabled ? 1 : 0 + + ecs_cluster_arn = local.ecs_cluster_arn + vpc_id = local.vpc_id + subnet_ids = local.subnet_ids + + container_definition_json = jsonencode(local.container_definition) + + # This is set to true to allow ingress from the ALB sg + use_alb_security_group = lookup(var.task, "use_alb_security_group", true) + alb_security_group = local.lb_sg_id + security_group_ids = [local.vpc_sg_id] + + # See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#load_balancer + ecs_load_balancers = [] + + assign_public_ip = false + ignore_changes_task_definition = lookup(var.task, "ignore_changes_task_definition", false) + ignore_changes_desired_count = lookup(var.task, "ignore_changes_desired_count", true) + launch_type = lookup(var.task, "launch_type", "FARGATE") + network_mode = lookup(var.task, "network_mode", "awsvpc") + propagate_tags = lookup(var.task, "propagate_tags", "SERVICE") + deployment_minimum_healthy_percent = lookup(var.task, "deployment_minimum_healthy_percent", null) + deployment_maximum_percent = lookup(var.task, "deployment_maximum_percent", null) + deployment_controller_type = lookup(var.task, "deployment_controller_type", null) + desired_count = lookup(var.task, "desired_count", 0) + task_memory = lookup(var.task, "task_memory", null) + task_cpu = lookup(var.task, "task_cpu", null) + wait_for_steady_state = lookup(var.task, "wait_for_steady_state", true) + circuit_breaker_deployment_enabled = lookup(var.task, "circuit_breaker_deployment_enabled", true) + circuit_breaker_rollback_enabled = lookup(var.task, "circuit_breaker_rollback_enabled ", true) + task_policy_arns = [] + ecs_service_enabled = lookup(var.task, "ecs_service_enabled", true) + capacity_provider_strategies = lookup(var.task, "capacity_provider_strategies", []) + + context = module.this.context +} diff --git a/modules/datadog-private-location-ecs/outputs.tf b/modules/datadog-private-location-ecs/outputs.tf new file mode 100644 index 000000000..f6e25b29a --- /dev/null +++ b/modules/datadog-private-location-ecs/outputs.tf @@ -0,0 +1,34 @@ +output "ecs_cluster_arn" { + value = local.ecs_cluster_arn + description = "Selected ECS cluster ARN" +} + +output "subnet_ids" { + value = local.subnet_ids + description = "Selected subnet IDs" +} + +output "vpc_id" { + value = local.vpc_id + description = "Selected VPC ID" +} + +output "vpc_sg_id" { + value = local.vpc_sg_id + description = "Selected VPC SG ID" +} + +output "lb_sg_id" { + value = local.lb_sg_id + description = "Selected LB SG ID" +} + +output "lb_arn" { + value = local.lb_arn + description = "Selected LB ARN" +} + +output "lb_listener_https" { + value = local.lb_listener_https_arn + description = "Selected LB HTTPS Listener" +} diff --git a/modules/datadog-private-location-ecs/provider-datadog.tf b/modules/datadog-private-location-ecs/provider-datadog.tf new file mode 100644 index 000000000..8db220f1f --- /dev/null +++ b/modules/datadog-private-location-ecs/provider-datadog.tf @@ -0,0 +1,12 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} diff --git a/modules/datadog-private-location-ecs/providers.tf b/modules/datadog-private-location-ecs/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/datadog-private-location-ecs/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/datadog-private-location-ecs/remote-state.tf b/modules/datadog-private-location-ecs/remote-state.tf new file mode 100644 index 000000000..97fe10aab --- /dev/null +++ b/modules/datadog-private-location-ecs/remote-state.tf @@ -0,0 +1,28 @@ +locals { + vpc_id = module.vpc.outputs.vpc_id + vpc_sg_id = module.vpc.outputs.vpc_default_security_group_id + subnet_ids = lookup(module.vpc.outputs.subnets, "private", { ids = [] }).ids + ecs_cluster_arn = module.ecs_cluster.outputs.cluster_arn + + lb_arn = try(module.ecs_cluster.outputs.alb[var.alb_configuration].alb_arn, null) + lb_listener_https_arn = try(module.ecs_cluster.outputs.alb[var.alb_configuration].https_listener_arn, null) + lb_sg_id = try(module.ecs_cluster.outputs.alb[var.alb_configuration].security_group_id, null) +} + +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" + + component = "vpc" + + context = module.this.context +} + +module "ecs_cluster" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" + + component = "ecs" + + context = module.this.context +} diff --git a/modules/datadog-private-location-ecs/variables.tf b/modules/datadog-private-location-ecs/variables.tf new file mode 100644 index 000000000..9464172e6 --- /dev/null +++ b/modules/datadog-private-location-ecs/variables.tf @@ -0,0 +1,28 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "private_location_description" { + type = string + description = "The description of the private location." + default = null +} + +variable "containers" { + type = any + description = "Feed inputs into container definition module" + default = {} +} + +variable "task" { + type = any + description = "Feed inputs into ecs_alb_service_task module" + default = {} +} + +variable "alb_configuration" { + type = string + description = "The configuration to use for the ALB, specifying which cluster alb configuration to use" + default = "default" +} diff --git a/modules/datadog-private-location-ecs/versions.tf b/modules/datadog-private-location-ecs/versions.tf new file mode 100644 index 000000000..a36b08e52 --- /dev/null +++ b/modules/datadog-private-location-ecs/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "= 4.0" + } + datadog = { + source = "datadog/datadog" + version = ">= 3.3.0" + } + } +} diff --git a/modules/ecs/README.md b/modules/ecs/README.md index 5c5e9b563..571cfb4f5 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -33,13 +33,13 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | > 4.0 | +| [aws](#requirement\_aws) | ~> 4.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | > 4.0 | +| [aws](#provider\_aws) | ~> 4.0 | ## Modules diff --git a/modules/ecs/versions.tf b/modules/ecs/versions.tf index 288178d45..e89eb16ed 100644 --- a/modules/ecs/versions.tf +++ b/modules/ecs/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "> 4.0" + version = "~> 4.0" } } } From ed4c806b239a3971329b4aa61d5af3e30ffb8c30 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 21 Dec 2022 10:37:17 -0800 Subject: [PATCH 004/501] upstream `ecs` & `ecs-service` (#529) --- deprecated/aws/account-dns/main.tf | 16 +- deprecated/aws/account-settings/main.tf | 12 +- deprecated/aws/account-settings/outputs.tf | 6 +- deprecated/aws/account-settings/variables.tf | 10 +- deprecated/aws/accounts/audit.tf | 16 +- deprecated/aws/accounts/corp.tf | 16 +- deprecated/aws/accounts/data.tf | 16 +- deprecated/aws/accounts/dev.tf | 16 +- deprecated/aws/accounts/identity.tf | 16 +- deprecated/aws/accounts/main.tf | 14 +- deprecated/aws/accounts/prod.tf | 16 +- deprecated/aws/accounts/security.tf | 16 +- deprecated/aws/accounts/stage/main.tf | 28 +- deprecated/aws/accounts/stage/outputs.tf | 6 +- deprecated/aws/accounts/stage/variables.tf | 12 +- deprecated/aws/accounts/staging.tf | 16 +- deprecated/aws/accounts/testing.tf | 16 +- deprecated/aws/acm-cloudfront/main.tf | 14 +- deprecated/aws/acm-teleport/main.tf | 8 +- deprecated/aws/acm-teleport/outputs.tf | 8 +- deprecated/aws/artifacts/main.tf | 34 +-- deprecated/aws/artifacts/outputs.tf | 38 +-- deprecated/aws/artifacts/variables.tf | 12 +- .../aws/audit-cloudtrail/cloudwatch_logs.tf | 16 +- deprecated/aws/audit-cloudtrail/main.tf | 26 +- deprecated/aws/audit-cloudtrail/output.tf | 8 +- deprecated/aws/audit-cloudtrail/s3_bucket.tf | 24 +- deprecated/aws/audit-cloudtrail/varaibles.tf | 10 +- deprecated/aws/aws-metrics-role/main.tf | 8 +- deprecated/aws/aws-metrics-role/variables.tf | 2 +- .../aws/backing-services/aurora-mysql.tf | 102 +++---- .../aurora-postgres-replica.tf | 46 ++-- .../aws/backing-services/aurora-postgres.tf | 100 +++---- .../aws/backing-services/elasticache-redis.tf | 42 +-- .../aws/backing-services/elasticsearch.tf | 52 ++-- deprecated/aws/backing-services/flow-logs.tf | 34 +-- .../aws/backing-services/kops-metadata.tf | 4 +- deprecated/aws/backing-services/main.tf | 20 +- .../aws/backing-services/rds-replica.tf | 100 +++---- deprecated/aws/backing-services/rds.tf | 158 +++++------ deprecated/aws/backing-services/vpc.tf | 30 +-- deprecated/aws/bootstrap/main.tf | 70 ++--- deprecated/aws/bootstrap/outputs.tf | 2 +- deprecated/aws/bootstrap/variables.tf | 18 +- deprecated/aws/chamber/kms-key.tf | 12 +- deprecated/aws/chamber/main.tf | 10 +- deprecated/aws/chamber/s3-bucket.tf | 24 +- deprecated/aws/chamber/user.tf | 18 +- deprecated/aws/cis-aggregator-auth/main.tf | 20 +- .../aws/cis-aggregator-auth/variables.tf | 14 +- deprecated/aws/cis-aggregator/main.tf | 20 +- deprecated/aws/cis-aggregator/variables.tf | 18 +- deprecated/aws/cis-executor/main.tf | 10 +- deprecated/aws/cis-executor/variables.tf | 12 +- deprecated/aws/cis-instances/main.tf | 18 +- deprecated/aws/cis-instances/variables.tf | 16 +- deprecated/aws/cis/main.tf | 18 +- deprecated/aws/cis/output.tf | 6 +- deprecated/aws/cis/variables.tf | 16 +- deprecated/aws/cloudtrail/cloudwatch_logs.tf | 16 +- deprecated/aws/cloudtrail/main.tf | 28 +- .../aws/codefresh-onprem/kops-metadata.tf | 4 +- deprecated/aws/codefresh-onprem/main.tf | 150 +++++------ deprecated/aws/datadog/main.tf | 20 +- deprecated/aws/datadog/variables.tf | 2 +- deprecated/aws/docs/main.tf | 48 ++-- deprecated/aws/docs/outputs.tf | 38 +-- deprecated/aws/ecr/kops_ecr_app.tf | 16 +- deprecated/aws/ecr/kops_ecr_user.tf | 24 +- deprecated/aws/ecr/main.tf | 22 +- .../aws/eks-backing-services-peering/main.tf | 14 +- .../eks-backing-services-peering/outputs.tf | 4 +- .../eks-backing-services-peering/variables.tf | 12 +- deprecated/aws/eks/eks.tf | 104 +++---- deprecated/aws/eks/kubectl.tf | 18 +- deprecated/aws/eks/main.tf | 4 +- deprecated/aws/eks/outputs.tf | 48 ++-- deprecated/aws/eks/variables.tf | 44 +-- deprecated/aws/iam/audit.tf | 14 +- deprecated/aws/iam/corp.tf | 14 +- deprecated/aws/iam/data.tf | 14 +- deprecated/aws/iam/dev.tf | 14 +- deprecated/aws/iam/identity.tf | 14 +- deprecated/aws/iam/main.tf | 2 +- deprecated/aws/iam/prod.tf | 14 +- deprecated/aws/iam/security.tf | 14 +- deprecated/aws/iam/staging.tf | 14 +- deprecated/aws/iam/testing.tf | 14 +- deprecated/aws/iam/variables.tf | 8 +- .../keycloak-backing-services/aurora-mysql.tf | 156 +++++------ .../aws/keycloak-backing-services/main.tf | 24 +- deprecated/aws/kops-aws-platform/acm.tf | 10 +- .../aws/kops-aws-platform/alb-ingress.tf | 20 +- .../aws/kops-aws-platform/autoscaler-role.tf | 16 +- .../aws/kops-aws-platform/chart-repo.tf | 26 +- .../aws/kops-aws-platform/efs-provisioner.tf | 72 ++--- .../aws/kops-aws-platform/external-dns.tf | 20 +- deprecated/aws/kops-aws-platform/flow-logs.tf | 32 +-- .../kops-aws-platform/iam-authenticator.tf | 76 +++--- deprecated/aws/kops-aws-platform/main.tf | 8 +- deprecated/aws/kops-aws-platform/variables.tf | 22 +- deprecated/aws/kops-iam-users/corp.tf | 28 +- deprecated/aws/kops-iam-users/data.tf | 28 +- deprecated/aws/kops-iam-users/dev.tf | 28 +- deprecated/aws/kops-iam-users/main.tf | 2 +- deprecated/aws/kops-iam-users/prod.tf | 28 +- deprecated/aws/kops-iam-users/staging.tf | 28 +- deprecated/aws/kops-iam-users/testing.tf | 28 +- deprecated/aws/kops-iam-users/variables.tf | 12 +- .../kops-legacy-account-vpc-peering/main.tf | 26 +- .../outputs.tf | 8 +- .../variables.tf | 22 +- deprecated/aws/kops/main.tf | 162 +++++------ deprecated/aws/kops/outputs.tf | 34 +-- deprecated/aws/kops/subnets/main.tf | 2 +- deprecated/aws/kops/subnets/outputs.tf | 4 +- deprecated/aws/kops/variables.tf | 36 +-- deprecated/aws/organization/main.tf | 18 +- deprecated/aws/root-dns/main.tf | 6 +- deprecated/aws/root-dns/ns/main.tf | 40 +-- deprecated/aws/root-dns/ns/outputs.tf | 4 +- deprecated/aws/root-dns/ns/variables.tf | 16 +- deprecated/aws/root-dns/parent-alerts-ns.tf | 8 +- deprecated/aws/root-dns/parent-audit-ns.tf | 8 +- deprecated/aws/root-dns/parent-corp-ns.tf | 8 +- deprecated/aws/root-dns/parent-data-ns.tf | 8 +- deprecated/aws/root-dns/parent-dev-ns.tf | 8 +- deprecated/aws/root-dns/parent-identity-ns.tf | 8 +- deprecated/aws/root-dns/parent-local-ns.tf | 4 +- deprecated/aws/root-dns/parent-prod-ns.tf | 8 +- deprecated/aws/root-dns/parent-qa-ns.tf | 8 +- deprecated/aws/root-dns/parent-security-ns.tf | 8 +- deprecated/aws/root-dns/parent-staging-ns.tf | 8 +- deprecated/aws/root-dns/parent-testing-ns.tf | 8 +- deprecated/aws/root-dns/parent.tf | 12 +- deprecated/aws/root-dns/root.tf | 14 +- deprecated/aws/root-iam/main.tf | 2 +- deprecated/aws/root-iam/root.tf | 14 +- deprecated/aws/root-iam/variables.tf | 6 +- deprecated/aws/security-baseline/main.tf | 20 +- deprecated/aws/security-baseline/output.tf | 18 +- deprecated/aws/security-baseline/variables.tf | 12 +- deprecated/aws/ses/emails.tf | 2 +- deprecated/aws/ses/main.tf | 30 +-- deprecated/aws/slack-archive/main.tf | 48 ++-- deprecated/aws/slack-archive/outputs.tf | 38 +-- deprecated/aws/teleport/main.tf | 96 +++---- deprecated/aws/teleport/outputs.tf | 12 +- deprecated/aws/teleport/variables.tf | 34 +-- deprecated/aws/tfstate-backend/main.tf | 32 +-- deprecated/aws/tfstate-backend/outputs.tf | 12 +- deprecated/aws/users/main.tf | 16 +- deprecated/aws/users/variables.tf | 12 +- .../aws/vpc-peering-intra-account/main.tf | 30 +-- .../aws/vpc-peering-intra-account/outputs.tf | 4 +- .../vpc-peering-intra-account/variables.tf | 22 +- deprecated/aws/vpc-peering/main.tf | 46 ++-- deprecated/aws/vpc-peering/variables.tf | 12 +- deprecated/aws/vpc/main.tf | 92 +++---- deprecated/aws/vpc/outputs.tf | 20 +- deprecated/aws/vpc/variables.tf | 16 +- modules/ecs-service/README.md | 99 ++++--- modules/ecs-service/datadog-agent.tf | 163 +++++++++++ .../ecs-service/github-actions-iam-policy.tf | 58 ++++ .../github-actions-iam-role.mixin.tf | 72 +++++ modules/ecs-service/main.tf | 255 ++++++++++++++---- modules/ecs-service/outputs.tf | 22 +- modules/ecs-service/providers.tf | 14 +- modules/ecs-service/remote-state.tf | 151 +++-------- modules/ecs-service/variables.tf | 212 +++++++++++++-- modules/ecs-service/versions.tf | 2 +- modules/ecs/README.md | 31 ++- modules/ecs/main.tf | 143 +++++++--- modules/ecs/outputs.tf | 4 +- modules/ecs/remote-state.tf | 4 +- modules/ecs/variables.tf | 196 ++++++++++++++ modules/s3-bucket/README.md | 2 +- modules/ssm-parameters/README.md | 4 +- 178 files changed, 3154 insertions(+), 2302 deletions(-) create mode 100644 modules/ecs-service/datadog-agent.tf create mode 100644 modules/ecs-service/github-actions-iam-policy.tf create mode 100644 modules/ecs-service/github-actions-iam-role.mixin.tf diff --git a/deprecated/aws/account-dns/main.tf b/deprecated/aws/account-dns/main.tf index e0593acf7..2cfbba207 100644 --- a/deprecated/aws/account-dns/main.tf +++ b/deprecated/aws/account-dns/main.tf @@ -5,28 +5,28 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "domain_name" { - type = "string" + type = string description = "Domain name" } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } resource "aws_route53_zone" "dns_zone" { - name = "${var.domain_name}" + name = var.domain_name } resource "aws_route53_record" "dns_zone_soa" { allow_overwrite = true - zone_id = "${aws_route53_zone.dns_zone.id}" - name = "${aws_route53_zone.dns_zone.name}" + zone_id = aws_route53_zone.dns_zone.id + name = aws_route53_zone.dns_zone.name type = "SOA" ttl = "60" @@ -36,9 +36,9 @@ resource "aws_route53_record" "dns_zone_soa" { } output "zone_id" { - value = "${aws_route53_zone.dns_zone.zone_id}" + value = aws_route53_zone.dns_zone.zone_id } output "name_servers" { - value = "${aws_route53_zone.dns_zone.name_servers}" + value = aws_route53_zone.dns_zone.name_servers } diff --git a/deprecated/aws/account-settings/main.tf b/deprecated/aws/account-settings/main.tf index fe8b0e093..b9f077900 100644 --- a/deprecated/aws/account-settings/main.tf +++ b/deprecated/aws/account-settings/main.tf @@ -6,16 +6,16 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } module "account_settings" { source = "git::https://github.com/cloudposse/terraform-aws-iam-account-settings.git?ref=tags/0.1.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - enabled = "${var.enabled}" + namespace = var.namespace + stage = var.stage + name = var.name + enabled = var.enabled - minimum_password_length = "${var.minimum_password_length}" + minimum_password_length = var.minimum_password_length } diff --git a/deprecated/aws/account-settings/outputs.tf b/deprecated/aws/account-settings/outputs.tf index 2cf493c1d..c6066c2a6 100644 --- a/deprecated/aws/account-settings/outputs.tf +++ b/deprecated/aws/account-settings/outputs.tf @@ -1,11 +1,11 @@ output "account_alias" { - value = "${module.account_settings.account_alias}" + value = module.account_settings.account_alias } output "minimum_password_length" { - value = "${module.account_settings.minimum_password_length}" + value = module.account_settings.minimum_password_length } output "signin_url" { - value = "${module.account_settings.signin_url}" + value = module.account_settings.signin_url } diff --git a/deprecated/aws/account-settings/variables.tf b/deprecated/aws/account-settings/variables.tf index cd2917577..a41e9aba4 100644 --- a/deprecated/aws/account-settings/variables.tf +++ b/deprecated/aws/account-settings/variables.tf @@ -1,5 +1,5 @@ variable "minimum_password_length" { - type = "string" + type = string description = "Minimum number of characters allowed in an IAM user password. Integer between 6 and 128, per https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html" ## Same default as https://github.com/cloudposse/terraform-aws-iam-account-settings: @@ -7,21 +7,21 @@ variable "minimum_password_length" { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string description = "Application or solution name (e.g. `app`)" default = "account" } diff --git a/deprecated/aws/accounts/audit.tf b/deprecated/aws/accounts/audit.tf index d4622aa94..826f0bc74 100644 --- a/deprecated/aws/accounts/audit.tf +++ b/deprecated/aws/accounts/audit.tf @@ -1,21 +1,21 @@ module "audit" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "audit" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "audit_account_arn" { - value = "${module.audit.account_arn}" + value = module.audit.account_arn } output "audit_account_id" { - value = "${module.audit.account_id}" + value = module.audit.account_id } output "audit_organization_account_access_role" { - value = "${module.audit.organization_account_access_role}" + value = module.audit.organization_account_access_role } diff --git a/deprecated/aws/accounts/corp.tf b/deprecated/aws/accounts/corp.tf index fdff28f83..39875f236 100644 --- a/deprecated/aws/accounts/corp.tf +++ b/deprecated/aws/accounts/corp.tf @@ -1,21 +1,21 @@ module "corp" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "corp" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "corp_account_arn" { - value = "${module.corp.account_arn}" + value = module.corp.account_arn } output "corp_account_id" { - value = "${module.corp.account_id}" + value = module.corp.account_id } output "corp_organization_account_access_role" { - value = "${module.corp.organization_account_access_role}" + value = module.corp.organization_account_access_role } diff --git a/deprecated/aws/accounts/data.tf b/deprecated/aws/accounts/data.tf index e252d8380..707e25188 100644 --- a/deprecated/aws/accounts/data.tf +++ b/deprecated/aws/accounts/data.tf @@ -1,21 +1,21 @@ module "data" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "data" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "data_account_arn" { - value = "${module.data.account_arn}" + value = module.data.account_arn } output "data_account_id" { - value = "${module.data.account_id}" + value = module.data.account_id } output "data_organization_account_access_role" { - value = "${module.data.organization_account_access_role}" + value = module.data.organization_account_access_role } diff --git a/deprecated/aws/accounts/dev.tf b/deprecated/aws/accounts/dev.tf index ef8127b81..d2748d997 100644 --- a/deprecated/aws/accounts/dev.tf +++ b/deprecated/aws/accounts/dev.tf @@ -1,21 +1,21 @@ module "dev" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "dev" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "dev_account_arn" { - value = "${module.dev.account_arn}" + value = module.dev.account_arn } output "dev_account_id" { - value = "${module.dev.account_id}" + value = module.dev.account_id } output "dev_organization_account_access_role" { - value = "${module.dev.organization_account_access_role}" + value = module.dev.organization_account_access_role } diff --git a/deprecated/aws/accounts/identity.tf b/deprecated/aws/accounts/identity.tf index d0c22578b..0a250eba3 100644 --- a/deprecated/aws/accounts/identity.tf +++ b/deprecated/aws/accounts/identity.tf @@ -1,21 +1,21 @@ module "identity" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "identity" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "identity_account_arn" { - value = "${module.identity.account_arn}" + value = module.identity.account_arn } output "identity_account_id" { - value = "${module.identity.account_id}" + value = module.identity.account_id } output "identity_organization_account_access_role" { - value = "${module.identity.organization_account_access_role}" + value = module.identity.organization_account_access_role } diff --git a/deprecated/aws/accounts/main.tf b/deprecated/aws/accounts/main.tf index 50e8b0e91..4cd1dd90a 100644 --- a/deprecated/aws/accounts/main.tf +++ b/deprecated/aws/accounts/main.tf @@ -5,39 +5,39 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "account_role_name" { - type = "string" + type = string description = "IAM role that Organization automatically preconfigures in the new member account" default = "OrganizationAccountAccessRole" } variable "account_email" { - type = "string" + type = string description = "Email address format for accounts (e.g. `%s@cloudposse.co`)" } variable "account_iam_user_access_to_billing" { - type = "string" + type = string description = "If set to `ALLOW`, the new account enables IAM users to access account billing information if they have the required permissions. If set to `DENY`, then only the root user of the new account can access account billing information" default = "DENY" } variable "accounts_enabled" { - type = "list" + type = list(string) description = "Accounts to enable" default = ["dev", "staging", "prod", "testing", "audit"] } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } diff --git a/deprecated/aws/accounts/prod.tf b/deprecated/aws/accounts/prod.tf index e0f86f5a2..06e7c2bd3 100644 --- a/deprecated/aws/accounts/prod.tf +++ b/deprecated/aws/accounts/prod.tf @@ -1,21 +1,21 @@ module "prod" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "prod" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "prod_account_arn" { - value = "${module.prod.account_arn}" + value = module.prod.account_arn } output "prod_account_id" { - value = "${module.prod.account_id}" + value = module.prod.account_id } output "prod_organization_account_access_role" { - value = "${module.prod.organization_account_access_role}" + value = module.prod.organization_account_access_role } diff --git a/deprecated/aws/accounts/security.tf b/deprecated/aws/accounts/security.tf index ffe42ba1f..bb44d49fc 100644 --- a/deprecated/aws/accounts/security.tf +++ b/deprecated/aws/accounts/security.tf @@ -1,21 +1,21 @@ module "security" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "security" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "security_account_arn" { - value = "${module.security.account_arn}" + value = module.security.account_arn } output "security_account_id" { - value = "${module.security.account_id}" + value = module.security.account_id } output "security_organization_account_access_role" { - value = "${module.security.organization_account_access_role}" + value = module.security.organization_account_access_role } diff --git a/deprecated/aws/accounts/stage/main.tf b/deprecated/aws/accounts/stage/main.tf index 1cf66ff1a..3f8914dad 100644 --- a/deprecated/aws/accounts/stage/main.tf +++ b/deprecated/aws/accounts/stage/main.tf @@ -1,41 +1,41 @@ resource "aws_organizations_account" "default" { - count = "${local.count}" - name = "${var.stage}" - email = "${format(var.account_email, var.stage)}" - iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - role_name = "${var.account_role_name}" + count = local.count + name = var.stage + email = format(var.account_email, var.stage) + iam_user_access_to_billing = var.account_iam_user_access_to_billing + role_name = var.account_role_name } locals { - count = "${contains(var.accounts_enabled, var.stage) == true ? 1 : 0}" - account_arn = "${join("", aws_organizations_account.default.*.arn)}" - account_id = "${join("", aws_organizations_account.default.*.id)}" + count = contains(var.accounts_enabled, var.stage) == true ? 1 : 0 + account_arn = join("", aws_organizations_account.default.*.arn) + account_id = join("", aws_organizations_account.default.*.id) organization_account_access_role = "arn:aws:iam::${join("", aws_organizations_account.default.*.id)}:role/OrganizationAccountAccessRole" } resource "aws_ssm_parameter" "account_id" { - count = "${local.count}" + count = local.count name = "/${var.namespace}/${var.stage}/account_id" description = "AWS Account ID" type = "String" - value = "${local.account_id}" + value = local.account_id overwrite = "true" } resource "aws_ssm_parameter" "account_arn" { - count = "${local.count}" + count = local.count name = "/${var.namespace}/${var.stage}/account_arn" description = "AWS Account ARN" type = "String" - value = "${local.account_arn}" + value = local.account_arn overwrite = "true" } resource "aws_ssm_parameter" "organization_account_access_role" { - count = "${local.count}" + count = local.count name = "/${var.namespace}/${var.stage}/organization_account_access_role" description = "AWS Organization Account Access Role" type = "String" - value = "${local.organization_account_access_role}" + value = local.organization_account_access_role overwrite = "true" } diff --git a/deprecated/aws/accounts/stage/outputs.tf b/deprecated/aws/accounts/stage/outputs.tf index b37a2b9f9..31e4ab19e 100644 --- a/deprecated/aws/accounts/stage/outputs.tf +++ b/deprecated/aws/accounts/stage/outputs.tf @@ -1,11 +1,11 @@ output "account_arn" { - value = "${local.account_arn}" + value = local.account_arn } output "account_id" { - value = "${local.account_id}" + value = local.account_id } output "organization_account_access_role" { - value = "${local.organization_account_access_role}" + value = local.organization_account_access_role } diff --git a/deprecated/aws/accounts/stage/variables.tf b/deprecated/aws/accounts/stage/variables.tf index 35f50545f..d984e6225 100644 --- a/deprecated/aws/accounts/stage/variables.tf +++ b/deprecated/aws/accounts/stage/variables.tf @@ -1,29 +1,29 @@ variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `audit`)" } variable "account_role_name" { - type = "string" + type = string description = "IAM role that Organization automatically preconfigures in the new member account" } variable "account_email" { - type = "string" + type = string description = "Email address format for accounts (e.g. `%s@cloudposse.co`)" } variable "account_iam_user_access_to_billing" { - type = "string" + type = string description = "If set to `ALLOW`, the new account enables IAM users to access account billing information if they have the required permissions. If set to `DENY`, then only the root user of the new account can access account billing information" } variable "accounts_enabled" { - type = "list" + type = list(string) description = "Accounts to enable" } diff --git a/deprecated/aws/accounts/staging.tf b/deprecated/aws/accounts/staging.tf index d2d78c97c..76f348531 100644 --- a/deprecated/aws/accounts/staging.tf +++ b/deprecated/aws/accounts/staging.tf @@ -1,21 +1,21 @@ module "staging" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "staging" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "staging_account_arn" { - value = "${module.staging.account_arn}" + value = module.staging.account_arn } output "staging_account_id" { - value = "${module.staging.account_id}" + value = module.staging.account_id } output "staging_organization_account_access_role" { - value = "${module.staging.organization_account_access_role}" + value = module.staging.organization_account_access_role } diff --git a/deprecated/aws/accounts/testing.tf b/deprecated/aws/accounts/testing.tf index 89ba2e589..f250253aa 100644 --- a/deprecated/aws/accounts/testing.tf +++ b/deprecated/aws/accounts/testing.tf @@ -1,21 +1,21 @@ module "testing" { source = "stage" - namespace = "${var.namespace}" + namespace = var.namespace stage = "testing" - accounts_enabled = "${var.accounts_enabled}" - account_email = "${var.account_email}" - account_iam_user_access_to_billing = "${var.account_iam_user_access_to_billing}" - account_role_name = "${var.account_role_name}" + accounts_enabled = var.accounts_enabled + account_email = var.account_email + account_iam_user_access_to_billing = var.account_iam_user_access_to_billing + account_role_name = var.account_role_name } output "testing_account_arn" { - value = "${module.testing.account_arn}" + value = module.testing.account_arn } output "testing_account_id" { - value = "${module.testing.account_id}" + value = module.testing.account_id } output "testing_organization_account_access_role" { - value = "${module.testing.organization_account_access_role}" + value = module.testing.organization_account_access_role } diff --git a/deprecated/aws/acm-cloudfront/main.tf b/deprecated/aws/acm-cloudfront/main.tf index ca9a32b5f..86e9d9546 100644 --- a/deprecated/aws/acm-cloudfront/main.tf +++ b/deprecated/aws/acm-cloudfront/main.tf @@ -5,7 +5,7 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } provider "aws" { @@ -17,7 +17,7 @@ provider "aws" { region = "us-east-1" assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -27,24 +27,24 @@ variable "domain_name" { module "certificate" { source = "git::https://github.com/cloudposse/terraform-aws-acm-request-certificate.git?ref=tags/0.1.1" - domain_name = "${var.domain_name}" + domain_name = var.domain_name proces_domain_validation_options = "true" ttl = "300" subject_alternative_names = ["*.${var.domain_name}"] } output "certificate_domain_name" { - value = "${var.domain_name}" + value = var.domain_name } output "certificate_id" { - value = "${module.certificate.id}" + value = module.certificate.id } output "certificate_arn" { - value = "${module.certificate.arn}" + value = module.certificate.arn } output "certificate_domain_validation_options" { - value = "${module.certificate.domain_validation_options}" + value = module.certificate.domain_validation_options } diff --git a/deprecated/aws/acm-teleport/main.tf b/deprecated/aws/acm-teleport/main.tf index aa3ebfbf1..f21fbe6da 100644 --- a/deprecated/aws/acm-teleport/main.tf +++ b/deprecated/aws/acm-teleport/main.tf @@ -8,21 +8,21 @@ variable "aws_assume_role_arn" {} provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } module "certificate" { source = "git::https://github.com/cloudposse/terraform-aws-acm-request-certificate.git?ref=tags/0.1.1" - domain_name = "${var.domain_name}" + domain_name = var.domain_name proces_domain_validation_options = "true" ttl = "300" subject_alternative_names = ["*.${var.domain_name}"] } resource "aws_ssm_parameter" "certificate_arn_parameter" { - name = "${format(var.chamber_parameter_name, var.chamber_service, var.certificate_arn_parameter_name)}" - value = "${module.certificate.arn}" + name = format(var.chamber_parameter_name, var.chamber_service, var.certificate_arn_parameter_name) + value = module.certificate.arn description = "Teleport ACM-issued TLS Certificate AWS ARN" type = "String" overwrite = "true" diff --git a/deprecated/aws/acm-teleport/outputs.tf b/deprecated/aws/acm-teleport/outputs.tf index 1b6994fcb..5ba8c2564 100644 --- a/deprecated/aws/acm-teleport/outputs.tf +++ b/deprecated/aws/acm-teleport/outputs.tf @@ -1,15 +1,15 @@ output "certificate_domain_name" { - value = "${var.domain_name}" + value = var.domain_name } output "certificate_id" { - value = "${module.certificate.id}" + value = module.certificate.id } output "certificate_arn" { - value = "${module.certificate.arn}" + value = module.certificate.arn } output "certificate_domain_validation_options" { - value = "${module.certificate.domain_validation_options}" + value = module.certificate.domain_validation_options } diff --git a/deprecated/aws/artifacts/main.tf b/deprecated/aws/artifacts/main.tf index bee2adc84..6fe5c7261 100644 --- a/deprecated/aws/artifacts/main.tf +++ b/deprecated/aws/artifacts/main.tf @@ -9,14 +9,14 @@ provider "aws" { region = "us-east-1" assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } # https://www.terraform.io/artifacts/providers/aws/d/acm_certificate.html data "aws_acm_certificate" "acm_cloudfront_certificate" { provider = "aws.virginia" - domain = "${var.domain_name}" + domain = var.domain_name statuses = ["ISSUED"] types = ["AMAZON_ISSUED"] } @@ -29,19 +29,19 @@ locals { module "artifacts_user" { source = "git::https://github.com/cloudposse/terraform-aws-iam-system-user.git?ref=tags/0.2.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" + namespace = var.namespace + stage = var.stage + name = local.name } module "origin" { source = "git::https://github.com/cloudposse/terraform-aws-s3-website.git?ref=tags/0.5.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" - hostname = "${local.cdn_domain}" - parent_zone_name = "${var.domain_name}" - region = "${var.region}" + namespace = var.namespace + stage = var.stage + name = local.name + hostname = local.cdn_domain + parent_zone_name = var.domain_name + region = var.region cors_allowed_headers = ["*"] cors_allowed_methods = ["GET"] cors_allowed_origins = ["*"] @@ -66,14 +66,14 @@ module "origin" { # CloudFront CDN fronting origin module "cdn" { source = "git::https://github.com/cloudposse/terraform-aws-cloudfront-cdn.git?ref=tags/0.5.7" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" + namespace = var.namespace + stage = var.stage + name = local.name aliases = ["${local.cdn_domain}", "artifacts.cloudposse.com"] - origin_domain_name = "${module.origin.s3_bucket_website_endpoint}" + origin_domain_name = module.origin.s3_bucket_website_endpoint origin_protocol_policy = "http-only" viewer_protocol_policy = "redirect-to-https" - parent_zone_name = "${var.domain_name}" + parent_zone_name = var.domain_name forward_cookies = "none" forward_headers = ["Origin", "Access-Control-Request-Headers", "Access-Control-Request-Method"] default_ttl = 60 @@ -84,5 +84,5 @@ module "cdn" { allowed_methods = ["GET", "HEAD", "OPTIONS"] price_class = "PriceClass_All" default_root_object = "index.html" - acm_certificate_arn = "${data.aws_acm_certificate.acm_cloudfront_certificate.arn}" + acm_certificate_arn = data.aws_acm_certificate.acm_cloudfront_certificate.arn } diff --git a/deprecated/aws/artifacts/outputs.tf b/deprecated/aws/artifacts/outputs.tf index 17b6f495e..9c8816058 100644 --- a/deprecated/aws/artifacts/outputs.tf +++ b/deprecated/aws/artifacts/outputs.tf @@ -1,94 +1,94 @@ output "artifacts_user_name" { - value = "${module.artifacts_user.user_name}" + value = module.artifacts_user.user_name description = "Normalized IAM user name" } output "artifacts_user_arn" { - value = "${module.artifacts_user.user_arn}" + value = module.artifacts_user.user_arn description = "The ARN assigned by AWS for the user" } output "artifacts_user_unique_id" { - value = "${module.artifacts_user.user_unique_id}" + value = module.artifacts_user.user_unique_id description = "The user unique ID assigned by AWS" } output "artifacts_user_access_key_id" { - value = "${module.artifacts_user.access_key_id}" + value = module.artifacts_user.access_key_id description = "The access key ID" } output "artifacts_user_secret_access_key" { - value = "${module.artifacts_user.secret_access_key}" + value = module.artifacts_user.secret_access_key description = "The secret access key. This will be written to the state file in plain-text" } output "artifacts_s3_bucket_name" { - value = "${module.origin.s3_bucket_name}" + value = module.origin.s3_bucket_name description = "The S3 bucket which serves as the origin for the CDN and S3 website" } output "artifacts_s3_bucket_domain_name" { - value = "${module.origin.s3_bucket_domain_name}" + value = module.origin.s3_bucket_domain_name description = "The bucket domain name. Will be of format bucketname.s3.amazonaws.com." } output "artifacts_s3_bucket_arn" { - value = "${module.origin.s3_bucket_arn}" + value = module.origin.s3_bucket_arn description = "The ARN of the bucket. Will be of format arn:aws:s3:::bucketname." } output "artifacts_s3_bucket_website_endpoint" { - value = "${module.origin.s3_bucket_website_endpoint}" + value = module.origin.s3_bucket_website_endpoint description = "The website endpoint, if the bucket is configured with a website. If not, this will be an empty string." } output "artifacts_s3_bucket_website_domain" { - value = "${module.origin.s3_bucket_website_domain}" + value = module.origin.s3_bucket_website_domain description = "The domain of the website endpoint, if the bucket is configured with a website. If not, this will be an empty string. This is used to create Route 53 alias records." } output "artifacts_s3_bucket_hosted_zone_id" { - value = "${module.origin.s3_bucket_hosted_zone_id}" + value = module.origin.s3_bucket_hosted_zone_id description = "The Route 53 Hosted Zone ID for this bucket's region." } output "artifacts_cloudfront_id" { - value = "${module.cdn.cf_id}" + value = module.cdn.cf_id description = "The identifier for the distribution. For example: EDFDVBD632BHDS5." } output "artifacts_cloudfront_arn" { - value = "${module.cdn.cf_arn}" + value = module.cdn.cf_arn description = "The ARN (Amazon Resource Name) for the distribution. For example: arn:aws:cloudfront::123456789012:distribution/EDFDVBD632BHDS5, where 123456789012 is your AWS account ID." } output "artifacts_cloudfront_aliases" { - value = "${module.cdn.cf_aliases}" + value = module.cdn.cf_aliases description = "Extra CNAMEs (alternate domain names), if any, for this distribution." } output "artifacts_cloudfront_status" { - value = "${module.cdn.cf_status}" + value = module.cdn.cf_status description = "The current status of the distribution. Deployed if the distribution's information is fully propagated throughout the Amazon CloudFront system." } output "artifacts_cloudfront_domain_name" { - value = "${module.cdn.cf_domain_name}" + value = module.cdn.cf_domain_name description = "The domain name corresponding to the distribution. For example: d604721fxaaqy9.cloudfront.net." } output "artifacts_cloudfront_etag" { - value = "${module.cdn.cf_etag}" + value = module.cdn.cf_etag description = "The current version of the distribution's information. For example: E2QWRUHAPOMQZL." } output "artifacts_cloudfront_hosted_zone_id" { - value = "${module.cdn.cf_hosted_zone_id}" + value = module.cdn.cf_hosted_zone_id description = "The CloudFront Route 53 zone ID that can be used to route an Alias Resource Record Set to. This attribute is simply an alias for the zone ID Z2FDTNDATAQYW2." } output "artifacts_cloudfront_origin_access_identity_path" { - value = "${module.cdn.cf_origin_access_identity}" + value = module.cdn.cf_origin_access_identity description = "The CloudFront origin access identity to associate with the origin." } diff --git a/deprecated/aws/artifacts/variables.tf b/deprecated/aws/artifacts/variables.tf index 7d5d33392..c0c635312 100644 --- a/deprecated/aws/artifacts/variables.tf +++ b/deprecated/aws/artifacts/variables.tf @@ -1,28 +1,28 @@ variable "aws_assume_role_arn" { - type = "string" + type = string description = "The ARN of the role to assume" } variable "domain_name" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "region" { - type = "string" + type = string description = "AWS region" } variable "aws_account_id" { - type = "string" + type = string description = "AWS account ID" } diff --git a/deprecated/aws/audit-cloudtrail/cloudwatch_logs.tf b/deprecated/aws/audit-cloudtrail/cloudwatch_logs.tf index a963ad9d6..23135b8df 100644 --- a/deprecated/aws/audit-cloudtrail/cloudwatch_logs.tf +++ b/deprecated/aws/audit-cloudtrail/cloudwatch_logs.tf @@ -1,11 +1,11 @@ module "logs" { source = "git::https://github.com/cloudposse/terraform-aws-cloudwatch-logs.git?ref=tags/0.3.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name attributes = ["cloudwatch", "logs"] - retention_in_days = "${var.cloudwatch_logs_retention_in_days}" + retention_in_days = var.cloudwatch_logs_retention_in_days principals = { Service = ["cloudtrail.amazonaws.com"] @@ -18,16 +18,16 @@ module "logs" { module "kms_key_logs" { source = "git::https://github.com/cloudposse/terraform-aws-kms-key.git?ref=tags/0.1.3" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" + namespace = var.namespace + name = var.name + stage = var.stage attributes = ["cloudwatch", "logs"] description = "KMS key for CloudWatch" deletion_window_in_days = 10 enable_key_rotation = "true" - policy = "${data.aws_iam_policy_document.kms_key_logs.json}" + policy = data.aws_iam_policy_document.kms_key_logs.json } data "aws_iam_policy_document" "kms_key_logs" { diff --git a/deprecated/aws/audit-cloudtrail/main.tf b/deprecated/aws/audit-cloudtrail/main.tf index 93847f9a5..6ac268b0e 100644 --- a/deprecated/aws/audit-cloudtrail/main.tf +++ b/deprecated/aws/audit-cloudtrail/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -15,35 +15,35 @@ data "aws_caller_identity" "default" {} data "aws_region" "default" {} locals { - region = "${length(var.region) > 0 ? var.region : data.aws_region.default.name}" + region = length(var.region) > 0 ? var.region : data.aws_region.default.name } module "cloudtrail" { source = "git::https://github.com/cloudposse/terraform-aws-cloudtrail.git?ref=tags/0.7.1" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name enable_logging = "true" enable_log_file_validation = "true" include_global_service_events = "true" is_multi_region_trail = "true" - s3_bucket_name = "${module.cloudtrail_s3_bucket.bucket_id}" - kms_key_arn = "${module.kms_key_cloudtrail.alias_arn}" - cloud_watch_logs_group_arn = "${module.logs.log_group_arn}" - cloud_watch_logs_role_arn = "${module.logs.role_arn}" + s3_bucket_name = module.cloudtrail_s3_bucket.bucket_id + kms_key_arn = module.kms_key_cloudtrail.alias_arn + cloud_watch_logs_group_arn = module.logs.log_group_arn + cloud_watch_logs_role_arn = module.logs.role_arn } module "kms_key_cloudtrail" { source = "git::https://github.com/cloudposse/terraform-aws-kms-key.git?ref=tags/0.1.3" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" + namespace = var.namespace + name = var.name + stage = var.stage description = "KMS key for CloudTrail" deletion_window_in_days = 10 enable_key_rotation = "true" - policy = "${data.aws_iam_policy_document.kms_key_cloudtrail.json}" + policy = data.aws_iam_policy_document.kms_key_cloudtrail.json } data "aws_iam_policy_document" "kms_key_cloudtrail" { diff --git a/deprecated/aws/audit-cloudtrail/output.tf b/deprecated/aws/audit-cloudtrail/output.tf index 81ae5425f..74d69d677 100644 --- a/deprecated/aws/audit-cloudtrail/output.tf +++ b/deprecated/aws/audit-cloudtrail/output.tf @@ -1,15 +1,15 @@ output "cloudtrail_kms_key_arn" { - value = "${module.kms_key_cloudtrail.alias_arn}" + value = module.kms_key_cloudtrail.alias_arn } output "cloudtrail_bucket_domain_name" { - value = "${module.cloudtrail_s3_bucket.bucket_domain_name}" + value = module.cloudtrail_s3_bucket.bucket_domain_name } output "cloudtrail_bucket_id" { - value = "${module.cloudtrail_s3_bucket.bucket_id}" + value = module.cloudtrail_s3_bucket.bucket_id } output "cloudtrail_bucket_arn" { - value = "${module.cloudtrail_s3_bucket.bucket_arn}" + value = module.cloudtrail_s3_bucket.bucket_arn } diff --git a/deprecated/aws/audit-cloudtrail/s3_bucket.tf b/deprecated/aws/audit-cloudtrail/s3_bucket.tf index 9f781322e..ffe61e47a 100644 --- a/deprecated/aws/audit-cloudtrail/s3_bucket.tf +++ b/deprecated/aws/audit-cloudtrail/s3_bucket.tf @@ -1,21 +1,21 @@ module "cloudtrail_s3_bucket" { source = "git::https://github.com/cloudposse/terraform-aws-cloudtrail-s3-bucket.git?ref=tags/0.3.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - region = "${local.region}" + namespace = var.namespace + stage = var.stage + name = var.name + region = local.region sse_algorithm = "aws:kms" - kms_master_key_arn = "${module.kms_key_s3_bucket.alias_arn}" + kms_master_key_arn = module.kms_key_s3_bucket.alias_arn access_logs_sse_algorithm = "aws:kms" - access_logs_kms_master_key_arn = "${module.kms_key_s3_bucket_logs.alias_arn}" + access_logs_kms_master_key_arn = module.kms_key_s3_bucket_logs.alias_arn } module "kms_key_s3_bucket" { source = "git::https://github.com/cloudposse/terraform-aws-kms-key.git?ref=tags/0.1.3" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" + namespace = var.namespace + name = var.name + stage = var.stage attributes = ["cloudtrail", "s3", "bucket"] @@ -26,9 +26,9 @@ module "kms_key_s3_bucket" { module "kms_key_s3_bucket_logs" { source = "git::https://github.com/cloudposse/terraform-aws-kms-key.git?ref=tags/0.1.3" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" + namespace = var.namespace + name = var.name + stage = var.stage attributes = ["cloudtrail", "s3", "bucket", "logs"] diff --git a/deprecated/aws/audit-cloudtrail/varaibles.tf b/deprecated/aws/audit-cloudtrail/varaibles.tf index cca94fffb..c3fc177be 100644 --- a/deprecated/aws/audit-cloudtrail/varaibles.tf +++ b/deprecated/aws/audit-cloudtrail/varaibles.tf @@ -1,26 +1,26 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `audit`)" default = "audit" } variable "name" { - type = "string" + type = string description = "Name (e.g. `account`)" default = "account" } variable "region" { - type = "string" + type = string description = "AWS region" default = "" } diff --git a/deprecated/aws/aws-metrics-role/main.tf b/deprecated/aws/aws-metrics-role/main.tf index 361b38918..f9a6dd53f 100644 --- a/deprecated/aws/aws-metrics-role/main.tf +++ b/deprecated/aws/aws-metrics-role/main.tf @@ -57,8 +57,8 @@ data "aws_iam_policy_document" "assume_role" { } resource "aws_iam_role_policy_attachment" "default" { - role = "${aws_iam_role.default.name}" - policy_arn = "${aws_iam_policy.default.arn}" + role = aws_iam_role.default.name + policy_arn = aws_iam_policy.default.arn lifecycle { create_before_destroy = true @@ -66,9 +66,9 @@ resource "aws_iam_role_policy_attachment" "default" { } resource "aws_iam_policy" "default" { - name = "${module.label.id}" + name = module.label.id description = "Grant permissions for external-dns" - policy = "${data.aws_iam_policy_document.default.json}" + policy = data.aws_iam_policy_document.default.json } data "aws_iam_policy_document" "default" { diff --git a/deprecated/aws/aws-metrics-role/variables.tf b/deprecated/aws/aws-metrics-role/variables.tf index b55d3f4ce..23c98dbf0 100644 --- a/deprecated/aws/aws-metrics-role/variables.tf +++ b/deprecated/aws/aws-metrics-role/variables.tf @@ -43,7 +43,7 @@ variable "kops_cluster_name" { } variable "assume_role_permitted_roles" { - type = "string" + type = string description = "Roles that are permitted to assume thie role. One of 'kiam', 'nodes', 'masters', or 'both' (nodes + masters)." default = "kiam" } diff --git a/deprecated/aws/backing-services/aurora-mysql.tf b/deprecated/aws/backing-services/aurora-mysql.tf index 67752a30b..95c167ae7 100644 --- a/deprecated/aws/backing-services/aurora-mysql.tf +++ b/deprecated/aws/backing-services/aurora-mysql.tf @@ -1,44 +1,44 @@ # https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBInstance.html variable "mysql_name" { - type = "string" + type = string description = "Name of the application, e.g. `app` or `analytics`" default = "mysql" } variable "mysql_admin_user" { - type = "string" + type = string description = "MySQL admin user name" default = "" } variable "mysql_admin_password" { - type = "string" + type = string description = "MySQL password for the admin user" default = "" } variable "mysql_db_name" { - type = "string" + type = string description = "MySQL database name" default = "" } # https://aws.amazon.com/rds/aurora/pricing variable "mysql_instance_type" { - type = "string" + type = string default = "db.t2.small" description = "EC2 instance type for Aurora MySQL cluster" } variable "mysql_cluster_size" { - type = "string" + type = string default = "2" description = "MySQL cluster size" } variable "mysql_cluster_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } @@ -49,134 +49,134 @@ variable "mysql_cluster_publicly_accessible" { } variable "mysql_cluster_allowed_cidr_blocks" { - type = "list" + type = list(string) default = ["0.0.0.0/0"] description = "List of CIDR blocks allowed to access the cluster" } resource "random_pet" "mysql_db_name" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" + count = local.mysql_cluster_enabled ? 1 : 0 separator = "_" } resource "random_string" "mysql_admin_user" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" + count = local.mysql_cluster_enabled ? 1 : 0 length = 8 number = false special = false } resource "random_string" "mysql_admin_password" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" + count = local.mysql_cluster_enabled ? 1 : 0 length = 16 special = true } locals { - mysql_cluster_enabled = "${var.mysql_cluster_enabled == "true"}" - mysql_admin_user = "${length(var.mysql_admin_user) > 0 ? var.mysql_admin_user : join("", random_string.mysql_admin_user.*.result)}" - mysql_admin_password = "${length(var.mysql_admin_password) > 0 ? var.mysql_admin_password : join("", random_string.mysql_admin_password.*.result)}" - mysql_db_name = "${join("", random_pet.mysql_db_name.*.id)}" + mysql_cluster_enabled = var.mysql_cluster_enabled == "true" + mysql_admin_user = length(var.mysql_admin_user) > 0 ? var.mysql_admin_user : join("", random_string.mysql_admin_user.*.result) + mysql_admin_password = length(var.mysql_admin_password) > 0 ? var.mysql_admin_password : join("", random_string.mysql_admin_password.*.result) + mysql_db_name = join("", random_pet.mysql_db_name.*.id) } module "aurora_mysql" { source = "git::https://github.com/cloudposse/terraform-aws-rds-cluster.git?ref=tags/0.8.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.mysql_name}" + namespace = var.namespace + stage = var.stage + name = var.mysql_name engine = "aurora-mysql" cluster_family = "aurora-mysql5.7" - instance_type = "${var.mysql_instance_type}" - cluster_size = "${var.mysql_cluster_size}" - admin_user = "${local.mysql_admin_user}" - admin_password = "${local.mysql_admin_password}" - db_name = "${local.mysql_db_name}" + instance_type = var.mysql_instance_type + cluster_size = var.mysql_cluster_size + admin_user = local.mysql_admin_user + admin_password = local.mysql_admin_password + db_name = local.mysql_db_name db_port = "3306" - vpc_id = "${module.vpc.vpc_id}" + vpc_id = module.vpc.vpc_id # Use module.subnets.private_subnet_ids if the cluster does not need to be publicly accessible subnets = ["${module.subnets.public_subnet_ids}"] - zone_id = "${local.zone_id}" - enabled = "${var.mysql_cluster_enabled}" - publicly_accessible = "${var.mysql_cluster_publicly_accessible}" - allowed_cidr_blocks = "${var.mysql_cluster_allowed_cidr_blocks}" + zone_id = local.zone_id + enabled = var.mysql_cluster_enabled + publicly_accessible = var.mysql_cluster_publicly_accessible + allowed_cidr_blocks = var.mysql_cluster_allowed_cidr_blocks } resource "aws_ssm_parameter" "aurora_mysql_database_name" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_database_name")}" - value = "${module.aurora_mysql.name}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_database_name") + value = module.aurora_mysql.name description = "Aurora MySQL Database Name" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_master_username" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_master_username")}" - value = "${module.aurora_mysql.user}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_master_username") + value = module.aurora_mysql.user description = "Aurora MySQL Username for the master DB user" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_master_password" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_master_password")}" - value = "${module.aurora_mysql.password}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_master_password") + value = module.aurora_mysql.password description = "Aurora MySQL Password for the master DB user" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_master_hostname" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_master_hostname")}" - value = "${module.aurora_mysql.master_host}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_master_hostname") + value = module.aurora_mysql.master_host description = "Aurora MySQL DB Master hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_replicas_hostname" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_replicas_hostname")}" - value = "${module.aurora_mysql.replicas_host}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_replicas_hostname") + value = module.aurora_mysql.replicas_host description = "Aurora MySQL DB Replicas hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_cluster_name" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_cluster_name")}" - value = "${module.aurora_mysql.cluster_name}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_mysql_cluster_name") + value = module.aurora_mysql.cluster_name description = "Aurora MySQL DB Cluster Identifier" type = "String" overwrite = "true" } output "aurora_mysql_database_name" { - value = "${module.aurora_mysql.name}" + value = module.aurora_mysql.name description = "Aurora MySQL Database name" } output "aurora_mysql_master_username" { - value = "${module.aurora_mysql.user}" + value = module.aurora_mysql.user description = "Aurora MySQL Username for the master DB user" } output "aurora_mysql_master_hostname" { - value = "${module.aurora_mysql.master_host}" + value = module.aurora_mysql.master_host description = "Aurora MySQL DB Master hostname" } output "aurora_mysql_replicas_hostname" { - value = "${module.aurora_mysql.replicas_host}" + value = module.aurora_mysql.replicas_host description = "Aurora MySQL Replicas hostname" } output "aurora_mysql_cluster_name" { - value = "${module.aurora_mysql.cluster_name}" + value = module.aurora_mysql.cluster_name description = "Aurora MySQL Cluster Identifier" } diff --git a/deprecated/aws/backing-services/aurora-postgres-replica.tf b/deprecated/aws/backing-services/aurora-postgres-replica.tf index 3657fee85..3c55c7b84 100644 --- a/deprecated/aws/backing-services/aurora-postgres-replica.tf +++ b/deprecated/aws/backing-services/aurora-postgres-replica.tf @@ -1,5 +1,5 @@ variable "postgres_replica_name" { - type = "string" + type = string description = "Name of the replica, e.g. `postgres` or `reporting`" default = "postgres" } @@ -7,75 +7,75 @@ variable "postgres_replica_name" { # db.r4.large is the smallest instance type supported by Aurora Postgres # https://aws.amazon.com/rds/aurora/pricing variable "postgres_replica_instance_type" { - type = "string" + type = string default = "db.r4.large" description = "EC2 instance type for Postgres cluster" } variable "postgres_replica_cluster_size" { - type = "string" + type = string default = "2" description = "Postgres cluster size" } variable "postgres_replica_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } variable "postgres_replica_cluster_identifier" { - type = "string" + type = string description = "The cluster identifier" default = "" } locals { - postgres_replica_enabled = "${var.postgres_replica_enabled == "true"}" + postgres_replica_enabled = var.postgres_replica_enabled == "true" } module "postgres_replica" { source = "git::https://github.com/cloudposse/terraform-aws-rds-cluster-instance-group.git?ref=tags/0.1.0" - enabled = "${var.postgres_replica_enabled}" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.postgres_replica_name}" - cluster_identifier = "${var.postgres_replica_cluster_identifier}" + enabled = var.postgres_replica_enabled + namespace = var.namespace + stage = var.stage + name = var.postgres_replica_name + cluster_identifier = var.postgres_replica_cluster_identifier cluster_family = "aurora-postgresql9.6" engine = "aurora-postgresql" - instance_type = "${var.postgres_replica_instance_type}" - cluster_size = "${var.postgres_replica_cluster_size}" + instance_type = var.postgres_replica_instance_type + cluster_size = var.postgres_replica_cluster_size db_port = "5432" - vpc_id = "${module.vpc.vpc_id}" + vpc_id = module.vpc.vpc_id subnets = ["${module.subnets.private_subnet_ids}"] - zone_id = "${local.zone_id}" + zone_id = local.zone_id security_groups = ["${module.kops_metadata.nodes_security_group_id}"] } resource "aws_ssm_parameter" "postgres_replica_hostname" { - count = "${local.postgres_replica_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "postgres_replica_hostname")}" - value = "${module.postgres_replica.hostname}" + count = local.postgres_replica_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "postgres_replica_hostname") + value = module.postgres_replica.hostname description = "RDS Cluster replica hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "postgres_replica_endpoint" { - count = "${local.postgres_replica_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "postgres_replica_endpoint")}" - value = "${module.postgres_replica.endpoint}" + count = local.postgres_replica_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "postgres_replica_endpoint") + value = module.postgres_replica.endpoint description = "RDS Cluster Replicas hostname" type = "String" overwrite = "true" } output "postgres_replica_hostname" { - value = "${module.postgres_replica.hostname}" + value = module.postgres_replica.hostname description = "RDS Cluster replica hostname" } output "postgres_replica_endpoint" { - value = "${module.postgres_replica.endpoint}" + value = module.postgres_replica.endpoint description = "RDS Cluster replica endpoint" } diff --git a/deprecated/aws/backing-services/aurora-postgres.tf b/deprecated/aws/backing-services/aurora-postgres.tf index 39f2f7f45..32af7164d 100644 --- a/deprecated/aws/backing-services/aurora-postgres.tf +++ b/deprecated/aws/backing-services/aurora-postgres.tf @@ -1,5 +1,5 @@ variable "postgres_name" { - type = "string" + type = string description = "Name of the application, e.g. `app` or `analytics`" default = "postgres" } @@ -8,7 +8,7 @@ variable "postgres_name" { # Read more: # ("MasterUsername admin cannot be used as it is a reserved word used by the engine") variable "postgres_admin_user" { - type = "string" + type = string description = "Postgres admin user name" default = "" } @@ -17,13 +17,13 @@ variable "postgres_admin_user" { # Read more: # ("The parameter MasterUserPassword is not a valid password because it is shorter than 8 characters") variable "postgres_admin_password" { - type = "string" + type = string description = "Postgres password for the admin user" default = "" } variable "postgres_db_name" { - type = "string" + type = string description = "Postgres database name" default = "" } @@ -31,151 +31,151 @@ variable "postgres_db_name" { # db.r4.large is the smallest instance type supported by Aurora Postgres # https://aws.amazon.com/rds/aurora/pricing variable "postgres_instance_type" { - type = "string" + type = string default = "db.r4.large" description = "EC2 instance type for Postgres cluster" } variable "postgres_cluster_size" { - type = "string" + type = string default = "2" description = "Postgres cluster size" } variable "postgres_cluster_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } variable "postgres_iam_database_authentication_enabled" { - type = "string" + type = string default = "false" description = "Specifies whether or mappings of AWS Identity and Access Management (IAM) accounts to database accounts is enabled." } resource "random_pet" "postgres_db_name" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" + count = local.postgres_cluster_enabled ? 1 : 0 separator = "_" } resource "random_string" "postgres_admin_user" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" + count = local.postgres_cluster_enabled ? 1 : 0 length = 8 special = false number = false } resource "random_string" "postgres_admin_password" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" + count = local.postgres_cluster_enabled ? 1 : 0 length = 16 special = true } locals { - postgres_cluster_enabled = "${var.postgres_cluster_enabled == "true"}" - postgres_admin_user = "${length(var.postgres_admin_user) > 0 ? var.postgres_admin_user : join("", random_string.postgres_admin_user.*.result)}" - postgres_admin_password = "${length(var.postgres_admin_password) > 0 ? var.postgres_admin_password : join("", random_string.postgres_admin_password.*.result)}" - postgres_db_name = "${join("", random_pet.postgres_db_name.*.id)}" + postgres_cluster_enabled = var.postgres_cluster_enabled == "true" + postgres_admin_user = length(var.postgres_admin_user) > 0 ? var.postgres_admin_user : join("", random_string.postgres_admin_user.*.result) + postgres_admin_password = length(var.postgres_admin_password) > 0 ? var.postgres_admin_password : join("", random_string.postgres_admin_password.*.result) + postgres_db_name = join("", random_pet.postgres_db_name.*.id) } module "aurora_postgres" { source = "git::https://github.com/cloudposse/terraform-aws-rds-cluster.git?ref=tags/0.8.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.postgres_name}" + namespace = var.namespace + stage = var.stage + name = var.postgres_name engine = "aurora-postgresql" cluster_family = "aurora-postgresql9.6" - instance_type = "${var.postgres_instance_type}" - cluster_size = "${var.postgres_cluster_size}" - admin_user = "${local.postgres_admin_user}" - admin_password = "${local.postgres_admin_password}" - db_name = "${local.postgres_db_name}" + instance_type = var.postgres_instance_type + cluster_size = var.postgres_cluster_size + admin_user = local.postgres_admin_user + admin_password = local.postgres_admin_password + db_name = local.postgres_db_name db_port = "5432" - vpc_id = "${module.vpc.vpc_id}" + vpc_id = module.vpc.vpc_id subnets = ["${module.subnets.private_subnet_ids}"] - zone_id = "${local.zone_id}" + zone_id = local.zone_id security_groups = ["${module.kops_metadata.nodes_security_group_id}"] - enabled = "${var.postgres_cluster_enabled}" + enabled = var.postgres_cluster_enabled - iam_database_authentication_enabled = "${var.postgres_iam_database_authentication_enabled}" + iam_database_authentication_enabled = var.postgres_iam_database_authentication_enabled } resource "aws_ssm_parameter" "aurora_postgres_database_name" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_database_name")}" - value = "${module.aurora_postgres.name}" + count = local.postgres_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_database_name") + value = module.aurora_postgres.name description = "Aurora Postgres Database Name" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_postgres_master_username" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_master_username")}" - value = "${module.aurora_postgres.user}" + count = local.postgres_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_master_username") + value = module.aurora_postgres.user description = "Aurora Postgres Username for the master DB user" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_postgres_master_password" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_master_password")}" - value = "${module.aurora_postgres.password}" + count = local.postgres_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_master_password") + value = module.aurora_postgres.password description = "Aurora Postgres Password for the master DB user" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_postgres_master_hostname" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_master_hostname")}" - value = "${module.aurora_postgres.master_host}" + count = local.postgres_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_master_hostname") + value = module.aurora_postgres.master_host description = "Aurora Postgres DB Master hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_postgres_replicas_hostname" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_replicas_hostname")}" - value = "${module.aurora_postgres.replicas_host}" + count = local.postgres_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_replicas_hostname") + value = module.aurora_postgres.replicas_host description = "Aurora Postgres DB Replicas hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_postgres_cluster_name" { - count = "${local.postgres_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_cluster_name")}" - value = "${module.aurora_postgres.cluster_name}" + count = local.postgres_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "aurora_postgres_cluster_name") + value = module.aurora_postgres.cluster_name description = "Aurora Postgres DB Cluster Identifier" type = "String" overwrite = "true" } output "aurora_postgres_database_name" { - value = "${module.aurora_postgres.name}" + value = module.aurora_postgres.name description = "Aurora Postgres Database name" } output "aurora_postgres_master_username" { - value = "${module.aurora_postgres.user}" + value = module.aurora_postgres.user description = "Aurora Postgres Username for the master DB user" } output "aurora_postgres_master_hostname" { - value = "${module.aurora_postgres.master_host}" + value = module.aurora_postgres.master_host description = "Aurora Postgres DB Master hostname" } output "aurora_postgres_replicas_hostname" { - value = "${module.aurora_postgres.replicas_host}" + value = module.aurora_postgres.replicas_host description = "Aurora Postgres Replicas hostname" } output "aurora_postgres_cluster_name" { - value = "${module.aurora_postgres.cluster_name}" + value = module.aurora_postgres.cluster_name description = "Aurora Postgres Cluster Identifier" } diff --git a/deprecated/aws/backing-services/elasticache-redis.tf b/deprecated/aws/backing-services/elasticache-redis.tf index fd57118f2..bf129c2dc 100644 --- a/deprecated/aws/backing-services/elasticache-redis.tf +++ b/deprecated/aws/backing-services/elasticache-redis.tf @@ -1,58 +1,58 @@ variable "redis_name" { - type = "string" + type = string default = "redis" description = "Redis name" } variable "redis_instance_type" { - type = "string" + type = string default = "cache.t2.medium" description = "EC2 instance type for Redis cluster" } variable "redis_cluster_size" { - type = "string" + type = string default = "2" description = "Redis cluster size" } variable "redis_cluster_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } variable "redis_auth_token" { - type = "string" + type = string default = "" description = "Auth token for password protecting redis, transit_encryption_enabled must be set to 'true'! Password must be longer than 16 chars" } variable "redis_transit_encryption_enabled" { - type = "string" + type = string default = "true" description = "Enable TLS" } variable "redis_params" { - type = "list" + type = list(string) default = [] description = "A list of Redis parameters to apply. Note that parameters may differ from a Redis family to another" } module "elasticache_redis" { source = "git::https://github.com/cloudposse/terraform-aws-elasticache-redis.git?ref=tags/0.7.1" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.redis_name}" - zone_id = "${local.zone_id}" + namespace = var.namespace + stage = var.stage + name = var.redis_name + zone_id = local.zone_id security_groups = ["${module.kops_metadata.nodes_security_group_id}"] - vpc_id = "${module.vpc.vpc_id}" + vpc_id = module.vpc.vpc_id subnets = ["${module.subnets.private_subnet_ids}"] maintenance_window = "sun:03:00-sun:04:00" - cluster_size = "${var.redis_cluster_size}" - instance_type = "${var.redis_instance_type}" - transit_encryption_enabled = "${var.redis_transit_encryption_enabled}" + cluster_size = var.redis_cluster_size + instance_type = var.redis_instance_type + transit_encryption_enabled = var.redis_transit_encryption_enabled engine_version = "3.2.6" family = "redis3.2" port = "6379" @@ -61,20 +61,20 @@ module "elasticache_redis" { apply_immediately = "true" availability_zones = ["${local.availability_zones}"] automatic_failover = "false" - enabled = "${var.redis_cluster_enabled}" - auth_token = "${var.redis_auth_token}" + enabled = var.redis_cluster_enabled + auth_token = var.redis_auth_token - parameter = "${var.redis_params}" + parameter = var.redis_params } output "elasticache_redis_id" { - value = "${module.elasticache_redis.id}" + value = module.elasticache_redis.id } output "elasticache_redis_security_group_id" { - value = "${module.elasticache_redis.security_group_id}" + value = module.elasticache_redis.security_group_id } output "elasticache_redis_host" { - value = "${module.elasticache_redis.host}" + value = module.elasticache_redis.host } diff --git a/deprecated/aws/backing-services/elasticsearch.tf b/deprecated/aws/backing-services/elasticsearch.tf index 24b1c7e91..9d8d5e5d5 100644 --- a/deprecated/aws/backing-services/elasticsearch.tf +++ b/deprecated/aws/backing-services/elasticsearch.tf @@ -1,18 +1,18 @@ variable "elasticsearch_name" { - type = "string" + type = string default = "elasticsearch" description = "Elasticsearch cluster name" } variable "elasticsearch_version" { - type = "string" + type = string default = "6.2" description = "Version of Elasticsearch to deploy" } # Encryption at rest is not supported with t2.small.elasticsearch instances variable "elasticsearch_encrypt_at_rest_enabled" { - type = "string" + type = string default = "false" description = "Whether to enable encryption at rest" } @@ -24,7 +24,7 @@ variable "elasticsearch_ebs_volume_size" { } variable "elasticsearch_instance_type" { - type = "string" + type = string default = "t2.small.elasticsearch" description = "Elasticsearch instance type for data nodes in the cluster" } @@ -36,19 +36,19 @@ variable "elasticsearch_instance_count" { # https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-ac.html variable "elasticsearch_iam_actions" { - type = "list" + type = list(string) default = ["es:ESHttpGet", "es:ESHttpPut", "es:ESHttpPost", "es:ESHttpHead", "es:Describe*", "es:List*"] description = "List of actions to allow for the IAM roles, _e.g._ `es:ESHttpGet`, `es:ESHttpPut`, `es:ESHttpPost`" } variable "elasticsearch_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } variable "elasticsearch_permitted_nodes" { - type = "string" + type = string description = "Kops kubernetes nodes that are permitted to access elastic search (e.g. 'nodes', 'masters', 'both' or 'any')" default = "nodes" } @@ -71,23 +71,23 @@ locals { module "elasticsearch" { source = "git::https://github.com/cloudposse/terraform-aws-elasticsearch.git?ref=tags/0.1.5" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.elasticsearch_name}" - dns_zone_id = "${local.zone_id}" + namespace = var.namespace + stage = var.stage + name = var.elasticsearch_name + dns_zone_id = local.zone_id security_groups = ["${local.security_groups[var.elasticsearch_permitted_nodes]}"] - vpc_id = "${module.vpc.vpc_id}" + vpc_id = module.vpc.vpc_id subnet_ids = ["${slice(module.subnets.private_subnet_ids, 0, min(2, length(module.subnets.private_subnet_ids)))}"] - zone_awareness_enabled = "${length(module.subnets.private_subnet_ids) > 1 ? "true" : "false"}" - elasticsearch_version = "${var.elasticsearch_version}" - instance_type = "${var.elasticsearch_instance_type}" - instance_count = "${var.elasticsearch_instance_count}" + zone_awareness_enabled = length(module.subnets.private_subnet_ids) > 1 ? "true" : "false" + elasticsearch_version = var.elasticsearch_version + instance_type = var.elasticsearch_instance_type + instance_count = var.elasticsearch_instance_count iam_role_arns = ["${local.role_arns[var.elasticsearch_permitted_nodes]}"] iam_actions = ["${var.elasticsearch_iam_actions}"] kibana_subdomain_name = "kibana-elasticsearch" - ebs_volume_size = "${var.elasticsearch_ebs_volume_size}" - encrypt_at_rest_enabled = "${var.elasticsearch_encrypt_at_rest_enabled}" - enabled = "${var.elasticsearch_enabled}" + ebs_volume_size = var.elasticsearch_ebs_volume_size + encrypt_at_rest_enabled = var.elasticsearch_encrypt_at_rest_enabled + enabled = var.elasticsearch_enabled advanced_options = { "rest.action.multi.allow_explicit_index" = "true" @@ -95,36 +95,36 @@ module "elasticsearch" { } output "elasticsearch_security_group_id" { - value = "${module.elasticsearch.security_group_id}" + value = module.elasticsearch.security_group_id description = "Security Group ID to control access to the Elasticsearch domain" } output "elasticsearch_domain_arn" { - value = "${module.elasticsearch.domain_arn}" + value = module.elasticsearch.domain_arn description = "ARN of the Elasticsearch domain" } output "elasticsearch_domain_id" { - value = "${module.elasticsearch.domain_id}" + value = module.elasticsearch.domain_id description = "Unique identifier for the Elasticsearch domain" } output "elasticsearch_domain_endpoint" { - value = "${module.elasticsearch.domain_endpoint}" + value = module.elasticsearch.domain_endpoint description = "Domain-specific endpoint used to submit index, search, and data upload requests" } output "elasticsearch_kibana_endpoint" { - value = "${module.elasticsearch.kibana_endpoint}" + value = module.elasticsearch.kibana_endpoint description = "Domain-specific endpoint for Kibana without https scheme" } output "elasticsearch_domain_hostname" { - value = "${module.elasticsearch.domain_hostname}" + value = module.elasticsearch.domain_hostname description = "Elasticsearch domain hostname to submit index, search, and data upload requests" } output "elasticsearch_kibana_hostname" { - value = "${module.elasticsearch.kibana_hostname}" + value = module.elasticsearch.kibana_hostname description = "Kibana hostname" } diff --git a/deprecated/aws/backing-services/flow-logs.tf b/deprecated/aws/backing-services/flow-logs.tf index a812b0b5c..978c1391a 100644 --- a/deprecated/aws/backing-services/flow-logs.tf +++ b/deprecated/aws/backing-services/flow-logs.tf @@ -1,64 +1,64 @@ variable "flow_logs_enabled" { - type = "string" + type = string default = "true" } module "flow_logs" { source = "git::https://github.com/cloudposse/terraform-aws-vpc-flow-logs-s3-bucket.git?ref=tags/0.1.0" - name = "${local.name}" - namespace = "${var.namespace}" - stage = "${var.stage}" - attributes = "${list("flow-logs")}" + name = local.name + namespace = var.namespace + stage = var.stage + attributes = list("flow-logs") - region = "${var.region}" + region = var.region - enabled = "${var.flow_logs_enabled}" + enabled = var.flow_logs_enabled - vpc_id = "${module.vpc.vpc_id}" + vpc_id = module.vpc.vpc_id } output "flow_logs_kms_key_arn" { - value = "${module.flow_logs.kms_key_arn}" + value = module.flow_logs.kms_key_arn description = "Flow logs KMS Key ARN" } output "flow_logs_kms_key_id" { - value = "${module.flow_logs.kms_key_id}" + value = module.flow_logs.kms_key_id description = "Flow logs KMS Key ID" } output "flow_logs_kms_alias_arn" { - value = "${module.flow_logs.kms_alias_arn}" + value = module.flow_logs.kms_alias_arn description = "Flow logs KMS Alias ARN" } output "flow_logs_kms_alias_name" { - value = "${module.flow_logs.kms_alias_name}" + value = module.flow_logs.kms_alias_name description = "Flow logs KMS Alias name" } output "flow_logs_bucket_domain_name" { - value = "${module.flow_logs.bucket_domain_name}" + value = module.flow_logs.bucket_domain_name description = "Flow logs FQDN of bucket" } output "flow_logs_bucket_id" { - value = "${module.flow_logs.bucket_id}" + value = module.flow_logs.bucket_id description = "Flow logs bucket Name (aka ID)" } output "flow_logs_bucket_arn" { - value = "${module.flow_logs.bucket_arn}" + value = module.flow_logs.bucket_arn description = "Flow logs bucket ARN" } output "flow_logs_bucket_prefix" { - value = "${module.flow_logs.bucket_prefix}" + value = module.flow_logs.bucket_prefix description = "Flow logs bucket prefix configured for lifecycle rules" } output "flow_logs_id" { - value = "${module.flow_logs.id}" + value = module.flow_logs.id description = "Flow logs ID" } diff --git a/deprecated/aws/backing-services/kops-metadata.tf b/deprecated/aws/backing-services/kops-metadata.tf index ea764cfb6..9433f8a5f 100644 --- a/deprecated/aws/backing-services/kops-metadata.tf +++ b/deprecated/aws/backing-services/kops-metadata.tf @@ -1,11 +1,11 @@ variable "kops_metadata_enabled" { description = "Set to false to prevent the module from creating any resources" - type = "string" + type = string default = "false" } module "kops_metadata" { source = "git::https://github.com/cloudposse/terraform-aws-kops-metadata.git?ref=tags/0.2.0" dns_zone = "${var.region}.${var.zone_name}" - enabled = "${var.kops_metadata_enabled}" + enabled = var.kops_metadata_enabled } diff --git a/deprecated/aws/backing-services/main.tf b/deprecated/aws/backing-services/main.tf index daf7ce7dc..abeece420 100644 --- a/deprecated/aws/backing-services/main.tf +++ b/deprecated/aws/backing-services/main.tf @@ -5,50 +5,50 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `eg` or `cp`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "region" { - type = "string" + type = string description = "AWS region" } variable "availability_zones" { - type = "list" + type = list(string) description = "AWS region availability zones to use (e.g.: ['us-west-2a', 'us-west-2b']). If empty will use all available zones" default = [] } variable "zone_name" { - type = "string" + type = string description = "DNS zone name" } data "aws_availability_zones" "available" {} data "aws_route53_zone" "default" { - name = "${var.zone_name}" + name = var.zone_name } locals { null = "" - zone_id = "${data.aws_route53_zone.default.zone_id}" + zone_id = data.aws_route53_zone.default.zone_id availability_zones = ["${split(",", length(var.availability_zones) == 0 ? join(",", data.aws_availability_zones.available.names) : join(",", var.availability_zones))}"] - chamber_service = "${var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service}" + chamber_service = var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } diff --git a/deprecated/aws/backing-services/rds-replica.tf b/deprecated/aws/backing-services/rds-replica.tf index 6bcf66db4..d509b87a5 100644 --- a/deprecated/aws/backing-services/rds-replica.tf +++ b/deprecated/aws/backing-services/rds-replica.tf @@ -1,23 +1,23 @@ variable "rds_replica_name" { - type = "string" + type = string default = "rds-replica" description = "RDS instance name" } variable "rds_replica_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } variable "rds_replica_replicate_source_db" { - type = "string" + type = string description = "Specifies that this resource is a Replicate database, and to use this value as the source database. This correlates to the identifier of another Amazon RDS Database to replicate. Note that if you are creating a cross-region replica of an encrypted database you will also need to specify a `kms_key_id`." default = "changeme" } variable "rds_replica_kms_key_id" { - type = "string" + type = string description = "The ARN for the KMS encryption key. If creating an encrypted replica, set this to the destination KMS ARN." default = "" } @@ -25,155 +25,155 @@ variable "rds_replica_kms_key_id" { # db.t2.micro is free tier # https://aws.amazon.com/rds/free variable "rds_replica_instance_type" { - type = "string" + type = string default = "db.t2.micro" description = "EC2 instance type for RDS DB" } variable "rds_replica_port" { - type = "string" + type = string default = "3306" description = "RDS DB port" } variable "rds_replica_snapshot" { - type = "string" + type = string default = "" description = "Set to a snapshot ID to restore from snapshot" } variable "rds_replica_multi_az" { - type = "string" + type = string default = "false" description = "Run instaces in multiple az" } variable "rds_replica_storage_type" { - type = "string" + type = string default = "gp2" description = "Storage type" } variable "rds_replica_storage_size" { - type = "string" + type = string default = "20" description = "Storage size in Gb" } variable "rds_replica_storage_encrypted" { - type = "string" + type = string default = "true" description = "Set to true to encrypt storage" } variable "rds_replica_auto_minor_version_upgrade" { - type = "string" + type = string default = "true" description = "Allow automated minor version upgrade (e.g. from Postgres 9.5.3 to Postgres 9.5.4)" } variable "rds_replica_allow_major_version_upgrade" { - type = "string" + type = string default = "false" description = "Allow major version upgrade" } variable "rds_replica_apply_immediately" { - type = "string" + type = string default = "true" description = "Specifies whether any database modifications are applied immediately, or during the next maintenance window" } variable "rds_replica_skip_final_snapshot" { - type = "string" + type = string default = "false" description = "If true (default), no snapshot will be made before deleting DB" } variable "rds_replica_backup_retention_period" { - type = "string" + type = string default = "7" description = "Backup retention period in days. Must be > 0 to enable backups" } variable "rds_replica_backup_window" { - type = "string" + type = string default = "22:00-03:00" description = "When AWS can perform DB snapshots, can't overlap with maintenance window" } locals { - rds_replica_enabled = "${var.rds_replica_enabled == "true"}" + rds_replica_enabled = var.rds_replica_enabled == "true" } module "rds_replica" { source = "git::https://github.com/cloudposse/terraform-aws-rds-replica.git?ref=tags/0.1.0" - enabled = "${var.rds_replica_enabled}" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.rds_replica_name}" - kms_key_id = "${var.rds_replica_kms_key_id}" - replicate_source_db = "${var.rds_replica_replicate_source_db}" - dns_zone_id = "${local.zone_id}" - host_name = "${var.rds_replica_name}" + enabled = var.rds_replica_enabled + namespace = var.namespace + stage = var.stage + name = var.rds_replica_name + kms_key_id = var.rds_replica_kms_key_id + replicate_source_db = var.rds_replica_replicate_source_db + dns_zone_id = local.zone_id + host_name = var.rds_replica_name security_group_ids = ["${module.kops_metadata.nodes_security_group_id}"] - database_port = "${var.rds_replica_port}" - multi_az = "${var.rds_replica_multi_az}" - storage_type = "${var.rds_replica_storage_type}" - storage_encrypted = "${var.rds_replica_storage_encrypted}" - instance_class = "${var.rds_replica_instance_type}" + database_port = var.rds_replica_port + multi_az = var.rds_replica_multi_az + storage_type = var.rds_replica_storage_type + storage_encrypted = var.rds_replica_storage_encrypted + instance_class = var.rds_replica_instance_type publicly_accessible = "false" subnet_ids = ["${module.subnets.private_subnet_ids}"] - vpc_id = "${module.vpc.vpc_id}" - snapshot_identifier = "${var.rds_replica_snapshot}" - auto_minor_version_upgrade = "${var.rds_replica_auto_minor_version_upgrade}" - allow_major_version_upgrade = "${var.rds_replica_allow_major_version_upgrade}" - apply_immediately = "${var.rds_replica_apply_immediately}" - skip_final_snapshot = "${var.rds_replica_skip_final_snapshot}" + vpc_id = module.vpc.vpc_id + snapshot_identifier = var.rds_replica_snapshot + auto_minor_version_upgrade = var.rds_replica_auto_minor_version_upgrade + allow_major_version_upgrade = var.rds_replica_allow_major_version_upgrade + apply_immediately = var.rds_replica_apply_immediately + skip_final_snapshot = var.rds_replica_skip_final_snapshot copy_tags_to_snapshot = "true" - backup_retention_period = "${var.rds_replica_backup_retention_period}" - backup_window = "${var.rds_replica_backup_window}" + backup_retention_period = var.rds_replica_backup_retention_period + backup_window = var.rds_replica_backup_window } resource "aws_ssm_parameter" "rds_replica_hostname" { - count = "${local.rds_replica_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "rds_replica_hostname")}" - value = "${module.rds_replica.hostname}" + count = local.rds_replica_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "rds_replica_hostname") + value = module.rds_replica.hostname description = "RDS replica hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "rds_replica_port" { - count = "${local.rds_replica_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "rds_replica_port")}" - value = "${var.rds_replica_port}" + count = local.rds_replica_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "rds_replica_port") + value = var.rds_replica_port description = "RDS replica port" type = "String" overwrite = "true" } output "rds_replica_instance_id" { - value = "${module.rds_replica.instance_id}" + value = module.rds_replica.instance_id description = "RDS replica ID of the instance" } output "rds_replica_instance_address" { - value = "${module.rds_replica.instance_address}" + value = module.rds_replica.instance_address description = "RDS replica address of the instance" } output "rds_replica_instance_endpoint" { - value = "${module.rds_replica.instance_endpoint}" + value = module.rds_replica.instance_endpoint description = "RDS replica DNS Endpoint of the instance" } output "rds_replica_port" { - value = "${local.rds_replica_enabled ? var.rds_replica_port : local.null}" + value = local.rds_replica_enabled ? var.rds_replica_port : local.null description = "RDS replica port" } output "rds_replica_hostname" { - value = "${module.rds_replica.hostname}" + value = module.rds_replica.hostname description = "RDS replica host name of the instance" } diff --git a/deprecated/aws/backing-services/rds.tf b/deprecated/aws/backing-services/rds.tf index 24f9883f0..5a892ab78 100644 --- a/deprecated/aws/backing-services/rds.tf +++ b/deprecated/aws/backing-services/rds.tf @@ -1,11 +1,11 @@ variable "rds_name" { - type = "string" + type = string default = "rds" description = "RDS instance name" } variable "rds_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } @@ -13,7 +13,7 @@ variable "rds_enabled" { # Don't use `root` # ("MasterUsername root cannot be used as it is a reserved word used by the engine") variable "rds_admin_user" { - type = "string" + type = string description = "RDS DB admin user name" default = "" } @@ -21,7 +21,7 @@ variable "rds_admin_user" { # Must be longer than 8 chars # ("The parameter MasterUserPassword is not a valid password because it is shorter than 8 characters") variable "rds_admin_password" { - type = "string" + type = string description = "RDS DB password for the admin user" default = "" } @@ -29,7 +29,7 @@ variable "rds_admin_password" { # Don't use `default` # ("DatabaseName default cannot be used as it is a reserved word used by the engine") variable "rds_db_name" { - type = "string" + type = string description = "RDS DB database name" default = "" } @@ -37,248 +37,248 @@ variable "rds_db_name" { # db.t2.micro is free tier # https://aws.amazon.com/rds/free variable "rds_instance_type" { - type = "string" + type = string default = "db.t2.micro" description = "EC2 instance type for RDS DB" } variable "rds_engine" { - type = "string" + type = string default = "mysql" description = "RDS DB engine" } variable "rds_engine_version" { - type = "string" + type = string default = "5.6" description = "RDS DB engine version" } variable "rds_port" { - type = "string" + type = string default = "3306" description = "RDS DB port" } variable "rds_db_parameter_group" { - type = "string" + type = string default = "mysql5.6" description = "RDS DB engine version" } variable "rds_snapshot" { - type = "string" + type = string default = "" description = "Set to a snapshot ID to restore from snapshot" } variable "rds_parameter_group_name" { - type = "string" + type = string default = "" description = "Existing parameter group name to use" } variable "rds_multi_az" { - type = "string" + type = string default = "false" description = "Run instaces in multiple az" } variable "rds_storage_type" { - type = "string" + type = string default = "gp2" description = "Storage type" } variable "rds_storage_size" { - type = "string" + type = string default = "20" description = "Storage size" } variable "rds_storage_encrypted" { - type = "string" + type = string default = "true" description = "Set true to encrypt storage" } variable "rds_auto_minor_version_upgrade" { - type = "string" + type = string default = "false" description = "Allow automated minor version upgrade (e.g. from Postgres 9.5.3 to Postgres 9.5.4)" } variable "rds_allow_major_version_upgrade" { - type = "string" + type = string default = "false" description = "Allow major version upgrade" } variable "rds_apply_immediately" { - type = "string" + type = string default = "true" description = "Specifies whether any database modifications are applied immediately, or during the next maintenance window" } variable "rds_skip_final_snapshot" { - type = "string" + type = string default = "false" description = "If true (default), no snapshot will be made before deleting DB" } variable "rds_backup_retention_period" { - type = "string" + type = string default = "7" description = "Backup retention period in days. Must be > 0 to enable backups" } variable "rds_backup_window" { - type = "string" + type = string default = "22:00-03:00" description = "When AWS can perform DB snapshots, can't overlap with maintenance window" } resource "random_pet" "rds_db_name" { - count = "${local.rds_enabled ? 1 : 0}" + count = local.rds_enabled ? 1 : 0 separator = "_" } resource "random_string" "rds_admin_user" { - count = "${local.rds_enabled ? 1 : 0}" + count = local.rds_enabled ? 1 : 0 length = 8 special = false number = false } resource "random_string" "rds_admin_password" { - count = "${local.rds_enabled ? 1 : 0}" + count = local.rds_enabled ? 1 : 0 length = 16 special = true } locals { - rds_enabled = "${var.rds_enabled == "true"}" - rds_admin_user = "${length(var.rds_admin_user) > 0 ? var.rds_admin_user : join("", random_string.rds_admin_user.*.result)}" - rds_admin_password = "${length(var.rds_admin_password) > 0 ? var.rds_admin_password : join("", random_string.rds_admin_password.*.result)}" - rds_db_name = "${join("", random_pet.rds_db_name.*.id)}" + rds_enabled = var.rds_enabled == "true" + rds_admin_user = length(var.rds_admin_user) > 0 ? var.rds_admin_user : join("", random_string.rds_admin_user.*.result) + rds_admin_password = length(var.rds_admin_password) > 0 ? var.rds_admin_password : join("", random_string.rds_admin_password.*.result) + rds_db_name = join("", random_pet.rds_db_name.*.id) } module "rds" { source = "git::https://github.com/cloudposse/terraform-aws-rds.git?ref=tags/0.4.4" - enabled = "${var.rds_enabled}" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.rds_name}" - dns_zone_id = "${local.zone_id}" - host_name = "${var.rds_name}" + enabled = var.rds_enabled + namespace = var.namespace + stage = var.stage + name = var.rds_name + dns_zone_id = local.zone_id + host_name = var.rds_name security_group_ids = ["${module.kops_metadata.nodes_security_group_id}"] - database_name = "${local.rds_db_name}" - database_user = "${local.rds_admin_user}" - database_password = "${local.rds_admin_password}" - database_port = "${var.rds_port}" - multi_az = "${var.rds_multi_az}" - storage_type = "${var.rds_storage_type}" - allocated_storage = "${var.rds_storage_size}" - storage_encrypted = "${var.rds_storage_encrypted}" - engine = "${var.rds_engine}" - engine_version = "${var.rds_engine_version}" - instance_class = "${var.rds_instance_type}" - db_parameter_group = "${var.rds_db_parameter_group}" - parameter_group_name = "${var.rds_parameter_group_name}" + database_name = local.rds_db_name + database_user = local.rds_admin_user + database_password = local.rds_admin_password + database_port = var.rds_port + multi_az = var.rds_multi_az + storage_type = var.rds_storage_type + allocated_storage = var.rds_storage_size + storage_encrypted = var.rds_storage_encrypted + engine = var.rds_engine + engine_version = var.rds_engine_version + instance_class = var.rds_instance_type + db_parameter_group = var.rds_db_parameter_group + parameter_group_name = var.rds_parameter_group_name publicly_accessible = "false" subnet_ids = ["${module.subnets.private_subnet_ids}"] - vpc_id = "${module.vpc.vpc_id}" - snapshot_identifier = "${var.rds_snapshot}" - auto_minor_version_upgrade = "${var.rds_auto_minor_version_upgrade}" - allow_major_version_upgrade = "${var.rds_allow_major_version_upgrade}" - apply_immediately = "${var.rds_apply_immediately}" - skip_final_snapshot = "${var.rds_skip_final_snapshot}" + vpc_id = module.vpc.vpc_id + snapshot_identifier = var.rds_snapshot + auto_minor_version_upgrade = var.rds_auto_minor_version_upgrade + allow_major_version_upgrade = var.rds_allow_major_version_upgrade + apply_immediately = var.rds_apply_immediately + skip_final_snapshot = var.rds_skip_final_snapshot copy_tags_to_snapshot = "true" - backup_retention_period = "${var.rds_backup_retention_period}" - backup_window = "${var.rds_backup_window}" + backup_retention_period = var.rds_backup_retention_period + backup_window = var.rds_backup_window } resource "aws_ssm_parameter" "rds_db_name" { - count = "${local.rds_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "rds_db_name")}" - value = "${local.rds_db_name}" + count = local.rds_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "rds_db_name") + value = local.rds_db_name description = "RDS Database Name" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "rds_admin_username" { - count = "${local.rds_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "rds_admin_username")}" - value = "${local.rds_admin_user}" + count = local.rds_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "rds_admin_username") + value = local.rds_admin_user description = "RDS Username for the admin DB user" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "rds_admin_password" { - count = "${local.rds_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "rds_admin_password")}" - value = "${local.rds_admin_password}" + count = local.rds_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "rds_admin_password") + value = local.rds_admin_password description = "RDS Password for the admin DB user" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "rds_hostname" { - count = "${local.rds_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "rds_hostname")}" - value = "${module.rds.hostname}" + count = local.rds_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "rds_hostname") + value = module.rds.hostname description = "RDS hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "rds_port" { - count = "${local.rds_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "rds_port")}" - value = "${var.rds_port}" + count = local.rds_enabled ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, "rds_port") + value = var.rds_port description = "RDS port" type = "String" overwrite = "true" } output "rds_instance_id" { - value = "${module.rds.instance_id}" + value = module.rds.instance_id description = "RDS ID of the instance" } output "rds_instance_address" { - value = "${module.rds.instance_address}" + value = module.rds.instance_address description = "RDS address of the instance" } output "rds_instance_endpoint" { - value = "${module.rds.instance_endpoint}" + value = module.rds.instance_endpoint description = "RDS DNS Endpoint of the instance" } output "rds_port" { - value = "${local.rds_enabled ? var.rds_port : local.null}" + value = local.rds_enabled ? var.rds_port : local.null description = "RDS port" } output "rds_db_name" { - value = "${local.rds_enabled ? local.rds_db_name : local.null}" + value = local.rds_enabled ? local.rds_db_name : local.null description = "RDS db name" } output "rds_admin_user" { - value = "${local.rds_enabled ? local.rds_admin_user : local.null}" + value = local.rds_enabled ? local.rds_admin_user : local.null description = "RDS admin user name" } output "rds_admin_password" { - value = "${local.rds_enabled ? local.rds_admin_password : local.null}" + value = local.rds_enabled ? local.rds_admin_password : local.null description = "RDS admin password" } output "rds_hostname" { - value = "${module.rds.hostname}" + value = module.rds.hostname description = "RDS host name of the instance" } diff --git a/deprecated/aws/backing-services/vpc.tf b/deprecated/aws/backing-services/vpc.tf index ad2f45ef2..2d0ab1e44 100644 --- a/deprecated/aws/backing-services/vpc.tf +++ b/deprecated/aws/backing-services/vpc.tf @@ -19,29 +19,29 @@ data "aws_region" "current" {} module "vpc" { source = "git::https://github.com/cloudposse/terraform-aws-vpc.git?ref=tags/0.4.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" - cidr_block = "${var.vpc_cidr_block}" + namespace = var.namespace + stage = var.stage + name = local.name + cidr_block = var.vpc_cidr_block } module "subnets" { source = "git::https://github.com/cloudposse/terraform-aws-dynamic-subnets.git?ref=tags/0.8.0" availability_zones = ["${local.availability_zones}"] - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" - region = "${var.region}" - vpc_id = "${module.vpc.vpc_id}" - igw_id = "${module.vpc.igw_id}" - cidr_block = "${module.vpc.vpc_cidr_block}" - nat_gateway_enabled = "${var.vpc_nat_gateway_enabled}" - max_subnet_count = "${var.vpc_max_subnet_count}" + namespace = var.namespace + stage = var.stage + name = local.name + region = var.region + vpc_id = module.vpc.vpc_id + igw_id = module.vpc.igw_id + cidr_block = module.vpc.vpc_cidr_block + nat_gateway_enabled = var.vpc_nat_gateway_enabled + max_subnet_count = var.vpc_max_subnet_count } output "vpc_id" { description = "VPC ID of backing services" - value = "${module.vpc.vpc_id}" + value = module.vpc.vpc_id } output "public_subnet_ids" { @@ -56,5 +56,5 @@ output "private_subnet_ids" { output "region" { description = "AWS region of backing services" - value = "${data.aws_region.current.name}" + value = data.aws_region.current.name } diff --git a/deprecated/aws/bootstrap/main.tf b/deprecated/aws/bootstrap/main.tf index 042fea3f5..04cb3fe9e 100644 --- a/deprecated/aws/bootstrap/main.tf +++ b/deprecated/aws/bootstrap/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -14,8 +14,8 @@ provider "aws" { module "user" { source = "git::https://github.com/cloudposse/terraform-aws-iam-system-user.git?ref=tags/0.3.2" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "bootstrap" } @@ -36,18 +36,18 @@ data "aws_iam_policy_document" "assume_role" { # Fetch the OrganizationAccountAccessRole ARNs from SSM module "organization_account_access_role_arns" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - parameter_read = "${formatlist("/${var.namespace}/%s/organization_account_access_role", var.accounts_enabled)}" + parameter_read = formatlist("/${var.namespace}/%s/organization_account_access_role", var.accounts_enabled) } # IAM role for bootstrapping; allow user to assume it resource "aws_iam_role" "bootstrap" { - name = "${module.user.user_name}" - assume_role_policy = "${data.aws_iam_policy_document.assume_role.json}" + name = module.user.user_name + assume_role_policy = data.aws_iam_policy_document.assume_role.json } # Grant Administrator Access to the current "root" account to the role resource "aws_iam_role_policy_attachment" "administrator_access" { - role = "${aws_iam_role.bootstrap.name}" + role = aws_iam_role.bootstrap.name policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" } @@ -65,83 +65,83 @@ data "aws_iam_policy_document" "organization_account_access_role" { # Create an IAM policy from the generated document resource "aws_iam_policy" "organization_account_access_role" { - name = "${aws_iam_role.bootstrap.name}" - policy = "${data.aws_iam_policy_document.organization_account_access_role.json}" + name = aws_iam_role.bootstrap.name + policy = data.aws_iam_policy_document.organization_account_access_role.json } # Assign the policy to the user resource "aws_iam_user_policy_attachment" "organization_account_access_role" { - user = "${aws_iam_role.bootstrap.name}" - policy_arn = "${aws_iam_policy.organization_account_access_role.arn}" + user = aws_iam_role.bootstrap.name + policy_arn = aws_iam_policy.organization_account_access_role.arn } # Render the env file with IAM credentials data "template_file" "env" { - template = "${file("${path.module}/env.tpl")}" + template = file("${path.module}/env.tpl") vars { - aws_access_key_id = "${module.user.access_key_id}" - aws_secret_access_key = "${module.user.secret_access_key}" - aws_assume_role_arn = "${aws_iam_role.bootstrap.arn}" - aws_data_path = "${dirname(local_file.config_file.filename)}" - aws_config_file = "${local_file.config_file.filename}" + aws_access_key_id = module.user.access_key_id + aws_secret_access_key = module.user.secret_access_key + aws_assume_role_arn = aws_iam_role.bootstrap.arn + aws_data_path = dirname(local_file.config_file.filename) + aws_config_file = local_file.config_file.filename } } # Write the env file to disk resource "local_file" "env_file" { - content = "${data.template_file.env.rendered}" + content = data.template_file.env.rendered filename = "${var.output_path}/${var.env_file}" } # Render the credentials file with IAM credentials data "template_file" "credentials" { - template = "${file("${path.module}/credentials.tpl")}" + template = file("${path.module}/credentials.tpl") vars { - source_profile_name = "${var.namespace}" - aws_access_key_id = "${module.user.access_key_id}" - aws_secret_access_key = "${module.user.secret_access_key}" - aws_assume_role_arn = "${aws_iam_role.bootstrap.arn}" + source_profile_name = var.namespace + aws_access_key_id = module.user.access_key_id + aws_secret_access_key = module.user.secret_access_key + aws_assume_role_arn = aws_iam_role.bootstrap.arn } } # Write the credentials file to disk resource "local_file" "credentials_file" { - content = "${data.template_file.credentials.rendered}" + content = data.template_file.credentials.rendered filename = "${var.output_path}/${var.credentials_file}" } # Render the config file with IAM credentials data "template_file" "config_root" { - template = "${file("${path.module}/config.tpl")}" + template = file("${path.module}/config.tpl") vars { profile_name = "${var.namespace}-${var.stage}-admin" - source_profile = "${var.namespace}" - region = "${var.aws_region}" - role_arn = "${aws_iam_role.bootstrap.arn}" + source_profile = var.namespace + region = var.aws_region + role_arn = aws_iam_role.bootstrap.arn } } # Render the config file with IAM credentials data "template_file" "config" { - count = "${length(module.organization_account_access_role_arns.values)}" - template = "${file("${path.module}/config.tpl")}" + count = length(module.organization_account_access_role_arns.values) + template = file("${path.module}/config.tpl") vars { profile_name = "${var.namespace}-${var.accounts_enabled[count.index]}-admin" - source_profile = "${var.namespace}" - region = "${var.aws_region}" - role_arn = "${module.organization_account_access_role_arns.values[count.index]}" + source_profile = var.namespace + region = var.aws_region + role_arn = module.organization_account_access_role_arns.values[count.index] } } # Write the config file to disk resource "local_file" "config_file" { - content = "${join("\n\n", + content = (join("\n\n", concat(list("[profile ${var.namespace}]"), - list(data.template_file.config_root.rendered), data.template_file.config.*.rendered))}" + list(data.template_file.config_root.rendered), data.template_file.config.*.rendered))) filename = "${var.output_path}/${var.config_file}" } diff --git a/deprecated/aws/bootstrap/outputs.tf b/deprecated/aws/bootstrap/outputs.tf index 972a349f8..f92e33a34 100644 --- a/deprecated/aws/bootstrap/outputs.tf +++ b/deprecated/aws/bootstrap/outputs.tf @@ -1,4 +1,4 @@ output "env_file" { description = "Env file with IAM bootstrap credentials" - value = "${var.env_file}" + value = var.env_file } diff --git a/deprecated/aws/bootstrap/variables.tf b/deprecated/aws/bootstrap/variables.tf index cbbe78083..4024c9fa2 100644 --- a/deprecated/aws/bootstrap/variables.tf +++ b/deprecated/aws/bootstrap/variables.tf @@ -1,47 +1,47 @@ variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "aws_region" { - type = "string" + type = string } variable "accounts_enabled" { - type = "list" + type = list(string) description = "Accounts to enable" default = ["dev", "staging", "prod", "testing", "audit"] } variable "output_path" { - type = "string" + type = string default = "./" description = "Base directory where files will be written" } variable "env_file" { - type = "string" + type = string description = "File to write the temporary bootstrap environment variable settings" default = ".envrc" } variable "config_file" { - type = "string" + type = string description = "File to write the temporary bootstrap AWS config" default = ".aws/config" } variable "credentials_file" { - type = "string" + type = string description = "File to write the temporary bootstrap AWS credentials" default = ".aws/credentials" } diff --git a/deprecated/aws/chamber/kms-key.tf b/deprecated/aws/chamber/kms-key.tf index e8fd50e00..dccf48af2 100644 --- a/deprecated/aws/chamber/kms-key.tf +++ b/deprecated/aws/chamber/kms-key.tf @@ -1,27 +1,27 @@ module "chamber_kms_key" { source = "git::https://github.com/cloudposse/terraform-aws-kms-key.git?ref=tags/0.1.0" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "chamber" description = "KMS key for chamber" } output "chamber_kms_key_arn" { - value = "${module.chamber_kms_key.key_arn}" + value = module.chamber_kms_key.key_arn description = "KMS key ARN" } output "chamber_kms_key_id" { - value = "${module.chamber_kms_key.key_id}" + value = module.chamber_kms_key.key_id description = "KMS key ID" } output "chamber_kms_key_alias_arn" { - value = "${module.chamber_kms_key.alias_arn}" + value = module.chamber_kms_key.alias_arn description = "KMS key alias ARN" } output "chamber_kms_key_alias_name" { - value = "${module.chamber_kms_key.alias_name}" + value = module.chamber_kms_key.alias_name description = "KMS key alias name" } diff --git a/deprecated/aws/chamber/main.tf b/deprecated/aws/chamber/main.tf index 315ed0d61..48f8edf6e 100644 --- a/deprecated/aws/chamber/main.tf +++ b/deprecated/aws/chamber/main.tf @@ -5,27 +5,27 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "parameter_groups" { - type = "list" + type = list(string) description = "Parameter group names" default = ["kops", "app"] } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } diff --git a/deprecated/aws/chamber/s3-bucket.tf b/deprecated/aws/chamber/s3-bucket.tf index f6136a358..81a0d999f 100644 --- a/deprecated/aws/chamber/s3-bucket.tf +++ b/deprecated/aws/chamber/s3-bucket.tf @@ -10,54 +10,54 @@ variable "s3_user_enabled" { module "s3_bucket" { source = "git::https://github.com/cloudposse/terraform-aws-s3-bucket.git?ref=tags/0.3.0" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "chamber" - enabled = "${var.s3_enabled}" + enabled = var.s3_enabled versioning_enabled = "false" - user_enabled = "${var.s3_user_enabled}" + user_enabled = var.s3_user_enabled sse_algorithm = "AES256" allow_encrypted_uploads_only = "true" } output "bucket_domain_name" { - value = "${module.s3_bucket.bucket_domain_name}" + value = module.s3_bucket.bucket_domain_name description = "FQDN of bucket" } output "bucket_id" { - value = "${module.s3_bucket.bucket_arn}" + value = module.s3_bucket.bucket_arn description = "Bucket Name (aka ID)" } output "bucket_arn" { - value = "${module.s3_bucket.bucket_arn}" + value = module.s3_bucket.bucket_arn description = "Bucket ARN" } output "user_name" { - value = "${module.s3_bucket.user_name}" + value = module.s3_bucket.user_name description = "Normalized IAM user name" } output "user_arn" { - value = "${module.s3_bucket.user_arn}" + value = module.s3_bucket.user_arn description = "The ARN assigned by AWS for the user" } output "user_unique_id" { - value = "${module.s3_bucket.user_unique_id}" + value = module.s3_bucket.user_unique_id description = "The user unique ID assigned by AWS" } output "access_key_id" { sensitive = true - value = "${module.s3_bucket.access_key_id}" + value = module.s3_bucket.access_key_id description = "The access key ID" } output "secret_access_key" { sensitive = true - value = "${module.s3_bucket.secret_access_key}" + value = module.s3_bucket.secret_access_key description = "The secret access key. This will be written to the state file in plain-text" } diff --git a/deprecated/aws/chamber/user.tf b/deprecated/aws/chamber/user.tf index 6f675dcdb..839d4192a 100644 --- a/deprecated/aws/chamber/user.tf +++ b/deprecated/aws/chamber/user.tf @@ -10,12 +10,12 @@ variable "chamber_user_enabled" { # https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html module "chamber_user" { source = "git::https://github.com/cloudposse/terraform-aws-iam-chamber-user.git?ref=tags/0.1.7" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "chamber" - enabled = "${var.chamber_user_enabled}" + enabled = var.chamber_user_enabled attributes = ["codefresh"] - kms_key_arn = "${module.chamber_kms_key.key_arn}" + kms_key_arn = module.chamber_kms_key.key_arn ssm_resources = [ "${formatlist("arn:aws:ssm:%s:%s:parameter/%s/*", data.aws_region.default.name, data.aws_caller_identity.default.account_id, var.parameter_groups)}", @@ -23,26 +23,26 @@ module "chamber_user" { } output "chamber_user_name" { - value = "${module.chamber_user.user_name}" + value = module.chamber_user.user_name description = "Normalized IAM user name" } output "chamber_user_arn" { - value = "${module.chamber_user.user_arn}" + value = module.chamber_user.user_arn description = "The ARN assigned by AWS for the user" } output "chamber_user_unique_id" { - value = "${module.chamber_user.user_unique_id}" + value = module.chamber_user.user_unique_id description = "The user unique ID assigned by AWS" } output "chamber_access_key_id" { - value = "${module.chamber_user.access_key_id}" + value = module.chamber_user.access_key_id description = "The access key ID" } output "chamber_secret_access_key" { - value = "${module.chamber_user.secret_access_key}" + value = module.chamber_user.secret_access_key description = "The secret access key. This will be written to the state file in plain-text" } diff --git a/deprecated/aws/cis-aggregator-auth/main.tf b/deprecated/aws/cis-aggregator-auth/main.tf index 6f82af27b..0889cf311 100644 --- a/deprecated/aws/cis-aggregator-auth/main.tf +++ b/deprecated/aws/cis-aggregator-auth/main.tf @@ -6,23 +6,23 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } # Define composite variables for resources module "label" { source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.5.3" - enabled = "${var.enabled}" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" - delimiter = "${var.delimiter}" - attributes = "${var.attributes}" - tags = "${var.tags}" + enabled = var.enabled + namespace = var.namespace + name = var.name + stage = var.stage + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags } resource "aws_config_aggregate_authorization" "default" { - account_id = "${var.aggregator_account}" - region = "${var.aggregator_region}" + account_id = var.aggregator_account + region = var.aggregator_region } diff --git a/deprecated/aws/cis-aggregator-auth/variables.tf b/deprecated/aws/cis-aggregator-auth/variables.tf index 310d3f774..785cd85da 100644 --- a/deprecated/aws/cis-aggregator-auth/variables.tf +++ b/deprecated/aws/cis-aggregator-auth/variables.tf @@ -1,5 +1,5 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "enabled" { @@ -8,12 +8,12 @@ variable "enabled" { } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } @@ -23,25 +23,25 @@ variable "name" { } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter between `name`, `namespace`, `stage` and `attributes`" } variable "attributes" { - type = "list" + type = list(string) description = "Additional attributes (_e.g._ \"1\")" default = [] } variable "tags" { - type = "map" + type = map(string) description = "Additional tags (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } variable "parameters" { - type = "map" + type = map(string) description = "Key-value map of input parameters for the Stack Set template. (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } diff --git a/deprecated/aws/cis-aggregator/main.tf b/deprecated/aws/cis-aggregator/main.tf index 8abe4e2a7..7d7afe099 100644 --- a/deprecated/aws/cis-aggregator/main.tf +++ b/deprecated/aws/cis-aggregator/main.tf @@ -6,25 +6,25 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } # Define composite variables for resources module "label" { source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.5.3" - enabled = "${var.enabled}" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" - delimiter = "${var.delimiter}" - attributes = "${var.attributes}" - tags = "${var.tags}" + enabled = var.enabled + namespace = var.namespace + name = var.name + stage = var.stage + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags } resource "aws_config_configuration_aggregator" "default" { - count = "${var.enabled == "true" ? 1 : 0}" - name = "${module.label.id}" + count = var.enabled == "true" ? 1 : 0 + name = module.label.id account_aggregation_source { account_ids = ["${var.accounts}"] diff --git a/deprecated/aws/cis-aggregator/variables.tf b/deprecated/aws/cis-aggregator/variables.tf index 95a170bb4..666d24b43 100644 --- a/deprecated/aws/cis-aggregator/variables.tf +++ b/deprecated/aws/cis-aggregator/variables.tf @@ -1,5 +1,5 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "enabled" { @@ -8,12 +8,12 @@ variable "enabled" { } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } @@ -23,35 +23,35 @@ variable "name" { } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter between `name`, `namespace`, `stage` and `attributes`" } variable "attributes" { - type = "list" + type = list(string) description = "Additional attributes (_e.g._ \"1\")" default = [] } variable "tags" { - type = "map" + type = map(string) description = "Additional tags (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } variable "parameters" { - type = "map" + type = map(string) description = "Key-value map of input parameters for the Stack Set template. (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } variable "accounts" { - type = "list" + type = list(string) default = [] } variable "regions" { - type = "list" + type = list(string) default = [] } diff --git a/deprecated/aws/cis-executor/main.tf b/deprecated/aws/cis-executor/main.tf index f750eb0e8..dd9c743d9 100644 --- a/deprecated/aws/cis-executor/main.tf +++ b/deprecated/aws/cis-executor/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -17,10 +17,10 @@ locals { module "default" { source = "git::https://github.com/cloudposse/terraform-aws-iam-role.git?ref=tags/0.3.0" - enabled = "${var.enabled}" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.executor_role_name}" + enabled = var.enabled + namespace = var.namespace + stage = var.stage + name = local.executor_role_name use_fullname = "false" attributes = ["${var.attributes}"] role_description = "IAM Role in all target accounts for Stack Set operations" diff --git a/deprecated/aws/cis-executor/variables.tf b/deprecated/aws/cis-executor/variables.tf index c13290666..e713d2042 100644 --- a/deprecated/aws/cis-executor/variables.tf +++ b/deprecated/aws/cis-executor/variables.tf @@ -1,5 +1,5 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "enabled" { @@ -8,29 +8,29 @@ variable "enabled" { } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter between `name`, `namespace`, `stage` and `attributes`" } variable "attributes" { - type = "list" + type = list(string) description = "Additional attributes (_e.g._ \"1\")" default = ["executor"] } variable "tags" { - type = "map" + type = map(string) description = "Additional tags (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } diff --git a/deprecated/aws/cis-instances/main.tf b/deprecated/aws/cis-instances/main.tf index 6af3f68bc..3a9a8278b 100644 --- a/deprecated/aws/cis-instances/main.tf +++ b/deprecated/aws/cis-instances/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -20,22 +20,22 @@ data "terraform_remote_state" "cis" { } resource "null_resource" "instances" { - count = "${var.enabled == "true" ? length(keys(var.cis_instances)) : 0}" + count = var.enabled == "true" ? length(keys(var.cis_instances)) : 0 triggers { - account = "${join("|", formatlist("%s:%s", element(keys(var.cis_instances), count.index), var.cis_instances[element(keys(var.cis_instances), count.index)]))}" + account = join("|", formatlist("%s:%s", element(keys(var.cis_instances), count.index), var.cis_instances[element(keys(var.cis_instances), count.index)])) } } locals { raw_instances = ["${split("|", join("|", null_resource.instances.*.triggers.account))}"] - instances = "${compact(local.raw_instances)}" + instances = compact(local.raw_instances) } resource "aws_cloudformation_stack_set_instance" "default" { - count = "${var.enabled == "true" && length(local.instances) > 0 ? length(local.instances) : 0}" - stack_set_name = "${data.terraform_remote_state.cis.name}" - account_id = "${element(split(":", element(local.instances, count.index)), 0)}" - region = "${element(split(":", element(local.instances, count.index)), 1)}" - parameter_overrides = "${var.parameters}" + count = var.enabled == "true" && length(local.instances) > 0 ? length(local.instances) : 0 + stack_set_name = data.terraform_remote_state.cis.name + account_id = element(split(":", element(local.instances, count.index)), 0) + region = element(split(":", element(local.instances, count.index)), 1) + parameter_overrides = var.parameters } diff --git a/deprecated/aws/cis-instances/variables.tf b/deprecated/aws/cis-instances/variables.tf index 99877542b..2f3995bf8 100644 --- a/deprecated/aws/cis-instances/variables.tf +++ b/deprecated/aws/cis-instances/variables.tf @@ -1,5 +1,5 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "enabled" { @@ -8,12 +8,12 @@ variable "enabled" { } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } @@ -23,30 +23,30 @@ variable "name" { } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter between `name`, `namespace`, `stage` and `attributes`" } variable "attributes" { - type = "list" + type = list(string) description = "Additional attributes (_e.g._ \"1\")" default = [] } variable "tags" { - type = "map" + type = map(string) description = "Additional tags (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } variable "parameters" { - type = "map" + type = map(string) description = "Key-value map of input parameters override for the Stack Set template. (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } variable "cis_instances" { - type = "map" + type = map(string) default = {} } diff --git a/deprecated/aws/cis/main.tf b/deprecated/aws/cis/main.tf index 9b376a29b..6437148ee 100644 --- a/deprecated/aws/cis/main.tf +++ b/deprecated/aws/cis/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -18,13 +18,13 @@ locals { module "default" { source = "git::https://github.com/cloudposse/terraform-aws-cloudformation-stack-set.git?ref=tags/0.1.0" - enabled = "${var.enabled}" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + enabled = var.enabled + namespace = var.namespace + stage = var.stage + name = var.name attributes = ["${var.attributes}"] - parameters = "${var.parameters}" - template_url = "${local.template_url}" - executor_role_name = "${local.executor_role_name}" - capabilities = "${var.capabilities}" + parameters = var.parameters + template_url = local.template_url + executor_role_name = local.executor_role_name + capabilities = var.capabilities } diff --git a/deprecated/aws/cis/output.tf b/deprecated/aws/cis/output.tf index 08c124c62..bc6089617 100644 --- a/deprecated/aws/cis/output.tf +++ b/deprecated/aws/cis/output.tf @@ -1,11 +1,11 @@ output "administrator_role_arn" { - value = "${module.default.administrator_role_arn}" + value = module.default.administrator_role_arn } output "executor_role_name" { - value = "${module.default.executor_role_name}" + value = module.default.executor_role_name } output "name" { - value = "${module.default.name}" + value = module.default.name } diff --git a/deprecated/aws/cis/variables.tf b/deprecated/aws/cis/variables.tf index 797c98ffb..0e51a7b26 100644 --- a/deprecated/aws/cis/variables.tf +++ b/deprecated/aws/cis/variables.tf @@ -1,5 +1,5 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "enabled" { @@ -8,12 +8,12 @@ variable "enabled" { } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } @@ -23,31 +23,31 @@ variable "name" { } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter between `name`, `namespace`, `stage` and `attributes`" } variable "attributes" { - type = "list" + type = list(string) description = "Additional attributes (_e.g._ \"1\")" default = [] } variable "tags" { - type = "map" + type = map(string) description = "Additional tags (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } variable "parameters" { - type = "map" + type = map(string) description = "Key-value map of input parameters for the Stack Set template. (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } variable "capabilities" { - type = "list" + type = list(string) description = "A list of capabilities. Valid values: CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND" default = [] } diff --git a/deprecated/aws/cloudtrail/cloudwatch_logs.tf b/deprecated/aws/cloudtrail/cloudwatch_logs.tf index a963ad9d6..23135b8df 100644 --- a/deprecated/aws/cloudtrail/cloudwatch_logs.tf +++ b/deprecated/aws/cloudtrail/cloudwatch_logs.tf @@ -1,11 +1,11 @@ module "logs" { source = "git::https://github.com/cloudposse/terraform-aws-cloudwatch-logs.git?ref=tags/0.3.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name attributes = ["cloudwatch", "logs"] - retention_in_days = "${var.cloudwatch_logs_retention_in_days}" + retention_in_days = var.cloudwatch_logs_retention_in_days principals = { Service = ["cloudtrail.amazonaws.com"] @@ -18,16 +18,16 @@ module "logs" { module "kms_key_logs" { source = "git::https://github.com/cloudposse/terraform-aws-kms-key.git?ref=tags/0.1.3" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" + namespace = var.namespace + name = var.name + stage = var.stage attributes = ["cloudwatch", "logs"] description = "KMS key for CloudWatch" deletion_window_in_days = 10 enable_key_rotation = "true" - policy = "${data.aws_iam_policy_document.kms_key_logs.json}" + policy = data.aws_iam_policy_document.kms_key_logs.json } data "aws_iam_policy_document" "kms_key_logs" { diff --git a/deprecated/aws/cloudtrail/main.tf b/deprecated/aws/cloudtrail/main.tf index 2ac7db661..777ac73c7 100644 --- a/deprecated/aws/cloudtrail/main.tf +++ b/deprecated/aws/cloudtrail/main.tf @@ -5,38 +5,38 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string description = "Name (e.g. `account`)" default = "account" } variable "kms_key_arn" { - type = "string" + type = string description = "" } variable "region" { - type = "string" + type = string description = "AWS region" default = "" } @@ -49,20 +49,20 @@ variable "cloudwatch_logs_retention_in_days" { data "aws_region" "default" {} locals { - region = "${length(var.region) > 0 ? var.region : data.aws_region.default.name}" + region = length(var.region) > 0 ? var.region : data.aws_region.default.name } module "cloudtrail" { source = "git::https://github.com/cloudposse/terraform-aws-cloudtrail.git?ref=tags/0.7.1" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name enable_logging = "true" enable_log_file_validation = "true" include_global_service_events = "true" is_multi_region_trail = "true" s3_bucket_name = "${var.namespace}-audit-account" - kms_key_arn = "${var.kms_key_arn}" - cloud_watch_logs_group_arn = "${module.logs.log_group_arn}" - cloud_watch_logs_role_arn = "${module.logs.role_arn}" + kms_key_arn = var.kms_key_arn + cloud_watch_logs_group_arn = module.logs.log_group_arn + cloud_watch_logs_role_arn = module.logs.role_arn } diff --git a/deprecated/aws/codefresh-onprem/kops-metadata.tf b/deprecated/aws/codefresh-onprem/kops-metadata.tf index ea764cfb6..9433f8a5f 100644 --- a/deprecated/aws/codefresh-onprem/kops-metadata.tf +++ b/deprecated/aws/codefresh-onprem/kops-metadata.tf @@ -1,11 +1,11 @@ variable "kops_metadata_enabled" { description = "Set to false to prevent the module from creating any resources" - type = "string" + type = string default = "false" } module "kops_metadata" { source = "git::https://github.com/cloudposse/terraform-aws-kops-metadata.git?ref=tags/0.2.0" dns_zone = "${var.region}.${var.zone_name}" - enabled = "${var.kops_metadata_enabled}" + enabled = var.kops_metadata_enabled } diff --git a/deprecated/aws/codefresh-onprem/main.tf b/deprecated/aws/codefresh-onprem/main.tf index 2d9ec2111..f55264d58 100644 --- a/deprecated/aws/codefresh-onprem/main.tf +++ b/deprecated/aws/codefresh-onprem/main.tf @@ -6,31 +6,31 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `eg` or `cp`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "region" { - type = "string" + type = string description = "AWS region" } variable "zone_name" { - type = "string" + type = string description = "DNS zone name" } @@ -44,25 +44,25 @@ variable "acm_primary_domain" { } variable "acm_san_domains" { - type = "list" + type = list(string) default = [] description = "A list of domains that should be SANs in the issued certificate" } variable "acm_zone_name" { - type = "string" + type = string default = "" description = "The name of the desired Route53 Hosted Zone" } variable "redis_cluster_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } variable "postgres_cluster_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } @@ -73,49 +73,49 @@ variable "documentdb_cluster_enabled" { } variable "documentdb_instance_class" { - type = "string" + type = string default = "db.r4.large" description = "The instance class to use. For more details, see https://docs.aws.amazon.com/documentdb/latest/developerguide/db-instance-classes.html#db-instance-class-specs" } variable "documentdb_cluster_size" { - type = "string" + type = string default = "3" description = "Number of DocumentDB instances to create in the cluster" } variable "documentdb_port" { - type = "string" + type = string default = "27017" description = "DocumentDB port" } variable "documentdb_master_username" { - type = "string" + type = string default = "" description = "Username for the master DocumentDB user. If left empty, will be generated automatically" } variable "documentdb_master_password" { - type = "string" + type = string default = "" description = "Password for the master DocumentDB user. If left empty, will be generated automatically. Note that this may show up in logs, and it will be stored in the state file" } variable "documentdb_retention_period" { - type = "string" + type = string default = "5" description = "Number of days to retain DocumentDB backups for" } variable "documentdb_preferred_backup_window" { - type = "string" + type = string default = "07:00-09:00" description = "Daily time range during which the DocumentDB backups happen" } variable "documentdb_cluster_parameters" { - type = "list" + type = list(string) default = [ { @@ -128,19 +128,19 @@ variable "documentdb_cluster_parameters" { } variable "documentdb_cluster_family" { - type = "string" + type = string default = "docdb3.6" description = "The family of the DocumentDB cluster parameter group. For more details, see https://docs.aws.amazon.com/documentdb/latest/developerguide/db-cluster-parameter-group-create.html" } variable "documentdb_engine" { - type = "string" + type = string default = "docdb" description = "The name of the database engine to be used for DocumentDB cluster. Defaults to `docdb`. Valid values: `docdb`" } variable "documentdb_engine_version" { - type = "string" + type = string default = "" description = "The version number of the DocumentDB database engine to use" } @@ -161,13 +161,13 @@ variable "documentdb_apply_immediately" { } variable "documentdb_enabled_cloudwatch_logs_exports" { - type = "list" + type = list(string) description = "List of DocumentDB log types to export to CloudWatch. The following log types are supported: audit, error, general, slowquery" default = [] } variable "documentdb_chamber_parameters_mapping" { - type = "map" + type = map(string) default = { documentdb_connection_uri = "MONGODB_URI" @@ -187,187 +187,187 @@ data "terraform_remote_state" "backing_services" { module "codefresh_enterprise_backing_services" { source = "git::https://github.com/cloudposse/terraform-aws-codefresh-backing-services.git?ref=tags/0.8.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - vpc_id = "${data.terraform_remote_state.backing_services.vpc_id}" + namespace = var.namespace + stage = var.stage + vpc_id = data.terraform_remote_state.backing_services.vpc_id subnet_ids = ["${data.terraform_remote_state.backing_services.private_subnet_ids}"] security_groups = ["${module.kops_metadata.nodes_security_group_id}"] - zone_name = "${var.zone_name}" + zone_name = var.zone_name chamber_service = "codefresh" - acm_enabled = "${var.acm_enabled}" - acm_primary_domain = "${var.acm_primary_domain}" + acm_enabled = var.acm_enabled + acm_primary_domain = var.acm_primary_domain acm_san_domains = ["${var.acm_san_domains}"] - redis_cluster_enabled = "${var.redis_cluster_enabled}" + redis_cluster_enabled = var.redis_cluster_enabled - postgres_cluster_enabled = "${var.postgres_cluster_enabled}" + postgres_cluster_enabled = var.postgres_cluster_enabled # DocumentDB - documentdb_cluster_enabled = "${var.documentdb_cluster_enabled}" - documentdb_instance_class = "${var.documentdb_instance_class}" - documentdb_cluster_size = "${var.documentdb_cluster_size}" - documentdb_port = "${var.documentdb_port}" - documentdb_master_username = "${var.documentdb_master_username}" - documentdb_master_password = "${var.documentdb_master_password}" - documentdb_retention_period = "${var.documentdb_retention_period}" - documentdb_preferred_backup_window = "${var.documentdb_preferred_backup_window}" + documentdb_cluster_enabled = var.documentdb_cluster_enabled + documentdb_instance_class = var.documentdb_instance_class + documentdb_cluster_size = var.documentdb_cluster_size + documentdb_port = var.documentdb_port + documentdb_master_username = var.documentdb_master_username + documentdb_master_password = var.documentdb_master_password + documentdb_retention_period = var.documentdb_retention_period + documentdb_preferred_backup_window = var.documentdb_preferred_backup_window documentdb_cluster_parameters = ["${var.documentdb_cluster_parameters}"] - documentdb_cluster_family = "${var.documentdb_cluster_family}" - documentdb_engine = "${var.documentdb_engine}" - documentdb_engine_version = "${var.documentdb_engine_version}" - documentdb_storage_encrypted = "${var.documentdb_storage_encrypted}" - documentdb_skip_final_snapshot = "${var.documentdb_skip_final_snapshot}" - documentdb_apply_immediately = "${var.documentdb_apply_immediately}" + documentdb_cluster_family = var.documentdb_cluster_family + documentdb_engine = var.documentdb_engine + documentdb_engine_version = var.documentdb_engine_version + documentdb_storage_encrypted = var.documentdb_storage_encrypted + documentdb_skip_final_snapshot = var.documentdb_skip_final_snapshot + documentdb_apply_immediately = var.documentdb_apply_immediately documentdb_enabled_cloudwatch_logs_exports = ["${var.documentdb_enabled_cloudwatch_logs_exports}"] - documentdb_chamber_parameters_mapping = "${var.documentdb_chamber_parameters_mapping}" + documentdb_chamber_parameters_mapping = var.documentdb_chamber_parameters_mapping } output "elasticache_redis_id" { - value = "${module.codefresh_enterprise_backing_services.elasticache_redis_id}" + value = module.codefresh_enterprise_backing_services.elasticache_redis_id description = "Elasticache Redis cluster ID" } output "elasticache_redis_security_group_id" { - value = "${module.codefresh_enterprise_backing_services.elasticache_redis_security_group_id}" + value = module.codefresh_enterprise_backing_services.elasticache_redis_security_group_id description = "Elasticache Redis security group ID" } output "elasticache_redis_host" { - value = "${module.codefresh_enterprise_backing_services.elasticache_redis_host}" + value = module.codefresh_enterprise_backing_services.elasticache_redis_host description = "Elasticache Redis host" } output "aurora_postgres_database_name" { - value = "${module.codefresh_enterprise_backing_services.aurora_postgres_database_name}" + value = module.codefresh_enterprise_backing_services.aurora_postgres_database_name description = "Aurora Postgres Database name" } output "aurora_postgres_master_username" { - value = "${module.codefresh_enterprise_backing_services.aurora_postgres_master_username}" + value = module.codefresh_enterprise_backing_services.aurora_postgres_master_username description = "Aurora Postgres Username for the master DB user" } output "aurora_postgres_master_hostname" { - value = "${module.codefresh_enterprise_backing_services.aurora_postgres_master_hostname}" + value = module.codefresh_enterprise_backing_services.aurora_postgres_master_hostname description = "Aurora Postgres DB Master hostname" } output "aurora_postgres_replicas_hostname" { - value = "${module.codefresh_enterprise_backing_services.aurora_postgres_replicas_hostname}" + value = module.codefresh_enterprise_backing_services.aurora_postgres_replicas_hostname description = "Aurora Postgres Replicas hostname" } output "aurora_postgres_cluster_name" { - value = "${module.codefresh_enterprise_backing_services.aurora_postgres_cluster_name}" + value = module.codefresh_enterprise_backing_services.aurora_postgres_cluster_name description = "Aurora Postgres Cluster Identifier" } output "s3_user_name" { - value = "${module.codefresh_enterprise_backing_services.s3_user_name}" + value = module.codefresh_enterprise_backing_services.s3_user_name description = "Normalized IAM user name" } output "s3_user_arn" { - value = "${module.codefresh_enterprise_backing_services.s3_user_arn}" + value = module.codefresh_enterprise_backing_services.s3_user_arn description = "The ARN assigned by AWS for the user" } output "s3_user_unique_id" { - value = "${module.codefresh_enterprise_backing_services.s3_user_unique_id}" + value = module.codefresh_enterprise_backing_services.s3_user_unique_id description = "The user unique ID assigned by AWS" } output "s3_access_key_id" { sensitive = true - value = "${module.codefresh_enterprise_backing_services.s3_access_key_id}" + value = module.codefresh_enterprise_backing_services.s3_access_key_id description = "The access key ID" } output "s3_secret_access_key" { sensitive = true - value = "${module.codefresh_enterprise_backing_services.s3_secret_access_key}" + value = module.codefresh_enterprise_backing_services.s3_secret_access_key description = "The secret access key. This will be written to the state file in plain-text" } output "s3_bucket_arn" { - value = "${module.codefresh_enterprise_backing_services.s3_bucket_arn}" + value = module.codefresh_enterprise_backing_services.s3_bucket_arn description = "The s3 bucket ARN" } output "backup_s3_user_name" { - value = "${module.codefresh_enterprise_backing_services.backup_s3_user_name}" + value = module.codefresh_enterprise_backing_services.backup_s3_user_name description = "Normalized IAM user name" } output "backup_s3_user_arn" { - value = "${module.codefresh_enterprise_backing_services.backup_s3_user_arn}" + value = module.codefresh_enterprise_backing_services.backup_s3_user_arn description = "The ARN assigned by AWS for the user" } output "backup_s3_user_unique_id" { - value = "${module.codefresh_enterprise_backing_services.backup_s3_user_unique_id}" + value = module.codefresh_enterprise_backing_services.backup_s3_user_unique_id description = "The user unique ID assigned by AWS" } output "backup_s3_access_key_id" { sensitive = true - value = "${module.codefresh_enterprise_backing_services.backup_s3_access_key_id}" + value = module.codefresh_enterprise_backing_services.backup_s3_access_key_id description = "The access key ID" } output "backup_s3_secret_access_key" { sensitive = true - value = "${module.codefresh_enterprise_backing_services.backup_s3_secret_access_key}" + value = module.codefresh_enterprise_backing_services.backup_s3_secret_access_key description = "The secret access key. This will be written to the state file in plain-text" } output "backup_s3_bucket_arn" { - value = "${module.codefresh_enterprise_backing_services.backup_s3_bucket_arn}" + value = module.codefresh_enterprise_backing_services.backup_s3_bucket_arn description = "The backup_s3 bucket ARN" } output "acm_arn" { - value = "${module.codefresh_enterprise_backing_services.acm_arn}" + value = module.codefresh_enterprise_backing_services.acm_arn description = "The ARN of the certificate" } output "acm_domain_validation_options" { - value = "${module.codefresh_enterprise_backing_services.acm_domain_validation_options}" + value = module.codefresh_enterprise_backing_services.acm_domain_validation_options description = "CNAME records that are added to the DNS zone to complete certificate validation" } output "documentdb_master_username" { - value = "${module.codefresh_enterprise_backing_services.documentdb_master_username}" + value = module.codefresh_enterprise_backing_services.documentdb_master_username description = "DocumentDB Username for the master DB user" } output "documentdb_cluster_name" { - value = "${module.codefresh_enterprise_backing_services.documentdb_cluster_name}" + value = module.codefresh_enterprise_backing_services.documentdb_cluster_name description = "DocumentDB Cluster Identifier" } output "documentdb_arn" { - value = "${module.codefresh_enterprise_backing_services.documentdb_arn}" + value = module.codefresh_enterprise_backing_services.documentdb_arn description = "Amazon Resource Name (ARN) of the DocumentDB cluster" } output "documentdb_endpoint" { - value = "${module.codefresh_enterprise_backing_services.documentdb_endpoint}" + value = module.codefresh_enterprise_backing_services.documentdb_endpoint description = "Endpoint of the DocumentDB cluster" } output "documentdb_reader_endpoint" { - value = "${module.codefresh_enterprise_backing_services.documentdb_reader_endpoint}" + value = module.codefresh_enterprise_backing_services.documentdb_reader_endpoint description = "Read-only endpoint of the DocumentDB cluster, automatically load-balanced across replicas" } output "documentdb_master_host" { - value = "${module.codefresh_enterprise_backing_services.documentdb_master_host}" + value = module.codefresh_enterprise_backing_services.documentdb_master_host description = "DocumentDB master hostname" } output "documentdb_replicas_host" { - value = "${module.codefresh_enterprise_backing_services.documentdb_replicas_host}" + value = module.codefresh_enterprise_backing_services.documentdb_replicas_host description = "DocumentDB replicas hostname" } diff --git a/deprecated/aws/datadog/main.tf b/deprecated/aws/datadog/main.tf index 650d3cd81..d69999239 100644 --- a/deprecated/aws/datadog/main.tf +++ b/deprecated/aws/datadog/main.tf @@ -5,12 +5,12 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -21,15 +21,15 @@ module "datadog_ids" { module "datadog_aws_integration" { source = "git::https://github.com/cloudposse/terraform-datadog-aws-integration.git?ref=tags/0.2.0" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "datadog" - datadog_external_id = "${lookup(module.datadog_ids.map, "/datadog/datadog_external_id")}" - integrations = "${var.integrations}" + datadog_external_id = lookup(module.datadog_ids.map, "/datadog/datadog_external_id") + integrations = var.integrations } locals { - chamber_service = "${var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service}" + chamber_service = var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service } resource "random_string" "tokens" { @@ -39,12 +39,12 @@ resource "random_string" "tokens" { number = false special = false - keepers = "${module.datadog_ids.map}" + keepers = module.datadog_ids.map } resource "aws_ssm_parameter" "datadog_cluster_agent_token" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "datadog_cluster_agent_token")}" - value = "${random_string.tokens.result}" + name = format(var.chamber_parameter_name, local.chamber_service, "datadog_cluster_agent_token") + value = random_string.tokens.result description = "A cluster-internal secret for agent-to-agent communication. Must be 32+ characters a-zA-Z" type = "String" overwrite = "true" diff --git a/deprecated/aws/datadog/variables.tf b/deprecated/aws/datadog/variables.tf index 63fd6a651..67c9153a3 100644 --- a/deprecated/aws/datadog/variables.tf +++ b/deprecated/aws/datadog/variables.tf @@ -7,7 +7,7 @@ variable "stage" { } variable "integrations" { - type = "list" + type = list(string) description = "List of integration names with permissions to apply (`all`, `core`, `rds`)" } diff --git a/deprecated/aws/docs/main.tf b/deprecated/aws/docs/main.tf index a4e39587d..03034d88e 100644 --- a/deprecated/aws/docs/main.tf +++ b/deprecated/aws/docs/main.tf @@ -5,36 +5,36 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "domain_name" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "region" { - type = "string" + type = string description = "AWS region" } variable "account_id" { - type = "string" + type = string description = "AWS account ID" } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -43,14 +43,14 @@ provider "aws" { region = "us-east-1" assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } # https://www.terraform.io/docs/providers/aws/d/acm_certificate.html data "aws_acm_certificate" "acm_cloudfront_certificate" { provider = "aws.virginia" - domain = "${var.domain_name}" + domain = var.domain_name statuses = ["ISSUED"] types = ["AMAZON_ISSUED"] } @@ -63,19 +63,19 @@ locals { module "docs_user" { source = "git::https://github.com/cloudposse/terraform-aws-iam-system-user.git?ref=tags/0.2.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" + namespace = var.namespace + stage = var.stage + name = local.name } module "origin" { source = "git::https://github.com/cloudposse/terraform-aws-s3-website.git?ref=tags/0.5.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" - hostname = "${local.cdn_domain}" - parent_zone_name = "${var.domain_name}" - region = "${var.region}" + namespace = var.namespace + stage = var.stage + name = local.name + hostname = local.cdn_domain + parent_zone_name = var.domain_name + region = var.region cors_allowed_headers = ["*"] cors_allowed_methods = ["GET"] cors_allowed_origins = ["*"] @@ -100,14 +100,14 @@ module "origin" { # CloudFront CDN fronting origin module "cdn" { source = "git::https://github.com/cloudposse/terraform-aws-cloudfront-cdn.git?ref=tags/0.4.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" + namespace = var.namespace + stage = var.stage + name = local.name aliases = ["${local.cdn_domain}", "docs.cloudposse.com"] - origin_domain_name = "${module.origin.s3_bucket_website_endpoint}" + origin_domain_name = module.origin.s3_bucket_website_endpoint origin_protocol_policy = "http-only" viewer_protocol_policy = "redirect-to-https" - parent_zone_name = "${var.domain_name}" + parent_zone_name = var.domain_name forward_cookies = "none" forward_headers = ["Origin", "Access-Control-Request-Headers", "Access-Control-Request-Method"] default_ttl = 60 @@ -118,5 +118,5 @@ module "cdn" { allowed_methods = ["GET", "HEAD", "OPTIONS"] price_class = "PriceClass_All" default_root_object = "index.html" - acm_certificate_arn = "${data.aws_acm_certificate.acm_cloudfront_certificate.arn}" + acm_certificate_arn = data.aws_acm_certificate.acm_cloudfront_certificate.arn } diff --git a/deprecated/aws/docs/outputs.tf b/deprecated/aws/docs/outputs.tf index 6e9724b43..4409c0281 100644 --- a/deprecated/aws/docs/outputs.tf +++ b/deprecated/aws/docs/outputs.tf @@ -1,80 +1,80 @@ output "docs_user_name" { - value = "${module.docs_user.user_name}" + value = module.docs_user.user_name description = "Normalized IAM user name" } output "docs_user_arn" { - value = "${module.docs_user.user_arn}" + value = module.docs_user.user_arn description = "The ARN assigned by AWS for the user" } output "docs_user_unique_id" { - value = "${module.docs_user.user_unique_id}" + value = module.docs_user.user_unique_id description = "The user unique ID assigned by AWS" } output "docs_user_access_key_id" { - value = "${module.docs_user.access_key_id}" + value = module.docs_user.access_key_id description = "The access key ID" } output "docs_user_secret_access_key" { - value = "${module.docs_user.secret_access_key}" + value = module.docs_user.secret_access_key description = "The secret access key. This will be written to the state file in plain-text" } output "docs_s3_bucket_name" { - value = "${module.origin.s3_bucket_name}" + value = module.origin.s3_bucket_name } output "docs_s3_bucket_domain_name" { - value = "${module.origin.s3_bucket_domain_name}" + value = module.origin.s3_bucket_domain_name } output "docs_s3_bucket_arn" { - value = "${module.origin.s3_bucket_arn}" + value = module.origin.s3_bucket_arn } output "docs_s3_bucket_website_endpoint" { - value = "${module.origin.s3_bucket_website_endpoint}" + value = module.origin.s3_bucket_website_endpoint } output "docs_s3_bucket_website_domain" { - value = "${module.origin.s3_bucket_website_domain}" + value = module.origin.s3_bucket_website_domain } output "docs_s3_bucket_hosted_zone_id" { - value = "${module.origin.s3_bucket_hosted_zone_id}" + value = module.origin.s3_bucket_hosted_zone_id } output "docs_cloudfront_id" { - value = "${module.cdn.cf_id}" + value = module.cdn.cf_id } output "docs_cloudfront_arn" { - value = "${module.cdn.cf_arn}" + value = module.cdn.cf_arn } output "docs_cloudfront_aliases" { - value = "${module.cdn.cf_aliases}" + value = module.cdn.cf_aliases } output "docs_cloudfront_status" { - value = "${module.cdn.cf_status}" + value = module.cdn.cf_status } output "docs_cloudfront_domain_name" { - value = "${module.cdn.cf_domain_name}" + value = module.cdn.cf_domain_name } output "docs_cloudfront_etag" { - value = "${module.cdn.cf_etag}" + value = module.cdn.cf_etag } output "docs_cloudfront_hosted_zone_id" { - value = "${module.cdn.cf_hosted_zone_id}" + value = module.cdn.cf_hosted_zone_id } output "docs_cloudfront_origin_access_identity_path" { - value = "${module.cdn.cf_origin_access_identity}" + value = module.cdn.cf_origin_access_identity } diff --git a/deprecated/aws/ecr/kops_ecr_app.tf b/deprecated/aws/ecr/kops_ecr_app.tf index ef5ff0d11..05c2768d9 100644 --- a/deprecated/aws/ecr/kops_ecr_app.tf +++ b/deprecated/aws/ecr/kops_ecr_app.tf @@ -10,29 +10,29 @@ variable "kops_ecr_app_enabled" { module "kops_ecr_app" { source = "git::https://github.com/cloudposse/terraform-aws-ecr.git?ref=tags/0.6.1" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.kops_ecr_app_repository_name}" + namespace = var.namespace + stage = var.stage + name = var.kops_ecr_app_repository_name - enabled = "${var.kops_ecr_app_enabled}" + enabled = var.kops_ecr_app_enabled principals_full_access = ["${local.principals_full_access}"] principals_readonly_access = ["${local.principals_readonly_access}"] - tags = "${module.label.tags}" + tags = module.label.tags } output "kops_ecr_app_registry_id" { - value = "${module.kops_ecr_app.registry_id}" + value = module.kops_ecr_app.registry_id description = "Registry app ID" } output "kops_ecr_app_registry_url" { - value = "${module.kops_ecr_app.registry_url}" + value = module.kops_ecr_app.registry_url description = "Registry app URL" } output "kops_ecr_app_repository_name" { - value = "${module.kops_ecr_app.repository_name}" + value = module.kops_ecr_app.repository_name description = "Registry app name" } diff --git a/deprecated/aws/ecr/kops_ecr_user.tf b/deprecated/aws/ecr/kops_ecr_user.tf index 2b5755311..a7547c386 100644 --- a/deprecated/aws/ecr/kops_ecr_user.tf +++ b/deprecated/aws/ecr/kops_ecr_user.tf @@ -1,10 +1,10 @@ module "kops_ecr_user" { source = "git::https://github.com/cloudposse/terraform-aws-iam-system-user.git?ref=tags/0.3.0" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "cicd" - tags = "${module.label.tags}" + tags = module.label.tags } data "aws_iam_policy_document" "login" { @@ -17,38 +17,38 @@ data "aws_iam_policy_document" "login" { } resource "aws_iam_policy" "login" { - name = "${module.label.id}" - policy = "${data.aws_iam_policy_document.login.json}" + name = module.label.id + policy = data.aws_iam_policy_document.login.json } resource "aws_iam_user_policy_attachment" "user_login" { - user = "${module.kops_ecr_user.user_name}" - policy_arn = "${aws_iam_policy.login.arn}" + user = module.kops_ecr_user.user_name + policy_arn = aws_iam_policy.login.arn } output "kops_ecr_user_name" { - value = "${module.kops_ecr_user.user_name}" + value = module.kops_ecr_user.user_name description = "Normalized IAM user name" } output "kops_ecr_user_arn" { - value = "${module.kops_ecr_user.user_arn}" + value = module.kops_ecr_user.user_arn description = "The ARN assigned by AWS for the user" } output "kops_ecr_user_unique_id" { - value = "${module.kops_ecr_user.user_unique_id}" + value = module.kops_ecr_user.user_unique_id description = "The user unique ID assigned by AWS" } output "kops_ecr_user_access_key_id" { sensitive = true - value = "${module.kops_ecr_user.access_key_id}" + value = module.kops_ecr_user.access_key_id description = "The access key ID" } output "kops_ecr_user_secret_access_key" { sensitive = true - value = "${module.kops_ecr_user.secret_access_key}" + value = module.kops_ecr_user.secret_access_key description = "The secret access key. This will be written to the state file in plain-text" } diff --git a/deprecated/aws/ecr/main.tf b/deprecated/aws/ecr/main.tf index 425dc3374..9ea6280f8 100644 --- a/deprecated/aws/ecr/main.tf +++ b/deprecated/aws/ecr/main.tf @@ -5,39 +5,39 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "cluster_name" { - type = "string" + type = string description = "kops cluster name" } variable "external_principals_full_access" { - type = "list" + type = list(string) description = "Principal ARN to provide with full access to the ECR" default = [] } variable "external_principals_readonly_access" { - type = "list" + type = list(string) description = "Principal ARN to provide with readonly access to the ECR" default = [] } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -48,13 +48,13 @@ locals { module "label" { source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.5.4" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "ecr" - tags = "${map("Cluster", var.cluster_name)}" + tags = map("Cluster", var.cluster_name) } module "kops_metadata" { source = "git::https://github.com/cloudposse/terraform-aws-kops-data-iam.git?ref=tags/0.1.0" - cluster_name = "${var.cluster_name}" + cluster_name = var.cluster_name } diff --git a/deprecated/aws/eks-backing-services-peering/main.tf b/deprecated/aws/eks-backing-services-peering/main.tf index 66e036e8c..0dea296dd 100644 --- a/deprecated/aws/eks-backing-services-peering/main.tf +++ b/deprecated/aws/eks-backing-services-peering/main.tf @@ -16,12 +16,12 @@ data "aws_vpc" "backing_services_vpc" { module "vpc_peering" { source = "git::https://github.com/cloudposse/terraform-aws-vpc-peering.git?ref=tags/0.1.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - delimiter = "${var.delimiter}" + namespace = var.namespace + stage = var.stage + name = var.name + delimiter = var.delimiter attributes = ["${compact(concat(var.attributes, list("peering")))}"] - tags = "${var.tags}" - requestor_vpc_id = "${data.aws_vpc.eks_vpc.id}" - acceptor_vpc_id = "${data.aws_vpc.backing_services_vpc.id}" + tags = var.tags + requestor_vpc_id = data.aws_vpc.eks_vpc.id + acceptor_vpc_id = data.aws_vpc.backing_services_vpc.id } diff --git a/deprecated/aws/eks-backing-services-peering/outputs.tf b/deprecated/aws/eks-backing-services-peering/outputs.tf index e5bed5be0..2f7ec3399 100644 --- a/deprecated/aws/eks-backing-services-peering/outputs.tf +++ b/deprecated/aws/eks-backing-services-peering/outputs.tf @@ -1,9 +1,9 @@ output "vpc_peering_connection_id" { - value = "${module.vpc_peering.connection_id}" + value = module.vpc_peering.connection_id description = "VPC peering connection ID" } output "vpc_peering_accept_status" { - value = "${module.vpc_peering.accept_status}" + value = module.vpc_peering.accept_status description = "The status of the VPC peering connection request" } diff --git a/deprecated/aws/eks-backing-services-peering/variables.tf b/deprecated/aws/eks-backing-services-peering/variables.tf index 61dca67f8..96bc04012 100644 --- a/deprecated/aws/eks-backing-services-peering/variables.tf +++ b/deprecated/aws/eks-backing-services-peering/variables.tf @@ -1,33 +1,33 @@ variable "namespace" { - type = "string" + type = string description = "Namespace, which could be your organization name, e.g. 'eg' or 'cp'" } variable "stage" { - type = "string" + type = string description = "Stage, e.g. 'prod', 'staging', 'dev' or 'testing'" } variable "name" { - type = "string" + type = string default = "eks" description = "Solution name, e.g. 'app' or 'cluster'" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `name`, `namespace`, `stage`, etc." } variable "attributes" { - type = "list" + type = list(string) default = [] description = "Additional attributes (e.g. `1`)" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. `map('BusinessUnit`,`XYZ`)" } diff --git a/deprecated/aws/eks/eks.tf b/deprecated/aws/eks/eks.tf index fe11eee22..27d70437e 100644 --- a/deprecated/aws/eks/eks.tf +++ b/deprecated/aws/eks/eks.tf @@ -1,89 +1,89 @@ module "label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.1.6" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" - delimiter = "${var.delimiter}" - attributes = "${var.attributes}" - tags = "${var.tags}" - enabled = "${var.enabled}" + namespace = var.namespace + name = var.name + stage = var.stage + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + enabled = var.enabled } locals { # The usage of the specific kubernetes.io/cluster/* resource tags below are required # for EKS and Kubernetes to discover and manage networking resources # https://www.terraform.io/docs/providers/aws/guides/eks-getting-started.html#base-vpc-networking - tags = "${merge(var.tags, map("kubernetes.io/cluster/${module.label.id}", "shared"))}" + tags = merge(var.tags, map("kubernetes.io/cluster/${module.label.id}", "shared")) } data "aws_availability_zones" "available" {} module "vpc" { source = "git::https://github.com/cloudposse/terraform-aws-vpc.git?ref=tags/0.3.4" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - attributes = "${var.attributes}" - tags = "${local.tags}" - cidr_block = "${var.vpc_cidr_block}" + namespace = var.namespace + stage = var.stage + name = var.name + attributes = var.attributes + tags = local.tags + cidr_block = var.vpc_cidr_block } module "subnets" { source = "git::https://github.com/cloudposse/terraform-aws-dynamic-subnets.git?ref=tags/0.3.6" availability_zones = ["${data.aws_availability_zones.available.names}"] - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - attributes = "${var.attributes}" - tags = "${local.tags}" - region = "${var.region}" - vpc_id = "${module.vpc.vpc_id}" - igw_id = "${module.vpc.igw_id}" - cidr_block = "${module.vpc.vpc_cidr_block}" + namespace = var.namespace + stage = var.stage + name = var.name + attributes = var.attributes + tags = local.tags + region = var.region + vpc_id = module.vpc.vpc_id + igw_id = module.vpc.igw_id + cidr_block = module.vpc.vpc_cidr_block nat_gateway_enabled = "true" } module "eks_cluster" { source = "git::https://github.com/cloudposse/terraform-aws-eks-cluster.git?ref=tags/0.1.1" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - attributes = "${var.attributes}" - tags = "${var.tags}" - vpc_id = "${module.vpc.vpc_id}" + namespace = var.namespace + stage = var.stage + name = var.name + attributes = var.attributes + tags = var.tags + vpc_id = module.vpc.vpc_id subnet_ids = ["${module.subnets.public_subnet_ids}"] allowed_security_groups = ["${distinct(compact(concat(var.allowed_security_groups_cluster, list(module.eks_workers.security_group_id))))}"] allowed_cidr_blocks = ["${var.allowed_cidr_blocks_cluster}"] - enabled = "${var.enabled}" + enabled = var.enabled } module "eks_workers" { source = "git::https://github.com/cloudposse/terraform-aws-eks-workers.git?ref=tags/0.1.1" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - attributes = "${var.attributes}" - tags = "${var.tags}" - image_id = "${var.image_id}" - eks_worker_ami_name_filter = "${var.eks_worker_ami_name_filter}" - instance_type = "${var.instance_type}" - vpc_id = "${module.vpc.vpc_id}" + namespace = var.namespace + stage = var.stage + name = var.name + attributes = var.attributes + tags = var.tags + image_id = var.image_id + eks_worker_ami_name_filter = var.eks_worker_ami_name_filter + instance_type = var.instance_type + vpc_id = module.vpc.vpc_id subnet_ids = ["${module.subnets.public_subnet_ids}"] - health_check_type = "${var.health_check_type}" - min_size = "${var.min_size}" - max_size = "${var.max_size}" - wait_for_capacity_timeout = "${var.wait_for_capacity_timeout}" - associate_public_ip_address = "${var.associate_public_ip_address}" - cluster_name = "${module.eks_cluster.eks_cluster_id}" - cluster_endpoint = "${module.eks_cluster.eks_cluster_endpoint}" - cluster_certificate_authority_data = "${module.eks_cluster.eks_cluster_certificate_authority_data}" - cluster_security_group_id = "${module.eks_cluster.security_group_id}" + health_check_type = var.health_check_type + min_size = var.min_size + max_size = var.max_size + wait_for_capacity_timeout = var.wait_for_capacity_timeout + associate_public_ip_address = var.associate_public_ip_address + cluster_name = module.eks_cluster.eks_cluster_id + cluster_endpoint = module.eks_cluster.eks_cluster_endpoint + cluster_certificate_authority_data = module.eks_cluster.eks_cluster_certificate_authority_data + cluster_security_group_id = module.eks_cluster.security_group_id allowed_security_groups = ["${var.allowed_security_groups_workers}"] allowed_cidr_blocks = ["${var.allowed_cidr_blocks_workers}"] - enabled = "${var.enabled}" + enabled = var.enabled # Auto-scaling policies and CloudWatch metric alarms - autoscaling_policies_enabled = "${var.autoscaling_policies_enabled}" - cpu_utilization_high_threshold_percent = "${var.cpu_utilization_high_threshold_percent}" - cpu_utilization_low_threshold_percent = "${var.cpu_utilization_low_threshold_percent}" + autoscaling_policies_enabled = var.autoscaling_policies_enabled + cpu_utilization_high_threshold_percent = var.cpu_utilization_high_threshold_percent + cpu_utilization_low_threshold_percent = var.cpu_utilization_low_threshold_percent } diff --git a/deprecated/aws/eks/kubectl.tf b/deprecated/aws/eks/kubectl.tf index 34afe77e0..38e03c9c6 100644 --- a/deprecated/aws/eks/kubectl.tf +++ b/deprecated/aws/eks/kubectl.tf @@ -4,26 +4,26 @@ locals { } resource "local_file" "kubeconfig" { - count = "${var.enabled == "true" && var.apply_config_map_aws_auth == "true" ? 1 : 0}" - content = "${module.eks_cluster.kubeconfig}" - filename = "${local.kubeconfig_filename}" + count = var.enabled == "true" && var.apply_config_map_aws_auth == "true" ? 1 : 0 + content = module.eks_cluster.kubeconfig + filename = local.kubeconfig_filename } resource "local_file" "config_map_aws_auth" { - count = "${var.enabled == "true" && var.apply_config_map_aws_auth == "true" ? 1 : 0}" - content = "${module.eks_workers.config_map_aws_auth}" - filename = "${local.config_map_aws_auth_filename}" + count = var.enabled == "true" && var.apply_config_map_aws_auth == "true" ? 1 : 0 + content = module.eks_workers.config_map_aws_auth + filename = local.config_map_aws_auth_filename } resource "null_resource" "apply_config_map_aws_auth" { - count = "${var.enabled == "true" && var.apply_config_map_aws_auth == "true" ? 1 : 0}" + count = var.enabled == "true" && var.apply_config_map_aws_auth == "true" ? 1 : 0 provisioner "local-exec" { command = "kubectl apply -f ${local.config_map_aws_auth_filename} --kubeconfig ${local.kubeconfig_filename}" } triggers { - kubeconfig_rendered = "${module.eks_cluster.kubeconfig}" - config_map_aws_auth_rendered = "${module.eks_workers.config_map_aws_auth}" + kubeconfig_rendered = module.eks_cluster.kubeconfig + config_map_aws_auth_rendered = module.eks_workers.config_map_aws_auth } } diff --git a/deprecated/aws/eks/main.tf b/deprecated/aws/eks/main.tf index ba39349f9..547a80599 100644 --- a/deprecated/aws/eks/main.tf +++ b/deprecated/aws/eks/main.tf @@ -5,11 +5,11 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } diff --git a/deprecated/aws/eks/outputs.tf b/deprecated/aws/eks/outputs.tf index cb9c921dd..1f926824c 100644 --- a/deprecated/aws/eks/outputs.tf +++ b/deprecated/aws/eks/outputs.tf @@ -1,119 +1,119 @@ output "kubeconfig" { description = "`kubeconfig` configuration to connect to the cluster using `kubectl`. https://www.terraform.io/docs/providers/aws/guides/eks-getting-started.html#obtaining-kubectl-configuration-from-terraform" - value = "${module.eks_cluster.kubeconfig}" + value = module.eks_cluster.kubeconfig } output "config_map_aws_auth" { description = "Kubernetes ConfigMap configuration to allow the worker nodes to join the EKS cluster. https://www.terraform.io/docs/providers/aws/guides/eks-getting-started.html#required-kubernetes-configuration-to-join-worker-nodes" - value = "${module.eks_workers.config_map_aws_auth}" + value = module.eks_workers.config_map_aws_auth } output "eks_cluster_security_group_id" { description = "ID of the EKS cluster Security Group" - value = "${module.eks_cluster.security_group_id}" + value = module.eks_cluster.security_group_id } output "eks_cluster_security_group_arn" { description = "ARN of the EKS cluster Security Group" - value = "${module.eks_cluster.security_group_arn}" + value = module.eks_cluster.security_group_arn } output "eks_cluster_security_group_name" { description = "Name of the EKS cluster Security Group" - value = "${module.eks_cluster.security_group_name}" + value = module.eks_cluster.security_group_name } output "eks_cluster_id" { description = "The name of the cluster" - value = "${module.eks_cluster.eks_cluster_id}" + value = module.eks_cluster.eks_cluster_id } output "eks_cluster_arn" { description = "The Amazon Resource Name (ARN) of the cluster" - value = "${module.eks_cluster.eks_cluster_arn}" + value = module.eks_cluster.eks_cluster_arn } output "eks_cluster_certificate_authority_data" { description = "The base64 encoded certificate data required to communicate with the cluster" - value = "${module.eks_cluster.eks_cluster_certificate_authority_data}" + value = module.eks_cluster.eks_cluster_certificate_authority_data } output "eks_cluster_endpoint" { description = "The endpoint for the Kubernetes API server" - value = "${module.eks_cluster.eks_cluster_endpoint}" + value = module.eks_cluster.eks_cluster_endpoint } output "eks_cluster_version" { description = "The Kubernetes server version of the cluster" - value = "${module.eks_cluster.eks_cluster_version}" + value = module.eks_cluster.eks_cluster_version } output "workers_launch_template_id" { description = "ID of the launch template" - value = "${module.eks_workers.launch_template_id}" + value = module.eks_workers.launch_template_id } output "workers_launch_template_arn" { description = "ARN of the launch template" - value = "${module.eks_workers.launch_template_arn}" + value = module.eks_workers.launch_template_arn } output "workers_autoscaling_group_id" { description = "The AutoScaling Group ID" - value = "${module.eks_workers.autoscaling_group_id}" + value = module.eks_workers.autoscaling_group_id } output "workers_autoscaling_group_name" { description = "The AutoScaling Group name" - value = "${module.eks_workers.autoscaling_group_name}" + value = module.eks_workers.autoscaling_group_name } output "workers_autoscaling_group_arn" { description = "ARN of the AutoScaling Group" - value = "${module.eks_workers.autoscaling_group_arn}" + value = module.eks_workers.autoscaling_group_arn } output "workers_autoscaling_group_min_size" { description = "The minimum size of the AutoScaling Group" - value = "${module.eks_workers.autoscaling_group_min_size}" + value = module.eks_workers.autoscaling_group_min_size } output "workers_autoscaling_group_max_size" { description = "The maximum size of the AutoScaling Group" - value = "${module.eks_workers.autoscaling_group_max_size}" + value = module.eks_workers.autoscaling_group_max_size } output "workers_autoscaling_group_desired_capacity" { description = "The number of Amazon EC2 instances that should be running in the group" - value = "${module.eks_workers.autoscaling_group_desired_capacity}" + value = module.eks_workers.autoscaling_group_desired_capacity } output "workers_autoscaling_group_default_cooldown" { description = "Time between a scaling activity and the succeeding scaling activity" - value = "${module.eks_workers.autoscaling_group_default_cooldown}" + value = module.eks_workers.autoscaling_group_default_cooldown } output "workers_autoscaling_group_health_check_grace_period" { description = "Time after instance comes into service before checking health" - value = "${module.eks_workers.autoscaling_group_health_check_grace_period}" + value = module.eks_workers.autoscaling_group_health_check_grace_period } output "workers_autoscaling_group_health_check_type" { description = "`EC2` or `ELB`. Controls how health checking is done" - value = "${module.eks_workers.autoscaling_group_health_check_type}" + value = module.eks_workers.autoscaling_group_health_check_type } output "workers_security_group_id" { description = "ID of the worker nodes Security Group" - value = "${module.eks_workers.security_group_id}" + value = module.eks_workers.security_group_id } output "workers_security_group_arn" { description = "ARN of the worker nodes Security Group" - value = "${module.eks_workers.security_group_arn}" + value = module.eks_workers.security_group_arn } output "workers_security_group_name" { description = "Name of the worker nodes Security Group" - value = "${module.eks_workers.security_group_name}" + value = module.eks_workers.security_group_name } diff --git a/deprecated/aws/eks/variables.tf b/deprecated/aws/eks/variables.tf index 831b30d05..251ef26d1 100644 --- a/deprecated/aws/eks/variables.tf +++ b/deprecated/aws/eks/variables.tf @@ -1,98 +1,98 @@ variable "namespace" { - type = "string" + type = string description = "Namespace, which could be your organization name, e.g. 'eg' or 'cp'" } variable "stage" { - type = "string" + type = string description = "Stage, e.g. 'prod', 'staging', 'dev' or 'testing'" } variable "name" { - type = "string" + type = string default = "eks" description = "Solution name, e.g. 'app' or 'cluster'" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `name`, `namespace`, `stage`, etc." } variable "attributes" { - type = "list" + type = list(string) default = [] description = "Additional attributes (e.g. `1`)" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. `map('BusinessUnit`,`XYZ`)" } variable "enabled" { - type = "string" + type = string description = "Whether to create the resources. Set to `false` to prevent the module from creating any resources" default = "true" } variable "allowed_security_groups_cluster" { - type = "list" + type = list(string) default = [] description = "List of Security Group IDs to be allowed to connect to the EKS cluster" } variable "allowed_security_groups_workers" { - type = "list" + type = list(string) default = [] description = "List of Security Group IDs to be allowed to connect to the worker nodes" } variable "allowed_cidr_blocks_cluster" { - type = "list" + type = list(string) default = [] description = "List of CIDR blocks to be allowed to connect to the EKS cluster" } variable "allowed_cidr_blocks_workers" { - type = "list" + type = list(string) default = [] description = "List of CIDR blocks to be allowed to connect to the worker nodes" } variable "region" { - type = "string" + type = string description = "AWS Region" } variable "vpc_cidr_block" { - type = "string" + type = string default = "172.30.0.0/16" description = "VPC CIDR block. See https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html for more details" } variable "image_id" { - type = "string" + type = string default = "" description = "EC2 image ID to launch. If not provided, the module will lookup the most recent EKS AMI. See https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html for more details on EKS-optimized images" } variable "eks_worker_ami_name_filter" { - type = "string" + type = string description = "AMI name filter to lookup the most recent EKS AMI if `image_id` is not provided" default = "amazon-eks-node-v*" } variable "instance_type" { - type = "string" + type = string default = "t2.medium" description = "Instance type to launch" } variable "health_check_type" { - type = "string" + type = string description = "Controls how health checking is done. Valid values are `EC2` or `ELB`" default = "EC2" } @@ -108,7 +108,7 @@ variable "min_size" { } variable "wait_for_capacity_timeout" { - type = "string" + type = string description = "A maximum duration that Terraform should wait for ASG instances to be healthy before timing out. Setting this to '0' causes Terraform to skip all Capacity Waiting behavior" default = "10m" } @@ -119,25 +119,25 @@ variable "associate_public_ip_address" { } variable "autoscaling_policies_enabled" { - type = "string" + type = string default = "true" description = "Whether to create `aws_autoscaling_policy` and `aws_cloudwatch_metric_alarm` resources to control Auto Scaling" } variable "cpu_utilization_high_threshold_percent" { - type = "string" + type = string default = "80" description = "Worker nodes AutoScaling Group CPU utilization high threshold percent" } variable "cpu_utilization_low_threshold_percent" { - type = "string" + type = string default = "20" description = "Worker nodes AutoScaling Group CPU utilization low threshold percent" } variable "apply_config_map_aws_auth" { - type = "string" + type = string default = "true" description = "Whether to generate local files from `kubeconfig` and `config_map_aws_auth` and perform `kubectl apply` to apply the ConfigMap to allow the worker nodes to join the EKS cluster" } diff --git a/deprecated/aws/iam/audit.tf b/deprecated/aws/iam/audit.tf index f3791f7b6..0045f9872 100644 --- a/deprecated/aws/iam/audit.tf +++ b/deprecated/aws/iam/audit.tf @@ -1,5 +1,5 @@ variable "audit_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `audit` account" default = [] } @@ -7,18 +7,18 @@ variable "audit_account_user_names" { # Provision group access to audit account. Careful! Very few people, if any should have access to this account. module "organization_access_group_audit" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "audit") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "audit") == true ? "true" : "false" + namespace = var.namespace stage = "audit" name = "admin" - user_names = "${var.audit_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.audit_account_id}" + user_names = var.audit_account_user_names + member_account_id = data.terraform_remote_state.accounts.audit_account_id require_mfa = "true" } module "organization_access_group_ssm_audit" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "audit") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "audit") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_audit" { output "audit_switchrole_url" { description = "URL to the IAM console to switch to the audit account organization access role" - value = "${module.organization_access_group_audit.switchrole_url}" + value = module.organization_access_group_audit.switchrole_url } diff --git a/deprecated/aws/iam/corp.tf b/deprecated/aws/iam/corp.tf index de4142758..5c0f0417c 100644 --- a/deprecated/aws/iam/corp.tf +++ b/deprecated/aws/iam/corp.tf @@ -1,5 +1,5 @@ variable "corp_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `corp` account" default = [] } @@ -7,18 +7,18 @@ variable "corp_account_user_names" { # Provision group access to corp account module "organization_access_group_corp" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "corp") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "corp") == true ? "true" : "false" + namespace = var.namespace stage = "corp" name = "admin" - user_names = "${var.corp_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.corp_account_id}" + user_names = var.corp_account_user_names + member_account_id = data.terraform_remote_state.accounts.corp_account_id require_mfa = "true" } module "organization_access_group_ssm_corp" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "corp") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "corp") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_corp" { output "corp_switchrole_url" { description = "URL to the IAM console to switch to the corp account organization access role" - value = "${module.organization_access_group_corp.switchrole_url}" + value = module.organization_access_group_corp.switchrole_url } diff --git a/deprecated/aws/iam/data.tf b/deprecated/aws/iam/data.tf index 5525b646a..43150f102 100644 --- a/deprecated/aws/iam/data.tf +++ b/deprecated/aws/iam/data.tf @@ -1,5 +1,5 @@ variable "data_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `data` account" default = [] } @@ -7,18 +7,18 @@ variable "data_account_user_names" { # Provision group access to data account module "organization_access_group_data" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "data") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "data") == true ? "true" : "false" + namespace = var.namespace stage = "data" name = "admin" - user_names = "${var.data_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.data_account_id}" + user_names = var.data_account_user_names + member_account_id = data.terraform_remote_state.accounts.data_account_id require_mfa = "true" } module "organization_access_group_ssm_data" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "data") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "data") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_data" { output "data_switchrole_url" { description = "URL to the IAM console to switch to the data account organization access role" - value = "${module.organization_access_group_data.switchrole_url}" + value = module.organization_access_group_data.switchrole_url } diff --git a/deprecated/aws/iam/dev.tf b/deprecated/aws/iam/dev.tf index 5cc223e15..2d4aff902 100644 --- a/deprecated/aws/iam/dev.tf +++ b/deprecated/aws/iam/dev.tf @@ -1,5 +1,5 @@ variable "dev_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `dev` account" default = [] } @@ -7,18 +7,18 @@ variable "dev_account_user_names" { # Provision group access to dev account module "organization_access_group_dev" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "dev") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "dev") == true ? "true" : "false" + namespace = var.namespace stage = "dev" name = "admin" - user_names = "${var.dev_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.dev_account_id}" + user_names = var.dev_account_user_names + member_account_id = data.terraform_remote_state.accounts.dev_account_id require_mfa = "true" } module "organization_access_group_ssm_dev" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "dev") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "dev") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_dev" { output "dev_switchrole_url" { description = "URL to the IAM console to switch to the dev account organization access role" - value = "${module.organization_access_group_dev.switchrole_url}" + value = module.organization_access_group_dev.switchrole_url } diff --git a/deprecated/aws/iam/identity.tf b/deprecated/aws/iam/identity.tf index 49e91853c..fca252d46 100644 --- a/deprecated/aws/iam/identity.tf +++ b/deprecated/aws/iam/identity.tf @@ -1,5 +1,5 @@ variable "identity_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `identity` account" default = [] } @@ -7,18 +7,18 @@ variable "identity_account_user_names" { # Provision group access to identity account module "organization_access_group_identity" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "identity") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "identity") == true ? "true" : "false" + namespace = var.namespace stage = "identity" name = "admin" - user_names = "${var.identity_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.identity_account_id}" + user_names = var.identity_account_user_names + member_account_id = data.terraform_remote_state.accounts.identity_account_id require_mfa = "true" } module "organization_access_group_ssm_identity" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "identity") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "identity") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_identity" { output "identity_switchrole_url" { description = "URL to the IAM console to switch to the identity account organization access role" - value = "${module.organization_access_group_identity.switchrole_url}" + value = module.organization_access_group_identity.switchrole_url } diff --git a/deprecated/aws/iam/main.tf b/deprecated/aws/iam/main.tf index 6f8d5bd96..cdf6f584c 100644 --- a/deprecated/aws/iam/main.tf +++ b/deprecated/aws/iam/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } diff --git a/deprecated/aws/iam/prod.tf b/deprecated/aws/iam/prod.tf index 2777c1b5f..5021d3f8e 100644 --- a/deprecated/aws/iam/prod.tf +++ b/deprecated/aws/iam/prod.tf @@ -1,5 +1,5 @@ variable "prod_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `prod` account" default = [] } @@ -7,18 +7,18 @@ variable "prod_account_user_names" { # Provision group access to production account module "organization_access_group_prod" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "prod") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "prod") == true ? "true" : "false" + namespace = var.namespace stage = "prod" name = "admin" - user_names = "${var.prod_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.prod_account_id}" + user_names = var.prod_account_user_names + member_account_id = data.terraform_remote_state.accounts.prod_account_id require_mfa = "true" } module "organization_access_group_ssm_prod" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "prod") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "prod") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_prod" { output "prod_switchrole_url" { description = "URL to the IAM console to switch to the prod account organization access role" - value = "${module.organization_access_group_prod.switchrole_url}" + value = module.organization_access_group_prod.switchrole_url } diff --git a/deprecated/aws/iam/security.tf b/deprecated/aws/iam/security.tf index 5b5374773..9a71504ac 100644 --- a/deprecated/aws/iam/security.tf +++ b/deprecated/aws/iam/security.tf @@ -1,5 +1,5 @@ variable "security_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `security` account" default = [] } @@ -7,18 +7,18 @@ variable "security_account_user_names" { # Provision group access to security account module "organization_access_group_security" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "security") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "security") == true ? "true" : "false" + namespace = var.namespace stage = "security" name = "admin" - user_names = "${var.security_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.security_account_id}" + user_names = var.security_account_user_names + member_account_id = data.terraform_remote_state.accounts.security_account_id require_mfa = "true" } module "organization_access_group_ssm_security" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "security") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "security") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_security" { output "security_switchrole_url" { description = "URL to the IAM console to switch to the security account organization access role" - value = "${module.organization_access_group_security.switchrole_url}" + value = module.organization_access_group_security.switchrole_url } diff --git a/deprecated/aws/iam/staging.tf b/deprecated/aws/iam/staging.tf index f9e2f87c1..0b4963760 100644 --- a/deprecated/aws/iam/staging.tf +++ b/deprecated/aws/iam/staging.tf @@ -1,5 +1,5 @@ variable "staging_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `staging` account" default = [] } @@ -7,18 +7,18 @@ variable "staging_account_user_names" { # Provision group access to staging account module "organization_access_group_staging" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "staging") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "staging") == true ? "true" : "false" + namespace = var.namespace stage = "staging" name = "admin" - user_names = "${var.staging_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.staging_account_id}" + user_names = var.staging_account_user_names + member_account_id = data.terraform_remote_state.accounts.staging_account_id require_mfa = "true" } module "organization_access_group_ssm_staging" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "staging") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "staging") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_staging" { output "staging_switchrole_url" { description = "URL to the IAM console to switch to the staging account organization access role" - value = "${module.organization_access_group_staging.switchrole_url}" + value = module.organization_access_group_staging.switchrole_url } diff --git a/deprecated/aws/iam/testing.tf b/deprecated/aws/iam/testing.tf index d19ec7a03..f568994c0 100644 --- a/deprecated/aws/iam/testing.tf +++ b/deprecated/aws/iam/testing.tf @@ -1,5 +1,5 @@ variable "testing_account_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant access to the `testing` account" default = [] } @@ -7,18 +7,18 @@ variable "testing_account_user_names" { # Provision group access to testing account module "organization_access_group_testing" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.accounts_enabled, "testing") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.accounts_enabled, "testing") == true ? "true" : "false" + namespace = var.namespace stage = "testing" name = "admin" - user_names = "${var.testing_account_user_names}" - member_account_id = "${data.terraform_remote_state.accounts.testing_account_id}" + user_names = var.testing_account_user_names + member_account_id = data.terraform_remote_state.accounts.testing_account_id require_mfa = "true" } module "organization_access_group_ssm_testing" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - enabled = "${contains(var.accounts_enabled, "testing") == true ? "true" : "false"}" + enabled = contains(var.accounts_enabled, "testing") == true ? "true" : "false" parameter_write = [ { @@ -33,5 +33,5 @@ module "organization_access_group_ssm_testing" { output "testing_switchrole_url" { description = "URL to the IAM console to switch to the testing account organization access role" - value = "${module.organization_access_group_testing.switchrole_url}" + value = module.organization_access_group_testing.switchrole_url } diff --git a/deprecated/aws/iam/variables.tf b/deprecated/aws/iam/variables.tf index bb2b1ca23..c88f445aa 100644 --- a/deprecated/aws/iam/variables.tf +++ b/deprecated/aws/iam/variables.tf @@ -1,19 +1,19 @@ variable "accounts_enabled" { - type = "list" + type = list(string) description = "Accounts to enable" default = ["dev", "staging", "prod", "testing", "audit"] } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage, e.g. 'prod', 'staging', 'dev', or 'test'" } diff --git a/deprecated/aws/keycloak-backing-services/aurora-mysql.tf b/deprecated/aws/keycloak-backing-services/aurora-mysql.tf index bf87e1f9a..c420bea54 100644 --- a/deprecated/aws/keycloak-backing-services/aurora-mysql.tf +++ b/deprecated/aws/keycloak-backing-services/aurora-mysql.tf @@ -1,44 +1,44 @@ # https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBInstance.html variable "mysql_name" { - type = "string" + type = string description = "Name of the application, e.g. `app` or `analytics`" default = "mysql" } variable "mysql_admin_user" { - type = "string" + type = string description = "MySQL admin user name" default = "" } variable "mysql_admin_password" { - type = "string" + type = string description = "MySQL password for the admin user" default = "" } variable "mysql_db_name" { - type = "string" + type = string description = "MySQL database name" default = "" } # https://aws.amazon.com/rds/aurora/pricing variable "mysql_instance_type" { - type = "string" + type = string default = "db.t3.small" description = "EC2 instance type for Aurora MySQL cluster" } variable "mysql_cluster_size" { - type = "string" + type = string default = "2" description = "MySQL cluster size" } variable "mysql_cluster_enabled" { - type = "string" + type = string default = "false" description = "Set to false to prevent the module from creating any resources" } @@ -49,108 +49,108 @@ variable "mysql_cluster_publicly_accessible" { } variable "mysql_cluster_allowed_cidr_blocks" { - type = "string" + type = string default = "0.0.0.0/0" description = "Comma separated string list of CIDR blocks allowed to access the cluster, or SSM parameter key for it" } variable "mysql_storage_encrypted" { - type = "string" + type = string default = "false" description = "Set to true to keep the database contents encrypted" } variable "mysql_kms_key_id" { - type = "string" + type = string default = "alias/aws/rds" description = "KMS key ID, ARN, or alias to use for encrypting MySQL database" } variable "mysql_deletion_protection" { - type = "string" + type = string default = "true" description = "Set to true to protect the database from deletion" } variable "mysql_skip_final_snapshot" { - type = "string" + type = string default = "false" description = "Determines whether a final DB snapshot is created before the DB cluster is deleted" } variable "vpc_id" { - type = "string" + type = string description = "The AWS ID of the VPC to create the cluster in, or SSM parameter key for it" } variable "vpc_subnet_ids" { - type = "string" + type = string description = "Comma separated string list of AWS Subnet IDs in which to place the database, or SSM parameter key for it" } resource "random_pet" "mysql_db_name" { - count = "${local.mysql_cluster_enabled && length(var.mysql_db_name) == 0 ? 1 : 0}" + count = local.mysql_cluster_enabled && length(var.mysql_db_name) == 0 ? 1 : 0 separator = "_" } resource "random_string" "mysql_admin_user" { - count = "${local.mysql_cluster_enabled && length(var.mysql_admin_user) == 0 ? 1 : 0}" + count = local.mysql_cluster_enabled && length(var.mysql_admin_user) == 0 ? 1 : 0 length = 8 number = false special = false } resource "random_string" "mysql_admin_password" { - count = "${local.mysql_cluster_enabled && length(var.mysql_admin_password) == 0 ? 1 : 0}" + count = local.mysql_cluster_enabled && length(var.mysql_admin_password) == 0 ? 1 : 0 length = 33 special = false } # "Read SSM parameter to get allowed CIDR blocks" data "aws_ssm_parameter" "allowed_cidr_blocks" { - count = "${local.allowed_cidr_blocks_use_ssm ? 1 : 0}" + count = local.allowed_cidr_blocks_use_ssm ? 1 : 0 # The data source will throw an error if it cannot find the parameter, # name = "${substr(mysql_cluster_allowed_cidr_blocks, 0, 1) == "/" ? mysql_cluster_allowed_cidr_blocks : "/aws/service/global-infrastructure/version"}" - name = "${var.mysql_cluster_allowed_cidr_blocks}" + name = var.mysql_cluster_allowed_cidr_blocks } # "Read SSM parameter to get allowed VPC ID" data "aws_ssm_parameter" "vpc_id" { - count = "${local.vpc_id_use_ssm ? 1 : 0}" + count = local.vpc_id_use_ssm ? 1 : 0 # The data source will throw an error if it cannot find the parameter, # name = "${substr(mysql_cluster_allowed_cidr_blocks, 0, 1) == "/" ? mysql_cluster_allowed_cidr_blocks : "/aws/service/global-infrastructure/version"}" - name = "${var.vpc_id}" + name = var.vpc_id } # "Read SSM parameter to get allowed VPC subnet IDs" data "aws_ssm_parameter" "vpc_subnet_ids" { - count = "${local.vpc_subnet_ids_use_ssm ? 1 : 0}" + count = local.vpc_subnet_ids_use_ssm ? 1 : 0 # The data source will throw an error if it cannot find the parameter, # name = "${substr(mysql_cluster_allowed_cidr_blocks, 0, 1) == "/" ? mysql_cluster_allowed_cidr_blocks : "/aws/service/global-infrastructure/version"}" - name = "${var.vpc_subnet_ids}" + name = var.vpc_subnet_ids } locals { - mysql_cluster_enabled = "${var.mysql_cluster_enabled == "true"}" - mysql_admin_user = "${length(var.mysql_admin_user) > 0 ? var.mysql_admin_user : join("", random_string.mysql_admin_user.*.result)}" - mysql_admin_password = "${length(var.mysql_admin_password) > 0 ? var.mysql_admin_password : join("", random_string.mysql_admin_password.*.result)}" - mysql_db_name = "${length(var.mysql_db_name) > 0 ? var.mysql_db_name : join("", random_pet.mysql_db_name.*.id)}" + mysql_cluster_enabled = var.mysql_cluster_enabled == "true" + mysql_admin_user = length(var.mysql_admin_user) > 0 ? var.mysql_admin_user : join("", random_string.mysql_admin_user.*.result) + mysql_admin_password = length(var.mysql_admin_password) > 0 ? var.mysql_admin_password : join("", random_string.mysql_admin_password.*.result) + mysql_db_name = length(var.mysql_db_name) > 0 ? var.mysql_db_name : join("", random_pet.mysql_db_name.*.id) - allowed_cidr_blocks_use_ssm = "${substr(var.mysql_cluster_allowed_cidr_blocks, 0, 1) == "/" && local.mysql_cluster_enabled}" - vpc_id_use_ssm = "${substr(var.vpc_id, 0, 1) == "/" && local.mysql_cluster_enabled}" - vpc_subnet_ids_use_ssm = "${substr(var.vpc_subnet_ids, 0, 1) == "/" && local.mysql_cluster_enabled}" + allowed_cidr_blocks_use_ssm = substr(var.mysql_cluster_allowed_cidr_blocks, 0, 1) == "/" && local.mysql_cluster_enabled + vpc_id_use_ssm = substr(var.vpc_id, 0, 1) == "/" && local.mysql_cluster_enabled + vpc_subnet_ids_use_ssm = substr(var.vpc_subnet_ids, 0, 1) == "/" && local.mysql_cluster_enabled - allowed_cidr_blocks_string = "${local.allowed_cidr_blocks_use_ssm ? join("", data.aws_ssm_parameter.allowed_cidr_blocks.*.value) : var.mysql_cluster_allowed_cidr_blocks}" - vpc_subnet_ids_string = "${local.vpc_subnet_ids_use_ssm ? join("", data.aws_ssm_parameter.vpc_subnet_ids.*.value) : var.vpc_subnet_ids}" + allowed_cidr_blocks_string = local.allowed_cidr_blocks_use_ssm ? join("", data.aws_ssm_parameter.allowed_cidr_blocks.*.value) : var.mysql_cluster_allowed_cidr_blocks + vpc_subnet_ids_string = local.vpc_subnet_ids_use_ssm ? join("", data.aws_ssm_parameter.vpc_subnet_ids.*.value) : var.vpc_subnet_ids allowed_cidr_blocks = [ "${split(",", local.allowed_cidr_blocks_string)}", ] - vpc_id = "${local.vpc_id_use_ssm ? join("", data.aws_ssm_parameter.vpc_id.*.value) : var.vpc_id}" + vpc_id = local.vpc_id_use_ssm ? join("", data.aws_ssm_parameter.vpc_id.*.value) : var.vpc_id vpc_subnet_ids = [ "${split(",", local.vpc_subnet_ids_string)}", @@ -158,76 +158,76 @@ locals { } data "aws_kms_key" "mysql" { - key_id = "${var.mysql_kms_key_id}" + key_id = var.mysql_kms_key_id } module "aurora_mysql" { source = "git::https://github.com/cloudposse/terraform-aws-rds-cluster.git?ref=tags/0.15.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.mysql_name}" + namespace = var.namespace + stage = var.stage + name = var.mysql_name attributes = ["keycloak"] engine = "aurora-mysql" cluster_family = "aurora-mysql5.7" - instance_type = "${var.mysql_instance_type}" - cluster_size = "${var.mysql_cluster_size}" - admin_user = "${local.mysql_admin_user}" - admin_password = "${local.mysql_admin_password}" - db_name = "${local.mysql_db_name}" + instance_type = var.mysql_instance_type + cluster_size = var.mysql_cluster_size + admin_user = local.mysql_admin_user + admin_password = local.mysql_admin_password + db_name = local.mysql_db_name db_port = "3306" - vpc_id = "${local.vpc_id}" - subnets = "${local.vpc_subnet_ids}" - zone_id = "${local.dns_zone_id}" - enabled = "${var.mysql_cluster_enabled}" + vpc_id = local.vpc_id + subnets = local.vpc_subnet_ids + zone_id = local.dns_zone_id + enabled = var.mysql_cluster_enabled - storage_encrypted = "${var.mysql_storage_encrypted}" - kms_key_arn = "${var.mysql_storage_encrypted ? data.aws_kms_key.mysql.arn : ""}" - deletion_protection = "${var.mysql_deletion_protection}" - skip_final_snapshot = "${var.mysql_skip_final_snapshot}" - publicly_accessible = "${var.mysql_cluster_publicly_accessible}" - allowed_cidr_blocks = "${local.allowed_cidr_blocks}" + storage_encrypted = var.mysql_storage_encrypted + kms_key_arn = var.mysql_storage_encrypted ? data.aws_kms_key.mysql.arn : "" + deletion_protection = var.mysql_deletion_protection + skip_final_snapshot = var.mysql_skip_final_snapshot + publicly_accessible = var.mysql_cluster_publicly_accessible + allowed_cidr_blocks = local.allowed_cidr_blocks } resource "aws_ssm_parameter" "aurora_mysql_database_name" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_name")}" - value = "${module.aurora_mysql.name}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_name") + value = module.aurora_mysql.name description = "Aurora MySQL Database Name" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_master_username" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_user")}" - value = "${module.aurora_mysql.user}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_user") + value = module.aurora_mysql.user description = "Aurora MySQL Username for the master DB user" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_master_password" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_password")}" - value = "${local.mysql_admin_password}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_password") + value = local.mysql_admin_password description = "Aurora MySQL Password for the master DB user" type = "SecureString" overwrite = "true" - key_id = "${var.chamber_kms_key_id}" + key_id = var.chamber_kms_key_id } resource "aws_ssm_parameter" "aurora_mysql_master_hostname" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_host")}" - value = "${module.aurora_mysql.master_host}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_host") + value = module.aurora_mysql.master_host description = "Aurora MySQL DB Master hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_port" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_port")}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_port") value = "3306" description = "Aurora MySQL DB Master hostname" type = "String" @@ -235,44 +235,44 @@ resource "aws_ssm_parameter" "aurora_mysql_port" { } resource "aws_ssm_parameter" "aurora_mysql_replicas_hostname" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_aurora_mysql_replicas_hostname")}" - value = "${module.aurora_mysql.replicas_host}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_aurora_mysql_replicas_hostname") + value = module.aurora_mysql.replicas_host description = "Aurora MySQL DB Replicas hostname" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "aurora_mysql_cluster_name" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_aurora_mysql_cluster_name")}" - value = "${module.aurora_mysql.cluster_name}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_aurora_mysql_cluster_name") + value = module.aurora_mysql.cluster_name description = "Aurora MySQL DB Cluster Identifier" type = "String" overwrite = "true" } output "aurora_mysql_database_name" { - value = "${module.aurora_mysql.name}" + value = module.aurora_mysql.name description = "Aurora MySQL Database name" } output "aurora_mysql_master_username" { - value = "${module.aurora_mysql.user}" + value = module.aurora_mysql.user description = "Aurora MySQL Username for the master DB user" } output "aurora_mysql_master_hostname" { - value = "${module.aurora_mysql.master_host}" + value = module.aurora_mysql.master_host description = "Aurora MySQL DB Master hostname" } output "aurora_mysql_replicas_hostname" { - value = "${module.aurora_mysql.replicas_host}" + value = module.aurora_mysql.replicas_host description = "Aurora MySQL Replicas hostname" } output "aurora_mysql_cluster_name" { - value = "${module.aurora_mysql.cluster_name}" + value = module.aurora_mysql.cluster_name description = "Aurora MySQL Cluster Identifier" } diff --git a/deprecated/aws/keycloak-backing-services/main.tf b/deprecated/aws/keycloak-backing-services/main.tf index 61fb6e9be..dad8a4e4e 100644 --- a/deprecated/aws/keycloak-backing-services/main.tf +++ b/deprecated/aws/keycloak-backing-services/main.tf @@ -8,31 +8,31 @@ provider "aws" { version = "~> 2.17" assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `eg` or `cp`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "region" { - type = "string" + type = string description = "AWS region" } variable "dns_zone_name" { - type = "string" + type = string description = "The DNS domain under which to put entries for the database. Usually the same as the cluster name, e.g. us-west-2.prod.cpco.io" } @@ -47,24 +47,24 @@ variable "chamber_parameter_name_pattern" { } variable "chamber_kms_key_id" { - type = "string" + type = string default = "alias/aws/ssm" description = "KMS key ID, ARN, or alias to use for encrypting SSM secrets" } data "aws_route53_zone" "default" { - name = "${var.dns_zone_name}" + name = var.dns_zone_name } locals { // availability_zones = ["${split(",", length(var.availability_zones) == 0 ? join(",", data.aws_availability_zones.available.names) : join(",", var.availability_zones))}"] - chamber_service = "${var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service}" - dns_zone_id = "${data.aws_route53_zone.default.zone_id}" + chamber_service = var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service + dns_zone_id = data.aws_route53_zone.default.zone_id } resource "aws_ssm_parameter" "keycloak_db_vendor" { - count = "${local.mysql_cluster_enabled ? 1 : 0}" - name = "${format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_vendor")}" + count = local.mysql_cluster_enabled ? 1 : 0 + name = format(var.chamber_parameter_name_pattern, local.chamber_service, "keycloak_db_vendor") value = "mysql" description = "Database Vendor, e.g. mysql, postgres" type = "String" diff --git a/deprecated/aws/kops-aws-platform/acm.tf b/deprecated/aws/kops-aws-platform/acm.tf index 70595b197..c83aba69d 100644 --- a/deprecated/aws/kops-aws-platform/acm.tf +++ b/deprecated/aws/kops-aws-platform/acm.tf @@ -4,17 +4,17 @@ variable "kops_acm_enabled" { } variable "kops_acm_san_domains" { - type = "list" + type = list(string) default = [] description = "A list of domains (except *.{cluster_name}) that should be SANs in the issued certificate" } resource "aws_acm_certificate" "default" { - count = "${var.kops_acm_enabled ? 1 : 0}" + count = var.kops_acm_enabled ? 1 : 0 domain_name = "*.${var.region}.${var.zone_name}" validation_method = "DNS" subject_alternative_names = ["${var.kops_acm_san_domains}"] - tags = "${var.tags}" + tags = var.tags lifecycle { create_before_destroy = true @@ -22,11 +22,11 @@ resource "aws_acm_certificate" "default" { } output "kops_acm_arn" { - value = "${join("", aws_acm_certificate.default.*.arn)}" + value = join("", aws_acm_certificate.default.*.arn) description = "The ARN of the certificate" } output "kops_acm_domain_validation_options" { - value = "${flatten(aws_acm_certificate.default.*.domain_validation_options)}" + value = flatten(aws_acm_certificate.default.*.domain_validation_options) description = "CNAME records that need to be added to the DNS zone to complete certificate validation" } diff --git a/deprecated/aws/kops-aws-platform/alb-ingress.tf b/deprecated/aws/kops-aws-platform/alb-ingress.tf index 5676889bb..79f1c66da 100644 --- a/deprecated/aws/kops-aws-platform/alb-ingress.tf +++ b/deprecated/aws/kops-aws-platform/alb-ingress.tf @@ -5,13 +5,13 @@ variable "kops_alb_ingress_enabled" { module "kops_alb_ingress" { source = "git::https://github.com/cloudposse/terraform-aws-kops-aws-alb-ingress.git?ref=tags/0.2.0" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "alb-ingress" cluster_name = "${var.region}.${var.zone_name}" - enabled = "${var.kops_alb_ingress_enabled}" + enabled = var.kops_alb_ingress_enabled - iam_role_max_session_duration = "${var.iam_role_max_session_duration}" + iam_role_max_session_duration = var.iam_role_max_session_duration tags = { Cluster = "${var.region}.${var.zone_name}" @@ -19,25 +19,25 @@ module "kops_alb_ingress" { } output "kops_alb_ingress_role_name" { - value = "${module.kops_alb_ingress.role_name}" + value = module.kops_alb_ingress.role_name } output "kops_alb_ingress_role_unique_id" { - value = "${module.kops_alb_ingress.role_unique_id}" + value = module.kops_alb_ingress.role_unique_id } output "kops_alb_ingress_role_arn" { - value = "${module.kops_alb_ingress.role_arn}" + value = module.kops_alb_ingress.role_arn } output "kops_alb_ingress_policy_name" { - value = "${module.kops_alb_ingress.policy_name}" + value = module.kops_alb_ingress.policy_name } output "kops_alb_ingress_policy_id" { - value = "${module.kops_alb_ingress.policy_id}" + value = module.kops_alb_ingress.policy_id } output "kops_alb_ingress_policy_arn" { - value = "${module.kops_alb_ingress.policy_arn}" + value = module.kops_alb_ingress.policy_arn } diff --git a/deprecated/aws/kops-aws-platform/autoscaler-role.tf b/deprecated/aws/kops-aws-platform/autoscaler-role.tf index ff60180ec..ea5cc6145 100644 --- a/deprecated/aws/kops-aws-platform/autoscaler-role.tf +++ b/deprecated/aws/kops-aws-platform/autoscaler-role.tf @@ -20,14 +20,14 @@ module "autoscaler_role" { source = "git::https://github.com/cloudposse/terraform-aws-iam-role.git?ref=tags/0.4.0" enabled = "true" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "kubernetes" attributes = ["autoscaler", "role"] role_description = "Role for Cluster Auto-Scaler" policy_description = "Permit auto-scaling operations on auto-scaling groups" - max_session_duration = "${var.iam_role_max_session_duration}" + max_session_duration = var.iam_role_max_session_duration principals = { AWS = ["${module.kops_metadata_iam.masters_role_arn}"] @@ -37,24 +37,24 @@ module "autoscaler_role" { } resource "aws_ssm_parameter" "kops_autoscaler_iam_role_name" { - name = "${format(local.chamber_parameter_format, var.chamber_service, "kubernetes_autoscaler_iam_role_name")}" - value = "${module.autoscaler_role.name}" + name = format(local.chamber_parameter_format, var.chamber_service, "kubernetes_autoscaler_iam_role_name") + value = module.autoscaler_role.name description = "IAM role name for cluster autoscaler" type = "String" overwrite = "true" } output "autoscaler_role_name" { - value = "${module.autoscaler_role.name}" + value = module.autoscaler_role.name description = "The name of the IAM role created" } output "autoscaler_role_id" { - value = "${module.autoscaler_role.id}" + value = module.autoscaler_role.id description = "The stable and unique string identifying the role" } output "autoscaler_role_arn" { - value = "${module.autoscaler_role.arn}" + value = module.autoscaler_role.arn description = "The Amazon Resource Name (ARN) specifying the role" } diff --git a/deprecated/aws/kops-aws-platform/chart-repo.tf b/deprecated/aws/kops-aws-platform/chart-repo.tf index 9b5e5384c..9845b77de 100644 --- a/deprecated/aws/kops-aws-platform/chart-repo.tf +++ b/deprecated/aws/kops-aws-platform/chart-repo.tf @@ -1,12 +1,12 @@ module "kops_chart_repo" { source = "git::https://github.com/cloudposse/terraform-aws-kops-chart-repo.git?ref=tags/0.3.0" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "chart-repo" cluster_name = "${var.region}.${var.zone_name}" - permitted_nodes = "${var.permitted_nodes}" + permitted_nodes = var.permitted_nodes - iam_role_max_session_duration = "${var.iam_role_max_session_duration}" + iam_role_max_session_duration = var.iam_role_max_session_duration tags = { Cluster = "${var.region}.${var.zone_name}" @@ -14,37 +14,37 @@ module "kops_chart_repo" { } output "kops_chart_repo_bucket_domain_name" { - value = "${module.kops_chart_repo.bucket_domain_name}" + value = module.kops_chart_repo.bucket_domain_name } output "kops_chart_repo_bucket_id" { - value = "${module.kops_chart_repo.bucket_id}" + value = module.kops_chart_repo.bucket_id } output "kops_chart_repo_bucket_arn" { - value = "${module.kops_chart_repo.bucket_arn}" + value = module.kops_chart_repo.bucket_arn } output "kops_chart_repo_role_name" { - value = "${module.kops_chart_repo.role_name}" + value = module.kops_chart_repo.role_name } output "kops_chart_repo_role_unique_id" { - value = "${module.kops_chart_repo.role_unique_id}" + value = module.kops_chart_repo.role_unique_id } output "kops_chart_repo_role_arn" { - value = "${module.kops_chart_repo.role_arn}" + value = module.kops_chart_repo.role_arn } output "kops_chart_repo_policy_name" { - value = "${module.kops_chart_repo.policy_name}" + value = module.kops_chart_repo.policy_name } output "kops_chart_repo_policy_id" { - value = "${module.kops_chart_repo.policy_id}" + value = module.kops_chart_repo.policy_id } output "kops_chart_repo_policy_arn" { - value = "${module.kops_chart_repo.policy_arn}" + value = module.kops_chart_repo.policy_arn } diff --git a/deprecated/aws/kops-aws-platform/efs-provisioner.tf b/deprecated/aws/kops-aws-platform/efs-provisioner.tf index 71e32c5b9..c3bf5f054 100644 --- a/deprecated/aws/kops-aws-platform/efs-provisioner.tf +++ b/deprecated/aws/kops-aws-platform/efs-provisioner.tf @@ -1,23 +1,23 @@ variable "efs_enabled" { - type = "string" + type = string description = "Set to true to allow the module to create EFS resources" default = "false" } variable "kops_dns_zone_id" { - type = "string" + type = string default = "" description = "DNS Zone ID for kops. EFS DNS entries will be added to this zone. If empyty, zone ID will be retrieved from SSM Parameter store" } variable "efs_encrypted" { - type = "string" + type = string description = "If true, the disk will be encrypted" default = "false" } variable "efs_performance_mode" { - type = "string" + type = string description = "The file system performance mode. Can be either `generalPurpose` or `maxIO`" default = "generalPurpose" } @@ -28,42 +28,42 @@ variable "efs_provisioned_throughput_in_mibps" { } variable "efs_throughput_mode" { - type = "string" + type = string description = "Throughput mode for the file system. Defaults to bursting. Valid values: bursting, provisioned. When using provisioned, also set provisioned_throughput_in_mibps" default = "bursting" } data "aws_ssm_parameter" "kops_availability_zones" { - name = "${format(local.chamber_parameter_format, var.chamber_service_kops, "kops_availability_zones")}" + name = format(local.chamber_parameter_format, var.chamber_service_kops, "kops_availability_zones") } data "aws_ssm_parameter" "kops_zone_id" { - count = "${var.efs_enabled == "true" && var.kops_dns_zone_id == "" ? 1 : 0}" - name = "${format(local.chamber_parameter_format, var.chamber_service_kops, "kops_dns_zone_id")}" + count = var.efs_enabled == "true" && var.kops_dns_zone_id == "" ? 1 : 0 + name = format(local.chamber_parameter_format, var.chamber_service_kops, "kops_dns_zone_id") } locals { - kops_zone_id = "${coalesce(var.kops_dns_zone_id, join("", data.aws_ssm_parameter.kops_zone_id.*.value))}" + kops_zone_id = coalesce(var.kops_dns_zone_id, join("", data.aws_ssm_parameter.kops_zone_id.*.value)) } module "kops_efs_provisioner" { source = "git::https://github.com/cloudposse/terraform-aws-kops-efs.git?ref=tags/0.6.0" - enabled = "${var.efs_enabled}" - namespace = "${var.namespace}" - stage = "${var.stage}" + enabled = var.efs_enabled + namespace = var.namespace + stage = var.stage name = "efs-provisioner" - region = "${var.region}" + region = var.region availability_zones = ["${split(",", data.aws_ssm_parameter.kops_availability_zones.value)}"] - zone_id = "${local.kops_zone_id}" + zone_id = local.kops_zone_id cluster_name = "${var.region}.${var.zone_name}" - encrypted = "${var.efs_encrypted}" - performance_mode = "${var.efs_performance_mode}" + encrypted = var.efs_encrypted + performance_mode = var.efs_performance_mode - throughput_mode = "${var.efs_throughput_mode}" - provisioned_throughput_in_mibps = "${var.efs_provisioned_throughput_in_mibps}" + throughput_mode = var.efs_throughput_mode + provisioned_throughput_in_mibps = var.efs_provisioned_throughput_in_mibps - iam_role_max_session_duration = "${var.iam_role_max_session_duration}" + iam_role_max_session_duration = var.iam_role_max_session_duration tags = { Cluster = "${var.region}.${var.zone_name}" @@ -71,71 +71,71 @@ module "kops_efs_provisioner" { } resource "aws_ssm_parameter" "kops_efs_provisioner_role_name" { - count = "${var.efs_enabled == "true" ? 1 : 0}" - name = "${format(local.chamber_parameter_format, var.chamber_service, "kops_efs_provisioner_role_name")}" - value = "${module.kops_efs_provisioner.role_name}" + count = var.efs_enabled == "true" ? 1 : 0 + name = format(local.chamber_parameter_format, var.chamber_service, "kops_efs_provisioner_role_name") + value = module.kops_efs_provisioner.role_name description = "IAM role name for EFS provisioner" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_efs_file_system_id" { - count = "${var.efs_enabled == "true" ? 1 : 0}" - name = "${format(local.chamber_parameter_format, var.chamber_service, "kops_efs_file_system_id")}" - value = "${module.kops_efs_provisioner.efs_id}" + count = var.efs_enabled == "true" ? 1 : 0 + name = format(local.chamber_parameter_format, var.chamber_service, "kops_efs_file_system_id") + value = module.kops_efs_provisioner.efs_id description = "ID for shared EFS file system" type = "String" overwrite = "true" } output "kops_efs_provisioner_role_name" { - value = "${module.kops_efs_provisioner.role_name}" + value = module.kops_efs_provisioner.role_name } output "kops_efs_provisioner_role_unique_id" { - value = "${module.kops_efs_provisioner.role_unique_id}" + value = module.kops_efs_provisioner.role_unique_id } output "kops_efs_provisioner_role_arn" { - value = "${module.kops_efs_provisioner.role_arn}" + value = module.kops_efs_provisioner.role_arn } output "efs_arn" { - value = "${module.kops_efs_provisioner.efs_arn}" + value = module.kops_efs_provisioner.efs_arn description = "EFS ARN" } output "efs_id" { - value = "${module.kops_efs_provisioner.efs_id}" + value = module.kops_efs_provisioner.efs_id description = "EFS ID" } output "efs_host" { - value = "${module.kops_efs_provisioner.efs_host}" + value = module.kops_efs_provisioner.efs_host description = "EFS host" } output "efs_dns_name" { - value = "${module.kops_efs_provisioner.efs_dns_name}" + value = module.kops_efs_provisioner.efs_dns_name description = "EFS DNS name" } output "efs_mount_target_dns_names" { - value = "${module.kops_efs_provisioner.efs_mount_target_dns_names}" + value = module.kops_efs_provisioner.efs_mount_target_dns_names description = "EFS mount target DNS name" } output "efs_mount_target_ids" { - value = "${module.kops_efs_provisioner.efs_mount_target_ids}" + value = module.kops_efs_provisioner.efs_mount_target_ids description = "EFS mount target IDs" } output "efs_mount_target_ips" { - value = "${module.kops_efs_provisioner.efs_mount_target_ips}" + value = module.kops_efs_provisioner.efs_mount_target_ips description = "EFS mount target IPs" } output "efs_network_interface_ids" { - value = "${module.kops_efs_provisioner.efs_network_interface_ids}" + value = module.kops_efs_provisioner.efs_network_interface_ids description = "EFS network interface IDs" } diff --git a/deprecated/aws/kops-aws-platform/external-dns.tf b/deprecated/aws/kops-aws-platform/external-dns.tf index 2fdee8000..20c9b7f45 100644 --- a/deprecated/aws/kops-aws-platform/external-dns.tf +++ b/deprecated/aws/kops-aws-platform/external-dns.tf @@ -1,12 +1,12 @@ module "kops_external_dns" { source = "git::https://github.com/cloudposse/terraform-aws-kops-external-dns.git?ref=tags/0.3.0" - namespace = "${var.namespace}" - stage = "${var.stage}" + namespace = var.namespace + stage = var.stage name = "external-dns" cluster_name = "${var.region}.${var.zone_name}" - dns_zone_names = "${var.dns_zone_names}" + dns_zone_names = var.dns_zone_names - iam_role_max_session_duration = "${var.iam_role_max_session_duration}" + iam_role_max_session_duration = var.iam_role_max_session_duration tags = { Cluster = "${var.region}.${var.zone_name}" @@ -14,25 +14,25 @@ module "kops_external_dns" { } output "kops_external_dns_role_name" { - value = "${module.kops_external_dns.role_name}" + value = module.kops_external_dns.role_name } output "kops_external_dns_role_unique_id" { - value = "${module.kops_external_dns.role_unique_id}" + value = module.kops_external_dns.role_unique_id } output "kops_external_dns_role_arn" { - value = "${module.kops_external_dns.role_arn}" + value = module.kops_external_dns.role_arn } output "kops_external_dns_policy_name" { - value = "${module.kops_external_dns.policy_name}" + value = module.kops_external_dns.policy_name } output "kops_external_dns_policy_id" { - value = "${module.kops_external_dns.policy_id}" + value = module.kops_external_dns.policy_id } output "kops_external_dns_policy_arn" { - value = "${module.kops_external_dns.policy_arn}" + value = module.kops_external_dns.policy_arn } diff --git a/deprecated/aws/kops-aws-platform/flow-logs.tf b/deprecated/aws/kops-aws-platform/flow-logs.tf index 4cd2ba8cb..0ab06ff8b 100644 --- a/deprecated/aws/kops-aws-platform/flow-logs.tf +++ b/deprecated/aws/kops-aws-platform/flow-logs.tf @@ -1,63 +1,63 @@ variable "flow_logs_enabled" { - type = "string" + type = string default = "true" } module "flow_logs" { source = "git::https://github.com/cloudposse/terraform-aws-vpc-flow-logs-s3-bucket.git?ref=tags/0.1.1" name = "kops" - namespace = "${var.namespace}" - stage = "${var.stage}" - attributes = "${list("flow-logs")}" + namespace = var.namespace + stage = var.stage + attributes = list("flow-logs") - region = "${var.region}" + region = var.region - enabled = "${var.flow_logs_enabled}" + enabled = var.flow_logs_enabled - vpc_id = "${module.kops_metadata.vpc_id}" + vpc_id = module.kops_metadata.vpc_id } output "flow_logs_kms_key_arn" { - value = "${module.flow_logs.kms_key_arn}" + value = module.flow_logs.kms_key_arn description = "Flow logs KMS Key ARN" } output "flow_logs_kms_key_id" { - value = "${module.flow_logs.kms_key_id}" + value = module.flow_logs.kms_key_id description = "Flow logs KMS Key ID" } output "flow_logs_kms_alias_arn" { - value = "${module.flow_logs.kms_alias_arn}" + value = module.flow_logs.kms_alias_arn description = "Flow logs KMS Alias ARN" } output "flow_logs_kms_alias_name" { - value = "${module.flow_logs.kms_alias_name}" + value = module.flow_logs.kms_alias_name description = "Flow logs KMS Alias name" } output "flow_logs_bucket_domain_name" { - value = "${module.flow_logs.bucket_domain_name}" + value = module.flow_logs.bucket_domain_name description = "Flow logs FQDN of bucket" } output "flow_logs_bucket_id" { - value = "${module.flow_logs.bucket_id}" + value = module.flow_logs.bucket_id description = "Flow logs bucket Name (aka ID)" } output "flow_logs_bucket_arn" { - value = "${module.flow_logs.bucket_arn}" + value = module.flow_logs.bucket_arn description = "Flow logs bucket ARN" } output "flow_logs_bucket_prefix" { - value = "${module.flow_logs.bucket_prefix}" + value = module.flow_logs.bucket_prefix description = "Flow logs bucket prefix configured for lifecycle rules" } output "flow_logs_id" { - value = "${module.flow_logs.id}" + value = module.flow_logs.id description = "Flow logs ID" } diff --git a/deprecated/aws/kops-aws-platform/iam-authenticator.tf b/deprecated/aws/kops-aws-platform/iam-authenticator.tf index d0002ef63..9e232c57e 100644 --- a/deprecated/aws/kops-aws-platform/iam-authenticator.tf +++ b/deprecated/aws/kops-aws-platform/iam-authenticator.tf @@ -1,47 +1,47 @@ variable "kops_iam_enabled" { - type = "string" + type = string description = "Set to true to allow the module to create Kubernetes and IAM resources" default = "false" } variable "cluster_id" { - type = "string" + type = string description = "A unique-per-cluster identifier to prevent replay attacks. Good choices are a random token or a domain name that will be unique to your cluster" default = "random" } variable "kube_config_path" { - type = "string" + type = string default = "/dev/shm/kubecfg" description = "Path to the kube config file" } variable "admin_k8s_username" { - type = "string" + type = string description = "Kops admin username to be mapped to `admin_iam_role_arn`" default = "kubernetes-admin" } variable "admin_k8s_groups" { - type = "list" + type = list(string) description = "List of Kops groups to be mapped to `admin_iam_role_arn`" default = ["system:masters"] } variable "readonly_k8s_username" { - type = "string" + type = string description = "Kops readonly username to be mapped to `readonly_iam_role_arn`" default = "kubernetes-readonly" } variable "readonly_k8s_groups" { - type = "list" + type = list(string) description = "List of Kops groups to be mapped to `readonly_iam_role_arn`" default = ["view"] } resource "kubernetes_cluster_role_binding" "view" { - count = "${var.kops_iam_enabled == "true" ? 1 : 0}" + count = var.kops_iam_enabled == "true" ? 1 : 0 metadata { name = "view-binding" @@ -61,34 +61,34 @@ resource "kubernetes_cluster_role_binding" "view" { } variable "aws_root_account_id" { - type = "string" + type = string description = "AWS root account ID" } module "kops_admin_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" - stage = "${var.stage}" + stage = var.stage attributes = ["admin"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" - enabled = "${var.kops_iam_enabled}" + delimiter = var.delimiter + tags = var.tags + enabled = var.kops_iam_enabled } module "kops_readonly_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" - stage = "${var.stage}" + stage = var.stage attributes = ["readonly"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" - enabled = "${var.kops_iam_enabled}" + delimiter = var.delimiter + tags = var.tags + enabled = var.kops_iam_enabled } data "aws_iam_policy_document" "readonly" { - count = "${var.kops_iam_enabled == "true" ? 1 : 0}" + count = var.kops_iam_enabled == "true" ? 1 : 0 statement { actions = [ @@ -105,16 +105,16 @@ data "aws_iam_policy_document" "readonly" { } resource "aws_iam_role" "readonly" { - count = "${var.kops_iam_enabled == "true" ? 1 : 0}" - name = "${module.kops_readonly_label.id}" - assume_role_policy = "${data.aws_iam_policy_document.readonly.json}" + count = var.kops_iam_enabled == "true" ? 1 : 0 + name = module.kops_readonly_label.id + assume_role_policy = data.aws_iam_policy_document.readonly.json description = "The Kops readonly role for aws-iam-authenticator" - max_session_duration = "${var.iam_role_max_session_duration}" + max_session_duration = var.iam_role_max_session_duration } data "aws_iam_policy_document" "admin" { - count = "${var.kops_iam_enabled == "true" ? 1 : 0}" + count = var.kops_iam_enabled == "true" ? 1 : 0 statement { actions = [ @@ -131,23 +131,23 @@ data "aws_iam_policy_document" "admin" { } resource "aws_iam_role" "admin" { - count = "${var.kops_iam_enabled == "true" ? 1 : 0}" - name = "${module.kops_admin_label.id}" - assume_role_policy = "${data.aws_iam_policy_document.admin.json}" + count = var.kops_iam_enabled == "true" ? 1 : 0 + name = module.kops_admin_label.id + assume_role_policy = data.aws_iam_policy_document.admin.json description = "The Kops admin role for aws-iam-authenticator" - max_session_duration = "${var.iam_role_max_session_duration}" + max_session_duration = var.iam_role_max_session_duration } module "iam_authenticator_config" { - enabled = "${var.kops_iam_enabled}" + enabled = var.kops_iam_enabled source = "git::https://github.com/cloudposse/terraform-aws-kops-iam-authenticator-config.git?ref=tags/0.2.2" - cluster_id = "${var.cluster_id}" - kube_config_path = "${var.kube_config_path}" - admin_iam_role_arn = "${element(concat(aws_iam_role.admin.*.arn, list("")), 0)}" - admin_k8s_username = "${var.admin_k8s_username}" - admin_k8s_groups = "${var.admin_k8s_groups}" - readonly_iam_role_arn = "${element(concat(aws_iam_role.readonly.*.arn, list("")), 0)}" - readonly_k8s_username = "${var.readonly_k8s_username}" - readonly_k8s_groups = "${var.readonly_k8s_groups}" + cluster_id = var.cluster_id + kube_config_path = var.kube_config_path + admin_iam_role_arn = element(concat(aws_iam_role.admin.*.arn, list("")), 0) + admin_k8s_username = var.admin_k8s_username + admin_k8s_groups = var.admin_k8s_groups + readonly_iam_role_arn = element(concat(aws_iam_role.readonly.*.arn, list("")), 0) + readonly_k8s_username = var.readonly_k8s_username + readonly_k8s_groups = var.readonly_k8s_groups } diff --git a/deprecated/aws/kops-aws-platform/main.tf b/deprecated/aws/kops-aws-platform/main.tf index 20308663b..119a12e47 100644 --- a/deprecated/aws/kops-aws-platform/main.tf +++ b/deprecated/aws/kops-aws-platform/main.tf @@ -6,13 +6,13 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } module "kops_metadata" { source = "git::https://github.com/cloudposse/terraform-aws-kops-data-network.git?ref=tags/0.1.1" - enabled = "${var.flow_logs_enabled}" + enabled = var.flow_logs_enabled cluster_name = "${var.region}.${var.zone_name}" } @@ -25,9 +25,9 @@ resource "aws_default_security_group" "default" { # If kops is using a shared VPC, then it is likely that the kops_metadata module will # return an empty vpc_id, in which case we will leave it to the VPC owner to manage # the default security group. - count = "${module.kops_metadata.vpc_id == "" ? 0 : 1}" + count = module.kops_metadata.vpc_id == "" ? 0 : 1 - vpc_id = "${module.kops_metadata.vpc_id}" + vpc_id = module.kops_metadata.vpc_id tags = { Name = "Default Security Group" diff --git a/deprecated/aws/kops-aws-platform/variables.tf b/deprecated/aws/kops-aws-platform/variables.tf index 842eeec88..e0ea2e48a 100644 --- a/deprecated/aws/kops-aws-platform/variables.tf +++ b/deprecated/aws/kops-aws-platform/variables.tf @@ -1,59 +1,59 @@ variable "aws_assume_role_arn" { - type = "string" + type = string description = "AWS IAM Role for Terraform to assume during operation" } variable "region" { - type = "string" + type = string description = "AWS region" } variable "zone_name" { - type = "string" + type = string description = "DNS zone name" } variable "dns_zone_names" { - type = "list" + type = list(string) description = "Names of zones for external-dns to manage (e.g. `us-east-1.cloudposse.com` or `cluster-1.cloudposse.com`)" } variable "permitted_nodes" { - type = "string" + type = string description = "Kops kubernetes nodes that are permitted to assume IAM roles (e.g. 'nodes', 'masters', or 'both')" default = "both" } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `namespace`, `stage`, `name` and `attributes`" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" } variable "chamber_service" { - type = "string" + type = string default = "kops" description = "Service under which to store SSM parameters" } variable "chamber_service_kops" { - type = "string" + type = string default = "kops" description = "Service where kops stores its configuration information" } diff --git a/deprecated/aws/kops-iam-users/corp.tf b/deprecated/aws/kops-iam-users/corp.tf index ca22476e5..1aaa56476 100644 --- a/deprecated/aws/kops-iam-users/corp.tf +++ b/deprecated/aws/kops-iam-users/corp.tf @@ -1,47 +1,47 @@ module "kops_admin_corp_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "corp" attributes = ["admin"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_readonly_corp_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "corp" attributes = ["readonly"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_admin_access_group_corp" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "corp") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "corp") == true ? "true" : "false" + namespace = var.namespace stage = "corp" name = "kops" attributes = ["admin"] - role_name = "${module.kops_admin_corp_label.id}" + role_name = module.kops_admin_corp_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.corp_account_id}" + member_account_id = data.terraform_remote_state.accounts.corp_account_id require_mfa = "true" } module "kops_readonly_access_group_corp" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "corp") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "corp") == true ? "true" : "false" + namespace = var.namespace stage = "corp" name = "kops" attributes = ["readonly"] - role_name = "${module.kops_readonly_corp_label.id}" + role_name = module.kops_readonly_corp_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.corp_account_id}" + member_account_id = data.terraform_remote_state.accounts.corp_account_id require_mfa = "true" } diff --git a/deprecated/aws/kops-iam-users/data.tf b/deprecated/aws/kops-iam-users/data.tf index 6d5aa462f..9fee6f4af 100644 --- a/deprecated/aws/kops-iam-users/data.tf +++ b/deprecated/aws/kops-iam-users/data.tf @@ -1,47 +1,47 @@ module "kops_admin_data_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "data" attributes = ["admin"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_readonly_data_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "data" attributes = ["readonly"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_admin_access_group_data" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "data") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "data") == true ? "true" : "false" + namespace = var.namespace stage = "data" name = "kops" attributes = ["admin"] - role_name = "${module.kops_admin_data_label.id}" + role_name = module.kops_admin_data_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.data_account_id}" + member_account_id = data.terraform_remote_state.accounts.data_account_id require_mfa = "true" } module "kops_readonly_access_group_data" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "data") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "data") == true ? "true" : "false" + namespace = var.namespace stage = "data" name = "kops" attributes = ["readonly"] - role_name = "${module.kops_readonly_data_label.id}" + role_name = module.kops_readonly_data_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.data_account_id}" + member_account_id = data.terraform_remote_state.accounts.data_account_id require_mfa = "true" } diff --git a/deprecated/aws/kops-iam-users/dev.tf b/deprecated/aws/kops-iam-users/dev.tf index dd4522cf9..8da80f9b4 100644 --- a/deprecated/aws/kops-iam-users/dev.tf +++ b/deprecated/aws/kops-iam-users/dev.tf @@ -1,47 +1,47 @@ module "kops_admin_dev_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "dev" attributes = ["admin"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_readonly_dev_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "dev" attributes = ["readonly"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_admin_access_group_dev" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "dev") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "dev") == true ? "true" : "false" + namespace = var.namespace stage = "dev" name = "kops" attributes = ["admin"] - role_name = "${module.kops_admin_dev_label.id}" + role_name = module.kops_admin_dev_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.dev_account_id}" + member_account_id = data.terraform_remote_state.accounts.dev_account_id require_mfa = "true" } module "kops_readonly_access_group_dev" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "dev") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "dev") == true ? "true" : "false" + namespace = var.namespace stage = "dev" name = "kops" attributes = ["readonly"] - role_name = "${module.kops_readonly_dev_label.id}" + role_name = module.kops_readonly_dev_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.dev_account_id}" + member_account_id = data.terraform_remote_state.accounts.dev_account_id require_mfa = "true" } diff --git a/deprecated/aws/kops-iam-users/main.tf b/deprecated/aws/kops-iam-users/main.tf index 90231fa3b..2d367c808 100644 --- a/deprecated/aws/kops-iam-users/main.tf +++ b/deprecated/aws/kops-iam-users/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } diff --git a/deprecated/aws/kops-iam-users/prod.tf b/deprecated/aws/kops-iam-users/prod.tf index 60f7bcb74..4d76b140a 100644 --- a/deprecated/aws/kops-iam-users/prod.tf +++ b/deprecated/aws/kops-iam-users/prod.tf @@ -1,47 +1,47 @@ module "kops_admin_prod_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "prod" attributes = ["admin"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_readonly_prod_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "prod" attributes = ["readonly"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_admin_access_group_prod" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "prod") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "prod") == true ? "true" : "false" + namespace = var.namespace stage = "prod" name = "kops" attributes = ["admin"] - role_name = "${module.kops_admin_prod_label.id}" + role_name = module.kops_admin_prod_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.prod_account_id}" + member_account_id = data.terraform_remote_state.accounts.prod_account_id require_mfa = "true" } module "kops_readonly_access_group_prod" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "prod") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "prod") == true ? "true" : "false" + namespace = var.namespace stage = "prod" name = "kops" attributes = ["readonly"] - role_name = "${module.kops_readonly_prod_label.id}" + role_name = module.kops_readonly_prod_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.prod_account_id}" + member_account_id = data.terraform_remote_state.accounts.prod_account_id require_mfa = "true" } diff --git a/deprecated/aws/kops-iam-users/staging.tf b/deprecated/aws/kops-iam-users/staging.tf index fba410e86..2677fa0de 100644 --- a/deprecated/aws/kops-iam-users/staging.tf +++ b/deprecated/aws/kops-iam-users/staging.tf @@ -1,47 +1,47 @@ module "kops_admin_staging_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "staging" attributes = ["admin"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_readonly_staging_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "staging" attributes = ["readonly"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_admin_access_group_staging" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "staging") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "staging") == true ? "true" : "false" + namespace = var.namespace stage = "staging" name = "kops" attributes = ["admin"] - role_name = "${module.kops_admin_staging_label.id}" + role_name = module.kops_admin_staging_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.staging_account_id}" + member_account_id = data.terraform_remote_state.accounts.staging_account_id require_mfa = "true" } module "kops_readonly_access_group_staging" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "staging") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "staging") == true ? "true" : "false" + namespace = var.namespace stage = "staging" name = "kops" attributes = ["readonly"] - role_name = "${module.kops_readonly_staging_label.id}" + role_name = module.kops_readonly_staging_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.staging_account_id}" + member_account_id = data.terraform_remote_state.accounts.staging_account_id require_mfa = "true" } diff --git a/deprecated/aws/kops-iam-users/testing.tf b/deprecated/aws/kops-iam-users/testing.tf index 515b16a12..393dc202a 100644 --- a/deprecated/aws/kops-iam-users/testing.tf +++ b/deprecated/aws/kops-iam-users/testing.tf @@ -1,47 +1,47 @@ module "kops_admin_testing_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "testing" attributes = ["admin"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_readonly_testing_label" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" - namespace = "${var.namespace}" + namespace = var.namespace name = "kops" stage = "testing" attributes = ["readonly"] - delimiter = "${var.delimiter}" - tags = "${var.tags}" + delimiter = var.delimiter + tags = var.tags enabled = "true" } module "kops_admin_access_group_testing" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "testing") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "testing") == true ? "true" : "false" + namespace = var.namespace stage = "testing" name = "kops" attributes = ["admin"] - role_name = "${module.kops_admin_testing_label.id}" + role_name = module.kops_admin_testing_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.testing_account_id}" + member_account_id = data.terraform_remote_state.accounts.testing_account_id require_mfa = "true" } module "kops_readonly_access_group_testing" { source = "git::https://github.com/cloudposse/terraform-aws-organization-access-group.git?ref=tags/0.4.0" - enabled = "${contains(var.kops_iam_accounts_enabled, "testing") == true ? "true" : "false"}" - namespace = "${var.namespace}" + enabled = contains(var.kops_iam_accounts_enabled, "testing") == true ? "true" : "false" + namespace = var.namespace stage = "testing" name = "kops" attributes = ["readonly"] - role_name = "${module.kops_readonly_testing_label.id}" + role_name = module.kops_readonly_testing_label.id user_names = [] - member_account_id = "${data.terraform_remote_state.accounts.testing_account_id}" + member_account_id = data.terraform_remote_state.accounts.testing_account_id require_mfa = "true" } diff --git a/deprecated/aws/kops-iam-users/variables.tf b/deprecated/aws/kops-iam-users/variables.tf index 4100e1ec6..6f3f3bb47 100644 --- a/deprecated/aws/kops-iam-users/variables.tf +++ b/deprecated/aws/kops-iam-users/variables.tf @@ -1,31 +1,31 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "kops_iam_accounts_enabled" { - type = "list" + type = list(string) description = "Accounts to create an IAM role and group for Kops users" default = ["dev", "staging", "prod", "testing"] } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage, e.g. 'prod', 'staging', 'dev', or 'test'" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `namespace`, `stage`, `name` and `attributes`" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" } diff --git a/deprecated/aws/kops-legacy-account-vpc-peering/main.tf b/deprecated/aws/kops-legacy-account-vpc-peering/main.tf index 902497d5d..35b3cd021 100644 --- a/deprecated/aws/kops-legacy-account-vpc-peering/main.tf +++ b/deprecated/aws/kops-legacy-account-vpc-peering/main.tf @@ -6,36 +6,36 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } # Lookup VPC of the kops cluster module "kops_metadata" { source = "git::https://github.com/cloudposse/terraform-aws-kops-metadata.git?ref=tags/0.2.1" - enabled = "${var.enabled}" + enabled = var.enabled dns_zone = "${var.region}.${var.zone_name}" - vpc_tag = "${var.vpc_tag}" + vpc_tag = var.vpc_tag vpc_tag_values = ["${var.vpc_tag_values}"] } module "kops_legacy_account_vpc_peering" { source = "git::https://github.com/cloudposse/terraform-aws-vpc-peering-multi-account.git?ref=tags/0.5.0" - enabled = "${var.enabled}" + enabled = var.enabled - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name auto_accept = true # Requester - requester_vpc_id = "${module.kops_metadata.vpc_id}" - requester_region = "${var.region}" - requester_aws_assume_role_arn = "${var.aws_assume_role_arn}" + requester_vpc_id = module.kops_metadata.vpc_id + requester_region = var.region + requester_aws_assume_role_arn = var.aws_assume_role_arn # Accepter - accepter_vpc_id = "${var.legacy_account_vpc_id}" - accepter_region = "${var.legacy_account_region}" - accepter_aws_assume_role_arn = "${var.legacy_account_assume_role_arn}" + accepter_vpc_id = var.legacy_account_vpc_id + accepter_region = var.legacy_account_region + accepter_aws_assume_role_arn = var.legacy_account_assume_role_arn } diff --git a/deprecated/aws/kops-legacy-account-vpc-peering/outputs.tf b/deprecated/aws/kops-legacy-account-vpc-peering/outputs.tf index 6e5a0ecae..f75cbe3da 100644 --- a/deprecated/aws/kops-legacy-account-vpc-peering/outputs.tf +++ b/deprecated/aws/kops-legacy-account-vpc-peering/outputs.tf @@ -1,19 +1,19 @@ output "accepter_accept_status" { description = "Accepter VPC peering connection request status" - value = "${module.kops_legacy_account_vpc_peering.accepter_accept_status}" + value = module.kops_legacy_account_vpc_peering.accepter_accept_status } output "accepter_connection_id" { description = "Accepter VPC peering connection ID" - value = "${module.kops_legacy_account_vpc_peering.accepter_connection_id}" + value = module.kops_legacy_account_vpc_peering.accepter_connection_id } output "requester_accept_status" { description = "Requester VPC peering connection request status" - value = "${module.kops_legacy_account_vpc_peering.requester_accept_status}" + value = module.kops_legacy_account_vpc_peering.requester_accept_status } output "requester_connection_id" { description = "Requester VPC peering connection ID" - value = "${module.kops_legacy_account_vpc_peering.requester_connection_id}" + value = module.kops_legacy_account_vpc_peering.requester_connection_id } diff --git a/deprecated/aws/kops-legacy-account-vpc-peering/variables.tf b/deprecated/aws/kops-legacy-account-vpc-peering/variables.tf index 7c742b0f9..44fd2fade 100644 --- a/deprecated/aws/kops-legacy-account-vpc-peering/variables.tf +++ b/deprecated/aws/kops-legacy-account-vpc-peering/variables.tf @@ -1,60 +1,60 @@ variable "aws_assume_role_arn" {} variable "enabled" { - type = "string" + type = string description = "Whether to create the resources. Set to `false` to prevent the module from creating any resources" default = "true" } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `eg` or `cp`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string description = "Application or solution name (e.g. `app`)" default = "vpc-peering" } variable "region" { - type = "string" + type = string description = "AWS region" } variable "zone_name" { - type = "string" + type = string description = "DNS zone name" } variable "vpc_tag" { - type = "string" + type = string default = "Name" description = "Tag used to lookup the Kops VPC" } variable "vpc_tag_values" { - type = "list" + type = list(string) default = [] description = "Tag values list to lookup the Kops VPC" } variable "legacy_account_assume_role_arn" { - type = "string" + type = string description = "Legacy account assume role ARN" } variable "legacy_account_region" { - type = "string" + type = string description = "Legacy account AWS region" } variable "legacy_account_vpc_id" { - type = "string" + type = string description = "Legacy account VPC ID" } diff --git a/deprecated/aws/kops/main.tf b/deprecated/aws/kops/main.tf index 37500de97..62c021823 100644 --- a/deprecated/aws/kops/main.tf +++ b/deprecated/aws/kops/main.tf @@ -6,49 +6,49 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } data "aws_availability_zones" "default" {} locals { - chamber_service = "${var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service}" - computed_availability_zones = "${data.aws_availability_zones.default.names}" - distinct_availability_zones = "${distinct(compact(concat(var.availability_zones, local.computed_availability_zones)))}" + chamber_service = var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service + computed_availability_zones = data.aws_availability_zones.default.names + distinct_availability_zones = distinct(compact(concat(var.availability_zones, local.computed_availability_zones))) # If we are creating the VPC, concatenate the predefined AZs with the computed AZs and select the first N distinct AZs. # If we are using a shared VPC, use the availability zones dictated by the VPC - availability_zones = "${split(",", var.create_vpc == "true" ? join(",", slice(local.distinct_availability_zones, 0, var.availability_zone_count)) : join("", data.aws_ssm_parameter.availability_zones.*.value))}" + availability_zones = split(",", var.create_vpc == "true" ? join(",", slice(local.distinct_availability_zones, 0, var.availability_zone_count)) : join("", data.aws_ssm_parameter.availability_zones.*.value)) - availability_zone_count = "${length(local.availability_zones)}" + availability_zone_count = length(local.availability_zones) } module "kops_state_backend" { source = "git::https://github.com/cloudposse/terraform-aws-kops-state-backend.git?ref=tags/0.3.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name attributes = ["${var.kops_attribute}"] - cluster_name = "${coalesce(var.cluster_name_prefix, var.resource_region, var.region)}" - parent_zone_name = "${var.zone_name}" - zone_name = "${var.complete_zone_name}" - domain_enabled = "${var.domain_enabled}" - force_destroy = "${var.force_destroy}" - region = "${coalesce(var.state_store_region, var.region)}" - create_bucket = "${var.create_state_store_bucket}" + cluster_name = coalesce(var.cluster_name_prefix, var.resource_region, var.region) + parent_zone_name = var.zone_name + zone_name = var.complete_zone_name + domain_enabled = var.domain_enabled + force_destroy = var.force_destroy + region = coalesce(var.state_store_region, var.region) + create_bucket = var.create_state_store_bucket } module "ssh_key_pair" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-tls-ssh-key-pair.git?ref=tags/0.2.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name attributes = ["${coalesce(var.resource_region, var.region)}"] - ssm_path_prefix = "${local.chamber_service}" - rsa_bits = "${var.ssh_key_rsa_bits}" - ssh_key_algorithm = "${var.ssh_key_algorithm}" - ecdsa_curve = "${var.ssh_key_ecdsa_curve}" + ssm_path_prefix = local.chamber_service + rsa_bits = var.ssh_key_rsa_bits + ssh_key_algorithm = var.ssh_key_algorithm + ecdsa_curve = var.ssh_key_ecdsa_curve ssh_public_key_name = "kops_ssh_public_key" ssh_private_key_name = "kops_ssh_private_key" } @@ -56,70 +56,70 @@ module "ssh_key_pair" { # Allocate one large subnet for each AZ, plus one additional one for the utility subnets. module "private_subnets" { source = "subnets" - iprange = "${local.vpc_network_cidr}" - newbits = "${var.private_subnets_newbits > 0 ? var.private_subnets_newbits : local.availability_zone_count}" - netnum = "${var.private_subnets_netnum}" - subnet_count = "${local.availability_zone_count + 1}" + iprange = local.vpc_network_cidr + newbits = var.private_subnets_newbits > 0 ? var.private_subnets_newbits : local.availability_zone_count + netnum = var.private_subnets_netnum + subnet_count = local.availability_zone_count + 1 } # Divide up the first private subnet and use it for the utility subnet module "utility_subnets" { source = "subnets" - iprange = "${module.private_subnets.cidrs[0]}" - newbits = "${var.utility_subnets_newbits > 0 ? var.utility_subnets_newbits : local.availability_zone_count}" - netnum = "${var.utility_subnets_netnum}" - subnet_count = "${local.availability_zone_count}" + iprange = module.private_subnets.cidrs[0] + newbits = var.utility_subnets_newbits > 0 ? var.utility_subnets_newbits : local.availability_zone_count + netnum = var.utility_subnets_netnum + subnet_count = local.availability_zone_count } ####### # If create_vpc is not true, then we import all the VPC configuration from the VPC chamber service # data "aws_ssm_parameter" "vpc_id" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "vpc_id")}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "vpc_id") } data "aws_ssm_parameter" "vpc_cidr_block" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "cidr_block")}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "cidr_block") } ### # The following are lists, and must all be the same size and in the same order # data "aws_ssm_parameter" "availability_zones" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "availability_zones")}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "availability_zones") } # List of NAT gateways from private subnet to public, one per subnet, which is one per availability zone data "aws_ssm_parameter" "nat_gateways" { - count = "${var.create_vpc == "false" && var.use_shared_nat_gateways == "true" ? 1 : 0}" - name = "${format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "nat_gateways")}" + count = var.create_vpc == "false" && var.use_shared_nat_gateways == "true" ? 1 : 0 + name = format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "nat_gateways") } # List of private subnet CIDR blocks, one per availability zone data "aws_ssm_parameter" "private_subnet_cidrs" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "private_subnet_cidrs")}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "private_subnet_cidrs") } # List of private subnet AWS IDs, one per availability zone data "aws_ssm_parameter" "private_subnet_ids" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "private_subnet_ids")}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "private_subnet_ids") } # List of public subnet CIDR blocks, one per availability zone data "aws_ssm_parameter" "public_subnet_cidrs" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "public_subnet_cidrs")}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "public_subnet_cidrs") } # List of public subnet AWS IDs, one per availability zone data "aws_ssm_parameter" "public_subnet_ids" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "public_subnet_ids")}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.vpc_chamber_parameter_name, var.vpc_chamber_service, var.vpc_paramter_prefix, "public_subnet_ids") } # @@ -127,24 +127,24 @@ data "aws_ssm_parameter" "public_subnet_ids" { ###### locals { - vpc_network_cidr = "${var.create_vpc == "true" ? var.network_cidr : join("", data.aws_ssm_parameter.vpc_cidr_block.*.value)}" - private_subnet_cidrs = "${var.create_vpc == "true" ? join(",", slice(module.private_subnets.cidrs, 1, local.availability_zone_count + 1)) : join("", data.aws_ssm_parameter.private_subnet_cidrs.*.value)}" - utility_subnet_cidrs = "${var.create_vpc == "true" ? join(",", module.utility_subnets.cidrs) : join("", data.aws_ssm_parameter.public_subnet_cidrs.*.value)}" + vpc_network_cidr = var.create_vpc == "true" ? var.network_cidr : join("", data.aws_ssm_parameter.vpc_cidr_block.*.value) + private_subnet_cidrs = var.create_vpc == "true" ? join(",", slice(module.private_subnets.cidrs, 1, local.availability_zone_count + 1)) : join("", data.aws_ssm_parameter.private_subnet_cidrs.*.value) + utility_subnet_cidrs = var.create_vpc == "true" ? join(",", module.utility_subnets.cidrs) : join("", data.aws_ssm_parameter.public_subnet_cidrs.*.value) } # These parameters correspond to the kops manifest template: # Read more: resource "aws_ssm_parameter" "kops_cluster_name" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_cluster_name")}" - value = "${module.kops_state_backend.zone_name}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_cluster_name") + value = module.kops_state_backend.zone_name description = "Kops cluster name" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_state_store" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_state_store")}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_state_store") value = "s3://${module.kops_state_backend.bucket_name}" description = "Kops state store S3 bucket name" type = "String" @@ -152,32 +152,32 @@ resource "aws_ssm_parameter" "kops_state_store" { } resource "aws_ssm_parameter" "kops_state_store_region" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_state_store_region")}" - value = "${module.kops_state_backend.bucket_region}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_state_store_region") + value = module.kops_state_backend.bucket_region description = "Kops state store (S3 bucket) region" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_dns_zone" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_dns_zone")}" - value = "${module.kops_state_backend.zone_name}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_dns_zone") + value = module.kops_state_backend.zone_name description = "Kops DNS zone name" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_dns_zone_id" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_dns_zone_id")}" - value = "${module.kops_state_backend.zone_id}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_dns_zone_id") + value = module.kops_state_backend.zone_id description = "Kops DNS zone ID" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_network_cidr" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_network_cidr")}" - value = "${local.vpc_network_cidr}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_network_cidr") + value = local.vpc_network_cidr description = "CIDR block of the kops virtual network" type = "String" overwrite = "true" @@ -185,9 +185,9 @@ resource "aws_ssm_parameter" "kops_network_cidr" { # If we are using a shared VPC, we save its AWS ID here. If kops is creating the VPC, we do not export the ID resource "aws_ssm_parameter" "kops_shared_vpc_id" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_shared_vpc_id")}" - value = "${join("", data.aws_ssm_parameter.vpc_id.*.value)}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.chamber_parameter_name, local.chamber_service, "kops_shared_vpc_id") + value = join("", data.aws_ssm_parameter.vpc_id.*.value) description = "Kops (shared) VPC AWS ID" type = "String" overwrite = "true" @@ -195,9 +195,9 @@ resource "aws_ssm_parameter" "kops_shared_vpc_id" { # If we are using a shared VPC, we save the list of NAT gateway IDs here. If kops is creating the VPC, we do not export the IDs resource "aws_ssm_parameter" "kops_shared_nat_gateways" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_shared_nat_gateways")}" - value = "${var.use_shared_nat_gateways == "true" ? join("", data.aws_ssm_parameter.nat_gateways.*.value) : replace(local.private_subnet_cidrs, "/[^,]+/", "External")}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.chamber_parameter_name, local.chamber_service, "kops_shared_nat_gateways") + value = var.use_shared_nat_gateways == "true" ? join("", data.aws_ssm_parameter.nat_gateways.*.value) : replace(local.private_subnet_cidrs, "/[^,]+/", "External") description = "Kops (shared) private subnet NAT gateway AWS IDs" type = "String" overwrite = "true" @@ -205,9 +205,9 @@ resource "aws_ssm_parameter" "kops_shared_nat_gateways" { # If we are using a shared VPC, we save the list of private subnet IDs here. If kops is creating the VPC, we do not export the IDs resource "aws_ssm_parameter" "kops_shared_private_subnet_ids" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_shared_private_subnet_ids")}" - value = "${join("", data.aws_ssm_parameter.private_subnet_ids.*.value)}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.chamber_parameter_name, local.chamber_service, "kops_shared_private_subnet_ids") + value = join("", data.aws_ssm_parameter.private_subnet_ids.*.value) description = "Kops private subnet AWS IDs" type = "String" overwrite = "true" @@ -215,41 +215,41 @@ resource "aws_ssm_parameter" "kops_shared_private_subnet_ids" { # If we are using a shared VPC, we save the list of utility (public) subnet IDs here. If kops is creating the VPC, we do not export the IDs resource "aws_ssm_parameter" "kops_shared_utility_subnet_ids" { - count = "${var.create_vpc == "true" ? 0 : 1}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_shared_utility_subnet_ids")}" - value = "${join("", data.aws_ssm_parameter.public_subnet_ids.*.value)}" + count = var.create_vpc == "true" ? 0 : 1 + name = format(var.chamber_parameter_name, local.chamber_service, "kops_shared_utility_subnet_ids") + value = join("", data.aws_ssm_parameter.public_subnet_ids.*.value) description = "Kops utility subnet AWS IDs" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_private_subnets" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_private_subnets")}" - value = "${local.private_subnet_cidrs}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_private_subnets") + value = local.private_subnet_cidrs description = "Kops private subnet CIDRs" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_utility_subnets" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_utility_subnets")}" - value = "${local.utility_subnet_cidrs}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_utility_subnets") + value = local.utility_subnet_cidrs description = "Kops utility subnet CIDRs" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_non_masquerade_cidr" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_non_masquerade_cidr")}" - value = "${var.kops_non_masquerade_cidr}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_non_masquerade_cidr") + value = var.kops_non_masquerade_cidr description = "The CIDR range for Pod IPs" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "kops_availability_zones" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "kops_availability_zones")}" - value = "${join(",", local.availability_zones)}" + name = format(var.chamber_parameter_name, local.chamber_service, "kops_availability_zones") + value = join(",", local.availability_zones) description = "Kops availability zones in which cluster will be provisioned" type = "String" overwrite = "true" diff --git a/deprecated/aws/kops/outputs.tf b/deprecated/aws/kops/outputs.tf index 3674c4fbe..79cc24170 100644 --- a/deprecated/aws/kops/outputs.tf +++ b/deprecated/aws/kops/outputs.tf @@ -1,67 +1,67 @@ output "parent_zone_id" { - value = "${module.kops_state_backend.parent_zone_id}" + value = module.kops_state_backend.parent_zone_id } output "parent_zone_name" { - value = "${module.kops_state_backend.parent_zone_name}" + value = module.kops_state_backend.parent_zone_name } output "zone_id" { - value = "${module.kops_state_backend.zone_id}" + value = module.kops_state_backend.zone_id } output "zone_name" { - value = "${module.kops_state_backend.zone_name}" + value = module.kops_state_backend.zone_name } output "bucket_name" { - value = "${module.kops_state_backend.bucket_name}" + value = module.kops_state_backend.bucket_name } output "bucket_region" { - value = "${module.kops_state_backend.bucket_region}" + value = module.kops_state_backend.bucket_region } output "bucket_domain_name" { - value = "${module.kops_state_backend.bucket_domain_name}" + value = module.kops_state_backend.bucket_domain_name } output "bucket_id" { - value = "${module.kops_state_backend.bucket_id}" + value = module.kops_state_backend.bucket_id } output "bucket_arn" { - value = "${module.kops_state_backend.bucket_arn}" + value = module.kops_state_backend.bucket_arn } output "ssh_public_key" { - value = "${module.ssh_key_pair.public_key}" + value = module.ssh_key_pair.public_key } output "availability_zones" { - value = "${join(",", local.availability_zones)}" + value = join(",", local.availability_zones) } output "kops_shared_vpc_id" { - value = "${join("", aws_ssm_parameter.kops_shared_vpc_id.*.value)}" + value = join("", aws_ssm_parameter.kops_shared_vpc_id.*.value) } output "kops_shared_nat_gateways" { - value = "${join("", aws_ssm_parameter.kops_shared_nat_gateways.*.value)}" + value = join("", aws_ssm_parameter.kops_shared_nat_gateways.*.value) } output "kops_shared_utility_subnet_ids" { - value = "${join("", aws_ssm_parameter.kops_shared_utility_subnet_ids.*.value)}" + value = join("", aws_ssm_parameter.kops_shared_utility_subnet_ids.*.value) } output "kops_shared_private_subnet_ids" { - value = "${join("", aws_ssm_parameter.kops_shared_private_subnet_ids.*.value)}" + value = join("", aws_ssm_parameter.kops_shared_private_subnet_ids.*.value) } output "private_subnets" { - value = "${local.private_subnet_cidrs}" + value = local.private_subnet_cidrs } output "utility_subnets" { - value = "${local.utility_subnet_cidrs}" + value = local.utility_subnet_cidrs } diff --git a/deprecated/aws/kops/subnets/main.tf b/deprecated/aws/kops/subnets/main.tf index 66b6ef7cf..7d7d5fea5 100644 --- a/deprecated/aws/kops/subnets/main.tf +++ b/deprecated/aws/kops/subnets/main.tf @@ -1,7 +1,7 @@ # Read more: data "null_data_source" "subnets" { - count = "${var.subnet_count}" + count = var.subnet_count inputs = { cidr = "${cidrsubnet(var.iprange, var.newbits, var.netnum + count.index)}" diff --git a/deprecated/aws/kops/subnets/outputs.tf b/deprecated/aws/kops/subnets/outputs.tf index 0c731acc5..fc92a19f5 100644 --- a/deprecated/aws/kops/subnets/outputs.tf +++ b/deprecated/aws/kops/subnets/outputs.tf @@ -1,7 +1,7 @@ output "iprange" { - value = "${var.iprange}" + value = var.iprange } output "cidrs" { - value = "${data.null_data_source.subnets.*.outputs.cidr}" + value = data.null_data_source.subnets.*.outputs.cidr } diff --git a/deprecated/aws/kops/variables.tf b/deprecated/aws/kops/variables.tf index 5b4a18d48..5651683a1 100644 --- a/deprecated/aws/kops/variables.tf +++ b/deprecated/aws/kops/variables.tf @@ -1,55 +1,55 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string description = "Name (e.g. `kops`)" default = "kops" } variable "region" { - type = "string" + type = string default = "" description = "AWS region for resources. Can be overriden by `resource_region` and `state_store_region`" } variable "state_store_region" { - type = "string" + type = string default = "" description = "Region where to create the S3 bucket for the kops state store. Defaults to `var.region`" } variable "resource_region" { - type = "string" + type = string default = "" description = "Region where to create region-specific resources. Defaults to `var.region`" } variable "create_state_store_bucket" { - type = "string" + type = string default = "true" description = "Set to `false` to use existing S3 bucket (e.g. from another region)" } variable "cluster_name_prefix" { - type = "string" + type = string default = "" description = "Prefix to add before parent DNS zone name to identify this cluster, e.g. `us-east-1`. Defaults to `var.resource_region`" } variable "availability_zones" { - type = "list" + type = list(string) description = "List of availability zones in which to provision the cluster (should be an odd number to avoid split-brain)." default = [] } @@ -60,48 +60,48 @@ variable "availability_zone_count" { } variable "zone_name" { - type = "string" + type = string description = "DNS zone name" } variable "domain_enabled" { - type = "string" + type = string description = "Enable DNS Zone creation for kops" default = "true" } variable "force_destroy" { - type = "string" + type = string description = "A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without errors. These objects are not recoverable." default = "false" } variable "ssh_key_algorithm" { - type = "string" + type = string default = "RSA" description = "SSH key algorithm to use. Currently-supported values are 'RSA' and 'ECDSA'" } variable "ssh_key_rsa_bits" { - type = "string" + type = string description = "When ssh_key_algorithm is 'RSA', the size of the generated RSA key in bits" default = "4096" } variable "ssh_key_ecdsa_curve" { - type = "string" + type = string description = "When ssh_key_algorithm is 'ECDSA', the name of the elliptic curve to use. May be any one of 'P256', 'P384' or P521'" default = "P521" } variable "kops_attribute" { - type = "string" + type = string description = "Additional attribute to kops state bucket" default = "state" } variable "complete_zone_name" { - type = "string" + type = string description = "Region or any classifier prefixed to zone name" default = "$${name}.$${parent_zone_name}" } diff --git a/deprecated/aws/organization/main.tf b/deprecated/aws/organization/main.tf index d02f4cbcd..e532c95fb 100644 --- a/deprecated/aws/organization/main.tf +++ b/deprecated/aws/organization/main.tf @@ -8,41 +8,41 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "organization_feature_set" { - type = "string" + type = string default = "ALL" description = "`ALL` (default) or `CONSOLIDATED_BILLING`" } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } resource "aws_organizations_organization" "default" { - feature_set = "${var.organization_feature_set}" + feature_set = var.organization_feature_set } output "organization_id" { - value = "${aws_organizations_organization.default.id}" + value = aws_organizations_organization.default.id } output "organization_arn" { - value = "${aws_organizations_organization.default.arn}" + value = aws_organizations_organization.default.arn } output "organization_master_account_id" { - value = "${aws_organizations_organization.default.master_account_id}" + value = aws_organizations_organization.default.master_account_id } output "organization_master_account_arn" { - value = "${aws_organizations_organization.default.master_account_arn}" + value = aws_organizations_organization.default.master_account_arn } output "organization_master_account_email" { - value = "${aws_organizations_organization.default.master_account_email}" + value = aws_organizations_organization.default.master_account_email } diff --git a/deprecated/aws/root-dns/main.tf b/deprecated/aws/root-dns/main.tf index bdccc4ab8..60a335b49 100644 --- a/deprecated/aws/root-dns/main.tf +++ b/deprecated/aws/root-dns/main.tf @@ -5,19 +5,19 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" {} variable "accounts_enabled" { - type = "list" + type = list(string) description = "Accounts to enable" default = ["dev", "staging", "prod", "testing", "audit"] } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } diff --git a/deprecated/aws/root-dns/ns/main.tf b/deprecated/aws/root-dns/ns/main.tf index 4b61edf80..b5616833e 100644 --- a/deprecated/aws/root-dns/ns/main.tf +++ b/deprecated/aws/root-dns/ns/main.tf @@ -1,52 +1,52 @@ locals { - enabled = "${contains(var.accounts_enabled, var.stage) == true}" - account = "${length(var.account) > 0 ? var.account : var.stage}" + enabled = contains(var.accounts_enabled, var.stage) == true + account = length(var.account) > 0 ? var.account : var.stage } module "label" { source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.5.4" - enabled = "${local.enabled ? "true" : "false"}" - namespace = "${var.namespace}" - stage = "${local.account}" - name = "${var.name}" - delimiter = "${var.delimiter}" - attributes = "${var.attributes}" - tags = "${var.tags}" + enabled = local.enabled ? "true" : "false" + namespace = var.namespace + stage = local.account + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags } # Fetch the OrganizationAccountAccessRole ARNs from SSM module "organization_account_access_role_arn" { - enabled = "${local.enabled ? "true" : "false"}" + enabled = local.enabled ? "true" : "false" source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" parameter_read = ["/${var.namespace}/${local.account}/organization_account_access_role"] } locals { - role_arn_values = "${module.organization_account_access_role_arn.values}" + role_arn_values = module.organization_account_access_role_arn.values } data "terraform_remote_state" "stage" { - count = "${local.enabled ? 1 : 0}" + count = local.enabled ? 1 : 0 backend = "s3" # This assumes stage is using a `terraform-aws-tfstate-backend` # https://github.com/cloudposse/terraform-aws-tfstate-backend config { - role_arn = "${local.role_arn_values[0]}" - bucket = "${module.label.id}" - key = "${var.key}" + role_arn = local.role_arn_values[0] + bucket = module.label.id + key = var.key } } locals { - name_servers = "${flatten(data.terraform_remote_state.stage.*.name_servers)}" + name_servers = flatten(data.terraform_remote_state.stage.*.name_servers) } resource "aws_route53_record" "dns_zone_ns" { - count = "${local.enabled ? 1 : 0}" - zone_id = "${var.zone_id}" - name = "${var.stage}" + count = local.enabled ? 1 : 0 + zone_id = var.zone_id + name = var.stage type = "NS" - ttl = "${var.ttl}" + ttl = var.ttl records = ["${local.name_servers}"] } diff --git a/deprecated/aws/root-dns/ns/outputs.tf b/deprecated/aws/root-dns/ns/outputs.tf index c0043cb74..4cf060185 100644 --- a/deprecated/aws/root-dns/ns/outputs.tf +++ b/deprecated/aws/root-dns/ns/outputs.tf @@ -1,9 +1,9 @@ output "stage" { description = "Name of the subaccount corresponding to the name servers" - value = "${var.stage}" + value = var.stage } output "name_servers" { description = "Name servers for the account's delegated DNS zone" - value = "${local.name_servers}" + value = local.name_servers } diff --git a/deprecated/aws/root-dns/ns/variables.tf b/deprecated/aws/root-dns/ns/variables.tf index 94002a645..3021d44ab 100644 --- a/deprecated/aws/root-dns/ns/variables.tf +++ b/deprecated/aws/root-dns/ns/variables.tf @@ -1,39 +1,39 @@ variable "accounts_enabled" { - type = "list" + type = list(string) description = "Accounts to enable" default = ["dev", "staging", "prod", "testing", "audit"] } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `eg` or `example`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string default = "terraform" description = "Name (e.g. `app` or `cluster`)" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `namespace`, `stage`, `name`, and `attributes`" } variable "attributes" { - type = "list" + type = list(string) default = ["state"] description = "Additional attributes (e.g. `state`)" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. map(`BusinessUnit`,`XYZ`)" } @@ -54,6 +54,6 @@ variable "key" { variable "account" { description = "If set, then it will be used instead of 'stage' to assume role. This is useful when you need another domain for existing stage" - type = "string" + type = string default = "" } diff --git a/deprecated/aws/root-dns/parent-alerts-ns.tf b/deprecated/aws/root-dns/parent-alerts-ns.tf index 93ccb28cf..413aa2798 100644 --- a/deprecated/aws/root-dns/parent-alerts-ns.tf +++ b/deprecated/aws/root-dns/parent-alerts-ns.tf @@ -1,11 +1,11 @@ module "alerts" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "alerts" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "alerts_name_servers" { - value = "${module.alerts.name_servers}" + value = module.alerts.name_servers } diff --git a/deprecated/aws/root-dns/parent-audit-ns.tf b/deprecated/aws/root-dns/parent-audit-ns.tf index b993a1bfa..3c88f495c 100644 --- a/deprecated/aws/root-dns/parent-audit-ns.tf +++ b/deprecated/aws/root-dns/parent-audit-ns.tf @@ -1,11 +1,11 @@ module "audit" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "audit" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "audit_name_servers" { - value = "${module.audit.name_servers}" + value = module.audit.name_servers } diff --git a/deprecated/aws/root-dns/parent-corp-ns.tf b/deprecated/aws/root-dns/parent-corp-ns.tf index 14e8caa27..835ff9b99 100644 --- a/deprecated/aws/root-dns/parent-corp-ns.tf +++ b/deprecated/aws/root-dns/parent-corp-ns.tf @@ -1,11 +1,11 @@ module "corp" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "corp" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "corp_name_servers" { - value = "${module.corp.name_servers}" + value = module.corp.name_servers } diff --git a/deprecated/aws/root-dns/parent-data-ns.tf b/deprecated/aws/root-dns/parent-data-ns.tf index 53ca6e730..a5933c5e5 100644 --- a/deprecated/aws/root-dns/parent-data-ns.tf +++ b/deprecated/aws/root-dns/parent-data-ns.tf @@ -1,11 +1,11 @@ module "data" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "data" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "data_name_servers" { - value = "${module.data.name_servers}" + value = module.data.name_servers } diff --git a/deprecated/aws/root-dns/parent-dev-ns.tf b/deprecated/aws/root-dns/parent-dev-ns.tf index 40564113c..b92fccab5 100644 --- a/deprecated/aws/root-dns/parent-dev-ns.tf +++ b/deprecated/aws/root-dns/parent-dev-ns.tf @@ -1,11 +1,11 @@ module "dev" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "dev" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "dev_name_servers" { - value = "${module.dev.name_servers}" + value = module.dev.name_servers } diff --git a/deprecated/aws/root-dns/parent-identity-ns.tf b/deprecated/aws/root-dns/parent-identity-ns.tf index fc6e31148..c7a1f3058 100644 --- a/deprecated/aws/root-dns/parent-identity-ns.tf +++ b/deprecated/aws/root-dns/parent-identity-ns.tf @@ -1,11 +1,11 @@ module "identity" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "identity" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "identity_name_servers" { - value = "${module.identity.name_servers}" + value = module.identity.name_servers } diff --git a/deprecated/aws/root-dns/parent-local-ns.tf b/deprecated/aws/root-dns/parent-local-ns.tf index 80fa3c24d..074282959 100644 --- a/deprecated/aws/root-dns/parent-local-ns.tf +++ b/deprecated/aws/root-dns/parent-local-ns.tf @@ -1,5 +1,5 @@ resource "aws_route53_record" "local_dns_name" { - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id name = "local" type = "A" ttl = "30" @@ -7,7 +7,7 @@ resource "aws_route53_record" "local_dns_name" { } resource "aws_route53_record" "local_dns_wildcard" { - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id name = "*.local" type = "A" ttl = "30" diff --git a/deprecated/aws/root-dns/parent-prod-ns.tf b/deprecated/aws/root-dns/parent-prod-ns.tf index f09a15418..09786a4bd 100644 --- a/deprecated/aws/root-dns/parent-prod-ns.tf +++ b/deprecated/aws/root-dns/parent-prod-ns.tf @@ -1,11 +1,11 @@ module "prod" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "prod" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "prod_name_servers" { - value = "${module.prod.name_servers}" + value = module.prod.name_servers } diff --git a/deprecated/aws/root-dns/parent-qa-ns.tf b/deprecated/aws/root-dns/parent-qa-ns.tf index 4af999b3a..7e4ced51d 100644 --- a/deprecated/aws/root-dns/parent-qa-ns.tf +++ b/deprecated/aws/root-dns/parent-qa-ns.tf @@ -1,13 +1,13 @@ module "qa" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "qa" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id account = "staging" key = "qa-dns/terraform.tfstate" } output "qa_name_servers" { - value = "${module.qa.name_servers}" + value = module.qa.name_servers } diff --git a/deprecated/aws/root-dns/parent-security-ns.tf b/deprecated/aws/root-dns/parent-security-ns.tf index c2f29892d..5b4021de8 100644 --- a/deprecated/aws/root-dns/parent-security-ns.tf +++ b/deprecated/aws/root-dns/parent-security-ns.tf @@ -1,11 +1,11 @@ module "security" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "security" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "security_name_servers" { - value = "${module.security.name_servers}" + value = module.security.name_servers } diff --git a/deprecated/aws/root-dns/parent-staging-ns.tf b/deprecated/aws/root-dns/parent-staging-ns.tf index 025c2ec6d..e97ca60a8 100644 --- a/deprecated/aws/root-dns/parent-staging-ns.tf +++ b/deprecated/aws/root-dns/parent-staging-ns.tf @@ -1,11 +1,11 @@ module "staging" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "staging" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "staging_name_servers" { - value = "${module.staging.name_servers}" + value = module.staging.name_servers } diff --git a/deprecated/aws/root-dns/parent-testing-ns.tf b/deprecated/aws/root-dns/parent-testing-ns.tf index 304eeb4c9..9b83b5492 100644 --- a/deprecated/aws/root-dns/parent-testing-ns.tf +++ b/deprecated/aws/root-dns/parent-testing-ns.tf @@ -1,11 +1,11 @@ module "testing" { source = "ns" - accounts_enabled = "${var.accounts_enabled}" - namespace = "${var.namespace}" + accounts_enabled = var.accounts_enabled + namespace = var.namespace stage = "testing" - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id } output "testing_name_servers" { - value = "${module.testing.name_servers}" + value = module.testing.name_servers } diff --git a/deprecated/aws/root-dns/parent.tf b/deprecated/aws/root-dns/parent.tf index 332b51ebe..af1245374 100644 --- a/deprecated/aws/root-dns/parent.tf +++ b/deprecated/aws/root-dns/parent.tf @@ -1,17 +1,17 @@ variable "parent_domain_name" { - type = "string" + type = string description = "Parent domain name" } resource "aws_route53_zone" "parent_dns_zone" { - name = "${var.parent_domain_name}" + name = var.parent_domain_name comment = "Parent domain name" } resource "aws_route53_record" "parent_dns_zone_soa" { allow_overwrite = true - zone_id = "${aws_route53_zone.parent_dns_zone.id}" - name = "${aws_route53_zone.parent_dns_zone.name}" + zone_id = aws_route53_zone.parent_dns_zone.id + name = aws_route53_zone.parent_dns_zone.name type = "SOA" ttl = "60" @@ -21,9 +21,9 @@ resource "aws_route53_record" "parent_dns_zone_soa" { } output "parent_zone_id" { - value = "${aws_route53_zone.parent_dns_zone.zone_id}" + value = aws_route53_zone.parent_dns_zone.zone_id } output "parent_name_servers" { - value = "${aws_route53_zone.parent_dns_zone.name_servers}" + value = aws_route53_zone.parent_dns_zone.name_servers } diff --git a/deprecated/aws/root-dns/root.tf b/deprecated/aws/root-dns/root.tf index 022d76dfc..e7d9ab3c5 100644 --- a/deprecated/aws/root-dns/root.tf +++ b/deprecated/aws/root-dns/root.tf @@ -1,17 +1,17 @@ variable "root_domain_name" { - type = "string" + type = string description = "Root domain name" } resource "aws_route53_zone" "root_dns_zone" { - name = "${var.root_domain_name}" + name = var.root_domain_name comment = "DNS Zone for Root Account" } resource "aws_route53_record" "root_dns_zone_soa" { allow_overwrite = true - zone_id = "${aws_route53_zone.root_dns_zone.id}" - name = "${aws_route53_zone.root_dns_zone.name}" + zone_id = aws_route53_zone.root_dns_zone.id + name = aws_route53_zone.root_dns_zone.name type = "SOA" ttl = "60" @@ -21,7 +21,7 @@ resource "aws_route53_record" "root_dns_zone_soa" { } resource "aws_route53_record" "root_dns_zone_ns" { - zone_id = "${aws_route53_zone.parent_dns_zone.zone_id}" + zone_id = aws_route53_zone.parent_dns_zone.zone_id name = "root" type = "NS" ttl = "30" @@ -29,9 +29,9 @@ resource "aws_route53_record" "root_dns_zone_ns" { } output "root_zone_id" { - value = "${aws_route53_zone.root_dns_zone.zone_id}" + value = aws_route53_zone.root_dns_zone.zone_id } output "root_name_servers" { - value = "${aws_route53_zone.root_dns_zone.name_servers}" + value = aws_route53_zone.root_dns_zone.name_servers } diff --git a/deprecated/aws/root-iam/main.tf b/deprecated/aws/root-iam/main.tf index 44acbb528..dc8b18ba6 100644 --- a/deprecated/aws/root-iam/main.tf +++ b/deprecated/aws/root-iam/main.tf @@ -6,6 +6,6 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } diff --git a/deprecated/aws/root-iam/root.tf b/deprecated/aws/root-iam/root.tf index 0ee96d510..03c2a5fdb 100644 --- a/deprecated/aws/root-iam/root.tf +++ b/deprecated/aws/root-iam/root.tf @@ -1,11 +1,11 @@ variable "root_account_admin_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant admin access to Root account" default = [] } variable "root_account_readonly_user_names" { - type = "list" + type = list(string) description = "IAM user names to grant readonly access to Root account" default = [] } @@ -13,7 +13,7 @@ variable "root_account_readonly_user_names" { # Provision group access to root account with MFA module "organization_access_group_root" { source = "git::https://github.com/cloudposse/terraform-aws-iam-assumed-roles.git?ref=tags/0.6.0" - namespace = "${var.namespace}" + namespace = var.namespace stage = "root" admin_name = "admin" readonly_name = "readonly" @@ -43,19 +43,19 @@ module "organization_access_group_ssm_root" { } output "admin_group" { - value = "${module.organization_access_group_root.group_admin_name}" + value = module.organization_access_group_root.group_admin_name } output "admin_switchrole_url" { description = "URL to the IAM console to switch to the admin role" - value = "${module.organization_access_group_root.switchrole_admin_url}" + value = module.organization_access_group_root.switchrole_admin_url } output "readonly_group" { - value = "${module.organization_access_group_root.group_readonly_name}" + value = module.organization_access_group_root.group_readonly_name } output "readonly_switchrole_url" { description = "URL to the IAM console to switch to the readonly role" - value = "${module.organization_access_group_root.switchrole_readonly_url}" + value = module.organization_access_group_root.switchrole_readonly_url } diff --git a/deprecated/aws/root-iam/variables.tf b/deprecated/aws/root-iam/variables.tf index a80b06405..ec6c40496 100644 --- a/deprecated/aws/root-iam/variables.tf +++ b/deprecated/aws/root-iam/variables.tf @@ -1,13 +1,13 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } diff --git a/deprecated/aws/security-baseline/main.tf b/deprecated/aws/security-baseline/main.tf index 20c3d36c8..21bb140eb 100644 --- a/deprecated/aws/security-baseline/main.tf +++ b/deprecated/aws/security-baseline/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -19,7 +19,7 @@ data "aws_vpc" "default" { } resource "aws_default_security_group" "default" { - vpc_id = "${data.aws_vpc.default.id}" + vpc_id = data.aws_vpc.default.id tags = { Name = "Default Security Group" @@ -30,15 +30,15 @@ module "flow_logs" { source = "git::https://github.com/cloudposse/terraform-aws-vpc-flow-logs-s3-bucket.git?ref=tags/0.1.0" name = "vpc" - namespace = "${var.namespace}" - stage = "${var.stage}" - tags = "${var.tags}" - attributes = "${concat(list("default"), var.attributes, list("flow-logs"))}" - delimiter = "${var.delimiter}" + namespace = var.namespace + stage = var.stage + tags = var.tags + attributes = concat(list("default"), var.attributes, list("flow-logs")) + delimiter = var.delimiter - region = "${var.region}" + region = var.region - enabled = "${var.flow_logs_enabled}" + enabled = var.flow_logs_enabled - vpc_id = "${data.aws_vpc.default.id}" + vpc_id = data.aws_vpc.default.id } diff --git a/deprecated/aws/security-baseline/output.tf b/deprecated/aws/security-baseline/output.tf index a1d44c807..730ed3346 100644 --- a/deprecated/aws/security-baseline/output.tf +++ b/deprecated/aws/security-baseline/output.tf @@ -1,44 +1,44 @@ output "flow_logs_kms_key_arn" { - value = "${module.flow_logs.kms_key_arn}" + value = module.flow_logs.kms_key_arn description = "Flow logs KMS Key ARN" } output "flow_logs_kms_key_id" { - value = "${module.flow_logs.kms_key_id}" + value = module.flow_logs.kms_key_id description = "Flow logs KMS Key ID" } output "flow_logs_kms_alias_arn" { - value = "${module.flow_logs.kms_alias_arn}" + value = module.flow_logs.kms_alias_arn description = "Flow logs KMS Alias ARN" } output "flow_logs_kms_alias_name" { - value = "${module.flow_logs.kms_alias_name}" + value = module.flow_logs.kms_alias_name description = "Flow logs KMS Alias name" } output "flow_logs_bucket_domain_name" { - value = "${module.flow_logs.bucket_domain_name}" + value = module.flow_logs.bucket_domain_name description = "Flow logs FQDN of bucket" } output "flow_logs_bucket_id" { - value = "${module.flow_logs.bucket_id}" + value = module.flow_logs.bucket_id description = "Flow logs bucket Name (aka ID)" } output "flow_logs_bucket_arn" { - value = "${module.flow_logs.bucket_arn}" + value = module.flow_logs.bucket_arn description = "Flow logs bucket ARN" } output "flow_logs_bucket_prefix" { - value = "${module.flow_logs.bucket_prefix}" + value = module.flow_logs.bucket_prefix description = "Flow logs bucket prefix configured for lifecycle rules" } output "flow_logs_id" { - value = "${module.flow_logs.id}" + value = module.flow_logs.id description = "Flow logs ID" } diff --git a/deprecated/aws/security-baseline/variables.tf b/deprecated/aws/security-baseline/variables.tf index d05414de3..6b4b46d89 100644 --- a/deprecated/aws/security-baseline/variables.tf +++ b/deprecated/aws/security-baseline/variables.tf @@ -1,5 +1,5 @@ variable "aws_assume_role_arn" { - type = "string" + type = string } variable "enabled" { @@ -8,29 +8,29 @@ variable "enabled" { } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter between `name`, `namespace`, `stage` and `attributes`" } variable "attributes" { - type = "list" + type = list(string) description = "Additional attributes (_e.g._ \"1\")" default = [] } variable "tags" { - type = "map" + type = map(string) description = "Additional tags (_e.g._ map(\"BusinessUnit\",\"ABC\")" default = {} } diff --git a/deprecated/aws/ses/emails.tf b/deprecated/aws/ses/emails.tf index 85ef9148c..19aad1cbd 100644 --- a/deprecated/aws/ses/emails.tf +++ b/deprecated/aws/ses/emails.tf @@ -3,7 +3,7 @@ variable "relay_email" { } variable "forward_emails" { - type = "map" + type = map(string) default = { "ops@example.com" = ["example@gmail.com"] diff --git a/deprecated/aws/ses/main.tf b/deprecated/aws/ses/main.tf index bfe46c5ac..cd18ea722 100644 --- a/deprecated/aws/ses/main.tf +++ b/deprecated/aws/ses/main.tf @@ -6,54 +6,54 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } - region = "${var.ses_region}" + region = var.ses_region } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "ses_region" { - type = "string" + type = string description = "AWS Region the SES should reside in" default = "us-west-2" } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "ses_name" { - type = "string" + type = string description = "Application or solution name (e.g. `app`)" default = "ses" } variable "parent_domain_name" { - type = "string" + type = string description = "Root domain name" } module "ses" { source = "git::https://github.com/cloudposse/terraform-aws-ses-lambda-forwarder.git?ref=tags/0.2.0" - namespace = "${var.namespace}" - name = "${var.ses_name}" - stage = "${var.stage}" + namespace = var.namespace + name = var.ses_name + stage = var.stage - region = "${var.ses_region}" + region = var.ses_region - relay_email = "${var.relay_email}" - domain = "${var.parent_domain_name}" + relay_email = var.relay_email + domain = var.parent_domain_name - forward_emails = "${var.forward_emails}" + forward_emails = var.forward_emails } diff --git a/deprecated/aws/slack-archive/main.tf b/deprecated/aws/slack-archive/main.tf index 5536ebfdc..ff1b923c1 100644 --- a/deprecated/aws/slack-archive/main.tf +++ b/deprecated/aws/slack-archive/main.tf @@ -5,38 +5,38 @@ terraform { } variable "aws_assume_role_arn" { - type = "string" + type = string description = "The Amazon Resource Name (ARN) of the role to assume." } variable "domain_name" { - type = "string" + type = string description = "Domain name for Slack Archive" } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `eg` or `cp`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "region" { - type = "string" + type = string description = "AWS region" } variable "account_id" { - type = "string" + type = string description = "AWS account ID" } provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -45,14 +45,14 @@ provider "aws" { region = "us-east-1" assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } # https://www.terraform.io/docs/providers/aws/d/acm_certificate.html data "aws_acm_certificate" "acm_cloudfront_certificate" { provider = "aws.virginia" - domain = "${var.domain_name}" + domain = var.domain_name statuses = ["ISSUED"] types = ["AMAZON_ISSUED"] } @@ -65,19 +65,19 @@ locals { module "slack_archive_user" { source = "git::https://github.com/cloudposse/terraform-aws-iam-system-user.git?ref=tags/0.2.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" + namespace = var.namespace + stage = var.stage + name = local.name } module "origin" { source = "git::https://github.com/cloudposse/terraform-aws-s3-website.git?ref=tags/0.5.2" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" - hostname = "${local.cdn_domain}" - parent_zone_name = "${var.domain_name}" - region = "${var.region}" + namespace = var.namespace + stage = var.stage + name = local.name + hostname = local.cdn_domain + parent_zone_name = var.domain_name + region = var.region cors_allowed_headers = ["*"] cors_allowed_methods = ["GET"] cors_allowed_origins = ["*"] @@ -102,14 +102,14 @@ module "origin" { # CloudFront CDN fronting origin module "cdn" { source = "git::https://github.com/cloudposse/terraform-aws-cloudfront-cdn.git?ref=tags/0.4.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${local.name}" + namespace = var.namespace + stage = var.stage + name = local.name aliases = ["${local.cdn_domain}", "archive.sweetops.com"] - origin_domain_name = "${module.origin.s3_bucket_website_endpoint}" + origin_domain_name = module.origin.s3_bucket_website_endpoint origin_protocol_policy = "http-only" viewer_protocol_policy = "redirect-to-https" - parent_zone_name = "${var.domain_name}" + parent_zone_name = var.domain_name forward_cookies = "none" forward_headers = ["Origin", "Access-Control-Request-Headers", "Access-Control-Request-Method"] default_ttl = 60 @@ -120,5 +120,5 @@ module "cdn" { allowed_methods = ["GET", "HEAD", "OPTIONS"] price_class = "PriceClass_All" default_root_object = "index.html" - acm_certificate_arn = "${data.aws_acm_certificate.acm_cloudfront_certificate.arn}" + acm_certificate_arn = data.aws_acm_certificate.acm_cloudfront_certificate.arn } diff --git a/deprecated/aws/slack-archive/outputs.tf b/deprecated/aws/slack-archive/outputs.tf index 2fc48a995..0dc4f9bc3 100644 --- a/deprecated/aws/slack-archive/outputs.tf +++ b/deprecated/aws/slack-archive/outputs.tf @@ -1,82 +1,82 @@ output "slack_archive_user_name" { - value = "${module.slack_archive_user.user_name}" + value = module.slack_archive_user.user_name description = "Normalized IAM user name" } output "slack_archive_user_arn" { - value = "${module.slack_archive_user.user_arn}" + value = module.slack_archive_user.user_arn description = "The ARN assigned by AWS for the user" } output "slack_archive_user_unique_id" { - value = "${module.slack_archive_user.user_unique_id}" + value = module.slack_archive_user.user_unique_id description = "The user unique ID assigned by AWS" } output "slack_archive_user_access_key_id" { - value = "${module.slack_archive_user.access_key_id}" + value = module.slack_archive_user.access_key_id description = "The access key ID" sensitive = true } output "slack_archive_user_secret_access_key" { - value = "${module.slack_archive_user.secret_access_key}" + value = module.slack_archive_user.secret_access_key description = "The secret access key. This will be written to the state file in plain-text" sensitive = true } output "slack_archive_s3_bucket_name" { - value = "${module.origin.s3_bucket_name}" + value = module.origin.s3_bucket_name } output "slack_archive_s3_bucket_domain_name" { - value = "${module.origin.s3_bucket_domain_name}" + value = module.origin.s3_bucket_domain_name } output "slack_archive_s3_bucket_arn" { - value = "${module.origin.s3_bucket_arn}" + value = module.origin.s3_bucket_arn } output "slack_archive_s3_bucket_website_endpoint" { - value = "${module.origin.s3_bucket_website_endpoint}" + value = module.origin.s3_bucket_website_endpoint } output "slack_archive_s3_bucket_website_domain" { - value = "${module.origin.s3_bucket_website_domain}" + value = module.origin.s3_bucket_website_domain } output "slack_archive_s3_bucket_hosted_zone_id" { - value = "${module.origin.s3_bucket_hosted_zone_id}" + value = module.origin.s3_bucket_hosted_zone_id } output "slack_archive_cloudfront_id" { - value = "${module.cdn.cf_id}" + value = module.cdn.cf_id } output "slack_archive_cloudfront_arn" { - value = "${module.cdn.cf_arn}" + value = module.cdn.cf_arn } output "slack_archive_cloudfront_aliases" { - value = "${module.cdn.cf_aliases}" + value = module.cdn.cf_aliases } output "slack_archive_cloudfront_status" { - value = "${module.cdn.cf_status}" + value = module.cdn.cf_status } output "slack_archive_cloudfront_domain_name" { - value = "${module.cdn.cf_domain_name}" + value = module.cdn.cf_domain_name } output "slack_archive_cloudfront_etag" { - value = "${module.cdn.cf_etag}" + value = module.cdn.cf_etag } output "slack_archive_cloudfront_hosted_zone_id" { - value = "${module.cdn.cf_hosted_zone_id}" + value = module.cdn.cf_hosted_zone_id } output "slack_archive_cloudfront_origin_access_identity_path" { - value = "${module.cdn.cf_origin_access_identity}" + value = module.cdn.cf_origin_access_identity } diff --git a/deprecated/aws/teleport/main.tf b/deprecated/aws/teleport/main.tf index fdf351951..557e1b979 100644 --- a/deprecated/aws/teleport/main.tf +++ b/deprecated/aws/teleport/main.tf @@ -6,52 +6,52 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } module "teleport_backend" { source = "git::https://github.com/cloudposse/terraform-aws-teleport-storage.git?ref=tags/0.4.0" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name attributes = [] - tags = "${var.tags}" - prefix = "${var.s3_prefix}" - standard_transition_days = "${var.s3_standard_transition_days}" - glacier_transition_days = "${var.s3_glacier_transition_days}" - expiration_days = "${var.s3_expiration_days}" + tags = var.tags + prefix = var.s3_prefix + standard_transition_days = var.s3_standard_transition_days + glacier_transition_days = var.s3_glacier_transition_days + expiration_days = var.s3_expiration_days - iam_role_max_session_duration = "${var.iam_role_max_session_duration}" + iam_role_max_session_duration = var.iam_role_max_session_duration # Autoscale min_read and min_write capacity will set the provisioned capacity for both cluster state and audit events - autoscale_min_read_capacity = "${var.autoscale_min_read_capacity}" - autoscale_min_write_capacity = "${var.autoscale_min_write_capacity}" + autoscale_min_read_capacity = var.autoscale_min_read_capacity + autoscale_min_write_capacity = var.autoscale_min_write_capacity # Currently the autoscalers for the cluster state and the audit events share the same settings - autoscale_read_target = "${var.autoscale_read_target}" - autoscale_write_target = "${var.autoscale_write_target}" - autoscale_max_read_capacity = "${var.autoscale_max_read_capacity}" - autoscale_max_write_capacity = "${var.autoscale_max_write_capacity}" + autoscale_read_target = var.autoscale_read_target + autoscale_write_target = var.autoscale_write_target + autoscale_max_read_capacity = var.autoscale_max_read_capacity + autoscale_max_write_capacity = var.autoscale_max_write_capacity } module "teleport_role_name" { source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.3.3" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - delimiter = "${var.delimiter}" + namespace = var.namespace + stage = var.stage + name = var.name + delimiter = var.delimiter attributes = ["auth"] - tags = "${var.tags}" + tags = var.tags } module "kops_metadata" { source = "git::https://github.com/cloudposse/terraform-aws-kops-data-iam.git?ref=tags/0.1.0" - cluster_name = "${var.cluster_name}" + cluster_name = var.cluster_name } locals { - chamber_service = "${var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service}" + chamber_service = var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service kops_arns = { masters = ["${module.kops_metadata.masters_role_arn}"] @@ -82,9 +82,9 @@ data "aws_iam_policy_document" "assume_role" { } resource "aws_iam_role" "teleport" { - name = "${module.teleport_role_name.id}" - assume_role_policy = "${data.aws_iam_policy_document.assume_role.json}" - max_session_duration = "${var.iam_role_max_session_duration}" + name = module.teleport_role_name.id + assume_role_policy = data.aws_iam_policy_document.assume_role.json + max_session_duration = var.iam_role_max_session_duration description = "The Teleport role to access teleport backend" } @@ -131,18 +131,18 @@ data "aws_iam_policy_document" "teleport" { } resource "aws_iam_policy" "teleport" { - name = "${module.teleport_role_name.id}" + name = module.teleport_role_name.id description = "Grant permissions for teleport" - policy = "${data.aws_iam_policy_document.teleport.json}" + policy = data.aws_iam_policy_document.teleport.json } resource "aws_iam_role_policy_attachment" "teleport" { - role = "${aws_iam_role.teleport.name}" - policy_arn = "${aws_iam_policy.teleport.arn}" + role = aws_iam_role.teleport.name + policy_arn = aws_iam_policy.teleport.arn } resource "aws_ssm_parameter" "teleport_audit_sessions_uri" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "teleport_audit_sessions_uri")}" + name = format(var.chamber_parameter_name, local.chamber_service, "teleport_audit_sessions_uri") value = "s3://${module.teleport_backend.s3_bucket_id}" description = "Teleport session logs storage URI" type = "String" @@ -150,7 +150,7 @@ resource "aws_ssm_parameter" "teleport_audit_sessions_uri" { } resource "aws_ssm_parameter" "teleport_audit_events_uri" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "teleport_audit_events_uri")}" + name = format(var.chamber_parameter_name, local.chamber_service, "teleport_audit_events_uri") value = "dynamodb://${module.teleport_backend.dynamodb_audit_table_id}" description = "Teleport audite events storage URI" type = "String" @@ -158,24 +158,24 @@ resource "aws_ssm_parameter" "teleport_audit_events_uri" { } resource "aws_ssm_parameter" "teleport_cluster_state_dynamodb_table" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "teleport_cluster_state_dynamodb_table")}" - value = "${module.teleport_backend.dynamodb_state_table_id}" + name = format(var.chamber_parameter_name, local.chamber_service, "teleport_cluster_state_dynamodb_table") + value = module.teleport_backend.dynamodb_state_table_id description = "Teleport cluster state storage dynamodb table" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "teleport_auth_iam_role" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "teleport_auth_iam_role")}" - value = "${aws_iam_role.teleport.name}" + name = format(var.chamber_parameter_name, local.chamber_service, "teleport_auth_iam_role") + value = aws_iam_role.teleport.name description = "Teleport auth IAM role" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "teleport_kubernetes_namespace" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "teleport_kubernetes_namespace")}" - value = "${var.kubernetes_namespace}" + name = format(var.chamber_parameter_name, local.chamber_service, "teleport_kubernetes_namespace") + value = var.kubernetes_namespace description = "Teleport auth IAM role" type = "String" overwrite = "true" @@ -186,35 +186,35 @@ locals { } resource "random_string" "tokens" { - count = "${length(local.token_names)}" + count = length(local.token_names) length = 32 special = false keepers { - cluster_name = "${var.cluster_name}" + cluster_name = var.cluster_name } } resource "aws_ssm_parameter" "teleport_tokens" { - count = "${length(local.token_names)}" - name = "${format(var.chamber_parameter_name, local.chamber_service, "${element(local.token_names, count.index)}")}" - value = "${element(random_string.tokens.*.result, count.index)}" + count = length(local.token_names) + name = format(var.chamber_parameter_name, local.chamber_service, "${element(local.token_names, count.index)}") + value = element(random_string.tokens.*.result, count.index) description = "Teleport join token: ${element(local.token_names, count.index)}" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "teleport_proxy_domain_name" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "teleport_proxy_domain_name")}" - value = "${var.teleport_proxy_domain_name}" + name = format(var.chamber_parameter_name, local.chamber_service, "teleport_proxy_domain_name") + value = var.teleport_proxy_domain_name description = "Teleport Proxy domain name" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "teleport_version" { - name = "${format(var.chamber_parameter_name, local.chamber_service, "teleport_version")}" - value = "${var.teleport_version}" + name = format(var.chamber_parameter_name, local.chamber_service, "teleport_version") + value = var.teleport_version description = "Teleport version to install" type = "String" overwrite = "true" @@ -230,6 +230,6 @@ resource "kubernetes_namespace" "default" { name = "${var.kubernetes_namespace}" } - name = "${var.kubernetes_namespace}" + name = var.kubernetes_namespace } } diff --git a/deprecated/aws/teleport/outputs.tf b/deprecated/aws/teleport/outputs.tf index 2ed293f5f..ad27c55b4 100644 --- a/deprecated/aws/teleport/outputs.tf +++ b/deprecated/aws/teleport/outputs.tf @@ -1,25 +1,25 @@ output "teleport_version" { - value = "${var.teleport_version}" + value = var.teleport_version } output "teleport_proxy_domain_name" { - value = "${var.teleport_proxy_domain_name}" + value = var.teleport_proxy_domain_name } output "parameter_store_prefix" { - value = "${format(var.chamber_parameter_name, local.chamber_service, "")}" + value = format(var.chamber_parameter_name, local.chamber_service, "") } output "teleport_kubernetes_namespace" { - value = "${var.kubernetes_namespace}" + value = var.kubernetes_namespace } output "teleport_auth_iam_role" { - value = "${aws_iam_role.teleport.name}" + value = aws_iam_role.teleport.name } output "teleport_cluster_state_dynamodb_table" { - value = "${module.teleport_backend.dynamodb_state_table_id}" + value = module.teleport_backend.dynamodb_state_table_id } output "teleport_audit_sessions_uri" { diff --git a/deprecated/aws/teleport/variables.tf b/deprecated/aws/teleport/variables.tf index 26a38e51f..e52ba7b5a 100644 --- a/deprecated/aws/teleport/variables.tf +++ b/deprecated/aws/teleport/variables.tf @@ -1,5 +1,5 @@ variable "permitted_nodes" { - type = "string" + type = string # Set to 'masters' if using kiam to control roles default = "both" @@ -7,67 +7,67 @@ variable "permitted_nodes" { } variable "cluster_name" { - type = "string" + type = string description = "Kops cluster name (e.g. `us-east-1.prod.cloudposse.co` or `cluster-1.cloudposse.co`)" } variable "aws_assume_role_arn" { - type = "string" + type = string description = "AWS IAM Role for Terraform to assume during operation" } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "kubernetes_namespace" { - type = "string" + type = string description = "Kubernetes namespace in which to place Teleport resources" default = "teleport" } variable "stage" { - type = "string" + type = string description = "Stage, e.g. 'prod', 'staging', 'dev', or 'test'" } variable "name" { - type = "string" + type = string description = "The name of the app" default = "teleport" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `namespace`, `stage`, `name` and `attributes`" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. map('BusinessUnit`,`XYZ`)" } variable "teleport_version" { - type = "string" + type = string description = "Version number of Teleport to install (e.g. \"4.0.9\")" } variable "teleport_proxy_domain_name" { - type = "string" + type = string description = "Domain name to use for Teleport Proxy" } variable "masters_name" { - type = "string" + type = string default = "masters" description = "Kops masters subdomain name in the cluster DNS zone" } variable "nodes_name" { - type = "string" + type = string default = "nodes" description = "Kops nodes subdomain name in the cluster DNS zone" } @@ -82,25 +82,25 @@ variable "chamber_parameter_name" { } variable "s3_prefix" { - type = "string" + type = string description = "S3 bucket prefix" default = "" } variable "s3_standard_transition_days" { - type = "string" + type = string description = "Number of days to persist in the standard storage tier before moving to the glacier tier" default = "30" } variable "s3_glacier_transition_days" { - type = "string" + type = string description = "Number of days after which to move the data to the glacier storage tier" default = "60" } variable "s3_expiration_days" { - type = "string" + type = string description = "Number of days after which to expunge the objects" default = "90" } diff --git a/deprecated/aws/tfstate-backend/main.tf b/deprecated/aws/tfstate-backend/main.tf index cbbeb258e..96822414f 100644 --- a/deprecated/aws/tfstate-backend/main.tf +++ b/deprecated/aws/tfstate-backend/main.tf @@ -8,63 +8,63 @@ variable "aws_assume_role_arn" {} provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string description = "Application or solution name (e.g. `app`)" default = "terraform" } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `namespace`, `stage`, `name` and `attributes`" } variable "attributes" { - type = "list" + type = list(string) default = ["state"] description = "Additional attributes (e.g. `1`)" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. map(`BusinessUnit`,`XYZ`)" } variable "region" { - type = "string" + type = string description = "AWS Region the S3 bucket should reside in" default = "us-west-2" } variable "force_destroy" { - type = "string" + type = string description = "A boolean that indicates the S3 bucket can be destroyed even if it contains objects. These objects are not recoverable." default = "false" } module "tfstate_backend" { source = "git::https://github.com/cloudposse/terraform-aws-tfstate-backend.git?ref=tags/0.7.0" - namespace = "${var.namespace}" - name = "${var.name}" - stage = "${var.stage}" - attributes = "${var.attributes}" - tags = "${var.tags}" - region = "${var.region}" - force_destroy = "${var.force_destroy}" + namespace = var.namespace + name = var.name + stage = var.stage + attributes = var.attributes + tags = var.tags + region = var.region + force_destroy = var.force_destroy } diff --git a/deprecated/aws/tfstate-backend/outputs.tf b/deprecated/aws/tfstate-backend/outputs.tf index 3ae318d44..2f67095d8 100644 --- a/deprecated/aws/tfstate-backend/outputs.tf +++ b/deprecated/aws/tfstate-backend/outputs.tf @@ -1,23 +1,23 @@ output "tfstate_backend_s3_bucket_domain_name" { - value = "${module.tfstate_backend.s3_bucket_domain_name}" + value = module.tfstate_backend.s3_bucket_domain_name } output "tfstate_backend_s3_bucket_id" { - value = "${module.tfstate_backend.s3_bucket_id}" + value = module.tfstate_backend.s3_bucket_id } output "tfstate_backend_s3_bucket_arn" { - value = "${module.tfstate_backend.s3_bucket_arn}" + value = module.tfstate_backend.s3_bucket_arn } output "tfstate_backend_dynamodb_table_name" { - value = "${module.tfstate_backend.dynamodb_table_name}" + value = module.tfstate_backend.dynamodb_table_name } output "tfstate_backend_dynamodb_table_id" { - value = "${module.tfstate_backend.dynamodb_table_id}" + value = module.tfstate_backend.dynamodb_table_id } output "tfstate_backend_dynamodb_table_arn" { - value = "${module.tfstate_backend.dynamodb_table_arn}" + value = module.tfstate_backend.dynamodb_table_arn } diff --git a/deprecated/aws/users/main.tf b/deprecated/aws/users/main.tf index 79dd367c8..b68506b65 100644 --- a/deprecated/aws/users/main.tf +++ b/deprecated/aws/users/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -29,29 +29,29 @@ data "terraform_remote_state" "root_iam" { } locals { - accounts_enabled = "${concat(list("root"), var.accounts_enabled)}" + accounts_enabled = concat(list("root"), var.accounts_enabled) } # Fetch the OrganizationAccountAccessRole ARNs from SSM module "admin_groups" { source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" - parameter_read = "${formatlist("/${var.namespace}/%s/admin_group", local.accounts_enabled)}" + parameter_read = formatlist("/${var.namespace}/%s/admin_group", local.accounts_enabled) } locals { - account_alias = "${data.terraform_remote_state.account_settings.account_alias}" - signin_url = "${data.terraform_remote_state.account_settings.signin_url}" + account_alias = data.terraform_remote_state.account_settings.account_alias + signin_url = data.terraform_remote_state.account_settings.signin_url admin_groups = ["${module.admin_groups.values}"] readonly_groups = ["${data.terraform_remote_state.root_iam.readonly_group}"] - minimum_password_length = "${data.terraform_remote_state.account_settings.minimum_password_length}" + minimum_password_length = data.terraform_remote_state.account_settings.minimum_password_length } output "account_alias" { description = "AWS IAM Account Alias" - value = "${local.account_alias}" + value = local.account_alias } output "signin_url" { description = "AWS Signin URL" - value = "${local.signin_url}" + value = local.signin_url } diff --git a/deprecated/aws/users/variables.tf b/deprecated/aws/users/variables.tf index d67b5a583..b50636363 100644 --- a/deprecated/aws/users/variables.tf +++ b/deprecated/aws/users/variables.tf @@ -1,30 +1,30 @@ variable "aws_assume_role_arn" {} variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string description = "Application or solution name (e.g. `app`)" default = "terraform" } variable "smtp_username" { description = "Username to authenticate with the SMTP server" - type = "string" + type = string default = "" } variable "smtp_password" { description = "Password to authenticate with the SMTP server" - type = "string" + type = string default = "" } @@ -39,7 +39,7 @@ variable "smtp_port" { } variable "accounts_enabled" { - type = "list" + type = list(string) description = "Accounts to enable" default = ["dev", "staging", "prod", "testing", "audit"] } diff --git a/deprecated/aws/vpc-peering-intra-account/main.tf b/deprecated/aws/vpc-peering-intra-account/main.tf index 8fa6b25ef..858d4e995 100644 --- a/deprecated/aws/vpc-peering-intra-account/main.tf +++ b/deprecated/aws/vpc-peering-intra-account/main.tf @@ -6,28 +6,28 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } module "vpc_peering" { source = "git::https://github.com/cloudposse/terraform-aws-vpc-peering.git?ref=tags/0.2.0" - enabled = "${var.enabled}" + enabled = var.enabled - stage = "${var.stage}" - namespace = "${var.namespace}" - name = "${var.name}" - delimiter = "${var.delimiter}" - attributes = "${var.attributes}" - tags = "${var.tags}" + stage = var.stage + namespace = var.namespace + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags - requestor_vpc_id = "${var.requestor_vpc_id}" - requestor_vpc_tags = "${var.requestor_vpc_tags}" - acceptor_vpc_id = "${var.acceptor_vpc_id}" - acceptor_vpc_tags = "${var.acceptor_vpc_tags}" - auto_accept = "${var.auto_accept}" + requestor_vpc_id = var.requestor_vpc_id + requestor_vpc_tags = var.requestor_vpc_tags + acceptor_vpc_id = var.acceptor_vpc_id + acceptor_vpc_tags = var.acceptor_vpc_tags + auto_accept = var.auto_accept - acceptor_allow_remote_vpc_dns_resolution = "${var.acceptor_allow_remote_vpc_dns_resolution}" - requestor_allow_remote_vpc_dns_resolution = "${var.requestor_allow_remote_vpc_dns_resolution}" + acceptor_allow_remote_vpc_dns_resolution = var.acceptor_allow_remote_vpc_dns_resolution + requestor_allow_remote_vpc_dns_resolution = var.requestor_allow_remote_vpc_dns_resolution } diff --git a/deprecated/aws/vpc-peering-intra-account/outputs.tf b/deprecated/aws/vpc-peering-intra-account/outputs.tf index 287462bd2..f7993616e 100644 --- a/deprecated/aws/vpc-peering-intra-account/outputs.tf +++ b/deprecated/aws/vpc-peering-intra-account/outputs.tf @@ -1,9 +1,9 @@ output "connection_id" { - value = "${module.vpc_peering.connection_id}" + value = module.vpc_peering.connection_id description = "VPC peering connection ID" } output "accept_status" { - value = "${module.vpc_peering.accept_status}" + value = module.vpc_peering.accept_status description = "The status of the VPC peering connection request" } diff --git a/deprecated/aws/vpc-peering-intra-account/variables.tf b/deprecated/aws/vpc-peering-intra-account/variables.tf index 2cc379d04..94c6739aa 100644 --- a/deprecated/aws/vpc-peering-intra-account/variables.tf +++ b/deprecated/aws/vpc-peering-intra-account/variables.tf @@ -4,29 +4,29 @@ variable "enabled" { } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "requestor_vpc_id" { - type = "string" + type = string description = "Requestor VPC ID" default = "" } variable "requestor_vpc_tags" { - type = "map" + type = map(string) description = "Requestor VPC tags" default = {} } variable "acceptor_vpc_id" { - type = "string" + type = string description = "Acceptor VPC ID" default = "" } variable "acceptor_vpc_tags" { - type = "map" + type = map(string) description = "Acceptor VPC tags" default = {} } @@ -48,33 +48,33 @@ variable "requestor_allow_remote_vpc_dns_resolution" { variable "namespace" { description = "Namespace (e.g. `cp` or `cloudposse`)" - type = "string" + type = string } variable "stage" { description = "Stage (e.g. `prod`, `dev`, `staging`)" - type = "string" + type = string } variable "name" { description = "Name (e.g. `app` or `cluster`)" - type = "string" + type = string } variable "delimiter" { - type = "string" + type = string default = "-" description = "Delimiter to be used between `namespace`, `stage`, `name`, and `attributes`" } variable "attributes" { - type = "list" + type = list(string) default = [] description = "Additional attributes (e.g. `policy` or `role`)" } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags (e.g. map('BusinessUnit`,`XYZ`)" } diff --git a/deprecated/aws/vpc-peering/main.tf b/deprecated/aws/vpc-peering/main.tf index 3a961f1ac..dc28aedd8 100644 --- a/deprecated/aws/vpc-peering/main.tf +++ b/deprecated/aws/vpc-peering/main.tf @@ -6,75 +6,75 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } # Fetch the OrganizationAccountAccessRole ARNs from SSM module "requester_role_arns" { - enabled = "${var.enabled}" + enabled = var.enabled source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" parameter_read = ["/${var.namespace}/${var.requester_account}/organization_account_access_role"] } locals { - requester_vpc_tags = "${var.requester_vpc_tags}" - requester_region = "${var.requester_region}" - requester_role_arn = "${join("", module.requester_role_arns.values)}" + requester_vpc_tags = var.requester_vpc_tags + requester_region = var.requester_region + requester_role_arn = join("", module.requester_role_arns.values) } # Fetch the OrganizationAccountAccessRole ARNs from SSM module "accepter_role_arns" { - enabled = "${var.enabled}" + enabled = var.enabled source = "git::https://github.com/cloudposse/terraform-aws-ssm-parameter-store?ref=tags/0.1.5" parameter_read = ["/${var.namespace}/${var.accepter_account}/organization_account_access_role"] } locals { - accepter_vpc_tags = "${var.accepter_vpc_tags}" - accepter_region = "${var.accepter_region}" - accepter_role_arn = "${join("", module.accepter_role_arns.values)}" + accepter_vpc_tags = var.accepter_vpc_tags + accepter_region = var.accepter_region + accepter_role_arn = join("", module.accepter_role_arns.values) } module "vpc_peering" { source = "git::https://github.com/cloudposse/terraform-aws-vpc-peering-multi-account.git?ref=tags/0.1.0" - enabled = "${var.enabled}" + enabled = var.enabled - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" + namespace = var.namespace + stage = var.stage + name = var.name attributes = ["${var.requester_account}", "${var.accepter_account}"] auto_accept = true # Requester - requester_vpc_tags = "${local.requester_vpc_tags}" - requester_region = "${local.requester_region}" - requester_aws_assume_role_arn = "${local.requester_role_arn}" + requester_vpc_tags = local.requester_vpc_tags + requester_region = local.requester_region + requester_aws_assume_role_arn = local.requester_role_arn # Accepter - accepter_vpc_tags = "${local.accepter_vpc_tags}" - accepter_region = "${local.accepter_region}" - accepter_aws_assume_role_arn = "${local.accepter_role_arn}" + accepter_vpc_tags = local.accepter_vpc_tags + accepter_region = local.accepter_region + accepter_aws_assume_role_arn = local.accepter_role_arn } output "accepter_accept_status" { description = "Accepter VPC peering connection request status" - value = "${module.vpc_peering.accepter_accept_status}" + value = module.vpc_peering.accepter_accept_status } output "accepter_connection_id" { description = "Accepter VPC peering connection ID" - value = "${module.vpc_peering.accepter_connection_id}" + value = module.vpc_peering.accepter_connection_id } output "requester_accept_status" { description = "Requester VPC peering connection request status" - value = "${module.vpc_peering.requester_accept_status}" + value = module.vpc_peering.requester_accept_status } output "requester_connection_id" { description = "Requester VPC peering connection ID" - value = "${module.vpc_peering.requester_connection_id}" + value = module.vpc_peering.requester_connection_id } diff --git a/deprecated/aws/vpc-peering/variables.tf b/deprecated/aws/vpc-peering/variables.tf index 85e4cce08..cdb936c32 100644 --- a/deprecated/aws/vpc-peering/variables.tf +++ b/deprecated/aws/vpc-peering/variables.tf @@ -1,23 +1,23 @@ variable "aws_assume_role_arn" {} variable "enabled" { - type = "string" + type = string description = "Whether to create the resources. Set to `false` to prevent the module from creating any resources" default = "true" } variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `eg` or `cp`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string description = "Application or solution name (e.g. `app`)" default = "vpc-peering" } @@ -31,7 +31,7 @@ variable "requester_region" { } variable "requester_vpc_tags" { - type = "map" + type = map(string) description = "Tags to filter for the requester's VPC" default = {} } @@ -45,7 +45,7 @@ variable "accepter_account" { } variable "accepter_vpc_tags" { - type = "map" + type = map(string) description = "Tags to filter for the accepter's VPC" default = {} } diff --git a/deprecated/aws/vpc/main.tf b/deprecated/aws/vpc/main.tf index 3cc87318c..0dd89393b 100644 --- a/deprecated/aws/vpc/main.tf +++ b/deprecated/aws/vpc/main.tf @@ -6,7 +6,7 @@ terraform { provider "aws" { assume_role { - role_arn = "${var.aws_assume_role_arn}" + role_arn = var.aws_assume_role_arn } } @@ -15,18 +15,18 @@ provider "null" { } locals { - chamber_service = "${var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service}" + chamber_service = var.chamber_service == "" ? basename(pathexpand(path.module)) : var.chamber_service # Work around limitation that conditional operator cannot be used with lists. https://github.com/hashicorp/terraform/issues/18259 - availability_zones = "${split("|", length(var.availability_zones) == 0 ? join("|", data.aws_availability_zones.available.names) : join("|", var.availability_zones))}" + availability_zones = split("|", length(var.availability_zones) == 0 ? join("|", data.aws_availability_zones.available.names) : join("|", var.availability_zones)) } module "parameter_prefix" { source = "git::https://github.com/cloudposse/terraform-terraform-label.git?ref=tags/0.2.1" namespace = "" stage = "" - name = "${var.name}" - attributes = "${var.attributes}" + name = var.name + attributes = var.attributes delimiter = "_" } @@ -34,108 +34,108 @@ data "aws_availability_zones" "available" {} module "vpc" { source = "git::https://github.com/cloudposse/terraform-aws-vpc.git?ref=tags/0.3.3" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - cidr_block = "${var.vpc_cidr_block}" - attributes = "${var.attributes}" - tags = "${var.tags}" + namespace = var.namespace + stage = var.stage + name = var.name + cidr_block = var.vpc_cidr_block + attributes = var.attributes + tags = var.tags } module "subnets" { source = "git::https://github.com/cloudposse/terraform-aws-dynamic-subnets.git?ref=tags/0.12.3" - availability_zones = "${local.availability_zones}" - max_subnet_count = "${var.max_subnet_count}" - namespace = "${var.namespace}" - stage = "${var.stage}" - name = "${var.name}" - vpc_id = "${module.vpc.vpc_id}" - igw_id = "${module.vpc.igw_id}" - cidr_block = "${module.vpc.vpc_cidr_block}" - nat_gateway_enabled = "${var.vpc_nat_gateway_enabled}" - nat_instance_enabled = "${var.vpc_nat_instance_enabled}" - nat_instance_type = "${var.vpc_nat_instance_type}" - attributes = "${var.attributes}" - tags = "${var.tags}" + availability_zones = local.availability_zones + max_subnet_count = var.max_subnet_count + namespace = var.namespace + stage = var.stage + name = var.name + vpc_id = module.vpc.vpc_id + igw_id = module.vpc.igw_id + cidr_block = module.vpc.vpc_cidr_block + nat_gateway_enabled = var.vpc_nat_gateway_enabled + nat_instance_enabled = var.vpc_nat_instance_enabled + nat_instance_type = var.vpc_nat_instance_type + attributes = var.attributes + tags = var.tags } resource "aws_ssm_parameter" "vpc_id" { description = "VPC ID of backing services" - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "vpc_id")}" - value = "${module.vpc.vpc_id}" + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "vpc_id") + value = module.vpc.vpc_id type = "String" overwrite = "true" } resource "aws_ssm_parameter" "igw_id" { description = "VPC ID of backing services" - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "igw_id")}" - value = "${module.vpc.igw_id}" + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "igw_id") + value = module.vpc.igw_id type = "String" overwrite = "true" } resource "aws_ssm_parameter" "cidr_block" { description = "VPC ID of backing services" - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "cidr_block")}" - value = "${module.vpc.vpc_cidr_block}" + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "cidr_block") + value = module.vpc.vpc_cidr_block type = "String" overwrite = "true" } resource "aws_ssm_parameter" "availability_zones" { - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "availability_zones")}" - value = "${join(",", local.availability_zones)}" + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "availability_zones") + value = join(",", local.availability_zones) description = "VPC subnet availability zones" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "nat_gateways" { - count = "${var.vpc_nat_gateway_enabled == "true" ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "nat_gateways")}" - value = "${join(",", module.subnets.nat_gateway_ids)}" + count = var.vpc_nat_gateway_enabled == "true" ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "nat_gateways") + value = join(",", module.subnets.nat_gateway_ids) description = "VPC private NAT gateways" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "nat_instances" { - count = "${var.vpc_nat_instance_enabled == "true" ? 1 : 0}" - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "nat_instances")}" - value = "${join(",", module.subnets.nat_instance_ids)}" + count = var.vpc_nat_instance_enabled == "true" ? 1 : 0 + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "nat_instances") + value = join(",", module.subnets.nat_instance_ids) description = "VPC private NAT instances" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "private_subnet_cidrs" { - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "private_subnet_cidrs")}" - value = "${join(",", module.subnets.private_subnet_cidrs)}" + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "private_subnet_cidrs") + value = join(",", module.subnets.private_subnet_cidrs) description = "VPC private subnet CIDRs" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "private_subnet_ids" { - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "private_subnet_ids")}" - value = "${join(",", module.subnets.private_subnet_ids)}" + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "private_subnet_ids") + value = join(",", module.subnets.private_subnet_ids) description = "VPC private subnet AWS IDs" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "public_subnet_cidrs" { - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "public_subnet_cidrs")}" - value = "${join(",", module.subnets.public_subnet_cidrs)}" + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "public_subnet_cidrs") + value = join(",", module.subnets.public_subnet_cidrs) description = "VPC public subnet CIDRs" type = "String" overwrite = "true" } resource "aws_ssm_parameter" "public_subnet_ids" { - name = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "public_subnet_ids")}" - value = "${join(",", module.subnets.public_subnet_ids)}" + name = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "public_subnet_ids") + value = join(",", module.subnets.public_subnet_ids) description = "VPC public subnet AWS IDs" type = "String" overwrite = "true" diff --git a/deprecated/aws/vpc/outputs.tf b/deprecated/aws/vpc/outputs.tf index 7c66d939a..46be2a120 100644 --- a/deprecated/aws/vpc/outputs.tf +++ b/deprecated/aws/vpc/outputs.tf @@ -1,48 +1,48 @@ output "parameter_store_prefix" { - value = "${format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "")}" + value = format(var.chamber_parameter_name, local.chamber_service, module.parameter_prefix.id, "") } output "vpc_id" { description = "AWS ID of the VPC created" - value = "${aws_ssm_parameter.vpc_id.value}" + value = aws_ssm_parameter.vpc_id.value } output "igw_id" { description = "AWS ID of Internet Gateway for the VPC" - value = "${aws_ssm_parameter.igw_id.value}" + value = aws_ssm_parameter.igw_id.value } output "nat_gateways" { description = "Comma-separated string list of AWS IDs of NAT Gateways for the VPC" - value = "${join("", aws_ssm_parameter.nat_gateways.*.value)}" + value = join("", aws_ssm_parameter.nat_gateways.*.value) } output "cidr_block" { description = "CIDR block of the VPC" - value = "${aws_ssm_parameter.cidr_block.value}" + value = aws_ssm_parameter.cidr_block.value } output "availability_zones" { description = "Comma-separated string list of avaialbility zones where subnets have been created" - value = "${aws_ssm_parameter.availability_zones.value}" + value = aws_ssm_parameter.availability_zones.value } output "public_subnet_cidrs" { description = "Comma-separated string list of CIDR blocks of public VPC subnets" - value = "${aws_ssm_parameter.public_subnet_cidrs.value}" + value = aws_ssm_parameter.public_subnet_cidrs.value } output "public_subnet_ids" { description = "Comma-separated string list of AWS IDs of public VPC subnets" - value = "${aws_ssm_parameter.public_subnet_ids.value}" + value = aws_ssm_parameter.public_subnet_ids.value } output "private_subnet_cidrs" { description = "Comma-separated string list of CIDR blocks of private VPC subnets" - value = "${aws_ssm_parameter.private_subnet_cidrs.value}" + value = aws_ssm_parameter.private_subnet_cidrs.value } output "private_subnet_ids" { description = "Comma-separated string list of AWS IDs of private VPC subnets" - value = "${aws_ssm_parameter.private_subnet_ids.value}" + value = aws_ssm_parameter.private_subnet_ids.value } diff --git a/deprecated/aws/vpc/variables.tf b/deprecated/aws/vpc/variables.tf index 76e5b19c1..ffc2423e1 100644 --- a/deprecated/aws/vpc/variables.tf +++ b/deprecated/aws/vpc/variables.tf @@ -1,31 +1,31 @@ variable "namespace" { - type = "string" + type = string description = "Namespace (e.g. `cp` or `cloudposse`)" } variable "stage" { - type = "string" + type = string description = "Stage (e.g. `prod`, `dev`, `staging`)" } variable "name" { - type = "string" + type = string description = "Name to distinguish this VPC from others in this account" default = "vpc" } variable "attributes" { - type = "list" + type = list(string) description = "Additional attributes to distinguish this VPC from others in this account" default = ["common"] } variable "aws_assume_role_arn" { - type = "string" + type = string } variable "region" { - type = "string" + type = string } variable "max_subnet_count" { @@ -34,7 +34,7 @@ variable "max_subnet_count" { } variable "availability_zones" { - type = "list" + type = list(string) default = [] description = "List of Availability Zones where subnets will be created. If empty, all zones will be used" } @@ -55,7 +55,7 @@ variable "vpc_nat_instance_type" { } variable "tags" { - type = "map" + type = map(string) default = {} description = "Additional tags, for example map(`KubernetesCluster`,`us-west-2.prod.example.com`)" } diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 544c9b41d..0a775d9f7 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -150,13 +150,14 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | = 4.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | = 4.0 | +| [template](#provider\_template) | n/a | ## Modules @@ -165,55 +166,80 @@ components: | [alb\_ecs\_label](#module\_alb\_ecs\_label) | cloudposse/label/null | 0.25.0 | | [alb\_ingress](#module\_alb\_ingress) | cloudposse/alb-ingress/aws | 0.24.3 | | [container\_definition](#module\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | -| [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.66.0 | +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [datadog\_container\_definition](#module\_datadog\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | +| [datadog\_fluent\_bit\_container\_definition](#module\_datadog\_fluent\_bit\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | +| [datadog\_sidecar\_logs](#module\_datadog\_sidecar\_logs) | cloudposse/cloudwatch-logs/aws | 0.6.6 | +| [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.66.4 | | [ecs\_cloudwatch\_autoscaling](#module\_ecs\_cloudwatch\_autoscaling) | cloudposse/ecs-cloudwatch-autoscaling/aws | 0.7.3 | -| [ecs\_label](#module\_ecs\_label) | cloudposse/label/null | 0.25.0 | +| [ecs\_cloudwatch\_sns\_alarms](#module\_ecs\_cloudwatch\_sns\_alarms) | cloudposse/ecs-cloudwatch-sns-alarms/aws | 0.12.3 | +| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | +| [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [logs](#module\_logs) | cloudposse/cloudwatch-logs/aws | 0.6.6 | -| [rds\_sg\_label](#module\_rds\_sg\_label) | cloudposse/label/null | 0.25.0 | +| [rds](#module\_rds) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [roles\_to\_principals](#module\_roles\_to\_principals) | ../account-map/modules/roles-to-principals | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vanity\_alias](#module\_vanity\_alias) | cloudposse/route53-alias/aws | 0.13.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | ## Resources | Name | Type | |------|------| -| [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | -| [aws_iam_role_policy_attachment.task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_kinesis_stream.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_stream) | resource | -| [aws_ecs_cluster.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_cluster) | data source | -| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_kms_alias.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | -| [aws_lb.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/lb) | data source | -| [aws_lb_listener.selected_https](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/lb_listener) | data source | -| [aws_route53_zone.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | -| [aws_route53_zone.selected_vanity](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | -| [aws_security_group.lb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_group) | data source | -| [aws_security_group.rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_group) | data source | -| [aws_security_group.vpc_default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_group) | data source | -| [aws_subnets.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnets) | data source | -| [aws_vpc.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source | +| [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/resources/iam_policy) | resource | +| [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/resources/iam_role) | resource | +| [aws_kinesis_stream.default](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/resources/kinesis_stream) | resource | +| [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/iam_policy_document) | data source | +| [aws_kms_alias.selected](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/kms_alias) | data source | +| [aws_route53_zone.selected](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/route53_zone) | data source | +| [aws_route53_zone.selected_vanity](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/route53_zone) | data source | +| [aws_ssm_parameters_by_path.default](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/ssm_parameters_by_path) | data source | +| [template_file.envs](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [account\_stage](#input\_account\_stage) | The ecr stage (account) name to use for the fully qualified stage parameter store. | `string` | `"auto"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_configuration](#input\_alb\_configuration) | The configuration to use for the ALB, specifying which cluster alb configuration to use | `string` | `"default"` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [autoscaling\_dimension](#input\_autoscaling\_dimension) | The dimension to use to decide to autoscale | `string` | `"cpu"` | no | +| [autoscaling\_enabled](#input\_autoscaling\_enabled) | Should this service autoscale using SNS alarams | `bool` | `true` | no | +| [chamber\_service](#input\_chamber\_service) | SSM parameter service name for use with chamber. This is used in chamber\_format where /$chamber\_service/$name/$container\_name/$parameter would be the default. | `string` | `"ecs-service"` | no | | [cluster\_attributes](#input\_cluster\_attributes) | The attributes of the cluster name e.g. if the full name is `namespace-tenant-environment-dev-ecs-b2b` then the `cluster_name` is `ecs` and this value should be `b2b`. | `list(string)` | `[]` | no | -| [cluster\_full\_name](#input\_cluster\_full\_name) | The fully qualified name of the cluster. This will override the `cluster_suffix`. | `string` | `""` | no | -| [cluster\_name](#input\_cluster\_name) | The name of the cluster | `string` | `"ecs"` | no | | [containers](#input\_containers) | Feed inputs into container definition module | `any` | `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [cpu\_utilization\_high\_alarm\_actions](#input\_cpu\_utilization\_high\_alarm\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization High Alarm action | `list(string)` | `[]` | no | +| [cpu\_utilization\_high\_evaluation\_periods](#input\_cpu\_utilization\_high\_evaluation\_periods) | Number of periods to evaluate for the alarm | `number` | `1` | no | +| [cpu\_utilization\_high\_ok\_actions](#input\_cpu\_utilization\_high\_ok\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization High OK action | `list(string)` | `[]` | no | +| [cpu\_utilization\_high\_period](#input\_cpu\_utilization\_high\_period) | Duration in seconds to evaluate for the alarm | `number` | `300` | no | +| [cpu\_utilization\_high\_threshold](#input\_cpu\_utilization\_high\_threshold) | The maximum percentage of CPU utilization average | `number` | `80` | no | +| [cpu\_utilization\_low\_alarm\_actions](#input\_cpu\_utilization\_low\_alarm\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization Low Alarm action | `list(string)` | `[]` | no | +| [cpu\_utilization\_low\_evaluation\_periods](#input\_cpu\_utilization\_low\_evaluation\_periods) | Number of periods to evaluate for the alarm | `number` | `1` | no | +| [cpu\_utilization\_low\_ok\_actions](#input\_cpu\_utilization\_low\_ok\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization Low OK action | `list(string)` | `[]` | no | +| [cpu\_utilization\_low\_period](#input\_cpu\_utilization\_low\_period) | Duration in seconds to evaluate for the alarm | `number` | `300` | no | +| [cpu\_utilization\_low\_threshold](#input\_cpu\_utilization\_low\_threshold) | The minimum percentage of CPU utilization average | `number` | `20` | no | +| [datadog\_agent\_sidecar\_enabled](#input\_datadog\_agent\_sidecar\_enabled) | Enable the Datadog Agent Sidecar | `bool` | `false` | no | +| [datadog\_log\_method\_is\_firelens](#input\_datadog\_log\_method\_is\_firelens) | Datadog logs can be sent via cloudwatch logs (and lambda) or firelens, set this to true to enable firelens via a sidecar container for fluentbit | `bool` | `false` | no | +| [datadog\_logging\_default\_tags\_enabled](#input\_datadog\_logging\_default\_tags\_enabled) | Add Default tags to all logs sent to Datadog | `bool` | `true` | no | +| [datadog\_logging\_tags](#input\_datadog\_logging\_tags) | Tags to add to all logs sent to Datadog | `map(string)` | `null` | no | +| [datadog\_sidecar\_containers\_logs\_enabled](#input\_datadog\_sidecar\_containers\_logs\_enabled) | Enable the Datadog Agent Sidecar to send logs to aws cloudwatch group, requires `datadog_agent_sidecar_enabled` to be true | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [domain\_name](#input\_domain\_name) | The domain name to use as the host header suffix | `string` | `""` | no | | [ecr\_region](#input\_ecr\_region) | The region to use for the fully qualified ECR image URL. Defaults to the current region. | `string` | `""` | no | | [ecr\_stage\_name](#input\_ecr\_stage\_name) | The ecr stage (account) name to use for the fully qualified ECR image URL. | `string` | `"auto"` | no | -| [ecs\_service\_enabled](#input\_ecs\_service\_enabled) | Whether to create the ECS service | `bool` | `true` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github\_actions\_allowed\_repos](#input\_github\_actions\_allowed\_repos) | A list of the GitHub repositories that are allowed to assume this role from GitHub Actions. For example,
["cloudposse/infra-live"]. Can contain "*" as wildcard.
If org part of repo name is omitted, "cloudposse" will be assumed. | `list(string)` | `[]` | no | +| [github\_actions\_iam\_role\_attributes](#input\_github\_actions\_iam\_role\_attributes) | Additional attributes to add to the role name | `list(string)` | `[]` | no | +| [github\_actions\_iam\_role\_enabled](#input\_github\_actions\_iam\_role\_enabled) | Flag to toggle creation of an IAM Role that GitHub Actions can assume to access AWS resources | `bool` | `false` | no | +| [github\_oidc\_trusted\_role\_arns](#input\_github\_oidc\_trusted\_role\_arns) | A list of IAM Role ARNs allowed to assume this cluster's GitHub OIDC role | `list(string)` | `[]` | no | +| [health\_check\_path](#input\_health\_check\_path) | The destination for the health check request | `string` | `"/health"` | no | +| [health\_check\_port](#input\_health\_check\_port) | The port to use to connect with the target. Valid values are either ports 1-65536, or `traffic-port`. Defaults to `traffic-port` | `string` | `"traffic-port"` | no | | [iam\_policy\_enabled](#input\_iam\_policy\_enabled) | If set to true will create IAM policy in AWS | `bool` | `false` | no | | [iam\_policy\_statements](#input\_iam\_policy\_statements) | Map of IAM policy statements to use in the policy. This can be used with or instead of the `var.iam_source_json_url`. | `any` | `{}` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | @@ -225,19 +251,30 @@ components: | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [lb\_match\_tags](#input\_lb\_match\_tags) | The additional matching tags for the LB data source. Used with current namespace, tenant, env, and stage tags. | `map(string)` | `{}` | no | +| [lb\_catch\_all](#input\_lb\_catch\_all) | Should this service act as catch all for all subdomain hosts of the vanity domain | `bool` | `false` | no | | [logs](#input\_logs) | Feed inputs into cloudwatch logs module | `any` | `{}` | no | +| [memory\_utilization\_high\_alarm\_actions](#input\_memory\_utilization\_high\_alarm\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization High Alarm action | `list(string)` | `[]` | no | +| [memory\_utilization\_high\_evaluation\_periods](#input\_memory\_utilization\_high\_evaluation\_periods) | Number of periods to evaluate for the alarm | `number` | `1` | no | +| [memory\_utilization\_high\_ok\_actions](#input\_memory\_utilization\_high\_ok\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization High OK action | `list(string)` | `[]` | no | +| [memory\_utilization\_high\_period](#input\_memory\_utilization\_high\_period) | Duration in seconds to evaluate for the alarm | `number` | `300` | no | +| [memory\_utilization\_high\_threshold](#input\_memory\_utilization\_high\_threshold) | The maximum percentage of Memory utilization average | `number` | `80` | no | +| [memory\_utilization\_low\_alarm\_actions](#input\_memory\_utilization\_low\_alarm\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization Low Alarm action | `list(string)` | `[]` | no | +| [memory\_utilization\_low\_evaluation\_periods](#input\_memory\_utilization\_low\_evaluation\_periods) | Number of periods to evaluate for the alarm | `number` | `1` | no | +| [memory\_utilization\_low\_ok\_actions](#input\_memory\_utilization\_low\_ok\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization Low OK action | `list(string)` | `[]` | no | +| [memory\_utilization\_low\_period](#input\_memory\_utilization\_low\_period) | Duration in seconds to evaluate for the alarm | `number` | `300` | no | +| [memory\_utilization\_low\_threshold](#input\_memory\_utilization\_low\_threshold) | The minimum percentage of Memory utilization average | `number` | `20` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [public\_lb\_enabled](#input\_public\_lb\_enabled) | Whether or not to use public LB and public subnets | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [retention\_period](#input\_retention\_period) | Length of time data records are accessible after they are added to the stream | `string` | `"48"` | no | | [shard\_count](#input\_shard\_count) | Number of shards that the stream will use | `string` | `"1"` | no | | [shard\_level\_metrics](#input\_shard\_level\_metrics) | List of shard-level CloudWatch metrics which can be enabled for the stream | `list` |
[
"IncomingBytes",
"IncomingRecords",
"IteratorAgeMilliseconds",
"OutgoingBytes",
"OutgoingRecords",
"ReadProvisionedThroughputExceeded",
"WriteProvisionedThroughputExceeded"
]
| no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [stickiness\_cookie\_duration](#input\_stickiness\_cookie\_duration) | The time period, in seconds, during which requests from a client should be routed to the same target. After this time period expires, the load balancer-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds). The default value is 1 day (86400 seconds) | `number` | `86400` | no | +| [stickiness\_enabled](#input\_stickiness\_enabled) | Boolean to enable / disable `stickiness`. Default is `true` | `bool` | `true` | no | +| [stickiness\_type](#input\_stickiness\_type) | The type of sticky sessions. The only current possible value is `lb_cookie` | `string` | `"lb_cookie"` | no | | [stream\_mode](#input\_stream\_mode) | Stream mode details for the Kinesis stream | `string` | `"PROVISIONED"` | no | -| [subnet\_match\_tags](#input\_subnet\_match\_tags) | The additional matching tags for the VPC subnet data source. Used with current namespace, tenant, env, and stage tags. | `map(string)` | `{}` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module | `any` | `{}` | no | | [task\_enabled](#input\_task\_enabled) | Whether or not to use the ECS task module | `bool` | `true` | no | @@ -246,21 +283,23 @@ components: | [use\_lb](#input\_use\_lb) | Whether use load balancer for the service | `bool` | `false` | no | | [use\_rds\_client\_sg](#input\_use\_rds\_client\_sg) | Use the RDS client security group | `bool` | `false` | no | | [vanity\_alias](#input\_vanity\_alias) | The vanity aliases to use for the public LB. | `list(string)` | `[]` | no | -| [vpc\_match\_tags](#input\_vpc\_match\_tags) | The additional matching tags for the VPC data source. Used with current namespace, tenant, env, and stage tags. | `map(any)` | `{}` | no | +| [vanity\_domain\_enabled](#input\_vanity\_domain\_enabled) | Whether to use the vanity domain alias for the service | `bool` | `false` | no | ## Outputs | Name | Description | |------|-------------| -| [container\_definition](#output\_container\_definition) | Output of container definition module | | [ecs\_cluster\_arn](#output\_ecs\_cluster\_arn) | Selected ECS cluster ARN | +| [environment\_map](#output\_environment\_map) | Environment variables to pass to the container, this is a map of key/value pairs, where the key is `containerName,variableName` | | [full\_domain](#output\_full\_domain) | Domain to respond to GET requests | +| [github\_actions\_iam\_role\_arn](#output\_github\_actions\_iam\_role\_arn) | ARN of IAM role for GitHub Actions | +| [github\_actions\_iam\_role\_name](#output\_github\_actions\_iam\_role\_name) | Name of IAM role for GitHub Actions | | [lb\_arn](#output\_lb\_arn) | Selected LB ARN | | [lb\_listener\_https](#output\_lb\_listener\_https) | Selected LB HTTPS Listener | | [lb\_sg\_id](#output\_lb\_sg\_id) | Selected LB SG ID | | [logs](#output\_logs) | Output of cloudwatch logs module | +| [service\_image](#output\_service\_image) | The image of the service container | | [subnet\_ids](#output\_subnet\_ids) | Selected subnet IDs | -| [task](#output\_task) | Output of service task module | | [vpc\_id](#output\_vpc\_id) | Selected VPC ID | | [vpc\_sg\_id](#output\_vpc\_sg\_id) | Selected VPC SG ID | diff --git a/modules/ecs-service/datadog-agent.tf b/modules/ecs-service/datadog-agent.tf new file mode 100644 index 000000000..a36abe1e6 --- /dev/null +++ b/modules/ecs-service/datadog-agent.tf @@ -0,0 +1,163 @@ +variable "datadog_agent_sidecar_enabled" { + type = bool + default = false + description = "Enable the Datadog Agent Sidecar" +} + +variable "datadog_log_method_is_firelens" { + type = bool + default = false + description = "Datadog logs can be sent via cloudwatch logs (and lambda) or firelens, set this to true to enable firelens via a sidecar container for fluentbit" +} + +variable "datadog_sidecar_containers_logs_enabled" { + type = bool + default = true + description = "Enable the Datadog Agent Sidecar to send logs to aws cloudwatch group, requires `datadog_agent_sidecar_enabled` to be true" +} + +variable "datadog_logging_tags" { + type = map(string) + default = null + description = "Tags to add to all logs sent to Datadog" +} + +variable "datadog_logging_default_tags_enabled" { + type = bool + default = true + description = "Add Default tags to all logs sent to Datadog" +} + +locals { + default_datadog_tags = var.datadog_logging_default_tags_enabled ? { + env = module.this.stage + account = format("%s-%s-%s", module.this.tenant, module.this.environment, module.this.stage) + } : null + + all_dd_tags = join(",", [for k, v in merge(local.default_datadog_tags, var.datadog_logging_tags) : format("%s:%s", k, v)]) + + datadog_logconfiguration_firelens = { + logDriver = "awsfirelens" + options = var.datadog_agent_sidecar_enabled ? { + Name = "datadog", + apikey = one(module.datadog_configuration[*].datadog_api_key), + Host = format("http-intake.logs.%s", one(module.datadog_configuration[*].datadog_site)) + dd_service = module.this.name, + dd_tags = local.all_dd_tags, + dd_source = "ecs", + dd_message_key = "log", + TLS = "on", + provider = "ecs" + } : {} + } +} + +module "datadog_sidecar_logs" { + source = "cloudposse/cloudwatch-logs/aws" + version = "0.6.6" + + # if we are using datadog firelens we don't need to create a log group + count = local.enabled && var.datadog_sidecar_containers_logs_enabled ? 1 : 0 + + stream_names = lookup(var.logs, "stream_names", []) + retention_in_days = lookup(var.logs, "retention_in_days", 90) + + principals = merge({ + Service = ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com"] + }, lookup(var.logs, "principals", {})) + + additional_permissions = concat([ + "logs:CreateLogStream", + "logs:DeleteLogStream", + ], lookup(var.logs, "additional_permissions", [])) + + context = module.this.context +} + +module "datadog_container_definition" { + source = "cloudposse/ecs-container-definition/aws" + version = "0.58.1" + + count = local.enabled && var.datadog_agent_sidecar_enabled ? 1 : 0 + + container_cpu = 256 + container_memory = 512 + container_name = "datadog-agent" + container_image = "public.ecr.aws/datadog/agent:latest" + essential = true + map_environment = { + "ECS_FARGATE" = var.task.launch_type == "FARGATE" ? true : false + "DD_API_KEY" = one(module.datadog_configuration[*].datadog_api_key) + "DD_SITE" = one(module.datadog_configuration[*].datadog_site) + "DD_ENV" = module.this.stage + "DD_LOGS_ENABLED" = true + "DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL" = true + "SD_BACKEND" = "docker" + "DD_PROCESS_AGENT_ENABLED" = true + "DD_DOGSTATSD_NON_LOCAL_TRAFFIC" = true + "DD_APM_ENABLED" = true + "DD_CONTAINER_LABELS_AS_TAGS" = jsonencode({ + "org.opencontainers.image.revision" = "version" + }) + } + + // Datadog DogStatsD/tracing ports + port_mappings = [{ + containerPort = 8125 + hostPort = 8125 + protocol = "udp" + }, { + containerPort = 8126 + hostPort = 8126 + protocol = "tcp" + }] + + log_configuration = var.datadog_sidecar_containers_logs_enabled ? { + logDriver = "awslogs" + options = { + "awslogs-group" = one(module.datadog_sidecar_logs[*].log_group_name) + "awslogs-region" = var.region + "awslogs-stream-prefix" = "datadog-agent" + } + } : null +} + +module "datadog_fluent_bit_container_definition" { + source = "cloudposse/ecs-container-definition/aws" + version = "0.58.1" + + count = local.enabled && var.datadog_agent_sidecar_enabled ? 1 : 0 + + container_cpu = 256 + container_memory = 512 + container_name = "datadog-log-router" + # From Datadog Support: + # In this case, the newest container image with the latest tag (corresponding to version 2.29.0) looks like it is crashing for certain customers, which is causing the Task to deprovision. + # Note: We recommend customers to use the stable tag for this type of reason + container_image = "amazon/aws-for-fluent-bit:stable" + essential = true + firelens_configuration = { + type = "fluentbit" + options = { + config-file-type = "file", + config-file-value = "/fluent-bit/configs/parse-json.conf", + enable-ecs-log-metadata = "true" + } + } + + log_configuration = var.datadog_sidecar_containers_logs_enabled ? { + logDriver = "awslogs" + options = { + "awslogs-group" = one(module.datadog_sidecar_logs[*].log_group_name) + "awslogs-region" = var.region + "awslogs-stream-prefix" = "datadog-log-router" + } + } : null +} + +module "datadog_configuration" { + count = var.datadog_agent_sidecar_enabled ? 1 : 0 + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} diff --git a/modules/ecs-service/github-actions-iam-policy.tf b/modules/ecs-service/github-actions-iam-policy.tf new file mode 100644 index 000000000..e2ea48b9d --- /dev/null +++ b/modules/ecs-service/github-actions-iam-policy.tf @@ -0,0 +1,58 @@ +variable "github_oidc_trusted_role_arns" { + type = list(string) + description = "A list of IAM Role ARNs allowed to assume this cluster's GitHub OIDC role" + default = [] +} + +locals { + github_actions_iam_policy = data.aws_iam_policy_document.github_actions_iam_policy.json +} + +data "aws_iam_policy_document" "github_actions_iam_policy" { + # Allows trusted roles to assume this role + statement { + sid = "TrustedRoleAccess" + effect = "Allow" + actions = [ + "sts:AssumeRole", + "sts:TagSession" + ] + resources = var.github_oidc_trusted_role_arns + } + + # Allow chamber to read secrets + statement { + sid = "AllowKMSAccess" + effect = "Allow" + actions = [ + "kms:Decrypt", + "kms:DescribeKey" + ] + resources = [ + "*" + ] + } + + statement { + effect = "Allow" + actions = [ + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:PutParameter" + ] + resources = [ + "arn:aws:ssm:*:*:parameter${format("/%s/%s/*", var.chamber_service, var.name)}" + ] + } + + statement { + effect = "Allow" + actions = [ + "ssm:DescribeParameters", + "ssm:GetParametersByPath" + ] + resources = [ + "*" + ] + } +} diff --git a/modules/ecs-service/github-actions-iam-role.mixin.tf b/modules/ecs-service/github-actions-iam-role.mixin.tf new file mode 100644 index 000000000..de68c6602 --- /dev/null +++ b/modules/ecs-service/github-actions-iam-role.mixin.tf @@ -0,0 +1,72 @@ +# This mixin requires that a local variable named `github_actions_iam_policy` be defined +# and its value to be a JSON IAM Policy Document defining the permissions for the role. +# It also requires that the `github-oidc-provider` has been previously installed and the +# `github-assume-role-policy.mixin.tf` has been added to `account-map/modules/team-assume-role-policy`. + +variable "github_actions_iam_role_enabled" { + type = bool + description = <<-EOF + Flag to toggle creation of an IAM Role that GitHub Actions can assume to access AWS resources + EOF + default = false +} + +variable "github_actions_allowed_repos" { + type = list(string) + description = < 0 +} + +module "gha_role_name" { + source = "cloudposse/label/null" + version = "0.25.0" + + enabled = local.github_actions_iam_role_enabled + attributes = compact(concat(var.github_actions_iam_role_attributes, ["gha"])) + + context = module.this.context +} + +module "gha_assume_role" { + source = "../account-map/modules/team-assume-role-policy" + + trusted_github_repos = var.github_actions_allowed_repos + + context = module.gha_role_name.context +} + +resource "aws_iam_role" "github_actions" { + count = local.github_actions_iam_role_enabled ? 1 : 0 + name = module.gha_role_name.id + assume_role_policy = module.gha_assume_role.github_assume_role_policy + + inline_policy { + name = module.gha_role_name.id + policy = local.github_actions_iam_policy + } +} + +output "github_actions_iam_role_arn" { + value = one(aws_iam_role.github_actions[*].arn) + description = "ARN of IAM role for GitHub Actions" +} + +output "github_actions_iam_role_name" { + value = one(aws_iam_role.github_actions[*].name) + description = "Name of IAM role for GitHub Actions" +} diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 005c3d79c..2367dd8f4 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -8,12 +8,17 @@ locals { assign_public_ip = lookup(var.task, "assign_public_ip", false) - container_definition = [ + container_definition = concat([ for container in module.container_definition : container.json_map_object - ] - - role_name = format("%s-%s-%s-%s-%s-role", var.namespace, var.tenant, var.environment, var.stage, var.name) + ], + [for container in module.datadog_container_definition : + container.json_map_object + ], + var.datadog_log_method_is_firelens ? [for container in module.datadog_fluent_bit_container_definition : + container.json_map_object + ] : [], + ) kinesis_kms_id = try(one(data.aws_kms_alias.selected[*].id), null) } @@ -22,6 +27,9 @@ module "logs" { source = "cloudposse/cloudwatch-logs/aws" version = "0.6.6" + # if we are using datadog firelens we don't need to create a log group + count = local.enabled && !var.datadog_log_method_is_firelens ? 1 : 0 + stream_names = lookup(var.logs, "stream_names", []) retention_in_days = lookup(var.logs, "retention_in_days", 90) @@ -37,19 +45,74 @@ module "logs" { context = module.this.context } +module "roles_to_principals" { + source = "../account-map/modules/roles-to-principals" + context = module.this.context + role_map = {} +} + +locals { + container_chamber = { for name, result in data.aws_ssm_parameters_by_path.default : + name => { for key, value in zipmap(result.names, result.values) : element(reverse(split("/", key)), 0) => value } + } + + containers = { for name, settings in var.containers : + name => merge(settings, local.container_chamber[name]) + if local.enabled + } +} + +data "aws_ssm_parameters_by_path" "default" { + for_each = { for k, v in var.containers : k => v if local.enabled } + path = format("/%s/%s/%s", var.chamber_service, var.name, each.key) +} + +locals { + containers_envs = merge([ + for name, settings in var.containers : + { for k, v in lookup(settings, "map_environment", {}) : "${name},${k}" => v if local.enabled } + ]...) +} + + +data "template_file" "envs" { + for_each = { for k, v in local.containers_envs : k => v if local.enabled } + + template = replace(each.value, "$$", "$") + + vars = { + stage = module.this.stage + namespace = module.this.namespace + name = module.this.name + full_domain = local.full_domain + vanity_domain = local.vanity_domain + # `service_domain` uses whatever the current service is (public/private) + service_domain = local.domain_no_service_name + service_domain_public = local.public_domain_no_service_name + service_domain_private = local.private_domain_no_service_name + } +} + +locals { + env_map_subst = { + for k, v in data.template_file.envs : + k => v.rendered + } +} + module "container_definition" { source = "cloudposse/ecs-container-definition/aws" version = "0.58.1" - for_each = var.containers + for_each = { for k, v in local.containers : k => v if local.enabled } container_name = lookup(each.value, "name") container_image = lookup(each.value, "ecr_image", null) != null ? format( "%s.dkr.ecr.%s.amazonaws.com/%s", - module.iam_roles.account_map.full_account_map[var.ecr_stage_name], + module.roles_to_principals.full_account_map[var.ecr_stage_name], coalesce(var.ecr_region, var.region), - lookup(each.value, "ecr_image", null), + lookup(each.value, "ecr_image", null) ) : lookup(each.value, "image") container_memory = lookup(each.value, "memory", null) @@ -57,45 +120,62 @@ module "container_definition" { container_cpu = lookup(each.value, "cpu", null) essential = lookup(each.value, "essential", true) readonly_root_filesystem = lookup(each.value, "readonly_root_filesystem", null) + mount_points = lookup(each.value, "mount_points", []) map_environment = lookup(each.value, "map_environment", null) != null ? merge( - lookup(each.value, "map_environment", {}), + { for k, v in local.env_map_subst : split(",", k)[1] => v if split(",", k)[0] == each.key }, { "APP_ENV" = format("%s-%s-%s-%s", var.namespace, var.tenant, var.environment, var.stage) }, { "RUNTIME_ENV" = format("%s-%s-%s", var.namespace, var.tenant, var.stage) }, - { "CLUSTER_NAME" = try(one(data.aws_ecs_cluster.selected[*].cluster_name), null) } + { "CLUSTER_NAME" = module.ecs_cluster.outputs.cluster_name }, + var.datadog_agent_sidecar_enabled ? { + "DD_DOGSTATSD_PORT" = 8125, + "DD_TRACING_ENABLED" = "true", + "DD_SERVICE_NAME" = var.name, + "DD_ENV" = var.stage, + "DD_PROFILING_EXPORTERS" = "agent" + } : {} ) : null map_secrets = lookup(each.value, "map_secrets", null) != null ? zipmap( keys(lookup(each.value, "map_secrets", null)), - formatlist("%s/%s", format("arn:aws:ssm:%s:%s:parameter", - coalesce(var.ecr_region, var.region), module.iam_roles.account_map.full_account_map[format("%s-%s", var.tenant, var.stage)]), + formatlist("%s/%s", format("arn:aws:ssm:%s:%s:parameter", var.region, module.roles_to_principals.full_account_map[format("%s-%s", var.tenant, var.stage)]), values(lookup(each.value, "map_secrets", null))) ) : null - port_mappings = lookup(each.value, "port_mappings", []) - command = lookup(each.value, "command", null) - entrypoint = lookup(each.value, "entrypoint", null) - healthcheck = lookup(each.value, "healthcheck", null) - ulimits = lookup(each.value, "ulimits", null) - volumes_from = lookup(each.value, "volumes_from", null) - - log_configuration = lookup(each.value["log_configuration"], "logDriver", {}) == "awslogs" ? merge(lookup(each.value, "log_configuration", {}), { - options = { - "awslogs-region" = var.region - "awslogs-group" = module.logs.log_group_name - "awslogs-stream-prefix" = var.name - } - }) : lookup(each.value, "log_configuration", {}) + port_mappings = lookup(each.value, "port_mappings", []) + command = lookup(each.value, "command", null) + entrypoint = lookup(each.value, "entrypoint", null) + healthcheck = lookup(each.value, "healthcheck", null) + ulimits = lookup(each.value, "ulimits", null) + volumes_from = lookup(each.value, "volumes_from", null) + docker_labels = lookup(each.value, "docker_labels", null) + container_depends_on = lookup(each.value, "container_depends_on", []) + + log_configuration = lookup(lookup(each.value, "log_configuration", {}), "logDriver", {}) == "awslogs" ? merge(lookup(each.value, "log_configuration", {}), { + logDriver = "awslogs" + options = tomap({ + awslogs-region = var.region, + awslogs-group = local.awslogs_group, + awslogs-stream-prefix = var.name, + }) + # if we are not using awslogs, we execute this line, which if we have dd enabled, means we are using firelens, so merge that config in. + }) : merge(lookup(each.value, "log_configuration", {}), local.datadog_logconfiguration_firelens) + firelens_configuration = lookup(each.value, "firelens_configuration", null) + # escape hatch for anything not specifically described above or unsupported by the upstream module container_definition = lookup(each.value, "container_definition", {}) } +locals { + awslogs_group = var.datadog_log_method_is_firelens ? "" : join("", module.logs.*.log_group_name) +} + module "ecs_alb_service_task" { source = "cloudposse/ecs-alb-service-task/aws" - version = "0.66.0" + version = "0.66.4" - count = var.enabled ? 1 : 0 + count = local.enabled ? 1 : 0 ecs_cluster_arn = local.ecs_cluster_arn vpc_id = local.vpc_id @@ -135,25 +215,19 @@ module "ecs_alb_service_task" { wait_for_steady_state = lookup(var.task, "wait_for_steady_state", true) circuit_breaker_deployment_enabled = lookup(var.task, "circuit_breaker_deployment_enabled", true) circuit_breaker_rollback_enabled = lookup(var.task, "circuit_breaker_rollback_enabled ", true) - task_policy_arns = tolist(aws_iam_policy.default[*].arn) + task_policy_arns = var.task_policy_arns ecs_service_enabled = lookup(var.task, "ecs_service_enabled", true) + bind_mount_volumes = lookup(var.task, "bind_mount_volumes", []) + task_role_arn = lookup(var.task, "task_role_arn", []) + capacity_provider_strategies = lookup(var.task, "capacity_provider_strategies", []) + depends_on = [ + module.alb_ingress + ] context = module.this.context } -# This resource is used instead of the ecs_alb_service_task module's `var.task_policy_arns` because -# the upstream module uses a "count" instead of a "for_each" -# -# See https://github.com/cloudposse/terraform-aws-ecs-alb-service-task/issues/167 -resource "aws_iam_role_policy_attachment" "task" { - for_each = local.enabled && length(var.task_policy_arns) > 0 ? toset(var.task_policy_arns) : toset([]) - - policy_arn = each.value - role = try(one(module.ecs_alb_service_task[*].task_role_name), null) -} - - module "alb_ecs_label" { source = "cloudposse/label/null" version = "0.25.0" # requires Terraform >= 0.13.0 @@ -170,22 +244,29 @@ module "alb_ingress" { source = "cloudposse/alb-ingress/aws" version = "0.24.3" - count = var.use_lb ? 1 : 0 + count = local.enabled && var.use_lb ? 1 : 0 target_group_name = module.alb_ecs_label.id vpc_id = local.vpc_id unauthenticated_listener_arns = [local.lb_listener_https_arn] - unauthenticated_hosts = [local.full_domain] - unauthenticated_priority = 0 - default_target_group_enabled = true - health_check_matcher = "200-404" + unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", local.vanity_domain), local.full_domain] : [local.full_domain] + # When set to catch-all, make priority super high to make sure last to match + unauthenticated_priority = var.lb_catch_all ? 99 : 0 + default_target_group_enabled = true + health_check_matcher = "200-404" + health_check_path = var.health_check_path + health_check_port = var.health_check_port + + stickiness_enabled = var.stickiness_enabled + stickiness_type = var.stickiness_type + stickiness_cookie_duration = var.stickiness_cookie_duration context = module.this.context } data "aws_iam_policy_document" "this" { - count = var.iam_policy_enabled ? 1 : 0 + count = local.enabled && var.iam_policy_enabled ? 1 : 0 dynamic "statement" { # Only flatten if a list(string) is passed in, otherwise use the map var as-is @@ -242,6 +323,8 @@ module "vanity_alias" { source = "cloudposse/route53-alias/aws" version = "0.13.0" + count = local.enabled ? 1 : 0 + aliases = var.vanity_alias parent_zone_id = local.vanity_domain_zone_id target_dns_name = local.lb_name @@ -254,10 +337,10 @@ module "ecs_cloudwatch_autoscaling" { source = "cloudposse/ecs-cloudwatch-autoscaling/aws" version = "0.7.3" - count = var.task_enabled ? 1 : 0 + count = local.enabled && var.task_enabled ? 1 : 0 service_name = module.ecs_alb_service_task[0].service_name - cluster_name = try(one(data.aws_ecs_cluster.selected[*].cluster_name), null) + cluster_name = module.ecs_cluster.outputs.cluster_name min_capacity = lookup(var.task, "min_capacity", 1) max_capacity = lookup(var.task, "max_capacity", 2) scale_up_adjustment = 1 @@ -272,8 +355,82 @@ module "ecs_cloudwatch_autoscaling" { ] } +locals { + cpu_utilization_high_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "cpu" ? module.ecs_cloudwatch_autoscaling[0].scale_up_policy_arn : "" + cpu_utilization_low_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "cpu" ? module.ecs_cloudwatch_autoscaling[0].scale_down_policy_arn : "" + memory_utilization_high_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "memory" ? module.ecs_cloudwatch_autoscaling[0].scale_up_policy_arn : "" + memory_utilization_low_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "memory" ? module.ecs_cloudwatch_autoscaling[0].scale_down_policy_arn : "" +} + +module "ecs_cloudwatch_sns_alarms" { + source = "cloudposse/ecs-cloudwatch-sns-alarms/aws" + version = "0.12.3" + + cluster_name = module.ecs_cluster.outputs.cluster_name + service_name = module.ecs_alb_service_task[0].service_name + + cpu_utilization_high_threshold = var.cpu_utilization_high_threshold + cpu_utilization_high_evaluation_periods = var.cpu_utilization_high_evaluation_periods + cpu_utilization_high_period = var.cpu_utilization_high_period + + cpu_utilization_high_alarm_actions = compact( + concat( + var.cpu_utilization_high_alarm_actions, + [local.cpu_utilization_high_alarm_actions], + ) + ) + + cpu_utilization_high_ok_actions = var.cpu_utilization_high_ok_actions + + cpu_utilization_low_threshold = var.cpu_utilization_low_threshold + cpu_utilization_low_evaluation_periods = var.cpu_utilization_low_evaluation_periods + cpu_utilization_low_period = var.cpu_utilization_low_period + + cpu_utilization_low_alarm_actions = compact( + concat( + var.cpu_utilization_low_alarm_actions, + [local.cpu_utilization_low_alarm_actions], + ) + ) + + cpu_utilization_low_ok_actions = var.cpu_utilization_low_ok_actions + + memory_utilization_high_threshold = var.memory_utilization_high_threshold + memory_utilization_high_evaluation_periods = var.memory_utilization_high_evaluation_periods + memory_utilization_high_period = var.memory_utilization_high_period + + memory_utilization_high_alarm_actions = compact( + concat( + var.memory_utilization_high_alarm_actions, + [local.memory_utilization_high_alarm_actions], + ) + ) + + memory_utilization_high_ok_actions = var.memory_utilization_high_ok_actions + + memory_utilization_low_threshold = var.memory_utilization_low_threshold + memory_utilization_low_evaluation_periods = var.memory_utilization_low_evaluation_periods + memory_utilization_low_period = var.memory_utilization_low_period + + memory_utilization_low_alarm_actions = compact( + concat( + var.memory_utilization_low_alarm_actions, + [local.memory_utilization_low_alarm_actions], + ) + ) + + memory_utilization_low_ok_actions = var.memory_utilization_low_ok_actions + + context = module.this.context + + depends_on = [ + module.ecs_alb_service_task + ] +} + resource "aws_kinesis_stream" "default" { - count = local.enabled && var.kinesis_enabled ? 1 : 0 + count = local.enabled && var.kinesis_enabled ? 1 : 0 + name = format("%s-%s", module.this.id, "kinesis-stream") shard_count = var.shard_count retention_period = var.retention_period diff --git a/modules/ecs-service/outputs.tf b/modules/ecs-service/outputs.tf index ce2609d1f..38d59b45d 100644 --- a/modules/ecs-service/outputs.tf +++ b/modules/ecs-service/outputs.tf @@ -1,18 +1,8 @@ output "logs" { - value = module.logs + value = one(module.logs[*]) description = "Output of cloudwatch logs module" } -output "container_definition" { - value = local.container_definition - description = "Output of container definition module" -} - -output "task" { - value = module.ecs_alb_service_task - description = "Output of service task module" -} - output "ecs_cluster_arn" { value = local.ecs_cluster_arn description = "Selected ECS cluster ARN" @@ -52,3 +42,13 @@ output "full_domain" { value = local.full_domain description = "Domain to respond to GET requests" } + +output "environment_map" { + value = local.env_map_subst + description = "Environment variables to pass to the container, this is a map of key/value pairs, where the key is `containerName,variableName`" +} + +output "service_image" { + value = try(nonsensitive(local.containers.service.image), null) + description = "The image of the service container" +} diff --git a/modules/ecs-service/providers.tf b/modules/ecs-service/providers.tf index 84a1dea80..08ee01b2a 100644 --- a/modules/ecs-service/providers.tf +++ b/modules/ecs-service/providers.tf @@ -1,13 +1,8 @@ -module "iam_roles" { - source = "../account-map/modules/iam-roles" - - context = module.this.context -} - provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_role_arn) : null + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { @@ -16,6 +11,11 @@ provider "aws" { } } +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + variable "import_profile_name" { type = string default = null diff --git a/modules/ecs-service/remote-state.tf b/modules/ecs-service/remote-state.tf index dc066edc9..b21680fda 100644 --- a/modules/ecs-service/remote-state.tf +++ b/modules/ecs-service/remote-state.tf @@ -1,31 +1,15 @@ locals { - # Grab only namespace, tenant, environment, stage since those will be the common tags across resources of interest in this account - match_tags = { - for key, value in module.this.tags : - key => value - if contains(["namespace", "tenant", "environment", "stage"], lower(key)) - } + vpc_id = module.vpc.outputs.vpc_id + vpc_sg_id = module.vpc.outputs.vpc_default_security_group_id + rds_sg_id = try(one(module.rds[*].outputs.exports.security_groups.client), null) + subnet_ids = lookup(module.vpc.outputs.subnets, local.assign_public_ip ? "public" : "private", { ids = [] }).ids + ecs_cluster_arn = module.ecs_cluster.outputs.cluster_arn - subnet_match_tags = merge({ - Attributes = local.assign_public_ip ? "public" : "private" - }, var.subnet_match_tags) - - lb_match_tags = merge({ - # e.g. platform-public - Attributes = format("%s-%s", local.cluster_type, local.domain_type) - }, var.lb_match_tags) - - vpc_id = try(one(data.aws_vpc.selected[*].id), null) - vpc_sg_id = try(one(data.aws_security_group.vpc_default[*].id), null) - rds_sg_id = try(one(data.aws_security_group.rds[*].id), null) - subnet_ids = try(one(data.aws_subnets.selected[*].ids), null) - ecs_cluster_arn = try(one(data.aws_ecs_cluster.selected[*].arn), null) - - lb_arn = try(one(data.aws_lb.selected[*].arn), null) - lb_name = try(one(data.aws_lb.selected[*].name), null) - lb_listener_https_arn = try(one(data.aws_lb_listener.selected_https[*].arn), null) - lb_sg_id = try(one(data.aws_security_group.lb[*].id), null) - lb_zone_id = try(one(data.aws_lb.selected[*].zone_id), null) + lb_arn = try(module.ecs_cluster.outputs.alb[var.alb_configuration].alb_arn, null) + lb_name = try(module.ecs_cluster.outputs.alb[var.alb_configuration].alb_name, null) + lb_listener_https_arn = try(module.ecs_cluster.outputs.alb[var.alb_configuration].https_listener_arn, null) + lb_sg_id = try(module.ecs_cluster.outputs.alb[var.alb_configuration].security_group_id, null) + lb_zone_id = try(module.ecs_cluster.outputs.alb[var.alb_configuration].alb_zone_id, null) } ## Company specific locals for domain convention @@ -35,11 +19,16 @@ locals { } zone_domain = format("%s.%s.%s", var.stage, var.tenant, coalesce(var.domain_name, local.domain_name[var.tenant])) - domain_type = var.public_lb_enabled ? "public" : "private" + domain_type = var.alb_configuration cluster_type = var.cluster_attributes[0] # e.g. example.public-platform.{environment}.{zone_domain} - full_domain = format("%s.%s-%s.%s.%s", var.name, local.domain_type, local.cluster_type, var.environment, local.zone_domain) + full_domain = format("%s.%s-%s.%s.%s", join("-", concat([ + var.name + ], var.attributes)), local.domain_type, local.cluster_type, var.environment, local.zone_domain) + domain_no_service_name = format("%s-%s.%s.%s", local.domain_type, local.cluster_type, var.environment, local.zone_domain) + public_domain_no_service_name = format("%s-%s.%s.%s", "public", local.cluster_type, var.environment, local.zone_domain) + private_domain_no_service_name = format("%s-%s.%s.%s", "private", local.cluster_type, var.environment, local.zone_domain) # tenant to domain mapping vanity_domain_names = { @@ -54,108 +43,32 @@ locals { vanity_domain_zone_id = try(one(data.aws_route53_zone.selected_vanity[*].zone_id), null) } -variable "vpc_match_tags" { - type = map(any) - description = "The additional matching tags for the VPC data source. Used with current namespace, tenant, env, and stage tags." - default = {} -} - -variable "subnet_match_tags" { - type = map(string) - description = "The additional matching tags for the VPC subnet data source. Used with current namespace, tenant, env, and stage tags." - default = {} -} - -variable "lb_match_tags" { - type = map(string) - description = "The additional matching tags for the LB data source. Used with current namespace, tenant, env, and stage tags." - default = {} -} - -data "aws_vpc" "selected" { - count = local.enabled ? 1 : 0 - - default = false - - tags = merge(local.match_tags, var.vpc_match_tags) -} - -data "aws_security_group" "vpc_default" { - count = local.enabled ? 1 : 0 - - name = "default" - - vpc_id = local.vpc_id - - tags = local.match_tags -} - -data "aws_subnets" "selected" { - count = local.enabled ? 1 : 0 - - filter { - name = "vpc-id" - values = [local.vpc_id] - } - - tags = merge(local.match_tags, local.subnet_match_tags) -} - -module "ecs_label" { - source = "cloudposse/label/null" - version = "0.25.0" +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" - name = var.cluster_name - attributes = var.cluster_attributes + component = "vpc" context = module.this.context } -module "rds_sg_label" { - source = "cloudposse/label/null" - version = "0.25.0" +module "rds" { + count = local.enabled && var.use_rds_client_sg ? 1 : 0 + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" - name = var.kms_key_alias - attributes = ["client"] + component = "rds" context = module.this.context } -data "aws_security_group" "rds" { - count = local.enabled && var.use_rds_client_sg ? 1 : 0 - - vpc_id = local.vpc_id - - tags = { - "Name" = module.rds_sg_label.id - } -} - -data "aws_ecs_cluster" "selected" { - count = local.enabled ? 1 : 0 - - cluster_name = coalesce(var.cluster_full_name, module.ecs_label.id) -} - -data "aws_security_group" "lb" { - count = local.enabled ? 1 : 0 - - vpc_id = local.vpc_id - - tags = merge(local.match_tags, local.lb_match_tags) -} +module "ecs_cluster" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" -data "aws_lb" "selected" { - count = local.enabled ? 1 : 0 + component = "ecs" - tags = merge(local.match_tags, local.lb_match_tags) -} - -data "aws_lb_listener" "selected_https" { - count = local.enabled ? 1 : 0 - - load_balancer_arn = local.lb_arn - port = 443 + context = module.this.context } # This is purely a check to ensure this zone exists @@ -167,7 +80,7 @@ data "aws_route53_zone" "selected" { } data "aws_route53_zone" "selected_vanity" { - count = local.enabled ? 1 : 0 + count = local.enabled && var.vanity_domain_enabled ? 1 : 0 name = local.vanity_domain private_zone = false diff --git a/modules/ecs-service/variables.tf b/modules/ecs-service/variables.tf index 085fa4212..0c204305c 100644 --- a/modules/ecs-service/variables.tf +++ b/modules/ecs-service/variables.tf @@ -9,18 +9,6 @@ variable "cluster_attributes" { default = [] } -variable "cluster_name" { - type = string - description = "The name of the cluster" - default = "ecs" -} - -variable "cluster_full_name" { - type = string - description = "The fully qualified name of the cluster. This will override the `cluster_suffix`." - default = "" -} - variable "logs" { type = any description = "Feed inputs into cloudwatch logs module" @@ -54,12 +42,6 @@ variable "domain_name" { default = "" } -variable "public_lb_enabled" { - type = bool - description = "Whether or not to use public LB and public subnets" - default = false -} - variable "task_enabled" { type = bool description = "Whether or not to use the ECS task module" @@ -78,12 +60,6 @@ variable "ecr_region" { default = "" } -variable "account_stage" { - type = string - description = "The ecr stage (account) name to use for the fully qualified stage parameter store." - default = "auto" -} - variable "iam_policy_statements" { type = any description = "Map of IAM policy statements to use in the policy. This can be used with or instead of the `var.iam_source_json_url`." @@ -155,8 +131,192 @@ variable "use_rds_client_sg" { default = false } -variable "ecs_service_enabled" { +variable "chamber_service" { + default = "ecs-service" + description = "SSM parameter service name for use with chamber. This is used in chamber_format where /$chamber_service/$name/$container_name/$parameter would be the default." +} + +variable "vanity_domain_enabled" { + default = false + type = bool + description = "Whether to use the vanity domain alias for the service" +} + +variable "alb_configuration" { + type = string + description = "The configuration to use for the ALB, specifying which cluster alb configuration to use" + default = "default" +} + +variable "health_check_path" { + type = string + description = "The destination for the health check request" + default = "/health" +} + +variable "health_check_port" { + type = string + default = "traffic-port" + description = "The port to use to connect with the target. Valid values are either ports 1-65536, or `traffic-port`. Defaults to `traffic-port`" +} + +variable "lb_catch_all" { + type = bool + description = "Should this service act as catch all for all subdomain hosts of the vanity domain" + default = false +} + +variable "stickiness_type" { + type = string + default = "lb_cookie" + description = "The type of sticky sessions. The only current possible value is `lb_cookie`" +} + +variable "stickiness_cookie_duration" { + type = number + default = 86400 + description = "The time period, in seconds, during which requests from a client should be routed to the same target. After this time period expires, the load balancer-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds). The default value is 1 day (86400 seconds)" +} + +variable "stickiness_enabled" { + type = bool default = true + description = "Boolean to enable / disable `stickiness`. Default is `true`" +} + +variable "autoscaling_enabled" { type = bool - description = "Whether to create the ECS service" + default = true + description = "Should this service autoscale using SNS alarams" +} + +variable "autoscaling_dimension" { + type = string + description = "The dimension to use to decide to autoscale" + default = "cpu" + + validation { + condition = contains(["cpu", "memory"], var.autoscaling_dimension) + error_message = "Allowed values for autoscaling_dimension are \"cpu\" or \"memory\"." + } +} + +variable "cpu_utilization_high_threshold" { + type = number + description = "The maximum percentage of CPU utilization average" + default = 80 +} + +variable "cpu_utilization_high_evaluation_periods" { + type = number + description = "Number of periods to evaluate for the alarm" + default = 1 +} + +variable "cpu_utilization_high_period" { + type = number + description = "Duration in seconds to evaluate for the alarm" + default = 300 +} + +variable "cpu_utilization_high_alarm_actions" { + type = list(string) + description = "A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization High Alarm action" + default = [] +} + +variable "cpu_utilization_high_ok_actions" { + type = list(string) + description = "A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization High OK action" + default = [] +} + +variable "cpu_utilization_low_threshold" { + type = number + description = "The minimum percentage of CPU utilization average" + default = 20 +} + +variable "cpu_utilization_low_evaluation_periods" { + type = number + description = "Number of periods to evaluate for the alarm" + default = 1 +} + +variable "cpu_utilization_low_period" { + type = number + description = "Duration in seconds to evaluate for the alarm" + default = 300 +} + +variable "cpu_utilization_low_alarm_actions" { + type = list(string) + description = "A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization Low Alarm action" + default = [] +} + +variable "cpu_utilization_low_ok_actions" { + type = list(string) + description = "A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization Low OK action" + default = [] +} + +variable "memory_utilization_high_threshold" { + type = number + description = "The maximum percentage of Memory utilization average" + default = 80 +} + +variable "memory_utilization_high_evaluation_periods" { + type = number + description = "Number of periods to evaluate for the alarm" + default = 1 +} + +variable "memory_utilization_high_period" { + type = number + description = "Duration in seconds to evaluate for the alarm" + default = 300 +} + +variable "memory_utilization_high_alarm_actions" { + type = list(string) + description = "A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization High Alarm action" + default = [] +} + +variable "memory_utilization_high_ok_actions" { + type = list(string) + description = "A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization High OK action" + default = [] +} + +variable "memory_utilization_low_threshold" { + type = number + description = "The minimum percentage of Memory utilization average" + default = 20 +} + +variable "memory_utilization_low_evaluation_periods" { + type = number + description = "Number of periods to evaluate for the alarm" + default = 1 +} + +variable "memory_utilization_low_period" { + type = number + description = "Duration in seconds to evaluate for the alarm" + default = 300 +} + +variable "memory_utilization_low_alarm_actions" { + type = list(string) + description = "A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization Low Alarm action" + default = [] +} + +variable "memory_utilization_low_ok_actions" { + type = list(string) + description = "A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization Low OK action" + default = [] } diff --git a/modules/ecs-service/versions.tf b/modules/ecs-service/versions.tf index e89eb16ed..5a6c84926 100644 --- a/modules/ecs-service/versions.tf +++ b/modules/ecs-service/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = "= 4.0" } } } diff --git a/modules/ecs/README.md b/modules/ecs/README.md index 571cfb4f5..067bb848e 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -18,13 +18,23 @@ The following will create components: terraform: ecs: + settings: + spacelift: + workspace_enabled: true vars: name: ecs + enabled: true acm_certificate_domain: example.com route53_record_name: "*" # Create records will be created in each zone zone_names: - example.com + capacity_providers_fargate: true + capacity_providers_fargate_spot: true + capacity_providers_ec2: + default: + instance_type: t3.medium + max_size: 2 ``` @@ -45,19 +55,23 @@ components: | Name | Source | Version | |------|--------|---------| -| [alb](#module\_alb) | cloudposse/alb/aws | 1.4.0 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [alb](#module\_alb) | cloudposse/alb/aws | 1.5.0 | +| [cluster](#module\_cluster) | cloudposse/ecs-cluster/aws | 0.2.2 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [target\_group\_label](#module\_target\_group\_label) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | ## Resources | Name | Type | |------|------| -| [aws_ecs_cluster.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | | [aws_route53_record.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | +| [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.ingress_cidr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.ingress_security_groups](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_acm_certificate.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/acm_certificate) | data source | ## Inputs @@ -70,9 +84,15 @@ components: | [alb\_configuration](#input\_alb\_configuration) | Map of multiple ALB configurations. | `map(any)` | `{}` | no | | [alb\_ingress\_cidr\_blocks\_http](#input\_alb\_ingress\_cidr\_blocks\_http) | List of CIDR blocks allowed to access environment over HTTP | `list(string)` |
[
"0.0.0.0/0"
]
| no | | [alb\_ingress\_cidr\_blocks\_https](#input\_alb\_ingress\_cidr\_blocks\_https) | List of CIDR blocks allowed to access environment over HTTPS | `list(string)` |
[
"0.0.0.0/0"
]
| no | +| [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | +| [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [capacity\_providers\_ec2](#input\_capacity\_providers\_ec2) | EC2 autoscale groups capacity providers |
map(object({
instance_type = string
max_size = number
security_group_ids = optional(list(string), [])
min_size = optional(number, 0)
image_id = optional(string)
instance_initiated_shutdown_behavior = optional(string, "terminate")
key_name = optional(string, "")
user_data = optional(string, "")
enable_monitoring = optional(bool, true)
instance_warmup_period = optional(number, 300)
maximum_scaling_step_size = optional(number, 1)
minimum_scaling_step_size = optional(number, 1)
target_capacity_utilization = optional(number, 100)
ebs_optimized = optional(bool, false)
block_device_mappings = optional(list(object({
device_name = string
no_device = bool
virtual_name = string
ebs = object({
delete_on_termination = bool
encrypted = bool
iops = number
kms_key_id = string
snapshot_id = string
volume_size = number
volume_ = string
})
})), [])
instance_market_options = optional(object({
market_ = string
spot_options = object({
block_duration_minutes = number
instance_interruption_behavior = string
max_price = number
spot_instance_ = string
valid_until = string
})
}))
instance_refresh = optional(object({
strategy = string
preferences = object({
instance_warmup = number
min_healthy_percentage = number
})
triggers = list(string)
}))
mixed_instances_policy = optional(object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
}), {
instances_distribution = null
})
placement = optional(object({
affinity = string
availability_zone = string
group_name = string
host_id = string
tenancy = string
}))
credit_specification = optional(object({
cpu_credits = string
}))
elastic_gpu_specifications = optional(object({
type = string
}))
disable_api_termination = optional(bool, false)
default_cooldown = optional(number, 300)
health_check_grace_period = optional(number, 300)
force_delete = optional(bool, false)
termination_policies = optional(list(string), ["Default"])
suspended_processes = optional(list(string), [])
placement_group = optional(string, "")
metrics_granularity = optional(string, "1Minute")
enabled_metrics = optional(list(string), [
"GroupMinSize",
"GroupMaxSize",
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupPendingInstances",
"GroupStandbyInstances",
"GroupTerminatingInstances",
"GroupTotalInstances",
"GroupInServiceCapacity",
"GroupPendingCapacity",
"GroupStandbyCapacity",
"GroupTerminatingCapacity",
"GroupTotalCapacity",
"WarmPoolDesiredCapacity",
"WarmPoolWarmedCapacity",
"WarmPoolPendingCapacity",
"WarmPoolTerminatingCapacity",
"WarmPoolTotalCapacity",
"GroupAndWarmPoolDesiredCapacity",
"GroupAndWarmPoolTotalCapacity",
])
wait_for_capacity_timeout = optional(string, "10m")
service_linked_role_arn = optional(string, "")
metadata_http_endpoint_enabled = optional(bool, true)
metadata_http_put_response_hop_limit = optional(number, 2)
metadata_http_tokens_required = optional(bool, true)
metadata_http_protocol_ipv6_enabled = optional(bool, false)
tag_specifications_resource_types = optional(set(string), ["instance", "volume"])
max_instance_lifetime = optional(number, null)
capacity_rebalance = optional(bool, false)
warm_pool = optional(object({
pool_state = string
min_size = number
max_group_prepared_capacity = number
}))
}))
| `{}` | no | +| [capacity\_providers\_fargate](#input\_capacity\_providers\_fargate) | Use FARGATE capacity provider | `bool` | `true` | no | +| [capacity\_providers\_fargate\_spot](#input\_capacity\_providers\_fargate\_spot) | Use FARGATE\_SPOT capacity provider | `bool` | `false` | no | | [container\_insights\_enabled](#input\_container\_insights\_enabled) | Whether or not to enable container insights | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [default\_capacity\_strategy](#input\_default\_capacity\_strategy) | The capacity provider strategy to use by default for the cluster |
object({
base = object({
provider = string
value = number
})
weights = map(number)
})
|
{
"base": {
"provider": "FARGATE",
"value": 1
},
"weights": {}
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | Use this component name to read from the remote state to get the dns\_delegated zone ID | `string` | `"dns-delegated"` | no | @@ -84,10 +104,13 @@ components: | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [internal\_enabled](#input\_internal\_enabled) | Whether to create an internal load balancer for services in this cluster | `bool` | `false` | no | +| [kms\_key\_id](#input\_kms\_key\_id) | The AWS Key Management Service key ID to encrypt the data between the local client and the container. | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [log\_configuration](#input\_log\_configuration) | The log configuration for the results of the execute command actions Required when logging is OVERRIDE |
object({
cloud_watch_encryption_enabled = string
cloud_watch_log_group_name = string
s3_bucket_name = string
s3_key_prefix = string
})
| `null` | no | +| [logging](#input\_logging) | The AWS Key Management Service key ID to encrypt the data between the local client and the container. (Valid values: 'NONE', 'DEFAULT', 'OVERRIDE') | `string` | `"DEFAULT"` | no | | [maintenance\_page\_path](#input\_maintenance\_page\_path) | The path from this directory to the text/html page to use as the maintenance page. Must be within 1024 characters | `string` | `"templates/503_example.html"` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | diff --git a/modules/ecs/main.tf b/modules/ecs/main.tf index d33f5549e..3347576c2 100644 --- a/modules/ecs/main.tf +++ b/modules/ecs/main.tf @@ -3,7 +3,7 @@ locals { dns_enabled = local.enabled && var.route53_enabled - acm_certificate_domain = length(var.acm_certificate_domain_suffix) > 0 ? format("%s.%s.%s", var.acm_certificate_domain_suffix, var.environment, module.dns_delegated.outputs.default_domain_name) : coalesce(var.acm_certificate_domain, module.dns_delegated.outputs.default_domain_name) + acm_certificate_domain = try(length(var.acm_certificate_domain_suffix) > 0, false) ? format("%s.%s.%s", var.acm_certificate_domain_suffix, var.environment, module.dns_delegated.outputs.default_domain_name) : coalesce(var.acm_certificate_domain, module.dns_delegated.outputs.default_domain_name) maintenance_page_fixed_response = { content_type = "text/html" @@ -27,47 +27,118 @@ module "target_group_label" { context = module.this.context } -resource "aws_ecs_cluster" "default" { - count = local.enabled ? 1 : 0 - - name = module.this.id - - # TODO: configuration.execute_command_configuration - # execute_command_configuration { - # kms_key_id = - # logging = "OVERRIDE" # "DEFAULT" - # # log_configuration is required when logging is set to "OVERRIDE" - # log_configuration { - # cloud_watch_encryption_enabled = var.cloud_watch_encryption_enabled - # cloud_watch_log_group_name = module.cloudwatch_log_group.name - # s3_bucket_name = module.logging_bucket.name - # s3_bucket_encryption_enabled = true - # s3_key_prefix = "/" - # } - # } - - setting { - name = "containerInsights" - value = var.container_insights_enabled ? "enabled" : "disabled" +resource "aws_security_group" "default" { + count = local.enabled ? 1 : 0 + name = module.this.id + description = "ECS cluster EC2 autoscale capacity providers" + vpc_id = module.vpc.outputs.vpc_id +} + +resource "aws_security_group_rule" "ingress_cidr" { + for_each = local.enabled ? toset(var.allowed_cidr_blocks) : [] + type = "ingress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = [each.value] + security_group_id = join("", aws_security_group.default.*.id) +} + +resource "aws_security_group_rule" "ingress_security_groups" { + for_each = local.enabled ? toset(var.allowed_security_groups) : [] + type = "ingress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + source_security_group_id = each.value + security_group_id = join("", aws_security_group.default.*.id) +} + +resource "aws_security_group_rule" "egress" { + count = local.enabled ? 1 : 0 + type = "egress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = join("", aws_security_group.default.*.id) +} + +module "cluster" { + source = "cloudposse/ecs-cluster/aws" + version = "0.2.2" + + context = module.this.context + + container_insights_enabled = var.container_insights_enabled + capacity_providers_fargate = var.capacity_providers_fargate + capacity_providers_fargate_spot = var.capacity_providers_fargate_spot + capacity_providers_ec2 = { + for name, provider in var.capacity_providers_ec2 : + name => merge( + provider, + { + security_group_ids = concat(aws_security_group.default.*.id, provider.security_group_ids) + subnet_ids = var.internal_enabled ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids + associate_public_ip_address = !var.internal_enabled + } + ) } - tags = module.this.tags + # external_ec2_capacity_providers = { + # external_default = { + # autoscaling_group_arn = module.autoscale_group.autoscaling_group_arn + # managed_termination_protection = false + # managed_scaling_status = false + # instance_warmup_period = 300 + # maximum_scaling_step_size = 1 + # minimum_scaling_step_size = 1 + # target_capacity_utilization = 100 + # } + # } + } -# TODO: setup capacity providers -# resource "aws_ecs_cluster_capacity_providers" "default" { -# count = local.enabled ? 1 : 0 +#locals { +# user_data = <> /etc/ecs/ecs.config +#echo ECS_ENABLE_CONTAINER_METADATA=true >> /etc/ecs/ecs.config +#echo ECS_POLL_METRICS=true >> /etc/ecs/ecs.config +#EOT +# +#} # -# cluster_name = join("", aws_ecs_cluster.default[*].name) +#data "aws_ssm_parameter" "ami" { +# name = "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" +#} # -# capacity_providers = ["FARGATE"] +#module "autoscale_group" { +# source = "cloudposse/ec2-autoscale-group/aws" +# version = "0.31.1" # -# default_capacity_provider_strategy { -# base = 1 -# weight = 100 -# capacity_provider = "FARGATE" -# } -# } +# context = module.this.context +# +# image_id = data.aws_ssm_parameter.ami.value +# instance_type = "t3.medium" +# security_group_ids = aws_security_group.default.*.id +# subnet_ids = var.internal_enabled ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids +# health_check_type = "EC2" +# desired_capacity = 1 +# min_size = 1 +# max_size = 2 +# wait_for_capacity_timeout = "5m" +# associate_public_ip_address = true +# user_data_base64 = base64encode(local.user_data) +# +# # Auto-scaling policies and CloudWatch metric alarms +# autoscaling_policies_enabled = true +# cpu_utilization_high_threshold_percent = "70" +# cpu_utilization_low_threshold_percent = "20" +# +# iam_instance_profile_name = module.cluster.role_name +#} + resource "aws_route53_record" "default" { for_each = local.dns_enabled ? var.alb_configuration : {} @@ -91,7 +162,7 @@ data "aws_acm_certificate" "default" { module "alb" { source = "cloudposse/alb/aws" - version = "1.4.0" + version = "1.5.0" for_each = local.enabled ? var.alb_configuration : {} diff --git a/modules/ecs/outputs.tf b/modules/ecs/outputs.tf index 87a80f618..4d730801f 100644 --- a/modules/ecs/outputs.tf +++ b/modules/ecs/outputs.tf @@ -1,10 +1,10 @@ output "cluster_arn" { - value = join("", aws_ecs_cluster.default[*].arn) + value = module.cluster.arn description = "ECS cluster ARN" } output "cluster_name" { - value = join("", aws_ecs_cluster.default[*].name) + value = module.cluster.name description = "ECS Cluster Name" } diff --git a/modules/ecs/remote-state.tf b/modules/ecs/remote-state.tf index 0112e39cd..d8a969005 100644 --- a/modules/ecs/remote-state.tf +++ b/modules/ecs/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.3.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.3.1" component = var.dns_delegated_component_name stage = var.dns_delegated_stage_name diff --git a/modules/ecs/variables.tf b/modules/ecs/variables.tf index a90e79ae5..3db6a1b03 100644 --- a/modules/ecs/variables.tf +++ b/modules/ecs/variables.tf @@ -80,3 +80,199 @@ variable "dns_delegated_component_name" { default = "dns-delegated" description = "Use this component name to read from the remote state to get the dns_delegated zone ID" } + +variable "allowed_security_groups" { + type = list(string) + default = [] + description = "List of Security Group IDs to be allowed to connect to the EKS cluster" +} + +variable "allowed_cidr_blocks" { + type = list(string) + default = [] + description = "List of CIDR blocks to be allowed to connect to the EKS cluster" +} + +variable "kms_key_id" { + description = "The AWS Key Management Service key ID to encrypt the data between the local client and the container." + type = string + default = null +} + +variable "logging" { + description = "The AWS Key Management Service key ID to encrypt the data between the local client and the container. (Valid values: 'NONE', 'DEFAULT', 'OVERRIDE')" + type = string + default = "DEFAULT" + validation { + condition = contains(["NONE", "DEFAULT", "OVERRIDE"], var.logging) + error_message = "The 'logging' value must be one of 'NONE', 'DEFAULT', 'OVERRIDE'" + } +} + +variable "log_configuration" { + description = "The log configuration for the results of the execute command actions Required when logging is OVERRIDE" + type = object({ + cloud_watch_encryption_enabled = string + cloud_watch_log_group_name = string + s3_bucket_name = string + s3_key_prefix = string + }) + default = null +} + +variable "capacity_providers_fargate" { + description = "Use FARGATE capacity provider" + type = bool + default = true +} + +variable "capacity_providers_fargate_spot" { + description = "Use FARGATE_SPOT capacity provider" + type = bool + default = false +} + +variable "capacity_providers_ec2" { + description = "EC2 autoscale groups capacity providers" + type = map(object({ + instance_type = string + max_size = number + security_group_ids = optional(list(string), []) + min_size = optional(number, 0) + image_id = optional(string) + instance_initiated_shutdown_behavior = optional(string, "terminate") + key_name = optional(string, "") + user_data = optional(string, "") + enable_monitoring = optional(bool, true) + instance_warmup_period = optional(number, 300) + maximum_scaling_step_size = optional(number, 1) + minimum_scaling_step_size = optional(number, 1) + target_capacity_utilization = optional(number, 100) + ebs_optimized = optional(bool, false) + block_device_mappings = optional(list(object({ + device_name = string + no_device = bool + virtual_name = string + ebs = object({ + delete_on_termination = bool + encrypted = bool + iops = number + kms_key_id = string + snapshot_id = string + volume_size = number + volume_ = string + }) + })), []) + instance_market_options = optional(object({ + market_ = string + spot_options = object({ + block_duration_minutes = number + instance_interruption_behavior = string + max_price = number + spot_instance_ = string + valid_until = string + }) + })) + instance_refresh = optional(object({ + strategy = string + preferences = object({ + instance_warmup = number + min_healthy_percentage = number + }) + triggers = list(string) + })) + mixed_instances_policy = optional(object({ + instances_distribution = object({ + on_demand_allocation_strategy = string + on_demand_base_capacity = number + on_demand_percentage_above_base_capacity = number + spot_allocation_strategy = string + spot_instance_pools = number + spot_max_price = string + }) + }), { + instances_distribution = null + }) + placement = optional(object({ + affinity = string + availability_zone = string + group_name = string + host_id = string + tenancy = string + })) + credit_specification = optional(object({ + cpu_credits = string + })) + elastic_gpu_specifications = optional(object({ + type = string + })) + disable_api_termination = optional(bool, false) + default_cooldown = optional(number, 300) + health_check_grace_period = optional(number, 300) + force_delete = optional(bool, false) + termination_policies = optional(list(string), ["Default"]) + suspended_processes = optional(list(string), []) + placement_group = optional(string, "") + metrics_granularity = optional(string, "1Minute") + enabled_metrics = optional(list(string), [ + "GroupMinSize", + "GroupMaxSize", + "GroupDesiredCapacity", + "GroupInServiceInstances", + "GroupPendingInstances", + "GroupStandbyInstances", + "GroupTerminatingInstances", + "GroupTotalInstances", + "GroupInServiceCapacity", + "GroupPendingCapacity", + "GroupStandbyCapacity", + "GroupTerminatingCapacity", + "GroupTotalCapacity", + "WarmPoolDesiredCapacity", + "WarmPoolWarmedCapacity", + "WarmPoolPendingCapacity", + "WarmPoolTerminatingCapacity", + "WarmPoolTotalCapacity", + "GroupAndWarmPoolDesiredCapacity", + "GroupAndWarmPoolTotalCapacity", + ]) + wait_for_capacity_timeout = optional(string, "10m") + service_linked_role_arn = optional(string, "") + metadata_http_endpoint_enabled = optional(bool, true) + metadata_http_put_response_hop_limit = optional(number, 2) + metadata_http_tokens_required = optional(bool, true) + metadata_http_protocol_ipv6_enabled = optional(bool, false) + tag_specifications_resource_types = optional(set(string), ["instance", "volume"]) + max_instance_lifetime = optional(number, null) + capacity_rebalance = optional(bool, false) + warm_pool = optional(object({ + pool_state = string + min_size = number + max_group_prepared_capacity = number + })) + })) + default = {} + validation { + condition = !contains(["FARGATE", "FARGATE_SPOT"], keys(var.capacity_providers_ec2)) + error_message = "'FARGATE' and 'FARGATE_SPOT' name is reserved" + } +} + +variable "default_capacity_strategy" { + description = "The capacity provider strategy to use by default for the cluster" + type = object({ + base = object({ + provider = string + value = number + }) + weights = map(number) + }) + default = { + base = { + provider = "FARGATE" + value = 1 + } + weights = {} + } +} + diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index a1a003dcf..a684ed2ec 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -105,7 +105,7 @@ components: |------|------| | [aws_iam_policy_document.custom_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | -| [template_file.bucket_policy](https://registry.terraform.io/providers/cloudposse/template/latest/docs/data-sources/file) | data source | +| [template_file.bucket_policy](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file) | data source | ## Inputs diff --git a/modules/ssm-parameters/README.md b/modules/ssm-parameters/README.md index 6126a3d32..01a0b6210 100644 --- a/modules/ssm-parameters/README.md +++ b/modules/ssm-parameters/README.md @@ -32,14 +32,14 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.2.5 | | [aws](#requirement\_aws) | >= 4.0 | -| [sops](#requirement\_sops) | >= 0.5 | +| [sops](#requirement\_sops) | >= 0.5, < 1.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [sops](#provider\_sops) | >= 0.5 | +| [sops](#provider\_sops) | >= 0.5, < 1.0 | ## Modules From 080859e776d9dbb3a83d8e18efc230592cd320c9 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 21 Dec 2022 10:37:56 -0800 Subject: [PATCH 005/501] upstream `spacelift` (#526) --- modules/spacelift/README.md | 148 +++++++++++------- modules/spacelift/bin/spacelift-configure | 0 modules/spacelift/bin/spacelift-git-use-https | 0 modules/spacelift/bin/spacelift-tf-workspace | 0 modules/spacelift/bin/spacelift-write-vars | 0 modules/spacelift/default.auto.tfvars | 3 - modules/spacelift/main.tf | 5 +- modules/spacelift/providers.tf | 29 ++++ .../rego-policies/access.default.rego | 10 +- .../rego-policies/plan.autodeployupdates.rego | 21 +++ modules/spacelift/rego-policies/plan.ecr.rego | 16 ++ modules/spacelift/spacelift-provider.tf | 26 +++ modules/spacelift/variables.tf | 22 ++- modules/spacelift/versions.tf | 7 +- 14 files changed, 203 insertions(+), 84 deletions(-) mode change 100755 => 100644 modules/spacelift/bin/spacelift-configure mode change 100755 => 100644 modules/spacelift/bin/spacelift-git-use-https mode change 100755 => 100644 modules/spacelift/bin/spacelift-tf-workspace mode change 100755 => 100644 modules/spacelift/bin/spacelift-write-vars delete mode 100644 modules/spacelift/default.auto.tfvars create mode 100644 modules/spacelift/providers.tf create mode 100644 modules/spacelift/rego-policies/plan.autodeployupdates.rego create mode 100644 modules/spacelift/rego-policies/plan.ecr.rego create mode 100644 modules/spacelift/spacelift-provider.tf diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index 85501df58..b452e329c 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -25,72 +25,94 @@ components: workspace_enabled: true administrative: true autodeploy: true - before_init: [] + before_init: + - spacelift-configure + - spacelift-write-vars + - spacelift-tf-workspace component_root: components/terraform/spacelift + description: Spacelift Administrative stack for the organization. stack_destructor_enabled: false - policies_enabled: [] + worker_pool_name: WORKER_POOL_NAME # TODO: replace with the name of the worker pool + repository: infra + branch: main + labels: + - folder:admin policies_by_id_enabled: - - trigger-administrative-policy + - global-administrative-trigger-policy vars: - # This is to locally apply the stack - external_execution: true - # This should match the version set in the Dockerfile - terraform_version: "1.2.3" - terraform_version_map: - "1": "1.2.3" - # additional defaults - infracost_enabled: false - git_repository: infrastructure - git_branch: main - runner_image: .dkr.ecr..amazonaws.com/:latest - administrative_trigger_policy_enabled: false - worker_pool_name_id_map: {} - autodeploy: true - stack_config_path_template: stacks/%s.yaml - spacelift_component_path: components/terraform + enabled: true + spacelift_api_endpoint: https://TODO.app.spacelift.io administrative_stack_drift_detection_enabled: true administrative_stack_drift_detection_reconcile: true - administrative_stack_drift_detection_schedule: - - 0 4 * * * + administrative_stack_drift_detection_schedule: ["0 4 * * *"] + administrative_trigger_policy_enabled: false + autodeploy: false + aws_role_enabled: false drift_detection_enabled: true drift_detection_reconcile: true - drift_detection_schedule: - - 0 4 * * * - aws_role_enabled: false - aws_role_generate_credentials_in_worker: false - stack_destructor_enabled: true - before_init: [] - # Add these existing policies by ID, do not create them, they are already provisioned in Spacelift - policies_by_id_enabled: - - git_push-proposed-run-policy - - git_push-auto-cancel-policy - - plan-default-policy - - trigger-dependencies-policy - policies_available: [] - policies_enabled: [] + drift_detection_schedule: ["0 4 * * *"] + external_execution: true + git_repository: infra # TODO: replace with your repository name + git_branch: main + + # List of available default Rego policies to create in Spacelift. + # These policies are defined in the catalog https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies + # These policies will not be attached to Spacelift stacks by default (but will be created in Spacelift, and could be attached to a stack manually). + # For specify policies to attach to each Spacelift stack, use `var.policies_enabled`. + policies_available: + - "git_push.proposed-run" + - "git_push.tracked-run" + - "plan.default" + - "trigger.dependencies" + - "trigger.retries" + + # List of default Rego policies to attach to all Spacelift stacks. + # These policies are defined in the catalog https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies + policies_enabled: + - "git_push.proposed-run" + - "git_push.tracked-run" + - "plan.default" + - "trigger.dependencies" + + # List of custom policy names to attach to all Spacelift stacks + # These policies must exist in `components/terraform/spacelift/rego-policies` policies_by_name_enabled: [] + runner_image: 000000000000.dkr.ecr.us-west-2.amazonaws.com/infra #TODO: replace with your ECR repository + spacelift_component_path: components/terraform + stack_config_path_template: stacks/%s.yaml + stack_destructor_enabled: false + worker_pool_name_id_map: + -spacelift-worker-pool: SOMEWORKERPOOLID #TODO: replace with your worker pool ID + infracost_enabled: false # TODO: decide on infracost + terraform_version: "1.3.5" + terraform_version_map: + "1": 1.3.5 + + # These could be moved to $PROJECT_ROOT/.spacelift/config.yml + before_init: + - spacelift-configure + - spacelift-write-vars + - spacelift-tf-workspace + before_plan: + - spacelift-configure + before_apply: + - spacelift-configure + # Manages policies, admin stacks, and core OU accounts spacelift: metadata: component: spacelift inherits: - spacelift-defaults + settings: + spacelift: + policies_by_id_enabled: + # This component also creates this policy so this is omitted prior to the first apply + # then added so it's consistent with all admin stacks. + - trigger-administrative-policy vars: enabled: true - # Use context_filters to split up admin stack management - # context_filters: - # stages: - # - artifacts - # - audit - # - auto - # - corp - # - dns - # - identity - # - marketplace - # - network - # - public - # - security # These are the policies created from https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies # Make sure to remove the .rego suffix policies_available: @@ -99,6 +121,8 @@ components: - plan.default - trigger.dependencies - trigger.retries + # This is to auto deploy launch template image id changes + - plan.warn-on-resource-changes-except-image-id # This is the global admin policy - trigger.administrative # These are the policies added to each spacelift stack created by this admin stack @@ -107,6 +131,9 @@ components: - git_push.tracked-run - plan.default - trigger.dependencies + # Keep these empty + policies_by_id_enabled: [] + ``` ## Prerequisites @@ -286,23 +313,28 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [spacelift](#requirement\_spacelift) | >= 0.1.29 | -| [utils](#requirement\_utils) | >= 1.3.0, != 1.4.0 | +| [spacelift](#requirement\_spacelift) | ~> 0.1.31 | ## Providers -No providers. +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | ## Modules | Name | Source | Version | |------|--------|---------| -| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.49.5 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.50.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_ssm_parameter.spacelift_key_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.spacelift_key_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs @@ -321,7 +353,7 @@ No resources. | [aws\_role\_generate\_credentials\_in\_worker](#input\_aws\_role\_generate\_credentials\_in\_worker) | Flag to enable/disable generating AWS credentials in the private worker after assuming the supplied IAM role | `bool` | `false` | no | | [before\_init](#input\_before\_init) | List of before-init scripts | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [context\_filters](#input\_context\_filters) | Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`. | `map(list(string))` | `{}` | no | +| [context\_filters](#input\_context\_filters) | Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`. | `map(any)` | `{}` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [drift\_detection\_enabled](#input\_drift\_detection\_enabled) | Flag to enable/disable drift detection on the infrastructure stacks | `bool` | `true` | no | @@ -334,6 +366,8 @@ No resources. | [git\_commit\_sha](#input\_git\_commit\_sha) | The commit SHA for which to trigger a run. Requires `var.spacelift_run_enabled` to be set to `true` | `string` | `null` | no | | [git\_repository](#input\_git\_repository) | The Git repository name | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [infracost\_enabled](#input\_infracost\_enabled) | Flag to enable/disable infracost. If this is enabled, it will add infracost label to each stack. See [spacelift infracost](https://docs.spacelift.io/vendors/terraform/infracost) docs for more details. | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | @@ -343,21 +377,23 @@ No resources. | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [policies\_available](#input\_policies\_available) | List of available default policies to create in Spacelift (these policies will not be attached to Spacelift stacks by default, use `var.policies_enabled`) | `list(string)` |
[
"git_push.proposed-run",
"git_push.tracked-run",
"plan.default",
"trigger.dependencies",
"trigger.retries"
]
| no | | [policies\_by\_id\_enabled](#input\_policies\_by\_id\_enabled) | List of existing policy IDs to attach to all Spacelift stacks. These policies must already exist in Spacelift | `list(string)` | `[]` | no | -| [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of existing policy names to attach to all Spacelift stacks. These policies must exist in `modules/spacelift/rego-policies` | `list(string)` | `[]` | no | +| [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of custom policy names to attach to all Spacelift stacks. These policies must exist in `components/terraform/spacelift/rego-policies` | `list(string)` | `[]` | no | | [policies\_enabled](#input\_policies\_enabled) | DEPRECATED: Use `policies_by_id_enabled` instead. List of default policies created by this stack to attach to all Spacelift stacks | `list(string)` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [runner\_image](#input\_runner\_image) | Full address & tag of the Spacelift runner image (e.g. on ECR) | `string` | n/a | yes | +| [space\_id](#input\_space\_id) | Place the stack(s) in the specified space\_id. | `string` | `"legacy"` | no | +| [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | | [spacelift\_component\_path](#input\_spacelift\_component\_path) | The Spacelift Component Path | `string` | `"components/terraform"` | no | | [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | | [stack\_config\_path\_template](#input\_stack\_config\_path\_template) | Stack config path template | `string` | `"stacks/%s.yaml"` | no | | [stack\_destructor\_enabled](#input\_stack\_destructor\_enabled) | Flag to enable/disable the stack destructor to destroy the resources of a stack before deleting the stack itself | `bool` | `false` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tag\_filters](#input\_tag\_filters) | A map of tags that will filter stack creation by the matching `tags` set in a component `vars` configuration. | `map(string)` | `{}` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [terraform\_version](#input\_terraform\_version) | Default Terraform version for all stacks created by this project | `string` | n/a | yes | | [terraform\_version\_map](#input\_terraform\_version\_map) | A map to determine which Terraform patch version to use for each minor version | `map(string)` | `{}` | no | -| [worker\_pool\_id](#input\_worker\_pool\_id) | DEPRECATED: Use worker\_pool\_name\_id\_map instead. Worker pool ID | `string` | `""` | no | | [worker\_pool\_name\_id\_map](#input\_worker\_pool\_name\_id\_map) | Map of worker pool names to worker pool IDs | `map(any)` | `{}` | no | ## Outputs diff --git a/modules/spacelift/bin/spacelift-configure b/modules/spacelift/bin/spacelift-configure old mode 100755 new mode 100644 diff --git a/modules/spacelift/bin/spacelift-git-use-https b/modules/spacelift/bin/spacelift-git-use-https old mode 100755 new mode 100644 diff --git a/modules/spacelift/bin/spacelift-tf-workspace b/modules/spacelift/bin/spacelift-tf-workspace old mode 100755 new mode 100644 diff --git a/modules/spacelift/bin/spacelift-write-vars b/modules/spacelift/bin/spacelift-write-vars old mode 100755 new mode 100644 diff --git a/modules/spacelift/default.auto.tfvars b/modules/spacelift/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/spacelift/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/spacelift/main.tf b/modules/spacelift/main.tf index 69d7c32b5..e39351eca 100644 --- a/modules/spacelift/main.tf +++ b/modules/spacelift/main.tf @@ -1,10 +1,9 @@ -provider "spacelift" {} - module "spacelift" { source = "cloudposse/cloud-infrastructure-automation/spacelift" - version = "0.49.5" + version = "0.50.2" context_filters = var.context_filters + tag_filters = var.tag_filters stack_config_path_template = var.stack_config_path_template components_path = var.spacelift_component_path diff --git a/modules/spacelift/providers.tf b/modules/spacelift/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/spacelift/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/spacelift/rego-policies/access.default.rego b/modules/spacelift/rego-policies/access.default.rego index 5b44c1595..6945b3a41 100644 --- a/modules/spacelift/rego-policies/access.default.rego +++ b/modules/spacelift/rego-policies/access.default.rego @@ -3,13 +3,7 @@ package spacelift # Access Policy Documentation: # https://docs.spacelift.io/concepts/policy/stack-access-policy -# TODO: Provide access to different users to different stacks depending on user permissions -# For example, you can give READ access to Spacelift stacks to the "data" team by using this Rego block -# read { -# input.session.teams[_] == "data" -# } - -# By default, allow WRITE access to everybody who has permissions to login to Spacelift -write { +# By default, allow READ access to everybody who has permissions to login to Spacelift +read { true } diff --git a/modules/spacelift/rego-policies/plan.autodeployupdates.rego b/modules/spacelift/rego-policies/plan.autodeployupdates.rego new file mode 100644 index 000000000..43a78ceba --- /dev/null +++ b/modules/spacelift/rego-policies/plan.autodeployupdates.rego @@ -0,0 +1,21 @@ +package spacelift + +# This policy allows autodeploy if there are only new resources or updates. +# It requires manual intervention (approval) if any of the resources will be deleted. + +# Usage: +# settings: +# spacelift: +# autodeploy: true +# policies_by_name_enabled: +# - plan.autodeployupdates + +warn[sprintf(message, [action, resource.address])] { + message := "action '%s' requires human review (%s)" + review := {"delete"} + + resource := input.terraform.resource_changes[_] + action := resource.change.actions[_] + + review[action] +} diff --git a/modules/spacelift/rego-policies/plan.ecr.rego b/modules/spacelift/rego-policies/plan.ecr.rego new file mode 100644 index 000000000..23f4ef8d4 --- /dev/null +++ b/modules/spacelift/rego-policies/plan.ecr.rego @@ -0,0 +1,16 @@ +package spacelift + +proposed := input.spacelift.run.type == "PROPOSED" + +deny[reason] { not proposed; reason := resource_deletion[_] } +warn[reason] { proposed; reason := resource_deletion[_] } + +resource_deletion[sprintf(message, [action, resource.address])] { + message := "action '%s' requires human review (%s)" + review := {"delete"} + types := {"aws_ecr_repository"} + resource := input.terraform.resource_changes[_] + action := resource.change.actions[_] + review[action] + types[resource.type] +} diff --git a/modules/spacelift/spacelift-provider.tf b/modules/spacelift/spacelift-provider.tf new file mode 100644 index 000000000..dc5400223 --- /dev/null +++ b/modules/spacelift/spacelift-provider.tf @@ -0,0 +1,26 @@ +variable "spacelift_api_endpoint" { + type = string + description = "The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io)" +} + +# The Spacelift always validates its credentials, so we always pass api_key_id and api_key_secret +data "aws_ssm_parameter" "spacelift_key_id" { + count = local.enabled ? 1 : 0 + name = "/spacelift/key_id" +} + +data "aws_ssm_parameter" "spacelift_key_secret" { + count = local.enabled ? 1 : 0 + name = "/spacelift/key_secret" +} + +locals { + enabled = module.this.enabled +} + +# This provider always validates its credentials, so we always pass api_key_id and api_key_secret +provider "spacelift" { + api_key_endpoint = var.spacelift_api_endpoint + api_key_id = local.enabled ? data.aws_ssm_parameter.spacelift_key_id[0].value : null + api_key_secret = local.enabled ? data.aws_ssm_parameter.spacelift_key_secret[0].value : null +} diff --git a/modules/spacelift/variables.tf b/modules/spacelift/variables.tf index cfbb4f020..9ea232218 100644 --- a/modules/spacelift/variables.tf +++ b/modules/spacelift/variables.tf @@ -8,12 +8,6 @@ variable "runner_image" { description = "Full address & tag of the Spacelift runner image (e.g. on ECR)" } -variable "worker_pool_id" { - type = string - description = "DEPRECATED: Use worker_pool_name_id_map instead. Worker pool ID" - default = "" -} - variable "worker_pool_name_id_map" { type = map(any) description = "Map of worker pool names to worker pool IDs" @@ -97,7 +91,7 @@ variable "policies_by_id_enabled" { variable "policies_by_name_enabled" { type = list(string) - description = "List of existing policy names to attach to all Spacelift stacks. These policies must exist in `modules/spacelift/rego-policies`" + description = "List of custom policy names to attach to all Spacelift stacks. These policies must exist in `components/terraform/spacelift/rego-policies`" default = [] } @@ -168,11 +162,17 @@ variable "stack_destructor_enabled" { } variable "context_filters" { - type = map(list(string)) + type = map(any) description = "Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`." default = {} } +variable "tag_filters" { + type = map(string) + description = "A map of tags that will filter stack creation by the matching `tags` set in a component `vars` configuration." + default = {} +} + variable "administrative_trigger_policy_enabled" { type = bool description = "Flag to enable/disable the global administrative trigger policy" @@ -196,3 +196,9 @@ variable "before_init" { description = "List of before-init scripts" default = [] } + +variable "space_id" { + type = string + description = "Place the stack(s) in the specified space_id." + default = "legacy" +} diff --git a/modules/spacelift/versions.tf b/modules/spacelift/versions.tf index d096a3b97..9d6f08edc 100644 --- a/modules/spacelift/versions.tf +++ b/modules/spacelift/versions.tf @@ -4,12 +4,7 @@ terraform { required_providers { spacelift = { source = "spacelift-io/spacelift" - version = ">= 0.1.29" - } - utils = { - source = "cloudposse/utils" - # problem with 1.4.0 - version = ">= 1.3.0, != 1.4.0" + version = "~> 0.1.31" } } } From 8300417af3ef2eb04d264b88b0292041f3289307 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 4 Jan 2023 18:44:15 -0500 Subject: [PATCH 006/501] fix(aws-sso): add missing tf update perms (#530) Co-authored-by: cloudpossebot --- modules/aws-sso/README.md | 9 ++++-- modules/aws-sso/main.tf | 1 + .../aws-sso/policy-TerraformUpdateAccess.tf | 30 +++++++++++++++++++ modules/aws-sso/remote-state.tf | 14 ++++++++- modules/aws-sso/variables.tf | 5 ++++ modules/aws-sso/versions.tf | 2 +- 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 modules/aws-sso/policy-TerraformUpdateAccess.tf diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index 7b6a4f7c3..b21fa7ab4 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -109,30 +109,32 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 0.6.2 | | [role\_prefix](#module\_role\_prefix) | cloudposse/label/null | 0.25.0 | | [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 0.6.2 | | [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 0.6.2 | +| [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources | Name | Type | |------|------| +| [aws_iam_policy_document.TerraformUpdateAccess](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.assume_identity_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.dns_administrator_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | @@ -167,6 +169,7 @@ components: | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [tfstate\_environment\_name](#input\_tfstate\_environment\_name) | The name of the environment where `tfstate-backend` is provisioned | `string` | n/a | yes | ## Outputs diff --git a/modules/aws-sso/main.tf b/modules/aws-sso/main.tf index ec0775976..69bb0e6eb 100644 --- a/modules/aws-sso/main.tf +++ b/modules/aws-sso/main.tf @@ -10,6 +10,7 @@ module "permission_sets" { local.identity_access_permission_sets, local.poweruser_access_permission_set, local.read_only_access_permission_set, + local.terraform_update_access_permission_set, ) context = module.this.context diff --git a/modules/aws-sso/policy-TerraformUpdateAccess.tf b/modules/aws-sso/policy-TerraformUpdateAccess.tf new file mode 100644 index 000000000..cd0fb915b --- /dev/null +++ b/modules/aws-sso/policy-TerraformUpdateAccess.tf @@ -0,0 +1,30 @@ + +data "aws_iam_policy_document" "TerraformUpdateAccess" { + statement { + sid = "TerraformStateBackendS3Bucket" + effect = "Allow" + actions = ["s3:ListBucket", "s3:GetObject", "s3:PutObject"] + resources = module.this.enabled ? [ + module.tfstate.outputs.tfstate_backend_s3_bucket_arn, + "${module.tfstate.outputs.tfstate_backend_s3_bucket_arn}/*" + ] : [] + } + statement { + sid = "TerraformStateBackendDynamoDbTable" + effect = "Allow" + actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"] + resources = module.this.enabled ? [module.tfstate.outputs.tfstate_backend_dynamodb_table_arn] : [] + } +} + +locals { + terraform_update_access_permission_set = [{ + name = "TerraformUpdateAccess", + description = "Allow access to Terraform state sufficient to make changes", + relay_state = "", + session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles + tags = {}, + inline_policy = data.aws_iam_policy_document.TerraformUpdateAccess.json, + policy_attachments = [] + }] +} diff --git a/modules/aws-sso/remote-state.tf b/modules/aws-sso/remote-state.tf index 6e8b0215f..b7e892a43 100644 --- a/modules/aws-sso/remote-state.tf +++ b/modules/aws-sso/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.3.1" component = "account-map" environment = var.global_environment_name @@ -9,3 +9,15 @@ module "account_map" { context = module.this.context } + +module "tfstate" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.3.1" + + component = "tfstate-backend" + environment = var.tfstate_environment_name + stage = var.root_account_stage_name + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/aws-sso/variables.tf b/modules/aws-sso/variables.tf index 02001bb74..1a4006d55 100644 --- a/modules/aws-sso/variables.tf +++ b/modules/aws-sso/variables.tf @@ -3,6 +3,11 @@ variable "region" { description = "AWS Region" } +variable "tfstate_environment_name" { + type = string + description = "The name of the environment where `tfstate-backend` is provisioned" +} + variable "global_environment_name" { type = string description = "Global environment name" diff --git a/modules/aws-sso/versions.tf b/modules/aws-sso/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/aws-sso/versions.tf +++ b/modules/aws-sso/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } From ebe192ffd3991d7d339e438e1de21cac7a16cdb2 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 5 Jan 2023 13:19:00 -0800 Subject: [PATCH 007/501] Datadog Upstreams and Account Settings (#533) --- modules/account-settings/README.md | 2 +- modules/account-settings/budgets.tf | 2 +- modules/datadog-configuration/README.md | 37 +- modules/datadog-configuration/main.tf | 3 +- .../modules/datadog_keys/README.md | 4 +- .../modules/datadog_keys/main.tf | 9 +- .../modules/datadog_keys/outputs.tf | 32 +- .../modules/datadog_keys/providers.tf | 30 ++ modules/datadog-configuration/outputs.tf | 7 +- modules/datadog-integration/README.md | 4 +- modules/datadog-integration/main.tf | 32 +- modules/datadog-integration/variables.tf | 1 + modules/datadog-lambda-forwarder/README.md | 6 +- modules/datadog-lambda-forwarder/main.tf | 13 +- modules/datadog-lambda-forwarder/variables.tf | 39 +- modules/datadog-logs-archive/README.md | 125 +++++++ modules/datadog-logs-archive/asm.tf | 19 + modules/datadog-logs-archive/context.tf | 279 ++++++++++++++ modules/datadog-logs-archive/main.tf | 352 ++++++++++++++++++ modules/datadog-logs-archive/outputs.tf | 44 +++ .../datadog-logs-archive/provider-datadog.tf | 10 + modules/datadog-logs-archive/providers.tf | 35 ++ modules/datadog-logs-archive/ssm.tf | 11 + modules/datadog-logs-archive/variables.tf | 83 +++++ modules/datadog-logs-archive/versions.tf | 18 + modules/datadog-monitor/README.md | 3 +- modules/datadog-monitor/variables.tf | 11 - .../README.md | 223 +++++++++++ .../context.tf | 279 ++++++++++++++ .../helm-variables.tf | 63 ++++ .../main.tf | 58 +++ .../outputs.tf | 9 + .../provider-datadog.tf | 12 + .../provider-helm.tf | 158 ++++++++ .../providers.tf | 29 ++ .../remote-state.tf | 8 + .../values.yaml.tpl | 34 ++ .../variables.tf | 16 + .../versions.tf | 30 ++ 39 files changed, 2046 insertions(+), 84 deletions(-) create mode 100644 modules/datadog-configuration/modules/datadog_keys/providers.tf create mode 100644 modules/datadog-logs-archive/README.md create mode 100644 modules/datadog-logs-archive/asm.tf create mode 100644 modules/datadog-logs-archive/context.tf create mode 100644 modules/datadog-logs-archive/main.tf create mode 100644 modules/datadog-logs-archive/outputs.tf create mode 100644 modules/datadog-logs-archive/provider-datadog.tf create mode 100755 modules/datadog-logs-archive/providers.tf create mode 100644 modules/datadog-logs-archive/ssm.tf create mode 100644 modules/datadog-logs-archive/variables.tf create mode 100644 modules/datadog-logs-archive/versions.tf create mode 100644 modules/datadog-synthetics-private-location/README.md create mode 100644 modules/datadog-synthetics-private-location/context.tf create mode 100644 modules/datadog-synthetics-private-location/helm-variables.tf create mode 100644 modules/datadog-synthetics-private-location/main.tf create mode 100644 modules/datadog-synthetics-private-location/outputs.tf create mode 100644 modules/datadog-synthetics-private-location/provider-datadog.tf create mode 100644 modules/datadog-synthetics-private-location/provider-helm.tf create mode 100644 modules/datadog-synthetics-private-location/providers.tf create mode 100644 modules/datadog-synthetics-private-location/remote-state.tf create mode 100644 modules/datadog-synthetics-private-location/values.yaml.tpl create mode 100644 modules/datadog-synthetics-private-location/variables.tf create mode 100755 modules/datadog-synthetics-private-location/versions.tf diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index 48206f889..12320f033 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -65,7 +65,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [budgets](#module\_budgets) | cloudposse/budgets/aws | 0.1.0 | +| [budgets](#module\_budgets) | cloudposse/budgets/aws | 0.2.1 | | [iam\_account\_settings](#module\_iam\_account\_settings) | cloudposse/iam-account-settings/aws | 0.4.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [service\_quotas](#module\_service\_quotas) | cloudposse/service-quotas/aws | 0.1.0 | diff --git a/modules/account-settings/budgets.tf b/modules/account-settings/budgets.tf index 29066cb7b..2a4093713 100644 --- a/modules/account-settings/budgets.tf +++ b/modules/account-settings/budgets.tf @@ -1,6 +1,6 @@ module "budgets" { source = "cloudposse/budgets/aws" - version = "0.1.0" + version = "0.2.1" enabled = module.this.enabled && var.budgets_enabled budgets = var.budgets diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index 24017ed95..d8aa79159 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -1,19 +1,29 @@ -# Component: `datadog-integration` +# Component: `datadog-configuration` This component is responsible for provisioning SSM or ASM entries for datadog api keys. It's required that the DataDog API and APP secret keys are available in the `var.datadog_secrets_source_store_account` account in AWS SSM Parameter Store at the `/datadog/%v/datadog_app_key` paths (where `%v` are the corresponding account names). +This component copies keys from the source account (e.g. `auto`) to the destination account where this is being deployed. The purpose of using this formatted copying of keys handles a couple of problems. +1. The keys are needed in each account where datadog resources will be deployed. +2. The keys might need to be different per account or tenant, or any subset of accounts. +3. If the keys need to be rotated they can be rotated from a single management account. + +This module also has a submodule which allows other resources to quickly use it to create a datadog provider. + See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. ## Usage -**Stack Level**: Regional +**Stack Level**: Global -This component should be deployed to every region where you want to provision datadog resources. +This component should be deployed to every account where you want to provision datadog resources. This is usually every account except `root` and `identity` Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts which you want to track AWS metrics with DataDog. +In this example we use the key paths `/datadog/%v/datadog_api_key` and `/datadog/%v/datadog_app_key` where `%v` is `default`, this can be changed through `datadog_app_secret_key` & `datadog_api_secret_key` variables. +The output Keys in the deployed account will be `/datadog/datadog_api_key` and `/datadog/datadog_app_key`. + ```yaml components: @@ -24,9 +34,27 @@ components: workspace_enabled: true vars: enabled: true + name: datadog-configuration datadog_secrets_store_type: SSM datadog_secrets_source_store_account_stage: auto - datadog_secrets_source_store_account_region: "us-west-2" + datadog_secrets_source_store_account_region: "us-east-2" +``` + +Here is a snippet of using the `datadog_keys` submodule: + +```terraform +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} ``` @@ -110,6 +138,7 @@ components: | [datadog\_app\_key\_location](#output\_datadog\_app\_key\_location) | The Datadog APP key location in the secrets store | | [datadog\_secrets\_store\_type](#output\_datadog\_secrets\_store\_type) | The type of the secrets store to use for Datadog API and APP keys | | [datadog\_site](#output\_datadog\_site) | The Datadog site to use | +| [region](#output\_region) | The region where the keys will be created | diff --git a/modules/datadog-configuration/main.tf b/modules/datadog-configuration/main.tf index aa90c5aa0..416dc171c 100644 --- a/modules/datadog-configuration/main.tf +++ b/modules/datadog-configuration/main.tf @@ -7,5 +7,6 @@ locals { datadog_api_key = var.datadog_secrets_store_type == "ASM" ? data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string : data.aws_ssm_parameter.datadog_api_key[0].value datadog_app_key = var.datadog_secrets_store_type == "ASM" ? data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string : data.aws_ssm_parameter.datadog_app_key[0].value - datadog_api_url = format("https://api.%s", var.datadog_site_url) + datadog_site = coalesce(var.datadog_site_url, "datadoghq.com") + datadog_api_url = format("https://api.%s", local.datadog_site) } diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index 5ad172ec1..7591a6f4a 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -7,14 +7,14 @@ Useful submodule for other modules to quickly configure the datadog provider ```hcl module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region + region = var.region context = module.this.context } provider "datadog" { - api_url = module.datadog_configuration.datadog_api_url api_key = module.datadog_configuration.datadog_api_key app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url validate = local.enabled } ``` diff --git a/modules/datadog-configuration/modules/datadog_keys/main.tf b/modules/datadog-configuration/modules/datadog_keys/main.tf index 496e57a48..7eb556f04 100644 --- a/modules/datadog-configuration/modules/datadog_keys/main.tf +++ b/modules/datadog-configuration/modules/datadog_keys/main.tf @@ -15,9 +15,6 @@ module "utils_example_complete" { } locals { - # if we are in the global region, use the - environment = module.this.environment == var.global_environment_name ? module.utils_example_complete.region_az_alt_code_maps[var.region_abbreviation_type][var.region] : var.environment - context_tags = { for k, v in module.this.tags : lower(k) => v @@ -33,18 +30,22 @@ module "datadog_configuration" { component = "datadog-configuration" - environment = local.environment + environment = var.global_environment_name context = module.always.context } data "aws_ssm_parameter" "datadog_api_key" { count = module.this.enabled ? 1 : 0 + provider = aws.dd_api_keys + name = module.datadog_configuration.outputs.datadog_api_key_location } data "aws_ssm_parameter" "datadog_app_key" { count = module.this.enabled ? 1 : 0 + provider = aws.dd_api_keys + name = module.datadog_configuration.outputs.datadog_app_key_location } diff --git a/modules/datadog-configuration/modules/datadog_keys/outputs.tf b/modules/datadog-configuration/modules/datadog_keys/outputs.tf index 00d2bd5c2..d24f5f6e8 100644 --- a/modules/datadog-configuration/modules/datadog_keys/outputs.tf +++ b/modules/datadog-configuration/modules/datadog_keys/outputs.tf @@ -1,17 +1,41 @@ output "datadog_api_key" { - value = one(data.aws_ssm_parameter.datadog_api_key[*].value) + value = one(data.aws_ssm_parameter.datadog_api_key[*].value) + description = "Datadog API Key" } output "datadog_app_key" { - value = one(data.aws_ssm_parameter.datadog_app_key[*].value) + value = one(data.aws_ssm_parameter.datadog_app_key[*].value) + description = "Datadog APP Key" } output "datadog_api_url" { - value = module.datadog_configuration.outputs.datadog_api_url + value = module.datadog_configuration.outputs.datadog_api_url + description = "Datadog API URL" } output "datadog_site" { - value = module.datadog_configuration.outputs.datadog_site + value = module.datadog_configuration.outputs.datadog_site + description = "Datadog Site" +} + +output "api_key_ssm_arn" { + value = one(data.aws_ssm_parameter.datadog_api_key[*].arn) + description = "Datadog API Key SSM ARN" +} + +output "datadog_secrets_store_type" { + value = module.datadog_configuration.outputs.datadog_secrets_store_type + description = "The type of the secrets store to use for Datadog API and APP keys" +} + +output "datadog_app_key_location" { + value = module.datadog_configuration.outputs.datadog_app_key_location + description = "The Datadog APP key location in the secrets store" +} + +output "datadog_api_key_location" { + value = module.datadog_configuration.outputs.datadog_api_key_location + description = "The Datadog API key in the secrets store" } output "datadog_tags" { diff --git a/modules/datadog-configuration/modules/datadog_keys/providers.tf b/modules/datadog-configuration/modules/datadog_keys/providers.tf new file mode 100644 index 000000000..068004cd4 --- /dev/null +++ b/modules/datadog-configuration/modules/datadog_keys/providers.tf @@ -0,0 +1,30 @@ +provider "aws" { + region = module.datadog_configuration.outputs.region + alias = "dd_api_keys" + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/datadog-configuration/outputs.tf b/modules/datadog-configuration/outputs.tf index 958ecd811..5fc7c32a0 100644 --- a/modules/datadog-configuration/outputs.tf +++ b/modules/datadog-configuration/outputs.tf @@ -1,3 +1,8 @@ +output "region" { + value = var.region + description = "The region where the keys will be created" +} + output "datadog_secrets_store_type" { value = var.datadog_secrets_store_type description = "The type of the secrets store to use for Datadog API and APP keys" @@ -19,6 +24,6 @@ output "datadog_api_key_location" { } output "datadog_site" { - value = var.datadog_site_url + value = local.datadog_site description = "The Datadog site to use" } diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index 9f6815aeb..a8e26b809 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -2,9 +2,6 @@ This component is responsible for provisioning Datadog AWS integrations. -It's required that the DataDog API and APP secret keys are available in the `var.datadog_secrets_source_store_account` account -in AWS SSM Parameter Store at the `/datadog/%v/datadog_app_key` paths (where `%v` are the corresponding account names). - See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. ## Usage @@ -44,6 +41,7 @@ No providers. | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [datadog\_integration](#module\_datadog\_integration) | cloudposse/datadog-integration/aws | 1.0.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.9.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/datadog-integration/main.tf b/modules/datadog-integration/main.tf index 160deceb9..1c4e572f1 100644 --- a/modules/datadog-integration/main.tf +++ b/modules/datadog-integration/main.tf @@ -20,7 +20,33 @@ locals { # Get the context tags and skip tags that we don't want applied to every resource. # i.e. we don't want name since each metric would be called something other than this component's name. # i.e. we don't want environment since each metric would come from gbl or a region and this component is deployed in gbl. - context_tags = [for k, v in module.this.tags : "${lower(k)}:${v}" if contains(var.context_host_and_filter_tags, lower(k))] - filter_tags = distinct(concat(var.filter_tags, local.context_tags)) - host_tags = distinct(concat(var.host_tags, local.context_tags)) + context_tags = [ + for k, v in module.this.tags : "${lower(k)}:${v}" if contains(var.context_host_and_filter_tags, lower(k)) + ] + filter_tags = distinct(concat(var.filter_tags, local.context_tags)) + host_tags = distinct(concat(var.host_tags, local.context_tags)) +} + +module "store_write" { + count = local.enabled ? 1 : 0 + source = "cloudposse/ssm-parameter-store/aws" + version = "0.9.1" + parameter_write = [ + { + name = "/datadog/datadog_external_id" + value = join("", module.datadog_integration[*].datadog_external_id) + type = "String" + overwrite = "true" + description = "External identifier for our dd integration" + }, + { + name = "/datadog/aws_role_name" + value = join("", module.datadog_integration[*].aws_role_name) + type = "String" + overwrite = "true" + description = "Name of the AWS IAM role used by our dd integration" + } + ] + + context = module.this.context } diff --git a/modules/datadog-integration/variables.tf b/modules/datadog-integration/variables.tf index 4783b255c..806d68b4d 100644 --- a/modules/datadog-integration/variables.tf +++ b/modules/datadog-integration/variables.tf @@ -44,3 +44,4 @@ variable "context_host_and_filter_tags" { description = "Automatically add host and filter tags for these context keys" default = ["namespace", "tenant", "stage"] } + diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index bd2e5281d..0b3a5d91a 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -41,9 +41,6 @@ components: transfer-sftp: name: "/aws/transfer/s-xxxxxxxxxxxx" filter_pattern: "" - dd_api_key_source: - resource: "ssm" - identifier: "datadog/datadog_api_key" ``` @@ -67,7 +64,7 @@ components: |------|--------|---------| | [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.0.0 | +| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.1.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [log\_group\_prefix](#module\_log\_group\_prefix) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -93,7 +90,6 @@ components: | [context\_tags\_enabled](#input\_context\_tags\_enabled) | Whether to add context tags to add to each monitor | `bool` | `true` | no | | [datadog\_forwarder\_lambda\_environment\_variables](#input\_datadog\_forwarder\_lambda\_environment\_variables) | Map of environment variables to pass to the Lambda Function | `map(string)` | `{}` | no | | [dd\_api\_key\_kms\_ciphertext\_blob](#input\_dd\_api\_key\_kms\_ciphertext\_blob) | CiphertextBlob stored in environment variable DD\_KMS\_API\_KEY used by the lambda function, along with the KMS key, to decrypt Datadog API key | `string` | `""` | no | -| [dd\_api\_key\_source](#input\_dd\_api\_key\_source) | One of: ARN for AWS Secrets Manager (asm) to retrieve the Datadog (DD) api key, ARN for the KMS (kms) key used to decrypt the ciphertext\_blob of the api key, or the name of the SSM (ssm) parameter used to retrieve the Datadog API key |
object({
resource = string
identifier = string
})
|
{
"identifier": "",
"resource": ""
}
| no | | [dd\_artifact\_filename](#input\_dd\_artifact\_filename) | The Datadog artifact filename minus extension | `string` | `"aws-dd-forwarder"` | no | | [dd\_forwarder\_version](#input\_dd\_forwarder\_version) | Version tag of Datadog lambdas to use. https://github.com/DataDog/datadog-serverless-functions/releases | `string` | `"3.61.0"` | no | | [dd\_module\_name](#input\_dd\_module\_name) | The Datadog GitHub repository name | `string` | `"datadog-serverless-functions"` | no | diff --git a/modules/datadog-lambda-forwarder/main.tf b/modules/datadog-lambda-forwarder/main.tf index 3f25f4442..7ef6c30f5 100644 --- a/modules/datadog-lambda-forwarder/main.tf +++ b/modules/datadog-lambda-forwarder/main.tf @@ -40,11 +40,14 @@ module "log_group_prefix" { module "datadog_lambda_forwarder" { source = "cloudposse/datadog-lambda-forwarder/aws" - version = "1.0.0" + version = "1.1.0" - cloudwatch_forwarder_log_groups = local.cloudwatch_forwarder_log_groups - dd_api_key_kms_ciphertext_blob = var.dd_api_key_kms_ciphertext_blob - dd_api_key_source = var.dd_api_key_source + cloudwatch_forwarder_log_groups = local.cloudwatch_forwarder_log_groups + dd_api_key_kms_ciphertext_blob = var.dd_api_key_kms_ciphertext_blob + dd_api_key_source = { + resource = lower(module.datadog_configuration.datadog_secrets_store_type) + identifier = module.datadog_configuration.datadog_api_key_location + } dd_artifact_filename = var.dd_artifact_filename dd_forwarder_version = var.dd_forwarder_version dd_module_name = var.dd_module_name @@ -76,6 +79,8 @@ module "datadog_lambda_forwarder" { datadog_forwarder_lambda_environment_variables = var.datadog_forwarder_lambda_environment_variables + api_key_ssm_arn = module.datadog_configuration.api_key_ssm_arn + context = module.this.context } diff --git a/modules/datadog-lambda-forwarder/variables.tf b/modules/datadog-lambda-forwarder/variables.tf index 95b0764f5..2e5d3be3d 100644 --- a/modules/datadog-lambda-forwarder/variables.tf +++ b/modules/datadog-lambda-forwarder/variables.tf @@ -34,43 +34,6 @@ variable "tracing_config_mode" { default = "PassThrough" } -variable "dd_api_key_source" { - description = "One of: ARN for AWS Secrets Manager (asm) to retrieve the Datadog (DD) api key, ARN for the KMS (kms) key used to decrypt the ciphertext_blob of the api key, or the name of the SSM (ssm) parameter used to retrieve the Datadog API key" - type = object({ - resource = string - identifier = string - }) - - default = { - resource = "" - identifier = "" - } - - # Resource can be one of kms, asm, ssm ("" to disable all lambda resources) - validation { - condition = can(regex("(kms|asm|ssm)", var.dd_api_key_source.resource)) || var.dd_api_key_source.resource == "" - error_message = "Provide one, and only one, ARN for (kms, asm) or name (ssm) to retrieve or decrypt Datadog api key." - } - - # Check KMS ARN format - validation { - condition = var.dd_api_key_source.resource == "kms" ? can(regex("arn:.*:kms:.*:key/.*", var.dd_api_key_source.identifier)) : true - error_message = "ARN for KMS key does not appear to be valid format (example: arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab)." - } - - # Check ASM ARN format - validation { - condition = var.dd_api_key_source.resource == "asm" ? can(regex("arn:.*:secretsmanager:.*:secret:.*", var.dd_api_key_source.identifier)) : true - error_message = "ARN for AWS Secrets Manager (asm) does not appear to be valid format (example: arn:aws:secretsmanager:us-west-2:111122223333:secret:aes128-1a2b3c)." - } - - # Check SSM name format - validation { - condition = var.dd_api_key_source.resource == "ssm" ? can(regex("^[a-zA-Z0-9_./-]+$", var.dd_api_key_source.identifier)) : true - error_message = "Name for SSM parameter does not appear to be valid format, acceptable characters are `a-zA-Z0-9_.-` and `/` to delineate hierarchies." - } -} - variable "dd_api_key_kms_ciphertext_blob" { type = string description = "CiphertextBlob stored in environment variable DD_KMS_API_KEY used by the lambda function, along with the KMS key, to decrypt Datadog API key" @@ -240,7 +203,7 @@ variable "lambda_arn_enabled" { curl -X GET "${DD_API_URL}/api/v1/integration/aws/logs/services" \ -H "Accept: application/json" \ -H "DD-API-KEY: ${DD_API_KEY}" \ --H "DD-APPLICATION-KEY: ${DD_APP_KEY}" +-H "DD-APPLICATION-KEY: ${DD_APP_KEY}" | jq '.[] | .id' **/ variable "log_collection_services" { type = list(string) diff --git a/modules/datadog-logs-archive/README.md b/modules/datadog-logs-archive/README.md new file mode 100644 index 000000000..293f6e132 --- /dev/null +++ b/modules/datadog-logs-archive/README.md @@ -0,0 +1,125 @@ +# Component: `datadog-logs-archive` + +This component is responsible for provisioning Datadog Log Archives. It creates a single log archive pipeline for each AWS account. If the `catchall` flag is set, it creates a catchall archive within the same S3 bucket. + +Each log archive filters for the tag `env:$env` where $env is the environment/account name (ie sbx, prd, tools, etc), as well as any tags identified in the additional_tags key. The `catchall` archive, as the name implies, filters for '*'. + +A second bucket is created for cloudtrail, and a cloudtrail is configured to monitor the log archive bucket and log activity to the cloudtrail bucket. To forward these cloudtrail logs to datadog, the cloudtrail bucket's id must be added to the s3_buckets key for our datadog-lambda-forwarder component. + +Both buckets support object lock, with overrideable defaults of COMPLIANCE mode with a duration of 7 days. + +## Prerequisites + +* Datadog integration set up in target environment + * We rely on the datadog api and app keys added by our datadog integration component + +## Issues, Gotchas, Good-to-Knows + +### Destroy/reprovision process + +Because of the protections for S3 buckets, if we want to destroy/replace our bucket, we need to do so in two passes or destroy the bucket manually and then use terraform to clean up the rest. If reprovisioning a recently provisioned bucket, the two-pass process works well. If the bucket has a full day or more of logs, though, deleting it manually first will avoid terraform timeouts, and then the terraform process can be used to clean up everything else. + +#### Two step process to destroy via terraform +* first set `s3_force_destroy` var to true and apply +* next set `enabled` to false and apply or use tf destroy + + +## Usage + +**Stack Level**: Global + +Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts from which Datadog receives logs. + +```yaml +components: + terraform: + datadog-logs-archive: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + additional_query_tags: + - "forwardername:tzl-*-dev-datadog-lambda-forwarder-logs" + - "account:852653222113" + +``` + +## Requirements + +| Name | Version | +|------|---------| +| terraform | >= 0.13.0 | +| aws | >= 2.0 | +| datadog | >= 3.3.0 | +| local | >= 1.3 | + +## Providers + +| Name | Version | +|------|---------| +| aws | >= 2.0 | +| datadog | >= 3.7.0 | +| http | >= 2.1.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| cloudtrail | cloudposse/cloudtrail/aws | 0.21.0 | +| cloudtrail_s3_bucket | cloudposse/cloudtrail-s3-bucket/aws | 0.23.1 | +| iam_roles | ../account-map/modules/iam-roles | n/a | +| s3_bucket | cloudposse/s3-bucket/aws | 0.46.0 | +| this | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| aws_caller_identity.current | data source | +| aws_partition.current | data source | +| aws_ssm_parameter.datadog_api_key | data source | +| aws_ssm_parameter.datadog_app_key | data source | +| aws_ssm_parameter.datadog_aws_role_name | data source | +| aws_ssm_parameter.datadog_external_id | data source | +| datadog_logs_archive.catchall_archive | resource | +| datadog_logs_archive.logs_archive | resource | +| http.current_order | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|----------| +| additional_query_tags | Additional tags to include in query for logs for this archive | `list` | [] | no | +| catchall | Set to true to enable a catchall for logs unmatched by any queries. This should only be used in one environment/account | `bool` | false | no | +| datadog_aws_account_id | The AWS account ID Datadog's integration servers use for all integrations | `string` | 464622532012 | no | +| enable_glacier_transition | Enable/disable transition to glacier. Has no effect unless `lifecycle_rules_enabled` set to true | `bool` | true | no | +| glacier_transition_days | Number of days after which to transition objects to glacier storage | `number` | 365 | no | +| lifecycle_rules_enabled | Enable/disable lifecycle management rules for s3 objects | `bool` | true | no | +| object_lock_days_archive | Set duration of archive bucket object lock | `number` | 7 | yes | +| object_lock_days_cloudtrail | Set duration of cloudtrail bucket object lock | `number` | 7 | yes | +| object_lock_mode_archive | Set mode of archive bucket object lock | `string` | COMPLIANCE | yes | +| object_lock_mode_cloudtrail | Set mode of cloudtrail bucket object lock | `string` | COMPLIANCE | yes | +| s3_force_destroy | Set to true to delete non-empty buckets when `enabled` is set to false | `bool` | false | for destroy only | + + +## Outputs + +| Name | Description | +|------|-------------| +| archive_id | The ID of the environment-specific log archive | +| bucket_arn | The ARN of the bucket used for log archive storage | +| bucket_domain_name | The FQDN of the bucket used for log archive storage | +| bucket_id | The ID (name) of the bucket used for log archive storage | +| bucket_region | The region of the bucket used for log archive storage | +| cloudtrail_bucket_arn | The ARN of the bucket used for cloudtrail log storage | +| cloudtrail_bucket_domain_name | The FQDN of the bucket used for cloudtrail log storage | +| cloudtrail_bucket_id | The ID (name) of the bucket used for cloudtrail log storage | +| catchall_id | The ID of the catchall log archive | + +## References + +* [cloudposse/s3-bucket/aws](https://registry.terraform.io/modules/cloudposse/s3-bucket/aws/latest) - Cloud Posse's S3 component +* [datadog_logs_archive resource] (https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/logs_archive) - Datadog's provider documentation for the datadog_logs_archive resource + +[](https://cpco.io/component) diff --git a/modules/datadog-logs-archive/asm.tf b/modules/datadog-logs-archive/asm.tf new file mode 100644 index 000000000..a4e64b0b6 --- /dev/null +++ b/modules/datadog-logs-archive/asm.tf @@ -0,0 +1,19 @@ +data "aws_secretsmanager_secret" "datadog_api_key" { + count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 + name = var.datadog_api_secret_key +} + +data "aws_secretsmanager_secret_version" "datadog_api_key" { + count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 + secret_id = data.aws_secretsmanager_secret.datadog_api_key[0].id +} + +data "aws_secretsmanager_secret" "datadog_app_key" { + count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 + name = var.datadog_app_secret_key +} + +data "aws_secretsmanager_secret_version" "datadog_app_key" { + count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 + secret_id = data.aws_secretsmanager_secret.datadog_app_key[0].id +} diff --git a/modules/datadog-logs-archive/context.tf b/modules/datadog-logs-archive/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/datadog-logs-archive/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/datadog-logs-archive/main.tf b/modules/datadog-logs-archive/main.tf new file mode 100644 index 000000000..102dce9b0 --- /dev/null +++ b/modules/datadog-logs-archive/main.tf @@ -0,0 +1,352 @@ + +locals { + enabled = module.this.enabled + + aws_account_id = join("", data.aws_caller_identity.current.*.account_id) + aws_partition = join("", data.aws_partition.current.*.partition) + + datadog_aws_role_name = nonsensitive(join("", data.aws_ssm_parameter.datadog_aws_role_name.*.value)) + principal_names = [ + format("arn:${local.aws_partition}:iam::%s:role/${local.datadog_aws_role_name}", local.aws_account_id), + ] + + privileged_principal_arns = [ + { + (local.principal_names[0]) = [""] + } + ] + + non_catchall_ids = [for x in jsondecode(join("", data.http.current_order.*.body)).data : x.id if x.attributes.name != "catchall"] + catchall_id = [for x in jsondecode(join("", data.http.current_order.*.body)).data : x.id if x.attributes.name == "catchall"] + ordered_ids = concat(local.non_catchall_ids, local.catchall_id) + + policy = jsondecode(data.aws_iam_policy_document.default[0].json) +} + +# We use the http data source due to lack of a data source for datadog_logs_archive_order +# This fetches the current order from DD's api so we can shuffle it around if needed to +# keep the catchall in last place. +data "http" "current_order" { + count = local.enabled ? 1 : 0 + + url = "https://api.datadoghq.com/api/v2/logs/config/archives" + depends_on = [datadog_logs_archive.logs_archive, datadog_logs_archive.catchall_archive] + request_headers = { + Accept = "application/json", + DD-API-KEY = local.datadog_api_key, + DD-APPLICATION-KEY = local.datadog_app_key + } +} + +# IAM policy document to allow cloudtrail to read and write to the +# cloudtrail bucket + +data "aws_iam_policy_document" "default" { + count = module.this.enabled ? 1 : 0 + statement { + sid = "AWSCloudTrailAclCheck" + principals { + type = "Service" + identifiers = ["cloudtrail.amazonaws.com"] + } + + actions = [ + "s3:GetBucketAcl", + ] + + resources = [ + "arn:${local.aws_partition}:s3:::${module.this.id}-cloudtrail", + ] + } + + # We're using two AWSCloudTrailWrite statements with the only + # difference being the principals identifier to avoid a bug + # where TF frequently wants to reorder multiple principals + statement { + sid = "AWSCloudTrailWrite1" + principals { + type = "Service" + identifiers = ["cloudtrail.amazonaws.com"] + } + + actions = [ + "s3:PutObject", + ] + + resources = [ + "arn:${local.aws_partition}:s3:::${module.this.id}-cloudtrail/*", + ] + + condition { + test = "StringEquals" + variable = "s3:x-amz-acl" + values = [ + "bucket-owner-full-control", + ] + } + condition { + test = "StringLike" + variable = "aws:SourceArn" + values = [ + "arn:${local.aws_partition}:cloudtrail:*:${local.aws_account_id}:trail/*datadog-logs-archive", + ] + } + + } + + # We're using two AWSCloudTrailWrite statements with the only + # difference being the principals identifier to avoid a bug + # where TF frequently wants to reorder multiple principals + statement { + sid = "AWSCloudTrailWrite2" + principals { + type = "Service" + identifiers = ["config.amazonaws.com"] + } + + actions = [ + "s3:PutObject", + ] + + resources = [ + "arn:${local.aws_partition}:s3:::${module.this.id}-cloudtrail/*", + ] + + condition { + test = "StringEquals" + variable = "s3:x-amz-acl" + values = [ + "bucket-owner-full-control", + ] + } + condition { + test = "StringLike" + variable = "aws:SourceArn" + values = [ + "arn:${local.aws_partition}:cloudtrail:*:${local.aws_account_id}:trail/*datadog-logs-archive", + ] + } + + } +} + +module "bucket_policy" { + source = "cloudposse/iam-policy/aws" + version = "0.3.0" + + iam_policy_statements = lookup(local.policy, "Statement") + + context = module.this.context +} + +data "aws_ssm_parameter" "datadog_aws_role_name" { + name = "/datadog/aws_role_name" +} + +data "aws_caller_identity" "current" { + count = local.enabled ? 1 : 0 +} + +data "aws_partition" "current" { + count = local.enabled ? 1 : 0 +} + +module "archive_bucket" { + source = "cloudposse/s3-bucket/aws" + version = "2.0.1" + + count = local.enabled ? 1 : 0 + + acl = "private" + enabled = local.enabled + force_destroy = var.s3_force_destroy + + lifecycle_rules = [ + { + prefix = null + enabled = var.lifecycle_rules_enabled + tags = {} + + abort_incomplete_multipart_upload_days = null + enable_glacier_transition = var.enable_glacier_transition + glacier_transition_days = var.glacier_transition_days + noncurrent_version_glacier_transition_days = 30 + enable_deeparchive_transition = false + deeparchive_transition_days = 0 + noncurrent_version_deeparchive_transition_days = 0 + enable_standard_ia_transition = false + standard_transition_days = 0 + enable_current_object_expiration = false + expiration_days = 0 + enable_noncurrent_version_expiration = false + noncurrent_version_expiration_days = 0 + }, + ] + + privileged_principal_actions = [ + "s3:PutObject", + "s3:GetObject", + "s3:ListBucket", + ] + + privileged_principal_arns = local.privileged_principal_arns + + tags = { + managed-by = "terraform" + env = var.stage + service = "datadog-logs-archive" + part-of = "observability" + } + + user_enabled = false + versioning_enabled = true + + object_lock_configuration = { + mode = var.object_lock_mode_archive + days = var.object_lock_days_archive + years = null + } + + context = module.this.context +} + +module "cloudtrail_s3_bucket" { + source = "cloudposse/s3-bucket/aws" + version = "2.0.1" + + depends_on = [data.aws_iam_policy_document.default] + + count = local.enabled ? 1 : 0 + + name = "datadog-logs-archive-cloudtrail" + acl = "private" + enabled = local.enabled + force_destroy = var.s3_force_destroy + + source_policy_documents = data.aws_iam_policy_document.default.*.json + + lifecycle_rules = [ + { + prefix = null + enabled = var.lifecycle_rules_enabled + tags = {} + + abort_incomplete_multipart_upload_days = null + enable_glacier_transition = var.enable_glacier_transition + glacier_transition_days = 365 + noncurrent_version_glacier_transition_days = 365 + enable_deeparchive_transition = false + deeparchive_transition_days = 0 + noncurrent_version_deeparchive_transition_days = 0 + enable_standard_ia_transition = false + standard_transition_days = 0 + enable_current_object_expiration = false + expiration_days = 0 + enable_noncurrent_version_expiration = false + noncurrent_version_expiration_days = 0 + }, + ] + + tags = { + managed-by = "terraform" + env = var.stage + service = "datadog-logs-archive" + part-of = "observability" + } + + user_enabled = false + versioning_enabled = true + + label_key_case = "lower" + label_value_case = "lower" + + object_lock_configuration = { + mode = var.object_lock_mode_cloudtrail + days = var.object_lock_days_cloudtrail + years = null + } + + # Setting this to `true` causes permanent Terraform drift: terraform plan wants to create it, and then the next plan wants to destroy it. + # This happens b/c Terraform sees different MD5 hash of the request body + # https://stackoverflow.com/questions/66605497/terraform-always-says-changes-on-templatefile-for-s3-bucket-policy + # https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketPolicy.html#API_PutBucketPolicy_RequestSyntax + # https://hands-on.cloud/terraform-how-to-enforce-tls-https-for-aws-s3-bucket/ + # https://github.com/hashicorp/terraform/issues/4948 + # https://stackoverflow.com/questions/69986387/s3-bucket-terraform-plan-shows-inexistent-changes-on-default-values + # https://github.com/hashicorp/terraform/issues/5613 + allow_ssl_requests_only = false + + context = module.this.context +} + +module "cloudtrail" { + count = local.enabled ? 1 : 0 + # We explicitly declare this dependency on the entire + # cloudtrail_s3_bucket module because tf doesn't autodetect the + # dependency on the attachment of the bucket policy, leading to + # insufficient permissions issues on cloudtrail creation if it + # happens to be attempted prior to completion of the policy attachment. + depends_on = [module.cloudtrail_s3_bucket] + source = "cloudposse/cloudtrail/aws" + version = "0.21.0" + + enable_log_file_validation = true + include_global_service_events = false + is_multi_region_trail = false + enabled = local.enabled + enable_logging = true + s3_bucket_name = module.cloudtrail_s3_bucket[0].bucket_id + + event_selector = [ + { + include_management_events = true + read_write_type = "WriteOnly" + data_resource = [ + { + type = "AWS::S3::Object" + values = ["${module.archive_bucket[0].bucket_arn}/"] + } + ] + } + ] + + context = module.this.context +} + +resource "datadog_logs_archive_order" "archive_order" { + count = var.enabled ? 1 : 0 + archive_ids = local.ordered_ids +} + +resource "datadog_logs_archive" "logs_archive" { + count = local.enabled ? 1 : 0 + + name = var.stage + include_tags = true + rehydration_tags = ["rehydrated:true"] + query = join(" OR ", concat([join(":", ["env", var.stage]), join(":", ["account", local.aws_account_id])], var.additional_query_tags)) + + s3_archive { + bucket = module.archive_bucket[0].bucket_id + path = "/" + account_id = local.aws_account_id + role_name = local.datadog_aws_role_name + } +} + +resource "datadog_logs_archive" "catchall_archive" { + count = local.enabled && var.catchall_enabled ? 1 : 0 + + depends_on = [datadog_logs_archive.logs_archive] + name = "catchall" + include_tags = true + rehydration_tags = ["rehydrated:true"] + query = "*" + + s3_archive { + bucket = module.archive_bucket[0].bucket_id + path = "/catchall" + account_id = local.aws_account_id + role_name = local.datadog_aws_role_name + } +} diff --git a/modules/datadog-logs-archive/outputs.tf b/modules/datadog-logs-archive/outputs.tf new file mode 100644 index 000000000..ca2157ddf --- /dev/null +++ b/modules/datadog-logs-archive/outputs.tf @@ -0,0 +1,44 @@ +output "bucket_arn" { + value = local.enabled ? module.archive_bucket[0].bucket_arn : "" + description = "The ARN of the bucket used for log archive storage" +} + +output "bucket_domain_name" { + value = local.enabled ? module.archive_bucket[0].bucket_domain_name : "" + description = "The FQDN of the bucket used for log archive storage" +} + +output "bucket_id" { + value = local.enabled ? module.archive_bucket[0].bucket_id : "" + description = "The ID (name) of the bucket used for log archive storage" +} + +output "bucket_region" { + value = local.enabled ? module.archive_bucket[0].bucket_region : "" + description = "The region of the bucket used for log archive storage" +} + +output "cloudtrail_bucket_arn" { + value = local.enabled ? module.cloudtrail_s3_bucket[0].bucket_arn : "" + description = "The ARN of the bucket used for access logging via cloudtrail" +} + +output "cloudtrail_bucket_domain_name" { + value = local.enabled ? module.cloudtrail_s3_bucket[0].bucket_domain_name : "" + description = "The FQDN of the bucket used for access logging via cloudtrail" +} + +output "cloudtrail_bucket_id" { + value = local.enabled ? module.cloudtrail_s3_bucket[0].bucket_id : "" + description = "The ID (name) of the bucket used for access logging via cloudtrail" +} + +output "archive_id" { + value = local.enabled ? datadog_logs_archive.logs_archive[0].id : "" + description = "The ID of the environment-specific log archive" +} + +output "catchall_id" { + value = local.enabled && var.catchall_enabled ? datadog_logs_archive.catchall_archive[0].id : "" + description = "The ID of the catchall log archive" +} diff --git a/modules/datadog-logs-archive/provider-datadog.tf b/modules/datadog-logs-archive/provider-datadog.tf new file mode 100644 index 000000000..886e23a30 --- /dev/null +++ b/modules/datadog-logs-archive/provider-datadog.tf @@ -0,0 +1,10 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +locals { + datadog_api_key = module.datadog_configuration.datadog_api_key + datadog_app_key = module.datadog_configuration.datadog_app_key +} diff --git a/modules/datadog-logs-archive/providers.tf b/modules/datadog-logs-archive/providers.tf new file mode 100755 index 000000000..24cf03434 --- /dev/null +++ b/modules/datadog-logs-archive/providers.tf @@ -0,0 +1,35 @@ +provider "datadog" { + api_key = local.datadog_api_key + app_key = local.datadog_app_key + validate = local.enabled +} + +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/datadog-logs-archive/ssm.tf b/modules/datadog-logs-archive/ssm.tf new file mode 100644 index 000000000..a78029cb9 --- /dev/null +++ b/modules/datadog-logs-archive/ssm.tf @@ -0,0 +1,11 @@ +data "aws_ssm_parameter" "datadog_api_key" { + count = local.enabled && var.secrets_store_type == "SSM" ? 1 : 0 + name = format("/%s", var.datadog_api_secret_key) + with_decryption = true +} + +data "aws_ssm_parameter" "datadog_app_key" { + count = local.enabled && var.secrets_store_type == "SSM" ? 1 : 0 + name = format("/%s", var.datadog_app_secret_key) + with_decryption = true +} diff --git a/modules/datadog-logs-archive/variables.tf b/modules/datadog-logs-archive/variables.tf new file mode 100644 index 000000000..60148a363 --- /dev/null +++ b/modules/datadog-logs-archive/variables.tf @@ -0,0 +1,83 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "additional_query_tags" { + type = list(any) + description = "Additional tags to be used in the query for this archive" + default = [] +} + + +variable "secrets_store_type" { + type = string + description = "Secret store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" + default = "SSM" +} + +variable "datadog_api_secret_key" { + type = string + description = "The key of the Datadog API secret" + default = "datadog/datadog_api_key" +} + +variable "datadog_app_secret_key" { + type = string + description = "The key of the Datadog Application secret" + default = "datadog/datadog_app_key" +} + +variable "catchall_enabled" { + type = bool + description = "Set to true to enable a catchall for logs unmatched by any queries. This should only be used in one environment/account" + default = false +} + +variable "lifecycle_rules_enabled" { + type = bool + description = "Enable/disable lifecycle management rules for log archive s3 objects" + default = true +} + +variable "enable_glacier_transition" { + type = bool + description = "Enable/disable transition to glacier for log archive bucket. Has no effect unless lifecycle_rules_enabled set to true" + default = true +} + +variable "glacier_transition_days" { + type = number + description = "Number of days after which to transition objects to glacier storage in log archive bucket" + default = 365 +} + +variable "object_lock_days_archive" { + type = number + description = "Object lock duration for archive buckets in days" + default = 7 +} + +variable "object_lock_days_cloudtrail" { + type = number + description = "Object lock duration for cloudtrail buckets in days" + default = 7 +} + +variable "object_lock_mode_archive" { + type = string + description = "Object lock mode for archive bucket. Possible values are COMPLIANCE or GOVERNANCE" + default = "COMPLIANCE" +} + +variable "object_lock_mode_cloudtrail" { + type = string + description = "Object lock mode for cloudtrail bucket. Possible values are COMPLIANCE or GOVERNANCE" + default = "COMPLIANCE" +} + +variable "s3_force_destroy" { + type = bool + description = "Set to true to delete non-empty buckets when enabled is set to false" + default = false +} diff --git a/modules/datadog-logs-archive/versions.tf b/modules/datadog-logs-archive/versions.tf new file mode 100644 index 000000000..4a385964f --- /dev/null +++ b/modules/datadog-logs-archive/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 0.13.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + datadog = { + source = "datadog/datadog" + version = ">= 3.3.0" + } + http = { + source = "hashicorp/http" + version = ">= 2.1.0" + } + } +} diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 784cdb977..785243127 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -1,4 +1,4 @@ -x# Component: `datadog-monitor` +# Component: `datadog-monitor` This component is responsible for provisioning Datadog monitors and assigning Datadog roles to the monitors. @@ -74,7 +74,6 @@ No resources. | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [datadog\_api\_secret\_key](#input\_datadog\_api\_secret\_key) | The key of the Datadog API secret | `string` | `"datadog/datadog_api_key"` | no | -| [datadog\_api\_url](#input\_datadog\_api\_url) | The Datadog API URL | `string` | `null` | no | | [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The key of the Datadog Application secret | `string` | `"datadog/datadog_app_key"` | no | | [datadog\_monitor\_context\_tags](#input\_datadog\_monitor\_context\_tags) | List of context tags to add to each monitor | `set(string)` |
[
"namespace",
"tenant",
"environment",
"stage"
]
| no | | [datadog\_monitor\_context\_tags\_enabled](#input\_datadog\_monitor\_context\_tags\_enabled) | Whether to add context tags to each monitor | `bool` | `true` | no | diff --git a/modules/datadog-monitor/variables.tf b/modules/datadog-monitor/variables.tf index 2efba3fff..afbdad7cc 100644 --- a/modules/datadog-monitor/variables.tf +++ b/modules/datadog-monitor/variables.tf @@ -104,14 +104,3 @@ variable "message_postfix" { description = "Additional information to put after each monitor message" default = "" } - -variable "datadog_api_url" { - type = string - description = "The Datadog API URL" - default = null - - validation { - condition = var.datadog_api_url == null ? true : contains(["https://api.datadoghq.com/", "https://api.us3.datadoghq.com/", "https://api.us5.datadoghq.com/", "https://api.datadoghq.eu/", "https://api.ddog-gov.com/"], var.datadog_api_url) - error_message = "Allowed values: null, `https://api.datadoghq.com/`, `https://api.us3.datadoghq.com/`, `https://api.us5.datadoghq.com/`, `https://api.datadoghq.eu/`, `https://api.ddog-gov.com/`." - } -} diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md new file mode 100644 index 000000000..8507a1544 --- /dev/null +++ b/modules/datadog-synthetics-private-location/README.md @@ -0,0 +1,223 @@ +# Component: `datadog-synthetics-private-location` + +This component provisions a Datadog synthetics private location on Datadog and a private location agent on EKS cluster. + +Private locations allow you to monitor internal-facing applications or any private URLs that are not accessible from the public internet. + +## Usage + +**Stack Level**: Regional + +Use this in the catalog or use these variables to overwrite the catalog values. + +```yaml +components: + terraform: + datadog-synthetics-private-location: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: "datadog-synthetics-private-location" + description: "Datadog Synthetics Private Location Agent" + kubernetes_namespace: "monitoring" + create_namespace: true + repository: "https://helm.datadoghq.com" + chart: "synthetics-private-location" + chart_version: "0.15.6" + timeout: 180 + wait: true + atomic: true + cleanup_on_fail: true +``` + +## Synthetics Private Location Config + +```shell +docker run --rm datadog/synthetics-private-location-worker --help +``` + +``` +The Datadog Synthetics Private Location Worker runs tests on privately accessible websites and brings results to Datadog + +Access keys: + --accessKey Access Key for Datadog API authentication [string] + --secretAccessKey Secret Access Key for Datadog API authentication [string] + --datadogApiKey Datadog API key to send browser tests artifacts (e.g. screenshots) [string] + --privateKey Private Key used to decrypt test configurations [array] + --publicKey Public Key used by Datadog to encrypt test results. Composed of --publicKey.pem and --publicKey.fingerprint + +Worker configuration: + --site Datadog site (datadoghq.com, us3.datadoghq.com, datadoghq.eu or ddog-gov.com) [string] [required] [default: "datadoghq.com"] + --concurrency Maximum number of tests executed in parallel [number] [default: 10] + --maxNumberMessagesToFetch Maximum number of tests that can be fetched at the same time [number] [default: 10] + --proxyDatadog Proxy URL used to send requests to Datadog [string] [default: none] + --dumpConfig Display non-secret worker configuration parameters [boolean] + --enableStatusProbes Enable the probes system for Kubernetes [boolean] [default: false] + --statusProbesPort The port for the probes server to listen on [number] [default: 8080] + --config Path to JSON config file [default: "/etc/datadog/synthetics-check-runner.json"] + +Tests configuration: + --maxTimeout Maximum test execution duration, in milliseconds [number] [default: 60000] + --proxyTestRequests Proxy URL used to send test requests [string] [default: none] + --proxyIgnoreSSLErrors Discard SSL errors when using a proxy [boolean] [default: false] + --dnsUseHost Use local DNS config for API tests and HTTP steps in browser tests (currently ["192.168.65.5"]) [boolean] [default: true] + --dnsServer DNS server IPs used in given order for API tests and HTTP steps in browser tests (--dnsServer="1.0.0.1" --dnsServer="9.9.9.9") and after local DNS config, if --dnsUseHost is present [array] [default: ["8.8.8.8","1.1.1.1"]] + +Network filtering: + --allowedIPRanges Grant access to IP ranges (has precedence over --blockedIPRanges) [default: none] + --blockedIPRanges Deny access to IP ranges (e.g. --blockedIPRanges.4="127.0.0.0/8" --blockedIPRanges.6="::1/128") [default: none] + --enableDefaultBlockedIpRanges Deny access to all reserved IP ranges, except for those explicitly set in --allowedIPRanges [boolean] [default: false] + --allowedDomainNames Grant access to domain names for API tests (has precedence over --blockedDomainNames, e.g. --allowedDomainNames="*.example.com") [array] [default: none] + --blockedDomainNames Deny access to domain names for API tests (e.g. --blockedDomainNames="example.org" --blockedDomainNames="*.com") [array] [default: none] + +Options: + --enableIPv6 Use IPv6 to perform tests. (Warning: IPv6 in Docker is only supported with Linux host) [boolean] [default: false] + --version Show version number [boolean] + -f, --logFormat Format log output [choices: "pretty", "pretty-compact", "json"] [default: "pretty"] + -h, --help Show help [boolean] + +Volumes: + /etc/datadog/certs/ .pem certificates present in this directory will be imported and trusted as certificate authorities for API and browser tests + +Environment variables: + Command options can also be set via environment variables (DATADOG_API_KEY="...", DATADOG_WORKER_CONCURRENCY="15", DATADOG_DNS_USE_HOST="true") + For options that accept multiple arguments, JSON string array notation should be used (DATADOG_TESTS_DNS_SERVER='["8.8.8.8", "1.1.1.1"]') + + Supported environment variables: + DATADOG_ACCESS_KEY, + DATADOG_API_KEY, + DATADOG_PRIVATE_KEY, + DATADOG_PUBLIC_KEY_FINGERPRINT, + DATADOG_PUBLIC_KEY_PEM, + DATADOG_SECRET_ACCESS_KEY, + DATADOG_SITE, + DATADOG_WORKER_CONCURRENCY, + DATADOG_WORKER_LOG_FORMAT, + DATADOG_WORKER_MAX_NUMBER_MESSAGES_TO_FETCH, + DATADOG_WORKER_PROXY, + DATADOG_TESTS_DNS_SERVER, + DATADOG_TESTS_DNS_USE_HOST, + DATADOG_TESTS_PROXY, + DATADOG_TESTS_PROXY_IGNORE_SSL_ERRORS, + DATADOG_TESTS_TIMEOUT, + DATADOG_ALLOWED_IP_RANGES_4, + DATADOG_ALLOWED_IP_RANGES_6, + DATADOG_BLOCKED_IP_RANGES_4, + DATADOG_BLOCKED_IP_RANGES_6, + DATADOG_ENABLE_DEFAULT_WINDOWS_FIREWALL_RULES, + DATADOG_ALLOWED_DOMAIN_NAMES, + DATADOG_BLOCKED_DOMAIN_NAMES, + DATADOG_WORKER_ENABLE_STATUS_PROBES, + DATADOG_WORKER_STATUS_PROBES_PORT +``` + +## References + +* https://docs.datadoghq.com/synthetics/private_locations +* https://docs.datadoghq.com/synthetics/private_locations/configuration/ +* https://github.com/DataDog/helm-charts/tree/main/charts/synthetics-private-location +* https://github.com/DataDog/helm-charts/blob/main/charts/synthetics-private-location/values.yaml + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [datadog](#requirement\_datadog) | >= 3.3.0 | +| [helm](#requirement\_helm) | >= 2.3.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [local](#requirement\_local) | >= 1.3 | +| [template](#requirement\_template) | >= 2.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [datadog](#provider\_datadog) | >= 3.3.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/helm-release/aws | 0.7.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [datadog_synthetics_private_location.this](https://registry.terraform.io/providers/datadog/datadog/latest/docs/resources/synthetics_private_location) | resource | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended | `string` | n/a | yes | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed | `string` | `null` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input\_description) | Release description attribute (visible in the history) | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1alpha1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | Kubernetes namespace to install the release into | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [private\_location\_tags](#input\_private\_location\_tags) | List of static tags to associate with the synthetics private location | `set(string)` | `[]` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [repository](#input\_repository) | Repository URL where to locate the requested chart | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | +| [verify](#input\_verify) | Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart | `bool` | `false` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true` | `bool` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [metadata](#output\_metadata) | Block status of the deployed release | +| [synthetics\_private\_location\_id](#output\_synthetics\_private\_location\_id) | Synthetics private location ID | + + +## References + +* https://docs.datadoghq.com/getting_started/synthetics/private_location +* https://docs.datadoghq.com/synthetics/private_locations/configuration +* https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_private_location +* https://github.com/DataDog/helm-charts/tree/main/charts/synthetics-private-location diff --git a/modules/datadog-synthetics-private-location/context.tf b/modules/datadog-synthetics-private-location/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/datadog-synthetics-private-location/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/datadog-synthetics-private-location/helm-variables.tf b/modules/datadog-synthetics-private-location/helm-variables.tf new file mode 100644 index 000000000..fade04b95 --- /dev/null +++ b/modules/datadog-synthetics-private-location/helm-variables.tf @@ -0,0 +1,63 @@ +variable "description" { + type = string + description = "Release description attribute (visible in the history)" + default = null +} + +variable "chart" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended" +} + +variable "repository" { + type = string + description = "Repository URL where to locate the requested chart" + default = null +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed" + default = null +} + +variable "kubernetes_namespace" { + type = string + description = "Kubernetes namespace to install the release into" +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = null +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails" + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used" + default = true +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`" + default = null +} + +variable "create_namespace" { + type = bool + description = "Create the Kubernetes namespace if it does not yet exist" + default = true +} + +variable "verify" { + type = bool + description = "Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart" + default = false +} diff --git a/modules/datadog-synthetics-private-location/main.tf b/modules/datadog-synthetics-private-location/main.tf new file mode 100644 index 000000000..6cd94dd64 --- /dev/null +++ b/modules/datadog-synthetics-private-location/main.tf @@ -0,0 +1,58 @@ +locals { + enabled = module.this.enabled + + # https://docs.datadoghq.com/synthetics/private_locations/configuration + # docker run --rm datadog/synthetics-private-location-worker --help + private_location_config = jsondecode(join("", datadog_synthetics_private_location.this.*.config)) +} + +resource "datadog_synthetics_private_location" "this" { + count = local.enabled ? 1 : 0 + name = module.this.id + description = module.this.id + tags = var.private_location_tags +} + + +module "datadog_synthetics_private_location" { + source = "cloudposse/helm-release/aws" + version = "0.7.0" + + name = module.this.name + chart = var.chart + description = var.description + repository = var.repository + chart_version = var.chart_version + namespace = var.kubernetes_namespace + create_namespace = var.create_namespace + verify = var.verify + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer + + service_account_name = module.this.name + service_account_namespace = var.kubernetes_namespace + + iam_role_enabled = false + + values = [ + templatefile( + "${path.module}/values.yaml.tpl", + { + id = local.private_location_config.id, + datadogApiKey = module.datadog_configuration.datadog_api_key, + accessKey = local.private_location_config.accessKey, + secretAccessKey = local.private_location_config.secretAccessKey, + privateKey = replace(local.private_location_config.privateKey, "\n", "\n "), + publicKey_pem = replace(local.private_location_config.publicKey.pem, "\n", "\n "), + publicKey_fingerprint = local.private_location_config.publicKey.fingerprint, + site = local.private_location_config.site + } + ) + ] + + context = module.this.context +} diff --git a/modules/datadog-synthetics-private-location/outputs.tf b/modules/datadog-synthetics-private-location/outputs.tf new file mode 100644 index 000000000..004d37a52 --- /dev/null +++ b/modules/datadog-synthetics-private-location/outputs.tf @@ -0,0 +1,9 @@ +output "synthetics_private_location_id" { + value = join("", datadog_synthetics_private_location.this.*.id) + description = "Synthetics private location ID" +} + +output "metadata" { + value = local.enabled ? module.datadog_synthetics_private_location.metadata : null + description = "Block status of the deployed release" +} diff --git a/modules/datadog-synthetics-private-location/provider-datadog.tf b/modules/datadog-synthetics-private-location/provider-datadog.tf new file mode 100644 index 000000000..8db220f1f --- /dev/null +++ b/modules/datadog-synthetics-private-location/provider-datadog.tf @@ -0,0 +1,12 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} diff --git a/modules/datadog-synthetics-private-location/provider-helm.tf b/modules/datadog-synthetics-private-location/provider-helm.tf new file mode 100644 index 000000000..d04bccf3d --- /dev/null +++ b/modules/datadog-synthetics-private-location/provider-helm.tf @@ -0,0 +1,158 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1alpha1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = true + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data + eks_cluster_id = module.eks.outputs.eks_cluster_id + eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/datadog-synthetics-private-location/providers.tf b/modules/datadog-synthetics-private-location/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/datadog-synthetics-private-location/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/datadog-synthetics-private-location/remote-state.tf b/modules/datadog-synthetics-private-location/remote-state.tf new file mode 100644 index 000000000..6ef90fd26 --- /dev/null +++ b/modules/datadog-synthetics-private-location/remote-state.tf @@ -0,0 +1,8 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" + + component = var.eks_component_name + + context = module.this.context +} diff --git a/modules/datadog-synthetics-private-location/values.yaml.tpl b/modules/datadog-synthetics-private-location/values.yaml.tpl new file mode 100644 index 000000000..d2b4c23e2 --- /dev/null +++ b/modules/datadog-synthetics-private-location/values.yaml.tpl @@ -0,0 +1,34 @@ +replicaCount: 1 + +podAnnotations: { } + +serviceAccount: + create: true + name: "datadog-synthetics-private-location" + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + +configFile: |- + { + "id": "${id}", + "datadogApiKey": "${datadogApiKey}", + "accessKey": "${accessKey}", + "secretAccessKey": "${secretAccessKey}", + "site": "${site}" + } + +env: + - name: DATADOG_PRIVATE_KEY + value: |- + ${privateKey} + - name: DATADOG_PUBLIC_KEY_PEM + value: |- + ${publicKey_pem} + - name: DATADOG_PUBLIC_KEY_FINGERPRINT + value: ${publicKey_fingerprint} diff --git a/modules/datadog-synthetics-private-location/variables.tf b/modules/datadog-synthetics-private-location/variables.tf new file mode 100644 index 000000000..ac1088260 --- /dev/null +++ b/modules/datadog-synthetics-private-location/variables.tf @@ -0,0 +1,16 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "private_location_tags" { + type = set(string) + description = "List of static tags to associate with the synthetics private location" + default = [] +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} diff --git a/modules/datadog-synthetics-private-location/versions.tf b/modules/datadog-synthetics-private-location/versions.tf new file mode 100755 index 000000000..7ded97311 --- /dev/null +++ b/modules/datadog-synthetics-private-location/versions.tf @@ -0,0 +1,30 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + template = { + source = "hashicorp/template" + version = ">= 2.0" + } + local = { + source = "hashicorp/local" + version = ">= 1.3" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.3.0" + } + datadog = { + source = "datadog/datadog" + version = ">= 3.3.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.14.0" + } + } +} From 2e4614cedfac2e38559dcf51637551bc25834c3f Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 10 Jan 2023 17:16:07 -0800 Subject: [PATCH 008/501] Upstream EKS Action Runner Controller (#528) --- .../eks/actions-runner-controller/README.md | 6 ++-- .../templates/horizontalrunnerautoscaler.yaml | 2 +- .../templates/runnerdeployment.yaml | 35 +++++++++++++++++++ .../charts/actions-runner/values.yaml | 2 ++ modules/eks/actions-runner-controller/main.tf | 2 ++ .../actions-runner-controller/variables.tf | 6 ++-- 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 620a883df..cd929bec0 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -165,7 +165,6 @@ After the webhook is created, select "edit" for the webhook and go to the "Recen (of a "ping" event) with a green check mark. If not, verify all the settings and consult the logs of the `actions-runner-controller-github-webhook-server` pod. -Useful Reference ### Updating CRDs @@ -180,6 +179,9 @@ If new CRDs are needed, install them manually via a command like kubectl create -f https://raw.githubusercontent.com/actions-runner-controller/actions-runner-controller/master/charts/actions-runner-controller/crds/actions.summerwind.dev_horizontalrunnerautoscalers.yaml ``` + +### Useful Reference + Consult [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) documentation for further details. @@ -264,7 +266,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, "")
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, "")
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, false)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, false)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | | [ssm\_github\_webhook\_secret\_token\_path](#input\_ssm\_github\_webhook\_secret\_token\_path) | The path in SSM to the GitHub Webhook Secret token. | `string` | `""` | no | diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml index b6db4c843..8e7979566 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml @@ -31,5 +31,5 @@ spec: - githubEvent: workflowJob: {} amount: 1 - duration: "{{ .Values.scale_down_delay_seconds }}s" + duration: "{{ .Values.webhook_startup_timeout }}" {{- end }} diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index 600364cab..e7512d07c 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -1,3 +1,24 @@ +{{- if .Values.pvc_enabled }} +--- +# Persistent Volumes can be used for image caching +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Values.release_name }} +spec: + accessModes: + - ReadWriteMany + # StorageClassName comes from efs-controller and must be deployed first. + storageClassName: efs-sc + resources: + requests: + # EFS is not actually storage constrained, but this storage request is + # required. 100Gi is a ballpark for how much we initially request, but this + # may grow. We are responsible for docker pruning this periodically to + # save space. + storage: 100Gi +{{- end }} +--- apiVersion: actions.summerwind.dev/v1alpha1 kind: RunnerDeployment metadata: @@ -56,7 +77,15 @@ spec: dockerVolumeMounts: - mountPath: /var/lib/docker name: docker-volume + {{- end }} + {{- if .Values.pvc_enabled }} + volumeMounts: + - mountPath: /home/runner/work/shared + name: shared-volume + {{- end }} + {{- if or (and .Values.dind_enabled .Values.storage) (.Values.pvc_enabled) }} volumes: + {{- if and .Values.dind_enabled .Values.storage }} - name: docker-volume ephemeral: volumeClaimTemplate: @@ -66,3 +95,9 @@ spec: requests: storage: {{ .Values.storage }} {{- end }} + {{- if .Values.pvc_enabled }} + - name: shared-volume + persistentVolumeClaim: + claimName: {{ .Values.release_name }} + {{- end }} + {{- end }} diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml index ad87705be..7f22f82ff 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml @@ -20,7 +20,9 @@ resources: cpu: 0.5 memory: 1Gi storage: "10Gi" +pvc_enabled: false webhook_driven_scaling_enabled: false +webhook_startup_timeout: "30m" pull_driven_scaling_enabled: false labels: - "Ubuntu" diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index cfb6d6309..f3003a392 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -216,7 +216,9 @@ module "actions_runner" { min_replicas = each.value.min_replicas max_replicas = each.value.max_replicas webhook_driven_scaling_enabled = each.value.webhook_driven_scaling_enabled + webhook_startup_timeout = try(each.value.webhook_startup_timeout, "${each.value.scale_down_delay_seconds}s") # if webhook_startup_timeout isnt defined, use scale_down_delay_seconds pull_driven_scaling_enabled = each.value.pull_driven_scaling_enabled + pvc_enabled = each.value.pvc_enabled }), local.busy_metrics_filtered[each.key] == null ? "" : yamlencode(local.busy_metrics_filtered[each.key]), ]) diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index 50bef3f50..c05962d98 100644 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -178,14 +178,16 @@ variable "runners" { scale_down_factor = optional(string) })) webhook_driven_scaling_enabled = bool + webhook_startup_timeout = optional(string, null) pull_driven_scaling_enabled = bool labels = list(string) - storage = optional(string, "") + storage = optional(string, false) + pvc_enabled = optional(string, false) resources = object({ limits = object({ cpu = string memory = string - ephemeral_storage = optional(string, "") + ephemeral_storage = optional(string, false) }) requests = object({ cpu = string From 6eafa147c73f812800da34927c3b80664afd7697 Mon Sep 17 00:00:00 2001 From: nitrocode <7775707+nitrocode@users.noreply.github.com> Date: Wed, 11 Jan 2023 19:12:16 -0500 Subject: [PATCH 009/501] Bump spacelift to latest (#532) --- .gitignore | 2 ++ modules/spacelift/README.md | 49 ++++++++++++++++++++++++---------- modules/spacelift/main.tf | 6 ++++- modules/spacelift/variables.tf | 21 ++++++++++----- modules/spacelift/versions.tf | 8 ++++-- 5 files changed, 62 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 23c3bd466..9dae6cd4b 100644 --- a/.gitignore +++ b/.gitignore @@ -189,3 +189,5 @@ dmypy.json # Cython debug symbols cython_debug/ + +*.backup diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index b452e329c..e00854e72 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -16,7 +16,7 @@ the stack can manage stacks in any region, it should be provisioned in the same ```yaml components: terraform: - spacelift-defaults: + spacelift/defaults: metadata: type: abstract component: spacelift @@ -29,16 +29,22 @@ components: - spacelift-configure - spacelift-write-vars - spacelift-tf-workspace + before_plan: + - spacelift-configure + before_apply: + - spacelift-configure component_root: components/terraform/spacelift - description: Spacelift Administrative stack for the organization. + description: Spacelift Administrative stack stack_destructor_enabled: false - worker_pool_name: WORKER_POOL_NAME # TODO: replace with the name of the worker pool + # TODO: replace with the name of the worker pool + worker_pool_name: WORKER_POOL_NAME repository: infra branch: main labels: - folder:admin - policies_by_id_enabled: - - global-administrative-trigger-policy + # Do not add normal set of child policies to admin stacks + policies_enabled: [] + policies_by_id_enabled: [] vars: enabled: true spacelift_api_endpoint: https://TODO.app.spacelift.io @@ -85,9 +91,9 @@ components: worker_pool_name_id_map: -spacelift-worker-pool: SOMEWORKERPOOLID #TODO: replace with your worker pool ID infracost_enabled: false # TODO: decide on infracost - terraform_version: "1.3.5" + terraform_version: "1.3.6" terraform_version_map: - "1": 1.3.5 + "1": "1.3.6" # These could be moved to $PROJECT_ROOT/.spacelift/config.yml before_init: @@ -104,7 +110,7 @@ components: metadata: component: spacelift inherits: - - spacelift-defaults + - spacelift/defaults settings: spacelift: policies_by_id_enabled: @@ -113,6 +119,19 @@ components: - trigger-administrative-policy vars: enabled: true + # Use context_filters to split up admin stack management + # context_filters: + # stages: + # - artifacts + # - audit + # - auto + # - corp + # - dns + # - identity + # - marketplace + # - network + # - public + # - security # These are the policies created from https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies # Make sure to remove the .rego suffix policies_available: @@ -312,21 +331,22 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | -| [spacelift](#requirement\_spacelift) | ~> 0.1.31 | +| [terraform](#requirement\_terraform) | >= 1.3 | +| [aws](#requirement\_aws) | >= 4.0 | +| [spacelift](#requirement\_spacelift) | >= 0.1.31 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | n/a | +| [aws](#provider\_aws) | >= 4.0 | ## Modules | Name | Source | Version | |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.50.2 | +| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.51.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -341,10 +361,12 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [administrative\_push\_policy\_enabled](#input\_administrative\_push\_policy\_enabled) | Flag to enable/disable the global administrative push policy | `bool` | `true` | no | | [administrative\_stack\_drift\_detection\_enabled](#input\_administrative\_stack\_drift\_detection\_enabled) | Flag to enable/disable administrative stack drift detection | `bool` | `true` | no | | [administrative\_stack\_drift\_detection\_reconcile](#input\_administrative\_stack\_drift\_detection\_reconcile) | Flag to enable/disable administrative stack drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | | [administrative\_stack\_drift\_detection\_schedule](#input\_administrative\_stack\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the administrative stack | `list(string)` |
[
"0 4 * * *"
]
| no | | [administrative\_trigger\_policy\_enabled](#input\_administrative\_trigger\_policy\_enabled) | Flag to enable/disable the global administrative trigger policy | `bool` | `true` | no | +| [attachment\_space\_id](#input\_attachment\_space\_id) | Specify the space ID for attachments (e.g. policies, contexts, etc.) | `string` | `"legacy"` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [autodeploy](#input\_autodeploy) | Default autodeploy value for all stacks created by this project | `bool` | n/a | yes | | [aws\_role\_arn](#input\_aws\_role\_arn) | ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment | `string` | `null` | no | @@ -380,14 +402,13 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of custom policy names to attach to all Spacelift stacks. These policies must exist in `components/terraform/spacelift/rego-policies` | `list(string)` | `[]` | no | | [policies\_enabled](#input\_policies\_enabled) | DEPRECATED: Use `policies_by_id_enabled` instead. List of default policies created by this stack to attach to all Spacelift stacks | `list(string)` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input\_region) | AWS Region | `string` | n/a | yes | | [runner\_image](#input\_runner\_image) | Full address & tag of the Spacelift runner image (e.g. on ECR) | `string` | n/a | yes | -| [space\_id](#input\_space\_id) | Place the stack(s) in the specified space\_id. | `string` | `"legacy"` | no | | [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | | [spacelift\_component\_path](#input\_spacelift\_component\_path) | The Spacelift Component Path | `string` | `"components/terraform"` | no | | [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | | [stack\_config\_path\_template](#input\_stack\_config\_path\_template) | Stack config path template | `string` | `"stacks/%s.yaml"` | no | | [stack\_destructor\_enabled](#input\_stack\_destructor\_enabled) | Flag to enable/disable the stack destructor to destroy the resources of a stack before deleting the stack itself | `bool` | `false` | no | +| [stacks\_space\_id](#input\_stacks\_space\_id) | Override the space ID for all stacks (unless the stack config has `dedicated_space` set to true). Otherwise, it will default to the admin stack's space. | `string` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tag\_filters](#input\_tag\_filters) | A map of tags that will filter stack creation by the matching `tags` set in a component `vars` configuration. | `map(string)` | `{}` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/modules/spacelift/main.tf b/modules/spacelift/main.tf index e39351eca..2587e8e27 100644 --- a/modules/spacelift/main.tf +++ b/modules/spacelift/main.tf @@ -1,6 +1,6 @@ module "spacelift" { source = "cloudposse/cloud-infrastructure-automation/spacelift" - version = "0.50.2" + version = "0.51.3" context_filters = var.context_filters tag_filters = var.tag_filters @@ -8,6 +8,9 @@ module "spacelift" { stack_config_path_template = var.stack_config_path_template components_path = var.spacelift_component_path + stacks_space_id = var.stacks_space_id + attachment_space_id = var.attachment_space_id + branch = var.git_branch repository = var.git_repository commit_sha = var.git_commit_sha @@ -30,6 +33,7 @@ module "spacelift" { policies_by_name_enabled = var.policies_by_name_enabled policies_by_name_path = format("%s/rego-policies", path.module) + administrative_push_policy_enabled = var.administrative_push_policy_enabled administrative_trigger_policy_enabled = var.administrative_trigger_policy_enabled administrative_stack_drift_detection_enabled = var.administrative_stack_drift_detection_enabled diff --git a/modules/spacelift/variables.tf b/modules/spacelift/variables.tf index 9ea232218..c1e59f278 100644 --- a/modules/spacelift/variables.tf +++ b/modules/spacelift/variables.tf @@ -1,8 +1,3 @@ -variable "region" { - type = string - description = "AWS Region" -} - variable "runner_image" { type = string description = "Full address & tag of the Spacelift runner image (e.g. on ECR)" @@ -173,6 +168,12 @@ variable "tag_filters" { default = {} } +variable "administrative_push_policy_enabled" { + type = bool + description = "Flag to enable/disable the global administrative push policy" + default = true +} + variable "administrative_trigger_policy_enabled" { type = bool description = "Flag to enable/disable the global administrative trigger policy" @@ -197,8 +198,14 @@ variable "before_init" { default = [] } -variable "space_id" { +variable "attachment_space_id" { type = string - description = "Place the stack(s) in the specified space_id." + description = "Specify the space ID for attachments (e.g. policies, contexts, etc.)" default = "legacy" } + +variable "stacks_space_id" { + type = string + description = "Override the space ID for all stacks (unless the stack config has `dedicated_space` set to true). Otherwise, it will default to the admin stack's space." + default = null +} diff --git a/modules/spacelift/versions.tf b/modules/spacelift/versions.tf index 9d6f08edc..1174cd191 100644 --- a/modules/spacelift/versions.tf +++ b/modules/spacelift/versions.tf @@ -1,10 +1,14 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3" required_providers { spacelift = { source = "spacelift-io/spacelift" - version = "~> 0.1.31" + version = ">= 0.1.31" + } + aws = { + source = "hashicorp/aws" + version = ">= 4.0" } } } From df8b8db3f0ed16e7cb351ec4c87e17275e727fd3 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 13 Jan 2023 09:58:48 -0500 Subject: [PATCH 010/501] fix(aws-sso): dont hardcode account name for root (#534) Co-authored-by: cloudpossebot --- modules/account-map/modules/iam-roles/variables.tf | 2 +- modules/aws-sso/main.tf | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/account-map/modules/iam-roles/variables.tf b/modules/account-map/modules/iam-roles/variables.tf index a0247e566..54967766a 100644 --- a/modules/account-map/modules/iam-roles/variables.tf +++ b/modules/account-map/modules/iam-roles/variables.tf @@ -7,7 +7,7 @@ variable "privileged" { variable "global_tenant_name" { type = string description = "The tenant name used for organization-wide resources" - default = "gov" + default = "core" } variable "global_environment_name" { diff --git a/modules/aws-sso/main.tf b/modules/aws-sso/main.tf index 69bb0e6eb..4765f5b25 100644 --- a/modules/aws-sso/main.tf +++ b/modules/aws-sso/main.tf @@ -39,7 +39,9 @@ module "sso_account_assignments_root" { locals { enabled = module.this.enabled - account_map = module.account_map.outputs.full_account_map + account_map = module.account_map.outputs.full_account_map + root_account = local.account_map[module.account_map.outputs.root_account_account_name] + account_assignments_groups = flatten([ for account_key, account in var.account_assignments : [ for principal_key, principal in account.groups : [ @@ -58,12 +60,12 @@ locals { account_assignments_groups_no_root = [ for val in local.account_assignments_groups : val - if val.account != local.account_map["root"] + if val.account != local.root_account ] account_assignments_groups_only_root = [ for val in local.account_assignments_groups : val - if val.account == local.account_map["root"] + if val.account == local.root_account ] account_assignments_users = flatten([ for account_key, account in var.account_assignments : [ @@ -82,12 +84,12 @@ locals { account_assignments_users_no_root = [ for val in local.account_assignments_users : val - if val.account != local.account_map["root"] + if val.account != local.root_account ] account_assignments_users_only_root = [ for val in local.account_assignments_users : val - if val.account == local.account_map["root"] + if val.account == local.root_account ] account_assignments = concat(local.account_assignments_groups_no_root, local.account_assignments_users_no_root) From 1ba36925365e233cf80665c2d50fa039122980c9 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Tue, 17 Jan 2023 09:41:35 +0200 Subject: [PATCH 011/501] Sync karpenter chart values with the schema (#535) --- modules/eks/karpenter/main.tf | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index f7f47e844..9edb24fdf 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -109,11 +109,13 @@ module "karpenter" { }), # karpenter-specific values yamlencode({ - aws = { - defaultInstanceProfile = one(aws_iam_instance_profile.default[*].name) + settings = { + aws = { + defaultInstanceProfile = one(aws_iam_instance_profile.default[*].name) + clusterName = local.eks_cluster_id + clusterEndpoint = local.eks_cluster_endpoint + } } - clusterName = local.eks_cluster_id - clusterEndpoint = local.eks_cluster_endpoint }), # additional values yamlencode(var.chart_values) From 7fec1ec23a7b4ec32cb9a8cbb75e6cac2c57a6c4 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Tue, 17 Jan 2023 09:47:07 +0200 Subject: [PATCH 012/501] Support setting consolidation in karpenter-provisioner (#536) Co-authored-by: cloudpossebot --- modules/eks/karpenter-provisioner/README.md | 2 +- modules/eks/karpenter-provisioner/main.tf | 1 + modules/eks/karpenter-provisioner/variables.tf | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/eks/karpenter-provisioner/README.md b/modules/eks/karpenter-provisioner/README.md index 3da2d102b..1afbedbd4 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/modules/eks/karpenter-provisioner/README.md @@ -159,7 +159,7 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [provisioners](#input\_provisioners) | Karpenter provisioners config |
map(object({
# The name of the Karpenter provisioner
name = string
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets
private_subnets_enabled = bool
# Configures Karpenter to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the value to `null` (never scales down if not set)
ttl_seconds_after_empty = number
# Configures Karpenter to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` (never expires if not set)
ttl_seconds_until_expired = number
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
values = list(string)
}))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter provisioner metadata options. See https://karpenter.sh/v0.18.0/aws/provisioning/#metadata-options for more details
metadata_options = optional(map(string), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter provisioner block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes. Karpenter uses default block device mappings for the AMI Family specified. For example, the Bottlerocket AMI Family defaults with two block device mappings. See https://karpenter.sh/v0.18.0/aws/provisioning/#block-device-mappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
}))
| n/a | yes | +| [provisioners](#input\_provisioners) | Karpenter provisioners config |
map(object({
# The name of the Karpenter provisioner
name = string
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets
private_subnets_enabled = bool
# Configures Karpenter to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the value to `null` (never scales down if not set)
ttl_seconds_after_empty = number
# Configures Karpenter to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` (never expires if not set)
ttl_seconds_until_expired = number
# Continuously binpack containers into least possible number of nodes. Mutually exclusive with ttl_seconds_after_empty.
consolidation = object({
enabled = bool
})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
values = list(string)
}))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter provisioner metadata options. See https://karpenter.sh/v0.18.0/aws/provisioning/#metadata-options for more details
metadata_options = optional(map(string), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter provisioner block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes. Karpenter uses default block device mappings for the AMI Family specified. For example, the Bottlerocket AMI Family defaults with two block device mappings. See https://karpenter.sh/v0.18.0/aws/provisioning/#block-device-mappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
}))
| n/a | yes | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/eks/karpenter-provisioner/main.tf b/modules/eks/karpenter-provisioner/main.tf index a4698822c..f9007354e 100644 --- a/modules/eks/karpenter-provisioner/main.tf +++ b/modules/eks/karpenter-provisioner/main.tf @@ -43,6 +43,7 @@ resource "kubernetes_manifest" "provisioner" { ttlSecondsAfterEmpty = each.value.ttl_seconds_after_empty }, each.value.ttl_seconds_until_expired == null ? {} : { ttlSecondsUntilExpired = each.value.ttl_seconds_until_expired + consolidation = each.value.consolidation }) } diff --git a/modules/eks/karpenter-provisioner/variables.tf b/modules/eks/karpenter-provisioner/variables.tf index 6745ad8b5..f132b7529 100644 --- a/modules/eks/karpenter-provisioner/variables.tf +++ b/modules/eks/karpenter-provisioner/variables.tf @@ -19,6 +19,10 @@ variable "provisioners" { ttl_seconds_after_empty = number # Configures Karpenter to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` (never expires if not set) ttl_seconds_until_expired = number + # Continuously binpack containers into least possible number of nodes. Mutually exclusive with ttl_seconds_after_empty. + consolidation = object({ + enabled = bool + }) # Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter total_cpu_limit = string # Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter From ca95bc25257d1a2074082eef351ae259fe2648c9 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Tue, 17 Jan 2023 18:09:34 +0200 Subject: [PATCH 013/501] Fix typo in karpenter-provisioner (#539) Co-authored-by: cloudpossebot --- modules/eks/karpenter-provisioner/main.tf | 50 +++++++++++++---------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/modules/eks/karpenter-provisioner/main.tf b/modules/eks/karpenter-provisioner/main.tf index f9007354e..36cce759c 100644 --- a/modules/eks/karpenter-provisioner/main.tf +++ b/modules/eks/karpenter-provisioner/main.tf @@ -22,29 +22,35 @@ resource "kubernetes_manifest" "provisioner" { metadata = { name = each.value.name } - spec = merge({ - limits = { - resources = { - cpu = each.value.total_cpu_limit - memory = each.value.total_memory_limit + spec = merge( + { + limits = { + resources = { + cpu = each.value.total_cpu_limit + memory = each.value.total_memory_limit + } } - } - providerRef = { - name = each.value.name - } - requirements = each.value.requirements - # Do not include keys with null values, or else Terraform will show a perpetual diff. - # Use `try(length(),0)` to detect both empty lists and nulls. - }, try(length(each.value.taints), 0) == 0 ? {} : { - taints = each.value.taints - }, try(length(each.value.startup_taints), 0) == 0 ? {} : { - startupTaints = each.value.startup_taints - }, each.value.ttl_seconds_after_empty == null ? {} : { - ttlSecondsAfterEmpty = each.value.ttl_seconds_after_empty - }, each.value.ttl_seconds_until_expired == null ? {} : { - ttlSecondsUntilExpired = each.value.ttl_seconds_until_expired - consolidation = each.value.consolidation - }) + providerRef = { + name = each.value.name + } + requirements = each.value.requirements + consolidation = each.value.consolidation + # Do not include keys with null values, or else Terraform will show a perpetual diff. + # Use `try(length(),0)` to detect both empty lists and nulls. + }, + try(length(each.value.taints), 0) == 0 ? {} : { + taints = each.value.taints + }, + try(length(each.value.startup_taints), 0) == 0 ? {} : { + startupTaints = each.value.startup_taints + }, + each.value.ttl_seconds_after_empty == null ? {} : { + ttlSecondsAfterEmpty = each.value.ttl_seconds_after_empty + }, + each.value.ttl_seconds_until_expired == null ? {} : { + ttlSecondsUntilExpired = each.value.ttl_seconds_until_expired + }, + ) } depends_on = [kubernetes_manifest.provider] From 52bde5d80421692bcfe5fea036e5fcc519510c82 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 17 Jan 2023 16:09:30 -0500 Subject: [PATCH 014/501] fix(dns-primary/acm): revert module to 0.16.2 (#540) Co-authored-by: cloudpossebot --- modules/dns-primary/README.md | 2 +- modules/dns-primary/acm.tf | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index 63cbe34eb..ddee9d352 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -51,7 +51,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.17.0 | +| [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.16.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/dns-primary/acm.tf b/modules/dns-primary/acm.tf index 013c319de..a86be253a 100644 --- a/modules/dns-primary/acm.tf +++ b/modules/dns-primary/acm.tf @@ -11,8 +11,9 @@ locals { module "acm" { for_each = local.domains_set - source = "cloudposse/acm-request-certificate/aws" - version = "0.17.0" + source = "cloudposse/acm-request-certificate/aws" + // Note: 0.17.0 is a 'preview' release, so we're using 0.16.2 + version = "0.16.2" enabled = local.certificate_enabled From 3ac9940b936d10cd6ad96623352a1174515542ae Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Wed, 18 Jan 2023 16:52:31 +0200 Subject: [PATCH 015/501] Pin kubernetes provider in metrics-server (#541) Co-authored-by: cloudpossebot --- modules/eks/metrics-server/README.md | 9 +++++---- modules/eks/metrics-server/versions.tf | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 3ecaf303d..e8ebba173 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -42,16 +42,17 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [kubernetes](#provider\_kubernetes) | n/a | +| [aws](#provider\_aws) | >= 4.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.14.0 | ## Modules diff --git a/modules/eks/metrics-server/versions.tf b/modules/eks/metrics-server/versions.tf index 58318d20e..57cc9f927 100644 --- a/modules/eks/metrics-server/versions.tf +++ b/modules/eks/metrics-server/versions.tf @@ -1,14 +1,18 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.9.0" } helm = { source = "hashicorp/helm" version = ">= 2.0" } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.14.0" + } } } From e456f4a62a25cd5ab641af0b619a4b4687a48cde Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Wed, 18 Jan 2023 17:23:45 +0200 Subject: [PATCH 016/501] Update k8s metrics-server to latest (#537) --- modules/eks/metrics-server/default.auto.tfvars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/eks/metrics-server/default.auto.tfvars b/modules/eks/metrics-server/default.auto.tfvars index 2e2708d6d..d0baeab36 100644 --- a/modules/eks/metrics-server/default.auto.tfvars +++ b/modules/eks/metrics-server/default.auto.tfvars @@ -4,7 +4,7 @@ name = "metrics-server" chart = "metrics-server" chart_repository = "https://charts.bitnami.com/bitnami" -chart_version = "5.11.4" +chart_version = "6.2.6" create_namespace = true kubernetes_namespace = "metrics-server" From 2b297a49cc0486a9c443b97555a93bc93d733e52 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Wed, 18 Jan 2023 17:36:24 +0200 Subject: [PATCH 017/501] Fix github actions runner controller default variables (#542) Co-authored-by: cloudpossebot --- modules/eks/actions-runner-controller/README.md | 2 +- modules/eks/actions-runner-controller/variables.tf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index cd929bec0..61e9e0992 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -266,7 +266,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, false)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, false)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | | [ssm\_github\_webhook\_secret\_token\_path](#input\_ssm\_github\_webhook\_secret\_token\_path) | The path in SSM to the GitHub Webhook Secret token. | `string` | `""` | no | diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index c05962d98..ee5bde6e3 100644 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -181,13 +181,13 @@ variable "runners" { webhook_startup_timeout = optional(string, null) pull_driven_scaling_enabled = bool labels = list(string) - storage = optional(string, false) + storage = optional(string, null) pvc_enabled = optional(string, false) resources = object({ limits = object({ cpu = string memory = string - ephemeral_storage = optional(string, false) + ephemeral_storage = optional(string, null) }) requests = object({ cpu = string From fa1cc03a7723224d2454ad730c87d5926b05ee72 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Thu, 19 Jan 2023 19:33:45 +0200 Subject: [PATCH 018/501] Update pod security context schema in cert-manager (#538) --- modules/eks/cert-manager/resources/cert-manager-values.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/eks/cert-manager/resources/cert-manager-values.yaml b/modules/eks/cert-manager/resources/cert-manager-values.yaml index d8fa6dd79..4e24e3d40 100644 --- a/modules/eks/cert-manager/resources/cert-manager-values.yaml +++ b/modules/eks/cert-manager/resources/cert-manager-values.yaml @@ -4,9 +4,8 @@ installCRDs: true serviceAccount: create: true securityContext: - enabled: true fsGroup: 1001 - runAsGroup: 1001 + runAsUser: 1001 prometheus: servicemonitor: prometheusInstance: default From 46bd77caa57187f504b27b154956e95dda757b2f Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 20 Jan 2023 14:36:34 -0800 Subject: [PATCH 019/501] EC2 Client VPN Version Bump (#544) --- modules/ec2-client-vpn/README.md | 2 +- modules/ec2-client-vpn/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index 8c3f291a1..46ae14a86 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -100,7 +100,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [ec2\_client\_vpn](#module\_ec2\_client\_vpn) | cloudposse/ec2-client-vpn/aws | 0.11.0 | +| [ec2\_client\_vpn](#module\_ec2\_client\_vpn) | cloudposse/ec2-client-vpn/aws | 0.14.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | diff --git a/modules/ec2-client-vpn/main.tf b/modules/ec2-client-vpn/main.tf index a488ace32..8e79a95f6 100644 --- a/modules/ec2-client-vpn/main.tf +++ b/modules/ec2-client-vpn/main.tf @@ -31,7 +31,7 @@ locals { module "ec2_client_vpn" { source = "cloudposse/ec2-client-vpn/aws" - version = "0.11.0" + version = "0.14.0" ca_common_name = var.ca_common_name root_common_name = var.root_common_name From 41f09a5f4a9e3bf1a7ee5136b3e8b0156888361e Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 24 Jan 2023 15:01:33 -0500 Subject: [PATCH 020/501] Chore/acme/bootcamp spacelift (#545) --- modules/spacelift/README.md | 2 +- modules/spacelift/variables.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index e00854e72..9aaf8d776 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -375,7 +375,7 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | [aws\_role\_generate\_credentials\_in\_worker](#input\_aws\_role\_generate\_credentials\_in\_worker) | Flag to enable/disable generating AWS credentials in the private worker after assuming the supplied IAM role | `bool` | `false` | no | | [before\_init](#input\_before\_init) | List of before-init scripts | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [context\_filters](#input\_context\_filters) | Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`. | `map(any)` | `{}` | no | +| [context\_filters](#input\_context\_filters) | Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`. | `map(list(string))` | `{}` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [drift\_detection\_enabled](#input\_drift\_detection\_enabled) | Flag to enable/disable drift detection on the infrastructure stacks | `bool` | `true` | no | diff --git a/modules/spacelift/variables.tf b/modules/spacelift/variables.tf index c1e59f278..5e87cc8e0 100644 --- a/modules/spacelift/variables.tf +++ b/modules/spacelift/variables.tf @@ -157,7 +157,7 @@ variable "stack_destructor_enabled" { } variable "context_filters" { - type = map(any) + type = map(list(string)) description = "Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`." default = {} } From 3f70dec243ce293a3cea4902cc1395e601041db5 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 25 Jan 2023 19:24:48 -0500 Subject: [PATCH 021/501] Chore/acme/bootcamp core tenant (#543) Co-authored-by: cloudpossebot --- modules/bastion/README.md | 9 +++++---- modules/bastion/main.tf | 7 +++---- modules/bastion/versions.tf | 6 +++++- modules/ec2-client-vpn/README.md | 2 +- modules/ec2-client-vpn/outputs.tf | 1 + modules/ec2-client-vpn/versions.tf | 2 +- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/modules/bastion/README.md b/modules/bastion/README.md index af2e8e8a0..f9a9e089c 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -57,14 +57,15 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [cloudinit](#requirement\_cloudinit) | >= 2.2 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [cloudinit](#provider\_cloudinit) | n/a | +| [aws](#provider\_aws) | >= 4.0 | +| [cloudinit](#provider\_cloudinit) | >= 2.2 | ## Modules @@ -72,7 +73,7 @@ components: |------|--------|---------| | [bastion\_autoscale\_group](#module\_bastion\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.30.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [sg](#module\_sg) | cloudposse/security-group/aws | 1.0.1 | +| [sg](#module\_sg) | cloudposse/security-group/aws | 2.0.0 | | [ssm\_tls\_ssh\_key\_pair](#module\_ssm\_tls\_ssh\_key\_pair) | cloudposse/ssm-tls-ssh-key-pair/aws | 0.10.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | diff --git a/modules/bastion/main.tf b/modules/bastion/main.tf index 3b39a74be..a34b27cb4 100644 --- a/modules/bastion/main.tf +++ b/modules/bastion/main.tf @@ -32,11 +32,10 @@ locals { module "sg" { source = "cloudposse/security-group/aws" - version = "1.0.1" + version = "2.0.0" - security_group_description = "Security group for Bastion Hosts" - allow_all_egress = true - vpc_id = local.vpc_id + rules = var.security_group_rules + vpc_id = local.vpc_id context = module.this.context } diff --git a/modules/bastion/versions.tf b/modules/bastion/versions.tf index e89eb16ed..6b9a92762 100644 --- a/modules/bastion/versions.tf +++ b/modules/bastion/versions.tf @@ -4,7 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = ">= 2.2" } } } diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index 46ae14a86..90563a9aa 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -89,7 +89,7 @@ Successful tests have been seen with MSK and RDS. | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [awsutils](#requirement\_awsutils) | >= 0.11.0 | ## Providers diff --git a/modules/ec2-client-vpn/outputs.tf b/modules/ec2-client-vpn/outputs.tf index 6c3bd2883..2154c3c2d 100644 --- a/modules/ec2-client-vpn/outputs.tf +++ b/modules/ec2-client-vpn/outputs.tf @@ -21,4 +21,5 @@ output "client_configuration" { output "full_client_configuration" { value = module.ec2_client_vpn.full_client_configuration description = "Client configuration including client certificate and private key for mutual authentication" + sensitive = true } diff --git a/modules/ec2-client-vpn/versions.tf b/modules/ec2-client-vpn/versions.tf index b6344fa87..4e65d1ce3 100644 --- a/modules/ec2-client-vpn/versions.tf +++ b/modules/ec2-client-vpn/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } awsutils = { source = "cloudposse/awsutils" From 2ab115d5c97464b22add496869b43eccbee2283e Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 31 Jan 2023 15:02:31 -0800 Subject: [PATCH 022/501] Update echo and alb-controller-ingress-group (#547) --- .../alb-controller-ingress-group/README.md | 9 ++--- .../eks/alb-controller-ingress-group/main.tf | 2 +- .../alb-controller-ingress-group/variables.tf | 6 ++++ .../alb-controller-ingress-group/versions.tf | 4 +-- .../charts/echo-server/templates/ingress.yaml | 36 ++++++++++++------- 5 files changed, 37 insertions(+), 20 deletions(-) diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index 58d861226..c9c48434c 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -39,15 +39,15 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | -| [kubernetes](#requirement\_kubernetes) | ~> 2.12.1 | +| [aws](#requirement\_aws) | >= 4.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [kubernetes](#provider\_kubernetes) | ~> 2.12.1 | +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | ## Modules @@ -81,6 +81,7 @@ components: | [alb\_access\_logs\_enabled](#input\_alb\_access\_logs\_enabled) | Whether or not to enable access logs for the ALB | `bool` | `false` | no | | [alb\_access\_logs\_s3\_bucket\_name](#input\_alb\_access\_logs\_s3\_bucket\_name) | The name of the S3 bucket to store the access logs in | `string` | `null` | no | | [alb\_access\_logs\_s3\_bucket\_prefix](#input\_alb\_access\_logs\_s3\_bucket\_prefix) | The prefix to use when storing the access logs | `string` | `"echo-server"` | no | +| [alb\_group\_name](#input\_alb\_group\_name) | The name of the alb group | `string` | `null` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `false` | no | diff --git a/modules/eks/alb-controller-ingress-group/main.tf b/modules/eks/alb-controller-ingress-group/main.tf index 8d6e4f1ea..c37561fd0 100644 --- a/modules/eks/alb-controller-ingress-group/main.tf +++ b/modules/eks/alb-controller-ingress-group/main.tf @@ -22,7 +22,7 @@ locals { global_accelerator.outputs.listener_ids[0] ] - ingress_controller_group_name = module.this.name + ingress_controller_group_name = coalesce(var.alb_group_name, module.this.name) kube_tags = join(",", [for k, v in module.this.tags : "${k}=${v}"]) diff --git a/modules/eks/alb-controller-ingress-group/variables.tf b/modules/eks/alb-controller-ingress-group/variables.tf index b984c0625..6c272d9c0 100644 --- a/modules/eks/alb-controller-ingress-group/variables.tf +++ b/modules/eks/alb-controller-ingress-group/variables.tf @@ -116,3 +116,9 @@ variable "fixed_response_vars" { email = "hello@cloudposse.com" } } + +variable "alb_group_name" { + type = string + description = "The name of the alb group" + default = null +} diff --git a/modules/eks/alb-controller-ingress-group/versions.tf b/modules/eks/alb-controller-ingress-group/versions.tf index e28af9b9a..8b70f9f52 100644 --- a/modules/eks/alb-controller-ingress-group/versions.tf +++ b/modules/eks/alb-controller-ingress-group/versions.tf @@ -4,11 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } kubernetes = { source = "hashicorp/kubernetes" - version = "~> 2.12.1" + version = ">= 2.7.1" } } } diff --git a/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml b/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml index f5e6473fa..f76922fae 100644 --- a/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml +++ b/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml @@ -9,10 +9,20 @@ metadata: name: {{ $fullName }} annotations: {{- if eq (printf "%v" .Values.ingress.nginx.enabled) "true" }} + kubernetes.io/ingress.class: {{ .Values.ingress.nginx.class }} {{- if (index .Values.ingress.nginx "tls_certificate_cluster_issuer") }} cert-manager.io/cluster-issuer: {{ .Values.ingress.nginx.tls_certificate_cluster_issuer }} {{- end }} {{- else if eq (printf "%v" .Values.ingress.alb.enabled) "true" }} + kubernetes.io/ingress.class: {{ .Values.ingress.alb.class }} + {{- if not .Values.ingress.alb.group_name }} + alb.ingress.kubernetes.io/load-balancer-name: {{ index .Values.ingress.alb "load_balancer_name" | default "k8s-common" }} + {{- end }} + alb.ingress.kubernetes.io/group.name: {{ index .Values.ingress.alb "group_name" | default "common" }} + alb.ingress.kubernetes.io/scheme: internet-facing + {{- if .Values.ingress.alb.access_logs.enabled }} + alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket={{.Values.ingress.alb.access_logs.s3_bucket_name}},access_logs.s3.prefix={{.Values.ingress.alb.access_logs.s3_bucket_prefix}} + {{- end }} alb.ingress.kubernetes.io/target-type: 'ip' {{- if eq (printf "%v" .Values.ingress.alb.ssl_redirect.enabled) "true" }} alb.ingress.kubernetes.io/ssl-redirect: '{{ .Values.ingress.alb.ssl_redirect.port }}' @@ -29,19 +39,19 @@ metadata: spec: {{- if $nginxTlsEnabled }} tls: # < placing a host in the TLS config will indicate a certificate should be created - - hosts: - - {{ .Values.ingress.hostname }} - secretName: {{ $svcName }}-cert # < cert-manager will store the created certificate in this secret. + - hosts: + - {{ .Values.ingress.hostname }} + secretName: {{ $svcName }}-cert # < cert-manager will store the created certificate in this secret. {{- end }} rules: - - host: {{ .Values.ingress.hostname }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: {{ $svcName }} - port: - number: {{ $svcPort }} + - host: {{ .Values.ingress.hostname }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ $svcName }} + port: + number: {{ $svcPort }} {{- end }} From 6fc46f94d516bf86ba86e5593dd43015c417a5a4 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 3 Feb 2023 11:02:33 -0800 Subject: [PATCH 023/501] `datadog-agent` allow values var merged (#548) --- modules/datadog-agent/README.md | 13 ++++++++----- modules/datadog-agent/main.tf | 17 +++++++++++++++-- modules/datadog-agent/outputs.tf | 3 ++- modules/datadog-agent/variables.tf | 6 ++++++ modules/datadog-agent/versions.tf | 6 +++++- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/modules/datadog-agent/README.md b/modules/datadog-agent/README.md index 3ee8f8770..ee37c144b 100644 --- a/modules/datadog-agent/README.md +++ b/modules/datadog-agent/README.md @@ -125,27 +125,29 @@ https-checks: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.3.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | | [utils](#requirement\_utils) | >= 0.3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [kubernetes](#provider\_kubernetes) | n/a | +| [aws](#provider\_aws) | >= 4.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.14.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.6.0 | +| [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.7.0 | | [datadog\_cluster\_check\_yaml\_config](#module\_datadog\_cluster\_check\_yaml\_config) | cloudposse/config/yaml | 1.0.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [values\_merge](#module\_values\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.1 | ## Resources @@ -210,6 +212,7 @@ https-checks: | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | +| [values](#input\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | | [verify](#input\_verify) | Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart | `bool` | `false` | no | | [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true` | `bool` | `null` | no | @@ -217,7 +220,7 @@ https-checks: | Name | Description | |------|-------------| -| [cluster\_checks](#output\_cluster\_checks) | n/a | +| [cluster\_checks](#output\_cluster\_checks) | Cluster Checks for the cluster | | [metadata](#output\_metadata) | Block status of the deployed release | diff --git a/modules/datadog-agent/main.tf b/modules/datadog-agent/main.tf index ffb43b84f..08ad386f8 100644 --- a/modules/datadog-agent/main.tf +++ b/modules/datadog-agent/main.tf @@ -69,6 +69,19 @@ module "datadog_cluster_check_yaml_config" { context = module.this.context } +module "values_merge" { + source = "cloudposse/config/yaml//modules/deepmerge" + version = "1.0.1" + + # Merge in order: datadog values, var.values + maps = [ + yamldecode( + file("${path.module}/values.yaml") + ), + var.values, + ] +} + resource "kubernetes_namespace" "default" { count = local.enabled && var.create_namespace ? 1 : 0 @@ -81,7 +94,7 @@ resource "kubernetes_namespace" "default" { module "datadog_agent" { source = "cloudposse/helm-release/aws" - version = "0.6.0" + version = "0.7.0" name = module.this.name chart = var.chart @@ -99,7 +112,7 @@ module "datadog_agent" { eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer values = [ - file("${path.module}/values.yaml") + yamlencode(module.values_merge.merged) ] set_sensitive = [ diff --git a/modules/datadog-agent/outputs.tf b/modules/datadog-agent/outputs.tf index 331f07a18..f63ad1975 100644 --- a/modules/datadog-agent/outputs.tf +++ b/modules/datadog-agent/outputs.tf @@ -4,5 +4,6 @@ output "metadata" { } output "cluster_checks" { - value = local.datadog_cluster_checks + value = local.datadog_cluster_checks + description = "Cluster Checks for the cluster" } diff --git a/modules/datadog-agent/variables.tf b/modules/datadog-agent/variables.tf index bdc7fa99d..1c3adbc3d 100644 --- a/modules/datadog-agent/variables.tf +++ b/modules/datadog-agent/variables.tf @@ -56,3 +56,9 @@ variable "eks_component_name" { description = "The name of the EKS component. Used to get the remote state" default = "eks/eks" } + +variable "values" { + type = any + description = "Additional values to yamlencode as `helm_release` values." + default = {} +} diff --git a/modules/datadog-agent/versions.tf b/modules/datadog-agent/versions.tf index 19dd8f964..fea35c3da 100644 --- a/modules/datadog-agent/versions.tf +++ b/modules/datadog-agent/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.9.0" } helm = { source = "hashicorp/helm" @@ -14,5 +14,9 @@ terraform { source = "cloudposse/utils" version = ">= 0.3.0" } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.14.0" + } } } From 3391a439ab56d857ee7ca9923d8e1fe980f0d6e2 Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv <1759112+zdmytriv@users.noreply.github.com> Date: Fri, 3 Feb 2023 21:59:44 +0200 Subject: [PATCH 024/501] Fixed non-html tags that fails rendering on docusaurus (#546) Co-authored-by: Zinovii Dmytriv --- README.md | 2 +- modules/datadog-agent/README.md | 4 ++-- modules/eks/external-dns/README.md | 2 +- modules/eks/external-dns/variables.tf | 2 +- modules/zscaler/README.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d7094ba3b..7285339e3 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. ## Copyright -Copyright © 2017-2022 [Cloud Posse, LLC](https://cpco.io/copyright) +Copyright © 2017-2023 [Cloud Posse, LLC](https://cpco.io/copyright) diff --git a/modules/datadog-agent/README.md b/modules/datadog-agent/README.md index ee37c144b..42ff86c3e 100644 --- a/modules/datadog-agent/README.md +++ b/modules/datadog-agent/README.md @@ -73,7 +73,7 @@ New Cluster Checks can be added to defaults to be applied in every account. Alte Once they are added, and properly configured, the new checks show up in the network monitor creation under `ssl` and `Http` -**Please note:** the yaml file name doesn't matter, but the root key inside which is `something.yaml` does matter. this is following [datadogs docs](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) for .yaml. +**Please note:** the yaml file name doesn't matter, but the root key inside which is `something.yaml` does matter. this is following [datadogs docs](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) for `.yaml`. ## Monitoring Cluster Checks @@ -89,7 +89,7 @@ https-checks: HTTPS Check failed on {{instance.name}} in Stage: {{stage.name}} escalation_message: "" - tags: + tags: managed-by: Terraform notify_no_data: false notify_audit: false diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index 823d5782f..b1076e38a 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -133,7 +133,7 @@ components: | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | -| [txt\_prefix](#input\_txt\_prefix) | Prefix to create a TXT record with a name following the pattern prefix.. | `string` | `"external-dns"` | no | +| [txt\_prefix](#input\_txt\_prefix) | Prefix to create a TXT record with a name following the pattern prefix.``. | `string` | `"external-dns"` | no | | [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `null` | no | ## Outputs diff --git a/modules/eks/external-dns/variables.tf b/modules/eks/external-dns/variables.tf index a63780e4a..0f596ae83 100644 --- a/modules/eks/external-dns/variables.tf +++ b/modules/eks/external-dns/variables.tf @@ -99,7 +99,7 @@ variable "chart_values" { variable "txt_prefix" { type = string default = "external-dns" - description = "Prefix to create a TXT record with a name following the pattern prefix.." + description = "Prefix to create a TXT record with a name following the pattern prefix.``." } variable "crd_enabled" { diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index cfae9f0db..b117bda96 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -10,7 +10,7 @@ This parameter should be populated using `chamber`, which is included in the geo chamber write zscaler key ``` -Where is the ZScaler App Connector Provisioning Key. For more information on how to generate this key, see: [ZScaler documentation on Configuring App Connectors](https://help.zscaler.com/zpa/configuring-connectors). +Where `` is the ZScaler App Connector Provisioning Key. For more information on how to generate this key, see: [ZScaler documentation on Configuring App Connectors](https://help.zscaler.com/zpa/configuring-connectors). ## Usage From f91ddaa73236035dc75015b78890e85b560912f2 Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Sun, 5 Feb 2023 20:26:00 -0600 Subject: [PATCH 025/501] Remove extra var from stack example (#550) --- modules/vpc/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 4ae449aa0..5e889cef6 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -22,7 +22,6 @@ components: vars: enabled: true name: vpc - eks_tags_enabled: true availability_zones: - "a" - "b" From 08b12aec38011473367d3afccf5388bef5110b64 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 6 Feb 2023 09:09:08 -0800 Subject: [PATCH 026/501] `datadog-private-locations` update helm provider (#549) --- modules/datadog-synthetics-private-location/README.md | 2 +- modules/datadog-synthetics-private-location/provider-helm.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index 8507a1544..bd7dd3f2c 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -186,7 +186,7 @@ Environment variables: | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | | [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | -| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1alpha1"` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | | [kubernetes\_namespace](#input\_kubernetes\_namespace) | Kubernetes namespace to install the release into | `string` | n/a | yes | diff --git a/modules/datadog-synthetics-private-location/provider-helm.tf b/modules/datadog-synthetics-private-location/provider-helm.tf index d04bccf3d..20e4d3837 100644 --- a/modules/datadog-synthetics-private-location/provider-helm.tf +++ b/modules/datadog-synthetics-private-location/provider-helm.tf @@ -73,7 +73,7 @@ variable "kube_exec_auth_aws_profile_enabled" { variable "kubeconfig_exec_auth_api_version" { type = string - default = "client.authentication.k8s.io/v1alpha1" + default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" } From 7271c29ac86d47210c9025e98a1d2ab15104ecff Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 6 Feb 2023 16:49:36 -0800 Subject: [PATCH 027/501] Upstream `aurora-postgres` (#518) Co-authored-by: Benjamin Smith --- modules/aurora-postgres-resources/README.md | 36 ++- .../additional-databases.tf | 4 - .../additional-users.tf | 19 -- .../default.auto.tfvars | 3 - modules/aurora-postgres-resources/main.tf | 80 ++++- .../modules/postgresql-user/main.tf | 67 +++-- .../modules/postgresql-user/outputs.tf | 5 - .../modules/postgresql-user/variables.tf | 19 +- .../modules/postgresql-user/versions.tf | 14 +- modules/aurora-postgres-resources/outputs.tf | 16 +- .../provider-postgres.tf | 22 ++ .../aurora-postgres-resources/providers.tf | 8 +- .../read-only-user.tf | 128 -------- .../aurora-postgres-resources/remote-state.tf | 2 +- .../aurora-postgres-resources/variables.tf | 74 ++++- modules/aurora-postgres-resources/versions.tf | 6 +- modules/aurora-postgres/README.md | 126 ++++---- modules/aurora-postgres/cluster-regional.tf | 28 +- modules/aurora-postgres/default.auto.tfvars | 25 -- modules/aurora-postgres/kms.tf | 2 +- modules/aurora-postgres/main.tf | 20 +- .../modules/postgresql-user/context.tf | 279 ------------------ .../modules/postgresql-user/main.tf | 42 --- .../modules/postgresql-user/outputs.tf | 19 -- .../modules/postgresql-user/variables.tf | 35 --- .../modules/postgresql-user/versions.tf | 18 -- modules/aurora-postgres/outputs.tf | 12 +- modules/aurora-postgres/providers.tf | 8 +- modules/aurora-postgres/read-only-user.tf | 136 --------- modules/aurora-postgres/remote-state.tf | 26 +- modules/aurora-postgres/ssm.tf | 104 ++++--- modules/aurora-postgres/variables.tf | 60 ++-- modules/aurora-postgres/versions.tf | 6 +- 33 files changed, 479 insertions(+), 970 deletions(-) delete mode 100644 modules/aurora-postgres-resources/additional-databases.tf delete mode 100644 modules/aurora-postgres-resources/additional-users.tf delete mode 100644 modules/aurora-postgres-resources/default.auto.tfvars create mode 100644 modules/aurora-postgres-resources/provider-postgres.tf delete mode 100644 modules/aurora-postgres-resources/read-only-user.tf delete mode 100644 modules/aurora-postgres/default.auto.tfvars delete mode 100644 modules/aurora-postgres/modules/postgresql-user/context.tf delete mode 100644 modules/aurora-postgres/modules/postgresql-user/main.tf delete mode 100644 modules/aurora-postgres/modules/postgresql-user/outputs.tf delete mode 100644 modules/aurora-postgres/modules/postgresql-user/variables.tf delete mode 100644 modules/aurora-postgres/modules/postgresql-user/versions.tf delete mode 100644 modules/aurora-postgres/read-only-user.tf diff --git a/modules/aurora-postgres-resources/README.md b/modules/aurora-postgres-resources/README.md index a3cefaab4..6e8bc0e9f 100644 --- a/modules/aurora-postgres-resources/README.md +++ b/modules/aurora-postgres-resources/README.md @@ -30,26 +30,25 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 3.0 | -| [postgresql](#requirement\_postgresql) | >= 1.11.2 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [postgresql](#requirement\_postgresql) | >= 1.17.1 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 3.0 | -| [postgresql](#provider\_postgresql) | >= 1.11.2 | +| [aws](#provider\_aws) | >= 4.9.0 | +| [postgresql](#provider\_postgresql) | >= 1.17.1 | ## Modules | Name | Source | Version | |------|--------|---------| +| [additional\_grants](#module\_additional\_grants) | ./modules/postgresql-user | n/a | | [additional\_users](#module\_additional\_users) | ./modules/postgresql-user | n/a | -| [aurora\_postgres](#module\_aurora\_postgres) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [aurora\_postgres](#module\_aurora\_postgres) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [read\_only\_cluster\_user](#module\_read\_only\_cluster\_user) | ./modules/postgresql-user | n/a | -| [read\_only\_db\_users](#module\_read\_only\_db\_users) | ./modules/postgresql-user | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -57,20 +56,25 @@ components: | Name | Type | |------|------| | [postgresql_database.additional](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/database) | resource | -| [postgresql_default_privileges.read_only_tables_cluster](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/default_privileges) | resource | -| [postgresql_default_privileges.read_only_tables_users](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/default_privileges) | resource | +| [postgresql_schema.additional](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/schema) | resource | | [aws_ssm_parameter.admin_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [additional\_databases](#input\_additional\_databases) | Define additional databases to create. | `set(string)` | `[]` | no | +| [additional\_databases](#input\_additional\_databases) | Additional databases to be created with the cluster | `set(string)` | `[]` | no | +| [additional\_grants](#input\_additional\_grants) | Create additional database user with specified grants.
If `var.ssm_password_source` is set, passwords will be retrieved from SSM parameter store,
otherwise, passwords will be generated and stored in SSM parameter store under the service's key. |
map(list(object({
grant : list(string)
db : string
})))
| `{}` | no | +| [additional\_schemas](#input\_additional\_schemas) | Create additonal schemas for a given database.
If no database is given, the schema will use the database used by the provider configuration |
map(object({
database : string
}))
| `{}` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [additional\_users](#input\_additional\_users) | Define additional users to create. |
map(object({
db_user : string
db_password : string
grants : list(object({
grant : list(string)
db : string
schema : string
object_type : string
}))
}))
| `{}` | no | +| [additional\_users](#input\_additional\_users) | Create additional database user for a service, specifying username, grants, and optional password.
If no password is specified, one will be generated. Username and password will be stored in
SSM parameter store under the service's key. |
map(object({
db_user : string
db_password : string
grants : list(object({
grant : list(string)
db : string
schema : string
object_type : string
}))
}))
| `{}` | no | +| [admin\_password](#input\_admin\_password) | postgresql password for the admin user | `string` | `""` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [aurora\_postgres\_component\_name](#input\_aurora\_postgres\_component\_name) | Aurora Postgres component name to read the remote state from | `string` | n/a | yes | +| [aurora\_postgres\_component\_name](#input\_aurora\_postgres\_component\_name) | Aurora Postgres component name to read the remote state from | `string` | `"aurora-postgres"` | no | +| [cluster\_enabled](#input\_cluster\_enabled) | Set to `false` to prevent the module from creating any resources | `string` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [db\_name](#input\_db\_name) | Database name (default is not to create a database) | `string` | `""` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -84,8 +88,11 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [read\_passwords\_from\_ssm](#input\_read\_passwords\_from\_ssm) | When `true`, fetch user passwords from SSM | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [ssm\_password\_source](#input\_ssm\_password\_source) | If var.read\_passwords\_from\_ssm is true, DB user passwords will be retrieved from SSM using `var.ssm_password_source` and the database username. If this value is not set, a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. | `string` | `""` | no | +| [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | SSM path prefix | `string` | `"aurora-postgres"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | @@ -95,8 +102,9 @@ components: | Name | Description | |------|-------------| | [additional\_databases](#output\_additional\_databases) | Additional databases | +| [additional\_grants](#output\_additional\_grants) | Additional grants | +| [additional\_schemas](#output\_additional\_schemas) | Additional schemas | | [additional\_users](#output\_additional\_users) | Additional users | -| [read\_only\_users](#output\_read\_only\_users) | Read-only users | diff --git a/modules/aurora-postgres-resources/additional-databases.tf b/modules/aurora-postgres-resources/additional-databases.tf deleted file mode 100644 index 370a24120..000000000 --- a/modules/aurora-postgres-resources/additional-databases.tf +++ /dev/null @@ -1,4 +0,0 @@ -resource "postgresql_database" "additional" { - for_each = local.enabled ? var.additional_databases : [] - name = each.key -} diff --git a/modules/aurora-postgres-resources/additional-users.tf b/modules/aurora-postgres-resources/additional-users.tf deleted file mode 100644 index 3dc02cbff..000000000 --- a/modules/aurora-postgres-resources/additional-users.tf +++ /dev/null @@ -1,19 +0,0 @@ -module "additional_users" { - source = "./modules/postgresql-user" - - for_each = var.additional_users - - enabled = local.enabled - - service_name = each.key - db_user = each.value.db_user - db_password = each.value.db_password - grants = each.value.grants - ssm_path_prefix = join("/", compact([local.ssm_path_prefix, local.cluster_name, "service"])) - - context = module.this.context - - depends_on = [ - postgresql_database.additional - ] -} diff --git a/modules/aurora-postgres-resources/default.auto.tfvars b/modules/aurora-postgres-resources/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/aurora-postgres-resources/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/aurora-postgres-resources/main.tf b/modules/aurora-postgres-resources/main.tf index 15c4bfdf5..e9676e600 100644 --- a/modules/aurora-postgres-resources/main.tf +++ b/modules/aurora-postgres-resources/main.tf @@ -1,18 +1,78 @@ locals { enabled = module.this.enabled - cluster_endpoint = try(module.aurora_postgres.outputs.primary_aurora_postgres_master_endpoint, module.aurora_postgres.outputs.endpoint) - cluster_name = try(module.aurora_postgres.outputs.primary_aurora_postgres_cluster_identifier, null) - database_name = try(module.aurora_postgres.outputs.aurora_postgres_database_name, module.aurora_postgres.outputs.database_name) - admin_user = try(module.aurora_postgres.outputs.aurora_postgres_admin_username, module.aurora_postgres.outputs.admin_username) - ssm_path_prefix = try(module.aurora_postgres.outputs.aurora_postgres_ssm_path_prefix, module.aurora_postgres.outputs.ssm_cluster_key_prefix) - admin_password_ssm_parameter = try(module.aurora_postgres.outputs.aurora_postgres_master_password_ssm_key, module.aurora_postgres.outputs.config_map.password_ssm_key) - admin_password = join("", data.aws_ssm_parameter.admin_password[*].value) + # If pulling passwords from SSM, determine the SSM path for passwords for each user + # example SSM password source: /rds/acme-platform-use1-dev-rds-shared/%s/password + read_passwords_from_ssm = local.enabled && var.read_passwords_from_ssm + password_users_to_fetch = local.read_passwords_from_ssm ? toset(keys(var.additional_grants)) : [] + ssm_path_prefix = format("/%s/%s", var.ssm_path_prefix, module.aurora_postgres.outputs.cluster_identifier) + ssm_password_source = length(var.ssm_password_source) > 0 ? var.ssm_password_source : format("%s/%s", local.ssm_path_prefix, "%s/password") + + kms_key_arn = module.aurora_postgres.outputs.kms_key_arn + + default_schema_owner = "postgres" } -data "aws_ssm_parameter" "admin_password" { - count = local.enabled ? 1 : 0 +data "aws_ssm_parameter" "password" { + for_each = local.password_users_to_fetch + + name = format(local.ssm_password_source, each.key) - name = local.admin_password_ssm_parameter with_decryption = true } + +resource "postgresql_database" "additional" { + for_each = local.enabled ? var.additional_databases : [] + + name = each.key +} + +resource "postgresql_schema" "additional" { + for_each = local.enabled ? var.additional_schemas : {} + + name = each.key + database = try(each.value.database, null) # If null, the database used by your provider configuration +} + +module "additional_users" { + for_each = local.enabled ? var.additional_users : {} + source = "./modules/postgresql-user" + + service_name = each.key + db_user = each.value.db_user + db_password = each.value.db_password + grants = each.value.grants + ssm_path_prefix = local.ssm_path_prefix + kms_key_id = local.kms_key_arn + + depends_on = [ + postgresql_database.additional, + postgresql_schema.additional, + ] + + context = module.this.context +} + +module "additional_grants" { + for_each = var.additional_grants + source = "./modules/postgresql-user" + + service_name = each.key + grants = each.value + kms_key_id = local.kms_key_arn + + # If `read_passwords_from_ssm` is true, that means passwords already exist in SSM + # If no password is given, a random password will be created + db_password = local.read_passwords_from_ssm ? data.aws_ssm_parameter.password[each.key].value : "" + # If generating a password, store it in SSM. Otherwise, we don't need to save an existing password in SSM + save_password_in_ssm = local.read_passwords_from_ssm ? false : true + ssm_path_prefix = local.ssm_path_prefix + + depends_on = [ + postgresql_database.additional, + postgresql_schema.additional, + ] + + context = module.this.context +} + diff --git a/modules/aurora-postgres-resources/modules/postgresql-user/main.tf b/modules/aurora-postgres-resources/modules/postgresql-user/main.tf index e4db84910..3332bc0f5 100644 --- a/modules/aurora-postgres-resources/modules/postgresql-user/main.tf +++ b/modules/aurora-postgres-resources/modules/postgresql-user/main.tf @@ -1,12 +1,33 @@ locals { - db_user = length(var.db_user) > 0 ? var.db_user : var.service_name - db_password = length(var.db_password) > 0 ? var.db_password : join("", random_password.db_password.*.result) - db_user_key = format("%s/%s/%s", var.ssm_path_prefix, var.service_name, "db_user") - db_password_key = format("%s/%s/%s", var.ssm_path_prefix, var.service_name, "db_password") + enabled = module.this.enabled + + db_user = length(var.db_user) > 0 ? var.db_user : var.service_name + db_password = length(var.db_password) > 0 ? var.db_password : join("", random_password.db_password.*.result) + + save_password_in_ssm = local.enabled && var.save_password_in_ssm + create_db_user = local.enabled && var.service_name != local.db_user + + db_password_key = format("%s/%s/passwords/%s", var.ssm_path_prefix, var.service_name, local.db_user) + db_password_ssm = local.save_password_in_ssm ? { + name = local.db_password_key + value = local.db_password + description = "Postgres Password for DB user ${local.db_user}" + type = "SecureString" + overwrite = true + } : null + + parameter_write = (local.create_db_user && local.save_password_in_ssm) ? [local.db_password_ssm] : [] + + # ALL grant always shows Terraform drift: + # https://github.com/cyrilgdn/terraform-provider-postgresql/issues/32 + # To workaround, expand what an ALL grant means for db or table + # https://github.com/cyrilgdn/terraform-provider-postgresql/blob/master/postgresql/helpers.go#L237-L244 + all_privileges_database = ["CREATE", "CONNECT", "TEMPORARY"] + all_privileges_schema = ["CREATE", "USAGE"] } resource "random_password" "db_password" { - count = var.enabled && length(var.db_password) == 0 ? 1 : 0 + count = local.enabled && length(var.db_password) == 0 ? 1 : 0 length = 33 special = false @@ -16,7 +37,7 @@ resource "random_password" "db_password" { } resource "postgresql_role" "default" { - count = var.enabled ? 1 : 0 + count = local.enabled ? 1 : 0 name = local.db_user password = local.db_password login = true @@ -24,30 +45,26 @@ resource "postgresql_role" "default" { # Apply the configured grants to the user resource "postgresql_grant" "default" { - count = var.enabled ? length(var.grants) : 0 + count = local.enabled ? length(var.grants) : 0 role = join("", postgresql_role.default.*.name) database = var.grants[count.index].db schema = var.grants[count.index].schema object_type = var.grants[count.index].object_type - privileges = var.grants[count.index].grant -} -resource "aws_ssm_parameter" "db_user" { - count = var.enabled ? 1 : 0 - name = local.db_user_key - value = local.db_user - description = "PostgreSQL Username (role) created by this module" - type = "String" - overwrite = true - tags = module.this.tags + # Conditionally set the privileges to either the explicit list of database privileges + # or schema privileges if this is a db grant or a schema grant respectively. + # We can determine this is a schema grant if a schema is given + privileges = contains(var.grants[count.index].grant, "ALL") ? ((length(var.grants[count.index].schema) > 0) ? local.all_privileges_schema : local.all_privileges_database) : var.grants[count.index].grant } -resource "aws_ssm_parameter" "db_password" { - count = var.enabled ? 1 : 0 - name = local.db_password_key - value = local.db_password - description = "PostgreSQL Password for the PostreSQL User (role) created by this module" - type = "SecureString" - overwrite = true - tags = module.this.tags +module "parameter_store_write" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.10.0" + + # kms_arn will only be used for SecureString parameters + kms_arn = var.kms_key_id # not necessarily ARN — alias works too + + parameter_write = local.parameter_write + + context = module.this.context } diff --git a/modules/aurora-postgres-resources/modules/postgresql-user/outputs.tf b/modules/aurora-postgres-resources/modules/postgresql-user/outputs.tf index 642e9268c..10bcebbce 100644 --- a/modules/aurora-postgres-resources/modules/postgresql-user/outputs.tf +++ b/modules/aurora-postgres-resources/modules/postgresql-user/outputs.tf @@ -8,11 +8,6 @@ output "db_user" { description = "DB user name" } -output "db_user_ssm_key" { - value = local.db_user_key - description = "SSM key under which user name is stored" -} - output "db_user_password" { value = local.db_password description = "DB user password" diff --git a/modules/aurora-postgres-resources/modules/postgresql-user/variables.tf b/modules/aurora-postgres-resources/modules/postgresql-user/variables.tf index 811b477a2..60e8c663e 100644 --- a/modules/aurora-postgres-resources/modules/postgresql-user/variables.tf +++ b/modules/aurora-postgres-resources/modules/postgresql-user/variables.tf @@ -19,13 +19,13 @@ variable "grants" { type = list(object({ grant : list(string) db : string - schema : string + schema : optional(string, "") object_type : string })) description = <<-EOT - List of { grant: [, , ...], db: "db", schema: null, object_type: "database"}. + List of { grant: [, , ...], db: "db", schema: "", object_type: "database"}. EOT - default = [{ grant : ["ALL"], db : "*", schema : null, object_type : "database" }] + default = [{ grant : ["ALL"], db : "*", schema : "", object_type : "database" }] } variable "ssm_path_prefix" { @@ -33,3 +33,16 @@ variable "ssm_path_prefix" { default = "aurora-postgres" description = "SSM path prefix (without leading or trailing slash)" } + +variable "save_password_in_ssm" { + type = bool + default = true + description = "If true, DB user's password will be stored in SSM" +} + +variable "kms_key_id" { + type = string + default = "alias/aws/rds" + description = "KMS key ID, ARN, or alias to use for encrypting the database" +} + diff --git a/modules/aurora-postgres-resources/modules/postgresql-user/versions.tf b/modules/aurora-postgres-resources/modules/postgresql-user/versions.tf index 37e88dc0a..6b2f61ae6 100644 --- a/modules/aurora-postgres-resources/modules/postgresql-user/versions.tf +++ b/modules/aurora-postgres-resources/modules/postgresql-user/versions.tf @@ -1,18 +1,18 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.0" - } - postgresql = { - source = "cyrilgdn/postgresql" - version = ">= 1.11.2" + version = ">= 4.9.0" } random = { source = "hashicorp/random" - version = ">= 3.0" + version = ">= 2.3" + } + postgresql = { + source = "cyrilgdn/postgresql" + version = ">= 1.17.1" } } } diff --git a/modules/aurora-postgres-resources/outputs.tf b/modules/aurora-postgres-resources/outputs.tf index 486fbbb6a..885e61f76 100644 --- a/modules/aurora-postgres-resources/outputs.tf +++ b/modules/aurora-postgres-resources/outputs.tf @@ -3,12 +3,18 @@ output "additional_users" { description = "Additional users" } -output "read_only_users" { - value = local.enabled ? local.sanitized_ro_users : null - description = "Read-only users" -} - output "additional_databases" { value = local.enabled ? values(postgresql_database.additional)[*].name : null description = "Additional databases" } + +output "additional_schemas" { + value = local.enabled ? values(postgresql_schema.additional)[*].name : null + description = "Additional schemas" +} + +output "additional_grants" { + value = keys(module.additional_grants) + description = "Additional grants" +} + diff --git a/modules/aurora-postgres-resources/provider-postgres.tf b/modules/aurora-postgres-resources/provider-postgres.tf new file mode 100644 index 000000000..f2bf51e34 --- /dev/null +++ b/modules/aurora-postgres-resources/provider-postgres.tf @@ -0,0 +1,22 @@ +locals { + cluster_endpoint = module.aurora_postgres.outputs.config_map.endpoint + admin_user = module.aurora_postgres.outputs.config_map.username + admin_password_key = module.aurora_postgres.outputs.config_map.password_ssm_key + + admin_password = length(var.admin_password) > 0 ? var.admin_password : data.aws_ssm_parameter.admin_password[0].value +} + +data "aws_ssm_parameter" "admin_password" { + count = length(var.admin_password) > 0 ? 0 : 1 + + name = local.admin_password_key + + with_decryption = true +} + +provider "postgresql" { + host = local.cluster_endpoint + username = local.admin_user + password = local.admin_password + superuser = false +} diff --git a/modules/aurora-postgres-resources/providers.tf b/modules/aurora-postgres-resources/providers.tf index 1ad8d232e..08ee01b2a 100644 --- a/modules/aurora-postgres-resources/providers.tf +++ b/modules/aurora-postgres-resources/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { @@ -26,10 +27,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - -provider "postgresql" { - host = local.cluster_endpoint - username = local.admin_user - password = local.admin_password - superuser = false -} diff --git a/modules/aurora-postgres-resources/read-only-user.tf b/modules/aurora-postgres-resources/read-only-user.tf deleted file mode 100644 index 8feb2a691..000000000 --- a/modules/aurora-postgres-resources/read-only-user.tf +++ /dev/null @@ -1,128 +0,0 @@ -locals { - cluster_ro_user = "cluster_ro" - - all_databases = local.enabled ? toset(compact(concat([local.database_name], tolist(var.additional_databases)))) : [] - - all_db_ro_grants = { for db in local.all_databases : db => [ - { - grant : ["CONNECT"] - db : db - schema : null - object_type : "database" - }, - { - grant : ["USAGE"] - db : db - schema : "public" - object_type : "schema" - }, - { - grant : ["SELECT"] - db : db - schema : "public" - object_type : "table" - }, - ] } - - # Need a placeholder for the derived admin_user so that we can use users_map in for_each - admin_user_placeholder = "+ADMIN_USER+" - - user_dbs = merge({ for service, v in var.additional_users : v.db_user => distinct([for g in v.grants : g.db if g.object_type == "database"]) }, - { (local.admin_user_placeholder) = local.all_databases }) - - users_map = merge(flatten([for u, dbs in local.user_dbs : { for db in dbs : "${db}_${u}" => { - user = u - db = db - } - if local.enabled - }])...) - - # all_users_map = merge(local.users_map, { - # for db in local.all_databases : "${local.cluster_ro_user}_${db}" => { - # user = local.cluster_ro_user - # db = db - # } - # }) - - read_only_users = local.enabled ? merge(module.read_only_db_users, - { cluster = module.read_only_cluster_user[0] }) : {} - - sanitized_ro_users = { for k, v in local.read_only_users : k => { for kk, vv in v : kk => vv if kk != "db_user_password" } } -} - -module "read_only_db_users" { - source = "./modules/postgresql-user" - - for_each = local.all_db_ro_grants - - enabled = local.enabled - - service_name = each.key - db_user = "${each.key}_ro" - db_password = "" - ssm_path_prefix = join("/", compact([local.ssm_path_prefix, local.cluster_name, "read-only"])) - - grants = each.value - - context = module.this.context - - depends_on = [ - postgresql_database.additional - ] -} - -module "read_only_cluster_user" { - source = "./modules/postgresql-user" - - count = local.enabled ? 1 : 0 - - enabled = local.enabled - service_name = "cluster" - db_user = local.cluster_ro_user - db_password = "" - ssm_path_prefix = join("/", compact([local.ssm_path_prefix, local.cluster_name, "read-only"])) - - grants = flatten(values(local.all_db_ro_grants)) - - context = module.this.context - - depends_on = [ - postgresql_database.additional - ] -} - -resource "postgresql_default_privileges" "read_only_tables_users" { - for_each = local.users_map - - role = "${each.value.db}_ro" - database = each.value.db - schema = "public" - - owner = each.value.user == local.admin_user_placeholder ? local.admin_user : each.value.user - object_type = "table" - privileges = ["SELECT"] - - depends_on = [ - module.read_only_db_users, - module.read_only_cluster_user, - postgresql_database.additional - ] -} - -resource "postgresql_default_privileges" "read_only_tables_cluster" { - for_each = local.users_map - - role = local.cluster_ro_user - database = each.value.db - schema = "public" - - owner = each.value.user == local.admin_user_placeholder ? local.admin_user : each.value.user - object_type = "table" - privileges = ["SELECT"] - - depends_on = [ - module.read_only_db_users, - module.read_only_cluster_user, - postgresql_database.additional - ] -} diff --git a/modules/aurora-postgres-resources/remote-state.tf b/modules/aurora-postgres-resources/remote-state.tf index c85882274..6ffd77e85 100644 --- a/modules/aurora-postgres-resources/remote-state.tf +++ b/modules/aurora-postgres-resources/remote-state.tf @@ -1,6 +1,6 @@ module "aurora_postgres" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.3.1" component = var.aurora_postgres_component_name diff --git a/modules/aurora-postgres-resources/variables.tf b/modules/aurora-postgres-resources/variables.tf index 56193b877..a1fcfc76d 100644 --- a/modules/aurora-postgres-resources/variables.tf +++ b/modules/aurora-postgres-resources/variables.tf @@ -6,12 +6,51 @@ variable "region" { variable "aurora_postgres_component_name" { type = string description = "Aurora Postgres component name to read the remote state from" + default = "aurora-postgres" +} + +variable "read_passwords_from_ssm" { + type = bool + default = true + description = "When `true`, fetch user passwords from SSM" +} + +variable "ssm_path_prefix" { + type = string + default = "aurora-postgres" + description = "SSM path prefix" +} + +variable "ssm_password_source" { + type = string + default = "" + description = <<-EOT + If var.read_passwords_from_ssm is true, DB user passwords will be retrieved from SSM using `var.ssm_password_source` and the database username. If this value is not set, a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. + EOT +} + +variable "admin_password" { + type = string + description = "postgresql password for the admin user" + default = "" +} + +variable "db_name" { + type = string + description = "Database name (default is not to create a database)" + default = "" +} + +variable "cluster_enabled" { + type = string + default = true + description = "Set to `false` to prevent the module from creating any resources" } variable "additional_databases" { type = set(string) default = [] - description = "Define additional databases to create." + description = "Additional databases to be created with the cluster" } variable "additional_users" { @@ -27,5 +66,36 @@ variable "additional_users" { })) })) default = {} - description = "Define additional users to create." + description = <<-EOT + Create additional database user for a service, specifying username, grants, and optional password. + If no password is specified, one will be generated. Username and password will be stored in + SSM parameter store under the service's key. + EOT +} + +variable "additional_grants" { + # map key is user name + type = map(list(object({ + grant : list(string) + db : string + }))) + default = {} + description = <<-EOT + Create additional database user with specified grants. + If `var.ssm_password_source` is set, passwords will be retrieved from SSM parameter store, + otherwise, passwords will be generated and stored in SSM parameter store under the service's key. + EOT } + +variable "additional_schemas" { + # Map key is the name of the schema + type = map(object({ + database : string + })) + default = {} + description = <<-EOT + Create additonal schemas for a given database. + If no database is given, the schema will use the database used by the provider configuration + EOT +} + diff --git a/modules/aurora-postgres-resources/versions.tf b/modules/aurora-postgres-resources/versions.tf index 30c0338b1..1911bc9af 100644 --- a/modules/aurora-postgres-resources/versions.tf +++ b/modules/aurora-postgres-resources/versions.tf @@ -1,14 +1,14 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.0" + version = ">= 4.9.0" } postgresql = { source = "cyrilgdn/postgresql" - version = ">= 1.11.2" + version = ">= 1.17.1" } } } diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 8d9ff6020..fda87583a 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -3,63 +3,81 @@ This component is responsible for provisioning Aurora Postgres RDS clusters. It seeds relevant database information (hostnames, username, password, etc.) into AWS SSM Parameter Store. -**NOTE**: Creating additional users (including read-only users) and databases -requires Spacelift, since that action to be done via the postgresql provider, -and by default only the automation account is whitelisted by the Aurora cluster. - ## Usage **Stack Level**: Regional Here's an example for how to use this component. -`stacks/catalog/aurora/defaults.yaml` file (base component for all Aurora Postgres clusters with default settings): +`stacks/catalog/aurora-postgres/defaults.yaml` file (base component for all Aurora Postgres clusters with default settings): ```yaml components: terraform: - aurora-postgres: + aurora-postgres/defaults: + metadata: + type: abstract vars: - instance_type: db.r5.large - cluster_size: 1 + enabled: true + name: aurora-postgres + tags: + Team: sre + Service: aurora-postgres + cluster_name: shared + deletion_protection: false + storage_encrypted: true engine: aurora-postgresql - cluster_family: aurora-postgresql12 - engine_version: 12.4 engine_mode: provisioned - iam_database_authentication_enabled: false - deletion_protection: true - storage_encrypted: true - database_name: "" - admin_user: "" - admin_password: "" - read_only_users_enabled: false + # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Updates.20180305.html + # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.PostgreSQL.html + # aws rds describe-db-engine-versions --engine aurora-postgresql --query 'DBEngineVersions[].EngineVersion' + engine_version: "13.4" + # engine and cluster family are notoriously hard to find. + # If you know the engine version (example here is "12.4"), use Engine and DBParameterGroupFamily from: + # aws rds describe-db-engine-versions --engine aurora-postgresql --query "DBEngineVersions[]" | \ + # jq '.[] | select(.EngineVersion == "12.4") | + # { Engine: .Engine, EngineVersion: .EngineVersion, DBParameterGroupFamily: .DBParameterGroupFamily }' + cluster_family: aurora-postgresql13 + # 1 writer, 1 reader + cluster_size: 1 + admin_user: postgres + admin_password: "" # generate random password + database_name: postgres + database_port: 5432 + # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.DBInstanceClass.html + instance_type: db.t3.medium + skip_final_snapshot: false + # Enhanced Monitoring + # A boolean flag to enable/disable the creation of the enhanced monitoring IAM role. + # If set to false, the module will not create a new role and will use rds_monitoring_role_arn for enhanced monitoring + enhanced_monitoring_role_enabled: true + # The interval, in seconds, between points when enhanced monitoring metrics are collected for the DB instance. + # To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. Valid Values: 0, 1, 5, 10, 15, 30, 60 + rds_monitoring_interval: 15 + # Allow ingress from the following accounts + # If any of tenant, stage, or environment aren't given, this will be taken + allow_ingress_from_vpc_accounts: + - tenant: core + stage: auto + ``` Example (not actual) `stacks/uw2-dev.yaml` file (override the default settings for the cluster in the `dev` account, create an additional database and user): ```yaml import: - - catalog/aurora/defaults + - catalog/aurora-postgres/defaults components: terraform: aurora-postgres: + metadata: + component: aurora-postgres + inherits: + - aurora-postgres/defaults vars: - instance_type: db.r5.large - cluster_size: 1 - cluster_name: main - database_name: main - additional_databases: - - example_db - additional_users: - example_service: - db_user: example_user - db_password: "" - grants: - - grant: [ "ALL" ] - db: example_db - object_type: database - schema: null + enabled: true + ``` @@ -67,60 +85,54 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 3.0 | -| [postgresql](#requirement\_postgresql) | >= 1.14.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [postgresql](#requirement\_postgresql) | >= 1.17.1 | | [random](#requirement\_random) | >= 2.3 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 3.0 | -| [postgresql](#provider\_postgresql) | >= 1.14.0 | +| [aws](#provider\_aws) | >= 4.9.0 | | [random](#provider\_random) | >= 2.3 | ## Modules | Name | Source | Version | |------|--------|---------| -| [additional\_users](#module\_additional\_users) | ./modules/postgresql-user | n/a | -| [aurora\_postgres\_cluster](#module\_aurora\_postgres\_cluster) | cloudposse/rds-cluster/aws | 0.47.2 | +| [aurora\_postgres\_cluster](#module\_aurora\_postgres\_cluster) | cloudposse/rds-cluster/aws | 1.3.2 | | [cluster](#module\_cluster) | cloudposse/label/null | 0.25.0 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.0 | -| [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.8.3 | -| [read\_only\_cluster\_user](#module\_read\_only\_cluster\_user) | ./modules/postgresql-user | n/a | -| [read\_only\_db\_users](#module\_read\_only\_db\_users) | ./modules/postgresql-user | n/a | +| [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | +| [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | -| [vpc\_spacelift](#module\_vpc\_spacelift) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | ## Resources | Name | Type | |------|------| -| [postgresql_database.additional](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/database) | resource | -| [postgresql_default_privileges.read_only_tables_cluster](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/default_privileges) | resource | -| [postgresql_default_privileges.read_only_tables_users](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/default_privileges) | resource | | [random_password.admin_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [random_pet.admin_user](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | | [random_pet.database_name](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.kms_key_rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_ssm_parameter.password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [additional\_databases](#input\_additional\_databases) | n/a | `set(string)` | `[]` | no | +| [additional\_databases](#input\_additional\_databases) | Additional databases to be created with the cluster | `set(string)` | `[]` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [additional\_users](#input\_additional\_users) | n/a |
map(object({
db_user : string
db_password : string
grants : list(object({
grant : list(string)
db : string
schema : string
object_type : string
}))
}))
| `{}` | no | | [admin\_password](#input\_admin\_password) | Postgres password for the admin user | `string` | `""` | no | | [admin\_user](#input\_admin\_user) | Postgres admin user name | `string` | `""` | no | +| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.
e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
} |
list(object({
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDRs allowed to access the database (in addition to security groups and subnets) | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [autoscaling\_enabled](#input\_autoscaling\_enabled) | Whether to enable cluster autoscaling | `bool` | `false` | no | @@ -142,6 +154,8 @@ components: | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_gbl\_delegated\_environment\_name](#input\_dns\_gbl\_delegated\_environment\_name) | The name of the environment where global `dns_delegated` is provisioned | `string` | `"gbl"` | no | +| [eks\_component\_names](#input\_eks\_component\_names) | The names of the eks components | `set(string)` |
[
"eks/cluster"
]
| no | +| [eks\_security\_group\_enabled](#input\_eks\_security\_group\_enabled) | Use the eks default security group | `bool` | `false` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [enabled\_cloudwatch\_logs\_exports](#input\_enabled\_cloudwatch\_logs\_exports) | List of log types to export to cloudwatch. The following log types are supported: audit, error, general, slowquery | `list(string)` | `[]` | no | | [engine](#input\_engine) | Name of the database engine to be used for the DB cluster | `string` | `"postgresql"` | no | @@ -165,30 +179,28 @@ components: | [performance\_insights\_enabled](#input\_performance\_insights\_enabled) | Whether to enable Performance Insights | `bool` | `false` | no | | [publicly\_accessible](#input\_publicly\_accessible) | Set true to make this database accessible from the public internet | `bool` | `false` | no | | [rds\_monitoring\_interval](#input\_rds\_monitoring\_interval) | The interval, in seconds, between points when enhanced monitoring metrics are collected for the DB instance. To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. Valid Values: 0, 1, 5, 10, 15, 30, 60 | `number` | `60` | no | -| [read\_only\_users\_enabled](#input\_read\_only\_users\_enabled) | Set `true` to automatically create read-only users for every database | `bool` | `false` | no | | [reader\_dns\_name\_part](#input\_reader\_dns\_name\_part) | Part of DNS name added to module and cluster name for DNS for cluster reader | `string` | `"reader"` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Normally AWS makes a snapshot of the database before deleting it. Set this to `true` in order to skip this.
NOTE: The final snapshot has a name derived from the cluster name. If you delete a cluster, get a final snapshot,
then create a cluster of the same name, its final snapshot will fail with a name collision unless you delete
the previous final snapshot first. | `bool` | `false` | no | | [snapshot\_identifier](#input\_snapshot\_identifier) | Specifies whether or not to create this cluster from a snapshot | `string` | `null` | no | +| [ssm\_password\_source](#input\_ssm\_password\_source) | If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using
`var.ssm_password_source` and the database username. If this value is not set,
a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. | `string` | `""` | no | | [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | Top level SSM path prefix (without leading or trailing slash) | `string` | `"aurora-postgres"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [storage\_encrypted](#input\_storage\_encrypted) | Specifies whether the DB cluster is encrypted | `bool` | `true` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [vpc\_spacelift\_stage\_name](#input\_vpc\_spacelift\_stage\_name) | The name of the stage of the `vpc` component where `spacelift-worker-pool` is provisioned | `string` | `"auto"` | no | ## Outputs | Name | Description | |------|-------------| -| [additional\_users](#output\_additional\_users) | Information about additional DB users created by request | | [admin\_username](#output\_admin\_username) | Postgres admin username | | [cluster\_identifier](#output\_cluster\_identifier) | Postgres cluster identifier | | [config\_map](#output\_config\_map) | Map containing information pertinent to a PostgreSQL client configuration. | | [database\_name](#output\_database\_name) | Postgres database name | +| [kms\_key\_arn](#output\_kms\_key\_arn) | KMS key ARN for Aurora Postgres | | [master\_hostname](#output\_master\_hostname) | Postgres master hostname | -| [read\_only\_users](#output\_read\_only\_users) | List of read only users. | | [replicas\_hostname](#output\_replicas\_hostname) | Postgres replicas hostname | | [ssm\_key\_paths](#output\_ssm\_key\_paths) | Names (key paths) of all SSM parameters stored for this cluster | diff --git a/modules/aurora-postgres/cluster-regional.tf b/modules/aurora-postgres/cluster-regional.tf index 4a3a6ba91..abdf00214 100644 --- a/modules/aurora-postgres/cluster-regional.tf +++ b/modules/aurora-postgres/cluster-regional.tf @@ -2,15 +2,10 @@ # This means that explicit provider blocks appear only in the root module, and downstream modules can simply # declare resources for that provider and have them automatically associated with the root provider configurations -locals { - additional_users = local.enabled ? var.additional_users : {} - sanitized_additional_users = { for k, v in module.additional_users : k => { for kk, vv in v : kk => vv if kk != "db_user_password" } } -} - # https://www.terraform.io/docs/providers/aws/r/rds_cluster.html module "aurora_postgres_cluster" { source = "cloudposse/rds-cluster/aws" - version = "0.47.2" + version = "1.3.2" cluster_type = "regional" engine = var.engine @@ -31,7 +26,7 @@ module "aurora_postgres_cluster" { cluster_dns_name = local.cluster_dns_name reader_dns_name = local.reader_dns_name security_groups = local.allowed_security_groups - allowed_cidr_blocks = concat(var.allowed_cidr_blocks, [module.vpc_spacelift.outputs.vpc_cidr]) + allowed_cidr_blocks = local.allowed_cidr_blocks iam_database_authentication_enabled = var.iam_database_authentication_enabled storage_encrypted = var.storage_encrypted kms_key_arn = var.storage_encrypted ? module.kms_key_rds.key_arn : null @@ -68,22 +63,3 @@ module "aurora_postgres_cluster" { context = module.cluster.context } - -resource "postgresql_database" "additional" { - for_each = module.this.enabled ? var.additional_databases : [] - name = each.key - depends_on = [module.aurora_postgres_cluster.cluster_identifier] -} - -module "additional_users" { - for_each = local.additional_users - source = "./modules/postgresql-user" - - service_name = each.key - db_user = each.value.db_user - db_password = each.value.db_password - grants = each.value.grants - ssm_path_prefix = local.ssm_path_prefix - - depends_on = [module.aurora_postgres_cluster.cluster_identifier] -} diff --git a/modules/aurora-postgres/default.auto.tfvars b/modules/aurora-postgres/default.auto.tfvars deleted file mode 100644 index 8e0ec92c3..000000000 --- a/modules/aurora-postgres/default.auto.tfvars +++ /dev/null @@ -1,25 +0,0 @@ -name = "pg" - -enabled = false - -deletion_protection = true - -storage_encrypted = true - -engine = "aurora-postgresql" - -engine_mode = "provisioned" - -# https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Updates.20180305.html -# https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.PostgreSQL.html -# aws rds describe-db-engine-versions --engine aurora-postgresql -# If you know the engine version (example here is "12.4"), use Engine and DBParameterGroupFamily from: -# aws rds describe-db-engine-versions --query "DBEngineVersions[]" | \ -# jq '.[] | select(.EngineVersion == "12.4") | -# { Engine: .Engine, EngineVersion: .EngineVersion, DBParameterGroupFamily: .DBParameterGroupFamily }' -# For Aurora Postgres 12.4: -# engine: "postgresql" -# cluster_family: "aurora-postgresql12" -engine_version = "13.4" - -cluster_family = "aurora-postgresql13" diff --git a/modules/aurora-postgres/kms.tf b/modules/aurora-postgres/kms.tf index 08d4d40cf..74af4621d 100644 --- a/modules/aurora-postgres/kms.tf +++ b/modules/aurora-postgres/kms.tf @@ -1,6 +1,6 @@ module "kms_key_rds" { source = "cloudposse/kms-key/aws" - version = "0.12.0" + version = "0.12.1" description = "KMS key for Aurora Postgres" deletion_window_in_days = 10 diff --git a/modules/aurora-postgres/main.tf b/modules/aurora-postgres/main.tf index 4e42525f1..ea99b06cd 100644 --- a/modules/aurora-postgres/main.tf +++ b/modules/aurora-postgres/main.tf @@ -1,9 +1,14 @@ locals { enabled = module.this.enabled - vpc_id = module.vpc.outputs.vpc_id - private_subnet_ids = module.vpc.outputs.private_subnet_ids - allowed_security_groups = [module.eks.outputs.eks_cluster_managed_security_group_id] + vpc_id = module.vpc.outputs.vpc_id + private_subnet_ids = module.vpc.outputs.private_subnet_ids + + eks_security_group_enabled = local.enabled && var.eks_security_group_enabled + allowed_security_groups = [ + for eks in module.eks : + eks.outputs.eks_cluster_managed_security_group_id + ] zone_id = module.dns_gbl_delegated.outputs.default_dns_zone_id @@ -15,8 +20,13 @@ locals { cluster_dns_name = format("%v%v", local.cluster_dns_name_prefix, var.cluster_dns_name_part) reader_dns_name = format("%v%v", local.cluster_dns_name_prefix, var.reader_dns_name_part) - ssm_path_prefix = format("/%s/%s", var.ssm_path_prefix, module.cluster.id) - ssm_cluster_key_prefix = format("%s/%s", local.ssm_path_prefix, "cluster") + allowed_cidr_blocks = concat( + var.allowed_cidr_blocks, + [ + for k in keys(module.vpc_ingress) : + module.vpc_ingress[k].outputs.vpc_cidr + ] + ) } module "cluster" { diff --git a/modules/aurora-postgres/modules/postgresql-user/context.tf b/modules/aurora-postgres/modules/postgresql-user/context.tf deleted file mode 100644 index 5e0ef8856..000000000 --- a/modules/aurora-postgres/modules/postgresql-user/context.tf +++ /dev/null @@ -1,279 +0,0 @@ -# -# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label -# All other instances of this file should be a copy of that one -# -# -# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf -# and then place it in your Terraform module to automatically get -# Cloud Posse's standard configuration inputs suitable for passing -# to Cloud Posse modules. -# -# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf -# -# Modules should access the whole context as `module.this.context` -# to get the input variables with nulls for defaults, -# for example `context = module.this.context`, -# and access individual variables as `module.this.`, -# with final values filled in. -# -# For example, when using defaults, `module.this.context.delimiter` -# will be null, and `module.this.delimiter` will be `-` (hyphen). -# - -module "this" { - source = "cloudposse/label/null" - version = "0.25.0" # requires Terraform >= 0.13.0 - - enabled = var.enabled - namespace = var.namespace - tenant = var.tenant - environment = var.environment - stage = var.stage - name = var.name - delimiter = var.delimiter - attributes = var.attributes - tags = var.tags - additional_tag_map = var.additional_tag_map - label_order = var.label_order - regex_replace_chars = var.regex_replace_chars - id_length_limit = var.id_length_limit - label_key_case = var.label_key_case - label_value_case = var.label_value_case - descriptor_formats = var.descriptor_formats - labels_as_tags = var.labels_as_tags - - context = var.context -} - -# Copy contents of cloudposse/terraform-null-label/variables.tf here - -variable "context" { - type = any - default = { - enabled = true - namespace = null - tenant = null - environment = null - stage = null - name = null - delimiter = null - attributes = [] - tags = {} - additional_tag_map = {} - regex_replace_chars = null - label_order = [] - id_length_limit = null - label_key_case = null - label_value_case = null - descriptor_formats = {} - # Note: we have to use [] instead of null for unset lists due to - # https://github.com/hashicorp/terraform/issues/28137 - # which was not fixed until Terraform 1.0.0, - # but we want the default to be all the labels in `label_order` - # and we want users to be able to prevent all tag generation - # by setting `labels_as_tags` to `[]`, so we need - # a different sentinel to indicate "default" - labels_as_tags = ["unset"] - } - description = <<-EOT - Single object for setting entire context at once. - See description of individual variables for details. - Leave string and numeric variables as `null` to use default value. - Individual variable settings (non-null) override settings in context object, - except for attributes, tags, and additional_tag_map, which are merged. - EOT - - validation { - condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) - error_message = "Allowed values: `lower`, `title`, `upper`." - } - - validation { - condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) - error_message = "Allowed values: `lower`, `title`, `upper`, `none`." - } -} - -variable "enabled" { - type = bool - default = null - description = "Set to false to prevent the module from creating any resources" -} - -variable "namespace" { - type = string - default = null - description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" -} - -variable "tenant" { - type = string - default = null - description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" -} - -variable "environment" { - type = string - default = null - description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" -} - -variable "stage" { - type = string - default = null - description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" -} - -variable "name" { - type = string - default = null - description = <<-EOT - ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. - This is the only ID element not also included as a `tag`. - The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. - EOT -} - -variable "delimiter" { - type = string - default = null - description = <<-EOT - Delimiter to be used between ID elements. - Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. - EOT -} - -variable "attributes" { - type = list(string) - default = [] - description = <<-EOT - ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, - in the order they appear in the list. New attributes are appended to the - end of the list. The elements of the list are joined by the `delimiter` - and treated as a single ID element. - EOT -} - -variable "labels_as_tags" { - type = set(string) - default = ["default"] - description = <<-EOT - Set of labels (ID elements) to include as tags in the `tags` output. - Default is to include all labels. - Tags with empty values will not be included in the `tags` output. - Set to `[]` to suppress all generated tags. - **Notes:** - The value of the `name` tag, if included, will be the `id`, not the `name`. - Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be - changed in later chained modules. Attempts to change it will be silently ignored. - EOT -} - -variable "tags" { - type = map(string) - default = {} - description = <<-EOT - Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). - Neither the tag keys nor the tag values will be modified by this module. - EOT -} - -variable "additional_tag_map" { - type = map(string) - default = {} - description = <<-EOT - Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. - This is for some rare cases where resources want additional configuration of tags - and therefore take a list of maps with tag key, value, and additional configuration. - EOT -} - -variable "label_order" { - type = list(string) - default = null - description = <<-EOT - The order in which the labels (ID elements) appear in the `id`. - Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. - EOT -} - -variable "regex_replace_chars" { - type = string - default = null - description = <<-EOT - Terraform regular expression (regex) string. - Characters matching the regex will be removed from the ID elements. - If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. - EOT -} - -variable "id_length_limit" { - type = number - default = null - description = <<-EOT - Limit `id` to this many characters (minimum 6). - Set to `0` for unlimited length. - Set to `null` for keep the existing setting, which defaults to `0`. - Does not affect `id_full`. - EOT - validation { - condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 - error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." - } -} - -variable "label_key_case" { - type = string - default = null - description = <<-EOT - Controls the letter case of the `tags` keys (label names) for tags generated by this module. - Does not affect keys of tags passed in via the `tags` input. - Possible values: `lower`, `title`, `upper`. - Default value: `title`. - EOT - - validation { - condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) - error_message = "Allowed values: `lower`, `title`, `upper`." - } -} - -variable "label_value_case" { - type = string - default = null - description = <<-EOT - Controls the letter case of ID elements (labels) as included in `id`, - set as tag values, and output by this module individually. - Does not affect values of tags passed in via the `tags` input. - Possible values: `lower`, `title`, `upper` and `none` (no transformation). - Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. - Default value: `lower`. - EOT - - validation { - condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) - error_message = "Allowed values: `lower`, `title`, `upper`, `none`." - } -} - -variable "descriptor_formats" { - type = any - default = {} - description = <<-EOT - Describe additional descriptors to be output in the `descriptors` output map. - Map of maps. Keys are names of descriptors. Values are maps of the form - `{ - format = string - labels = list(string) - }` - (Type is `any` so the map values can later be enhanced to provide additional options.) - `format` is a Terraform format string to be passed to the `format()` function. - `labels` is a list of labels, in order, to pass to `format()` function. - Label values will be normalized before being passed to `format()` so they will be - identical to how they appear in `id`. - Default is `{}` (`descriptors` output will be empty). - EOT -} - -#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/aurora-postgres/modules/postgresql-user/main.tf b/modules/aurora-postgres/modules/postgresql-user/main.tf deleted file mode 100644 index 74c79093f..000000000 --- a/modules/aurora-postgres/modules/postgresql-user/main.tf +++ /dev/null @@ -1,42 +0,0 @@ -locals { - db_user = length(var.db_user) > 0 ? var.db_user : var.service_name - db_password = length(var.db_password) > 0 ? var.db_password : join("", random_password.db_password.*.result) - db_password_key = format("%s/%s/%s/%s", var.ssm_path_prefix, var.service_name, local.db_user, "db_password") -} - -resource "random_password" "db_password" { - count = var.enabled && length(var.db_password) == 0 ? 1 : 0 - length = 33 - special = false - - keepers = { - db_user = local.db_user - } -} - -resource "postgresql_role" "default" { - count = var.enabled ? 1 : 0 - name = local.db_user - password = local.db_password - login = true -} - -# Apply the configured grants to the user -resource "postgresql_grant" "default" { - count = var.enabled ? length(var.grants) : 0 - role = join("", postgresql_role.default.*.name) - database = var.grants[count.index].db - schema = var.grants[count.index].schema - object_type = var.grants[count.index].object_type - privileges = var.grants[count.index].grant -} - -resource "aws_ssm_parameter" "db_password" { - count = var.enabled ? 1 : 0 - name = local.db_password_key - value = local.db_password - description = "PostgreSQL Password for the PostreSQL User (role) created by this module" - type = "SecureString" - overwrite = true - tags = module.this.tags -} diff --git a/modules/aurora-postgres/modules/postgresql-user/outputs.tf b/modules/aurora-postgres/modules/postgresql-user/outputs.tf deleted file mode 100644 index f21892a0f..000000000 --- a/modules/aurora-postgres/modules/postgresql-user/outputs.tf +++ /dev/null @@ -1,19 +0,0 @@ -output "notice" { - value = "Password for user ${local.db_user} is stored in SSM under ${local.db_password_key}" - description = "Note to user" -} - -output "db_user" { - value = local.db_user - description = "DB user name" -} - -output "db_user_password_ssm_key" { - value = local.db_password_key - description = "SSM key under which user password is stored" -} - -output "service_name" { - value = var.service_name - description = "Service for which this user was created" -} diff --git a/modules/aurora-postgres/modules/postgresql-user/variables.tf b/modules/aurora-postgres/modules/postgresql-user/variables.tf deleted file mode 100644 index 39426489d..000000000 --- a/modules/aurora-postgres/modules/postgresql-user/variables.tf +++ /dev/null @@ -1,35 +0,0 @@ -variable "service_name" { - type = string - description = "Name of service owning the database (used in SSM key)" -} - -variable "db_user" { - type = string - description = "PostgreSQL user name to create (default is service name)" - default = "" -} - -variable "db_password" { - type = string - description = "PostgreSQL password created user (generated if not provided)" - default = "" -} - -variable "grants" { - type = list(object({ - grant : list(string) - db : string - schema : string - object_type : string - })) - description = <<-EOT - List of { grant: [, , ...], db: "db", schema: null, object_type: "database"}. - EOT - default = [{ grant : ["ALL"], db : "*", schema : null, object_type : "database" }] -} - -variable "ssm_path_prefix" { - type = string - default = "aurora-postgres" - description = "SSM path prefix (with leading but not trailing slash, e.g. \"/rds/cluster_name\")" -} diff --git a/modules/aurora-postgres/modules/postgresql-user/versions.tf b/modules/aurora-postgres/modules/postgresql-user/versions.tf deleted file mode 100644 index ecb1ce1aa..000000000 --- a/modules/aurora-postgres/modules/postgresql-user/versions.tf +++ /dev/null @@ -1,18 +0,0 @@ -terraform { - required_version = ">= 1.0.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 3.0" - } - random = { - source = "hashicorp/random" - version = ">= 2.3" - } - postgresql = { - source = "cyrilgdn/postgresql" - version = ">= 1.14.0" - } - } -} diff --git a/modules/aurora-postgres/outputs.tf b/modules/aurora-postgres/outputs.tf index a545cd631..c28db2be9 100644 --- a/modules/aurora-postgres/outputs.tf +++ b/modules/aurora-postgres/outputs.tf @@ -6,6 +6,7 @@ output "database_name" { output "admin_username" { value = module.aurora_postgres_cluster.master_username description = "Postgres admin username" + sensitive = true } output "master_hostname" { @@ -34,13 +35,16 @@ output "config_map" { database = local.database_name hostname = module.aurora_postgres_cluster.master_host port = var.database_port + endpoint = module.aurora_postgres_cluster.endpoint username = module.aurora_postgres_cluster.master_username - password_ssm_key = format("%s/%s", local.ssm_cluster_key_prefix, "admin_password") + password_ssm_key = local.admin_password_key } description = "Map containing information pertinent to a PostgreSQL client configuration." + sensitive = true } -output "additional_users" { - value = local.sanitized_additional_users - description = "Information about additional DB users created by request" +output "kms_key_arn" { + value = module.kms_key_rds.key_arn + description = "KMS key ARN for Aurora Postgres" } + diff --git a/modules/aurora-postgres/providers.tf b/modules/aurora-postgres/providers.tf index af20eae2a..08ee01b2a 100644 --- a/modules/aurora-postgres/providers.tf +++ b/modules/aurora-postgres/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { @@ -26,10 +27,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - -provider "postgresql" { - host = module.aurora_postgres_cluster.master_host - username = module.aurora_postgres_cluster.master_username - password = local.admin_password - superuser = false -} diff --git a/modules/aurora-postgres/read-only-user.tf b/modules/aurora-postgres/read-only-user.tf deleted file mode 100644 index 1fdaeb70f..000000000 --- a/modules/aurora-postgres/read-only-user.tf +++ /dev/null @@ -1,136 +0,0 @@ -locals { - ro_users_enabled = local.enabled && var.read_only_users_enabled - ro_user_ssm_path_prefix = format("%v/read_only", local.ssm_path_prefix) - - cluster_ro_user = "cluster_ro" - all_databases = local.ro_users_enabled ? toset(compact(concat([var.database_name], tolist(var.additional_databases)))) : [] - all_db_ro_grants = { for db in local.all_databases : db => [ - { - grant : ["CONNECT"] - db : db - schema : null - object_type : "database" - }, - { - grant : ["USAGE"] - db : db - schema : "public" - object_type : "schema" - }, - { - grant : ["SELECT"] - db : db - schema : "public" - object_type : "table" - }, - ] } - - # Need a placeholder for the derived admin_user so that we can use users_map in for_each - admin_user_placeholder = "+ADMIN_USER+" - - # Map each db user to the list of databases they have access to - user_dbs = merge({ for service, v in var.additional_users : v.db_user => distinct([for g in v.grants : g.db if g.object_type == "database"]) }, - { (local.admin_user_placeholder) = local.all_databases }) - # Flatten user_dbs into a list of (db, user) pairs - users_map = merge(flatten([for u, dbs in local.user_dbs : { for db in dbs : "${db}_${u}" => { - user = u - db = db - } }])...) - - read_only_users = local.ro_users_enabled ? merge(module.read_only_db_users, - { - cluster = module.read_only_cluster_user[0] - } - ) : {} - sanitized_ro_users = { - for k, v in local.read_only_users : - k => { for kk, vv in v : kk => vv if kk != "db_user_password" } - } -} - -variable "read_only_users_enabled" { - type = bool - default = false - description = "Set `true` to automatically create read-only users for every database" -} - -module "read_only_db_users" { - for_each = local.all_db_ro_grants - source = "./modules/postgresql-user" - - service_name = each.key - db_user = "${each.key}_ro" - db_password = "" - ssm_path_prefix = local.ro_user_ssm_path_prefix - - grants = each.value - - depends_on = [ - module.aurora_postgres_cluster.cluster_identifier, - postgresql_database.additional, - ] - - context = module.this.context -} - -module "read_only_cluster_user" { - count = local.ro_users_enabled ? 1 : 0 - source = "./modules/postgresql-user" - - service_name = "cluster" - db_user = local.cluster_ro_user - db_password = "" - ssm_path_prefix = local.ro_user_ssm_path_prefix - - grants = flatten(values(local.all_db_ro_grants)) - - depends_on = [ - module.aurora_postgres_cluster.cluster_identifier, - postgresql_database.additional, - ] - - context = module.this.context -} - -# For every user with access to a database, ensure that user by default -# grants "SELECT" privileges to every table they create to the db RO user -resource "postgresql_default_privileges" "read_only_tables_users" { - for_each = local.users_map - - role = "${each.value.db}_ro" - database = each.value.db - schema = "public" - - owner = each.value.user == local.admin_user_placeholder ? local.admin_user : each.value.user - object_type = "table" - privileges = ["SELECT"] - - depends_on = [ - module.read_only_db_users, - module.read_only_cluster_user, - ] -} - -# For every user with access to a database, ensure that user by default -# grants "SELECT" privileges to every table they create to the cluster_ro user -resource "postgresql_default_privileges" "read_only_tables_cluster" { - for_each = local.users_map - - role = local.cluster_ro_user - database = each.value.db - schema = "public" - - owner = each.value.user == local.admin_user_placeholder ? local.admin_user : each.value.user - object_type = "table" - privileges = ["SELECT"] - - depends_on = [ - module.read_only_db_users, - module.read_only_cluster_user, - ] -} - -output "read_only_users" { - value = local.ro_users_enabled ? local.sanitized_ro_users : null - description = "List of read only users." -} diff --git a/modules/aurora-postgres/remote-state.tf b/modules/aurora-postgres/remote-state.tf index 69598d5b1..0551f6d60 100644 --- a/modules/aurora-postgres/remote-state.tf +++ b/modules/aurora-postgres/remote-state.tf @@ -1,34 +1,44 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.3.1" component = "vpc" context = module.cluster.context } -module "vpc_spacelift" { +module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.3.1" - component = "vpc" - stage = var.vpc_spacelift_stage_name + for_each = { + for i, account in var.allow_ingress_from_vpc_accounts : + try(account.tenant, module.this.tenant) != null ? + format("%s-%s", account.tenant, account.stage) : account.stage => account + } + + component = "vpc" + tenant = try(each.value.tenant, module.this.tenant) + environment = try(each.value.environment, module.this.environment) + stage = try(each.value.stage, module.this.stage) context = module.cluster.context } module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.3.1" - component = "eks" + for_each = local.eks_security_group_enabled ? var.eks_component_names : toset([]) + component = each.value context = module.cluster.context } + module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.3.1" component = "dns-delegated" environment = var.dns_gbl_delegated_environment_name diff --git a/modules/aurora-postgres/ssm.tf b/modules/aurora-postgres/ssm.tf index 397811354..8d006e569 100644 --- a/modules/aurora-postgres/ssm.tf +++ b/modules/aurora-postgres/ssm.tf @@ -1,81 +1,87 @@ -module "parameter_store_write" { - source = "cloudposse/ssm-parameter-store/aws" - version = "0.8.3" +locals { + fetch_admin_password = length(var.ssm_password_source) > 0 - # kms_arn will only be used for SecureString parameters - kms_arn = var.kms_alias_name_ssm # not necessarily ARN — alias works too - parameter_write = [ - { - name = format("%s/%s", local.ssm_cluster_key_prefix, "database_name") - value = local.database_name - description = "Aurora Postgres database name" - type = "String" - overwrite = true - }, - { - name = format("%s/%s", local.ssm_cluster_key_prefix, "database_port") - value = var.database_port - description = "Aurora Postgres database name" - type = "String" - overwrite = true - }, - { - name = format("%s/%s", local.ssm_cluster_key_prefix, "admin_username") - value = module.aurora_postgres_cluster.master_username - description = "Aurora Postgres admin username" - type = "String" - overwrite = true - }, + ssm_path_prefix = format("/%s/%s", var.ssm_path_prefix, module.cluster.id) + + admin_user_key = format("%s/%s/%s", local.ssm_path_prefix, "admin", "user") + admin_password_key = format("%s/%s/%s", local.ssm_path_prefix, "admin", "password") + + cluster_domain = trimprefix(module.aurora_postgres_cluster.endpoint, "${module.aurora_postgres_cluster.cluster_identifier}.cluster-") + + default_parameters = [ { - name = format("%s/%s", local.ssm_cluster_key_prefix, "admin_password") - value = local.admin_password - description = "Aurora Postgres admin password" + name = format("%s/%s", local.ssm_path_prefix, "cluster_domain") + value = local.cluster_domain + description = "AWS DNS name under which DB instances are provisioned" type = "String" overwrite = true }, { - name = format("%s/%s", local.ssm_cluster_key_prefix, "admin/db_username") - value = module.aurora_postgres_cluster.master_username - description = "Aurora Postgres Username for the admin DB user" + name = format("%s/%s", local.ssm_path_prefix, "db_host") + value = module.aurora_postgres_cluster.master_host + description = "Aurora Postgres DB Master hostname" type = "String" overwrite = true }, { - name = format("%s/%s", local.ssm_cluster_key_prefix, "admin/db_password") - value = local.admin_password - description = "Aurora Postgres Password for the admin DB user" - type = "SecureString" - overwrite = true - }, - { - name = format("%s/%s", local.ssm_cluster_key_prefix, "master_hostname") - value = module.aurora_postgres_cluster.master_host - description = "Aurora Postgres DB Master hostname" + name = format("%s/%s", local.ssm_path_prefix, "db_port") + value = var.database_port + description = "Aurora Postgres DB Master TCP port" type = "String" overwrite = true }, { - name = format("%s/%s", local.ssm_cluster_key_prefix, "replicas_hostname") + name = format("%s/%s", local.ssm_path_prefix, "replicas_hostname") value = module.aurora_postgres_cluster.replicas_host description = "Aurora Postgres DB Replicas hostname" type = "String" overwrite = true }, { - name = format("%s/%s", local.ssm_cluster_key_prefix, "cluster_id") + name = format("%s/%s", local.ssm_path_prefix, "cluster_name") value = module.aurora_postgres_cluster.cluster_identifier description = "Aurora Postgres DB Cluster Identifier" type = "String" overwrite = true - }, + } + ] + admin_user_parameters = [ { - name = format("%s/%s", local.ssm_cluster_key_prefix, "db_port") - value = var.database_port - description = "Aurora Postgres DB Master port" + name = local.admin_user_key + value = local.admin_user + description = "Aurora Postgres DB admin user" type = "String" overwrite = true + }, + { + name = local.admin_password_key + value = local.admin_password + description = "Aurora Postgres DB admin password" + type = "SecureString" + overwrite = true } ] + parameter_write = concat(local.default_parameters, local.admin_user_parameters) +} + +data "aws_ssm_parameter" "password" { + count = local.fetch_admin_password ? 1 : 0 + + name = format(var.ssm_password_source, local.admin_user) + + with_decryption = true +} + +module "parameter_store_write" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.10.0" + + # kms_arn will only be used for SecureString parameters + kms_arn = module.kms_key_rds.key_arn + + parameter_write = local.parameter_write + context = module.this.context } + diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 2cb81e38d..4eb03408c 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -146,23 +146,9 @@ variable "reader_dns_name_part" { } variable "additional_databases" { - type = set(string) - default = [] -} - -variable "additional_users" { - # map key is service name, becomes part of SSM key name - type = map(object({ - db_user : string - db_password : string - grants : list(object({ - grant : list(string) - db : string - schema : string - object_type : string - })) - })) - default = {} + type = set(string) + default = [] + description = "Additional databases to be created with the cluster" } variable "ssm_path_prefix" { @@ -267,9 +253,43 @@ variable "snapshot_identifier" { description = "Specifies whether or not to create this cluster from a snapshot" } -variable "vpc_spacelift_stage_name" { +variable "eks_security_group_enabled" { + type = bool + description = "Use the eks default security group" + default = false +} + +variable "eks_component_names" { + type = set(string) + description = "The names of the eks components" + default = ["eks/cluster"] +} + +variable "allow_ingress_from_vpc_accounts" { + type = list(object({ + environment = optional(string) + stage = optional(string) + tenant = optional(string) + })) + default = [] + description = <<-EOF + List of account contexts to pull VPC ingress CIDR and add to cluster security group. + e.g. + { + environment = "ue2", + stage = "auto", + tenant = "core" + } + EOF +} + +variable "ssm_password_source" { type = string - default = "auto" - description = "The name of the stage of the `vpc` component where `spacelift-worker-pool` is provisioned" + default = "" + description = <<-EOT + If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using + `var.ssm_password_source` and the database username. If this value is not set, + a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. + EOT } diff --git a/modules/aurora-postgres/versions.tf b/modules/aurora-postgres/versions.tf index ecb1ce1aa..6b2f61ae6 100644 --- a/modules/aurora-postgres/versions.tf +++ b/modules/aurora-postgres/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.0" + version = ">= 4.9.0" } random = { source = "hashicorp/random" @@ -12,7 +12,7 @@ terraform { } postgresql = { source = "cyrilgdn/postgresql" - version = ">= 1.14.0" + version = ">= 1.17.1" } } } From 6712629f809bfefe8f42cb039eaefeb4c9ef2c56 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 6 Feb 2023 16:52:04 -0800 Subject: [PATCH 028/501] Upstream `aurora-mysql` (#517) Co-authored-by: cloudpossebot --- modules/aurora-mysql-resources/README.md | 17 ++++---- modules/aurora-mysql-resources/main.tf | 4 +- .../modules/mysql-user/main.tf | 40 ++----------------- .../modules/mysql-user/versions.tf | 6 +-- .../aurora-mysql-resources/provider-mysql.tf | 31 ++++++++++++++ modules/aurora-mysql-resources/providers.tf | 6 --- .../aurora-mysql-resources/remote-state.tf | 2 +- modules/aurora-mysql-resources/variables.tf | 11 ++--- modules/aurora-mysql-resources/versions.tf | 9 +++-- modules/aurora-mysql/README.md | 17 ++++---- modules/aurora-mysql/cluster-regional.tf | 2 +- modules/aurora-mysql/main.tf | 12 ++++++ modules/aurora-mysql/providers.tf | 6 --- modules/aurora-mysql/remote-state.tf | 27 +++++++++++-- modules/aurora-mysql/variables.tf | 21 +++++++++- modules/aurora-mysql/versions.tf | 6 +-- 16 files changed, 122 insertions(+), 95 deletions(-) create mode 100644 modules/aurora-mysql-resources/provider-mysql.tf diff --git a/modules/aurora-mysql-resources/README.md b/modules/aurora-mysql-resources/README.md index 1f148e28f..3daec0034 100644 --- a/modules/aurora-mysql-resources/README.md +++ b/modules/aurora-mysql-resources/README.md @@ -55,15 +55,15 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | -| [mysql](#requirement\_mysql) | >= 1.9 | +| [aws](#requirement\_aws) | >= 4.0 | +| [mysql](#requirement\_mysql) | >= 3.0.22 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [mysql](#provider\_mysql) | >= 1.9 | +| [aws](#provider\_aws) | >= 4.0 | +| [mysql](#provider\_mysql) | >= 3.0.22 | ## Modules @@ -71,7 +71,7 @@ components: |------|--------|---------| | [additional\_grants](#module\_additional\_grants) | ./modules/mysql-user | n/a | | [additional\_users](#module\_additional\_users) | ./modules/mysql-user | n/a | -| [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -79,14 +79,15 @@ components: | Name | Type | |------|------| -| [mysql_database.additional](https://registry.terraform.io/providers/terraform-providers/mysql/latest/docs/resources/database) | resource | +| [mysql_database.additional](https://registry.terraform.io/providers/petoju/mysql/latest/docs/resources/database) | resource | +| [aws_ssm_parameter.admin_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [additional\_databases](#input\_additional\_databases) | n/a | `set(string)` | `[]` | no | +| [additional\_databases](#input\_additional\_databases) | Additional databases to be created with the cluster | `set(string)` | `[]` | no | | [additional\_grants](#input\_additional\_grants) | Create additional database user with specified grants.
If `var.ssm_password_source` is set, passwords will be retrieved from SSM parameter store,
otherwise, passwords will be generated and stored in SSM parameter store under the service's key. |
map(list(object({
grant : list(string)
db : string
})))
| `{}` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [additional\_users](#input\_additional\_users) | Create additional database user for a service, specifying username, grants, and optional password.
If no password is specified, one will be generated. Username and password will be stored in
SSM parameter store under the service's key. |
map(object({
db_user : string
db_password : string
grants : list(object({
grant : list(string)
db : string
}))
}))
| `{}` | no | @@ -104,7 +105,7 @@ components: | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [mysql\_admin\_password](#input\_mysql\_admin\_password) | MySQL password for the admin user | `string` | `""` | no | +| [mysql\_admin\_password](#input\_mysql\_admin\_password) | MySQL password for the admin user. If not provided, the password will be pulled from SSM | `string` | `""` | no | | [mysql\_cluster\_enabled](#input\_mysql\_cluster\_enabled) | Set to `false` to prevent the module from creating any resources | `string` | `true` | no | | [mysql\_db\_name](#input\_mysql\_db\_name) | Database name (default is not to create a database | `string` | `""` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | diff --git a/modules/aurora-mysql-resources/main.tf b/modules/aurora-mysql-resources/main.tf index 6ed495bea..6c09f8c4f 100644 --- a/modules/aurora-mysql-resources/main.tf +++ b/modules/aurora-mysql-resources/main.tf @@ -8,9 +8,7 @@ locals { ssm_path_prefix = format("/%s/%s", var.ssm_path_prefix, module.aurora_mysql.outputs.aurora_mysql_cluster_id) ssm_password_source = length(var.ssm_password_source) > 0 ? var.ssm_password_source : format("%s/%s", local.ssm_path_prefix, "%s/password") - password_users_to_fetch = local.read_passwords_from_ssm ? toset(concat(["admin"], keys(var.additional_grants))) : [] - - mysql_admin_password = length(var.mysql_admin_password) > 0 ? var.mysql_admin_password : data.aws_ssm_parameter.password["admin"].value + password_users_to_fetch = local.read_passwords_from_ssm ? toset(keys(var.additional_grants)) : [] kms_key_arn = module.aurora_mysql.outputs.kms_key_arn } diff --git a/modules/aurora-mysql-resources/modules/mysql-user/main.tf b/modules/aurora-mysql-resources/modules/mysql-user/main.tf index 53be6e03b..06f6e7f81 100644 --- a/modules/aurora-mysql-resources/modules/mysql-user/main.tf +++ b/modules/aurora-mysql-resources/modules/mysql-user/main.tf @@ -18,39 +18,11 @@ locals { parameter_write = (local.create_db_user && local.save_password_in_ssm) ? [local.db_password_ssm] : [] - # You cannot grant "ALL" to an RDS user because "ALL" includes privileges that - # Master does not have (because this is a managed database). + # You cannot grant "ALL" to an RDS user because "ALL" includes privileges that Master does not have (because this is a managed database). + # Instead, use "ALL PRIVILEGES" # See the full list of available options at https://docs.amazonaws.cn/en_us/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Security.html - # This is all the privileges an application should need. - # Privileges not listed are not available. - # Privileges commented out are dangerous or cannot be limited to 1 database and should not be needed by an app all_rds_app_grants = [ - "ALTER", - "ALTER ROUTINE", - "CREATE", - "CREATE ROUTINE", - "CREATE TEMPORARY TABLES", - # "CREATE USER", - "CREATE VIEW", - "DELETE", - "DROP", - "EVENT", - "EXECUTE", - # "GRANT OPTION", - "INDEX", - "INSERT", - "LOAD FROM S3", - "LOCK TABLES", - # "PROCESS", - "REFERENCES", - # "RELOAD", - # "REPLICATION CLIENT", - # "REPLICATION SLAVE", - "SELECT", - # "SHOW DATABASES", - "SHOW VIEW", - "TRIGGER", - "UPDATE" + "ALL PRIVILEGES" ] all_rds_other_grants = [ "CREATE USER", @@ -79,8 +51,6 @@ resource "mysql_user" "default" { user = local.db_user host = "%" plaintext_password = local.db_password - - depends_on = [var.instance_ids] } # Grant the user full access to this specific database @@ -100,10 +70,6 @@ resource "mysql_grant" "default" { )]) depends_on = [mysql_user.default] - # Apparently this is needed. See https://github.com/terraform-providers/terraform-provider-mysql/issues/55#issuecomment-615463296 - lifecycle { - create_before_destroy = true - } } module "parameter_store_write" { diff --git a/modules/aurora-mysql-resources/modules/mysql-user/versions.tf b/modules/aurora-mysql-resources/modules/mysql-user/versions.tf index 37e4284eb..c62bcb732 100644 --- a/modules/aurora-mysql-resources/modules/mysql-user/versions.tf +++ b/modules/aurora-mysql-resources/modules/mysql-user/versions.tf @@ -4,11 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } mysql = { - source = "terraform-providers/mysql" - version = ">= 1.9" + source = "petoju/mysql" + version = ">= 3.0.22" } random = { source = "hashicorp/random" diff --git a/modules/aurora-mysql-resources/provider-mysql.tf b/modules/aurora-mysql-resources/provider-mysql.tf new file mode 100644 index 000000000..43186eed9 --- /dev/null +++ b/modules/aurora-mysql-resources/provider-mysql.tf @@ -0,0 +1,31 @@ +variable "mysql_admin_password" { + type = string + description = "MySQL password for the admin user. If not provided, the password will be pulled from SSM" + default = "" +} + +locals { + cluster_endpoint = module.aurora_mysql.outputs.aurora_mysql_endpoint + + mysql_admin_user = module.aurora_mysql.outputs.aurora_mysql_master_username + mysql_admin_password_key = module.aurora_mysql.outputs.aurora_mysql_master_password_ssm_key + mysql_admin_password = length(var.mysql_admin_password) > 0 ? var.mysql_admin_password : data.aws_ssm_parameter.admin_password[0].value +} + +data "aws_ssm_parameter" "admin_password" { + count = length(var.mysql_admin_password) > 0 ? 0 : 1 + + name = local.mysql_admin_password_key + + with_decryption = true +} + +provider "mysql" { + endpoint = local.cluster_endpoint + username = local.mysql_admin_user + password = local.mysql_admin_password + + # Useful for debugging provider + # https://github.com/petoju/terraform-provider-mysql/blob/master/mysql/provider.go + connect_retry_timeout_sec = 60 +} diff --git a/modules/aurora-mysql-resources/providers.tf b/modules/aurora-mysql-resources/providers.tf index 93af053cb..08ee01b2a 100644 --- a/modules/aurora-mysql-resources/providers.tf +++ b/modules/aurora-mysql-resources/providers.tf @@ -27,9 +27,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - -provider "mysql" { - endpoint = module.aurora_mysql.outputs.aurora_mysql_endpoint - username = module.aurora_mysql.outputs.aurora_mysql_master_username - password = local.mysql_admin_password -} diff --git a/modules/aurora-mysql-resources/remote-state.tf b/modules/aurora-mysql-resources/remote-state.tf index e31385bac..d52bbe6cd 100644 --- a/modules/aurora-mysql-resources/remote-state.tf +++ b/modules/aurora-mysql-resources/remote-state.tf @@ -1,6 +1,6 @@ module "aurora_mysql" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.3.1" component = var.aurora_mysql_component_name diff --git a/modules/aurora-mysql-resources/variables.tf b/modules/aurora-mysql-resources/variables.tf index 8e864b122..62490b0c4 100644 --- a/modules/aurora-mysql-resources/variables.tf +++ b/modules/aurora-mysql-resources/variables.tf @@ -28,12 +28,6 @@ variable "ssm_password_source" { EOT } -variable "mysql_admin_password" { - type = string - description = "MySQL password for the admin user" - default = "" -} - variable "mysql_db_name" { type = string description = "Database name (default is not to create a database" @@ -47,8 +41,9 @@ variable "mysql_cluster_enabled" { } variable "additional_databases" { - type = set(string) - default = [] + type = set(string) + default = [] + description = "Additional databases to be created with the cluster" } variable "additional_users" { diff --git a/modules/aurora-mysql-resources/versions.tf b/modules/aurora-mysql-resources/versions.tf index 194539462..bfae21c8d 100644 --- a/modules/aurora-mysql-resources/versions.tf +++ b/modules/aurora-mysql-resources/versions.tf @@ -4,11 +4,14 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } + # terraform-providers/mysql is archived + # https://github.com/hashicorp/terraform-provider-mysql + # replacing with petoju/terraform-provider-mysql mysql = { - source = "terraform-providers/mysql" - version = ">= 1.9" + source = "petoju/mysql" + version = ">= 3.0.22" } } } diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index e048aa0df..8747e5925 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -155,15 +155,14 @@ Reploying the component should show no changes. For example, `atmos terraform ap | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | -| [mysql](#requirement\_mysql) | >= 1.9 | +| [aws](#requirement\_aws) | >= 4.0 | | [random](#requirement\_random) | >= 2.2 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | | [random](#provider\_random) | >= 2.2 | ## Modules @@ -172,14 +171,15 @@ Reploying the component should show no changes. For example, `atmos terraform ap |------|--------|---------| | [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/rds-cluster/aws | 1.3.1 | | [cluster](#module\_cluster) | cloudposse/label/null | 0.25.0 | -| [dns-delegated](#module\_dns-delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [dns-delegated](#module\_dns-delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | | [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | -| [primary\_cluster](#module\_primary\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [primary\_cluster](#module\_primary\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | ## Resources @@ -198,6 +198,7 @@ Reploying the component should show no changes. For example, `atmos terraform ap | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the RDS cluster | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [aurora\_mysql\_cluster\_family](#input\_aurora\_mysql\_cluster\_family) | DBParameterGroupFamily (e.g. `aurora5.6`, `aurora-mysql5.7` for Aurora MySQL databases). See https://stackoverflow.com/a/55819394 for help finding the right one to use. | `string` | n/a | yes | @@ -239,7 +240,7 @@ Reploying the component should show no changes. For example, `atmos terraform ap | [performance\_insights\_enabled](#input\_performance\_insights\_enabled) | Set `true` to enable Performance Insights | `bool` | `false` | no | | [primary\_cluster\_component](#input\_primary\_cluster\_component) | If this cluster is a read replica and no replication source is explicitly given, the component name for the primary cluster | `string` | `"aurora-mysql"` | no | | [primary\_cluster\_region](#input\_primary\_cluster\_region) | If this cluster is a read replica and no replication source is explicitly given, the region to look for a matching cluster | `string` | `""` | no | -| [publicly\_accessible](#input\_publicly\_accessible) | n/a | `bool` | `false` | no | +| [publicly\_accessible](#input\_publicly\_accessible) | Set to true to create the cluster in a public subnet | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [replication\_source\_identifier](#input\_replication\_source\_identifier) | ARN of a source DB cluster or DB instance if this DB cluster is to be created as a Read Replica.
If this value is empty and replication is enabled, remote state will attempt to find
a matching cluster in the Primary DB Cluster's region | `string` | `""` | no | diff --git a/modules/aurora-mysql/cluster-regional.tf b/modules/aurora-mysql/cluster-regional.tf index 109a03e48..d09d90390 100644 --- a/modules/aurora-mysql/cluster-regional.tf +++ b/modules/aurora-mysql/cluster-regional.tf @@ -21,7 +21,7 @@ module "aurora_mysql" { vpc_id = local.vpc_id publicly_accessible = var.publicly_accessible subnets = var.publicly_accessible ? local.public_subnet_ids : local.private_subnet_ids - allowed_cidr_blocks = var.publicly_accessible ? coalescelist(var.allowed_cidr_blocks, ["0.0.0.0/0"]) : var.allowed_cidr_blocks + allowed_cidr_blocks = local.allowed_cidr_blocks security_groups = local.eks_cluster_managed_security_group_ids zone_id = local.zone_id diff --git a/modules/aurora-mysql/main.tf b/modules/aurora-mysql/main.tf index 2c1f0dac0..201b489ba 100644 --- a/modules/aurora-mysql/main.tf +++ b/modules/aurora-mysql/main.tf @@ -37,6 +37,18 @@ locals { cluster_domain = trimprefix(module.aurora_mysql.endpoint, "${module.aurora_mysql.cluster_identifier}.cluster-") cluster_subdomain = var.mysql_name == "" ? module.this.name : "${var.mysql_name}.${module.this.name}" + + # Join a list of all allowed cidr blocks from: + # 1. VPCs from all given accounts + # 2. Additionally given CIDR blocks + all_allowed_cidr_blocks = concat( + var.allowed_cidr_blocks, + [ + for k in keys(module.vpc_ingress) : + module.vpc_ingress[k].outputs.vpc_cidr + ] + ) + allowed_cidr_blocks = var.publicly_accessible ? coalescelist(local.all_allowed_cidr_blocks, ["0.0.0.0/0"]) : local.all_allowed_cidr_blocks } module "cluster" { diff --git a/modules/aurora-mysql/providers.tf b/modules/aurora-mysql/providers.tf index a989bcb9c..08ee01b2a 100644 --- a/modules/aurora-mysql/providers.tf +++ b/modules/aurora-mysql/providers.tf @@ -27,9 +27,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - -provider "mysql" { - endpoint = time_sleep.mysql_cluster_propagation[0].triggers["endpoint"] - username = time_sleep.mysql_cluster_propagation[0].triggers["username"] - password = local.mysql_admin_password -} diff --git a/modules/aurora-mysql/remote-state.tf b/modules/aurora-mysql/remote-state.tf index f54100557..d4287a49e 100644 --- a/modules/aurora-mysql/remote-state.tf +++ b/modules/aurora-mysql/remote-state.tf @@ -1,6 +1,10 @@ +locals { + accounts_with_vpc = { for i, account in var.allow_ingress_from_vpc_accounts : try(account.tenant, module.this.tenant) != null ? format("%s-%s", account.tenant, account.stage) : account.stage => account } +} + module "dns-delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.3.1" component = "dns-delegated" environment = "gbl" @@ -10,7 +14,7 @@ module "dns-delegated" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.3.1" for_each = var.eks_component_names @@ -21,16 +25,31 @@ module "eks" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.3.1" component = "vpc" context = module.this.context } +module "vpc_ingress" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.3.1" + + for_each = local.accounts_with_vpc + + component = "vpc" + environment = try(each.value.environment, module.this.environment) + stage = try(each.value.stage, module.this.environment) + tenant = try(each.value.tenant, module.this.tenant) + + context = module.this.context +} + + module "primary_cluster" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.3.1" count = local.remote_read_replica_enabled ? 1 : 0 diff --git a/modules/aurora-mysql/variables.tf b/modules/aurora-mysql/variables.tf index 87f88c73e..547a34e42 100644 --- a/modules/aurora-mysql/variables.tf +++ b/modules/aurora-mysql/variables.tf @@ -153,8 +153,9 @@ variable "auto_minor_version_upgrade" { } variable "publicly_accessible" { - type = bool - default = false + type = bool + default = false + description = "Set to true to create the cluster in a public subnet" } variable "eks_component_names" { @@ -197,3 +198,19 @@ variable "primary_cluster_component" { default = "aurora-mysql" } +variable "allow_ingress_from_vpc_accounts" { + type = any + default = [] + description = <<-EOF + List of account contexts to pull VPC ingress CIDR and add to cluster security group. + + e.g. + + { + environment = "ue2", + stage = "auto", + tenant = "core" + } + EOF +} + diff --git a/modules/aurora-mysql/versions.tf b/modules/aurora-mysql/versions.tf index 37e4284eb..06ec5fbfa 100644 --- a/modules/aurora-mysql/versions.tf +++ b/modules/aurora-mysql/versions.tf @@ -4,11 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" - } - mysql = { - source = "terraform-providers/mysql" - version = ">= 1.9" + version = ">= 4.0" } random = { source = "hashicorp/random" From 487db0de2b182e837ff01cf78c1b1257396f9b6a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 7 Feb 2023 11:44:09 -0800 Subject: [PATCH 029/501] Upstream `spa-s3-cloudfront` (#500) Co-authored-by: Nuru Co-authored-by: Benjamin Smith Co-authored-by: cloudpossebot --- modules/spa-s3-cloudfront/README.md | 239 +++++++++ modules/spa-s3-cloudfront/context.tf | 279 ++++++++++ modules/spa-s3-cloudfront/failover.tf | 18 + .../github-actions-iam-policy.tf | 29 ++ .../github-actions-iam-role.mixin.tf | 72 +++ modules/spa-s3-cloudfront/main.tf | 182 +++++++ .../modules/lambda-edge-preview/context.tf | 279 ++++++++++ .../modules/lambda-edge-preview/main.tf | 73 +++ .../modules/lambda-edge-preview/outputs.tf | 4 + .../modules/lambda-edge-preview/variables.tf | 19 + .../modules/lambda-edge-preview/versions.tf | 11 + .../lambda_edge_redirect_404/context.tf | 279 ++++++++++ .../modules/lambda_edge_redirect_404/index.js | 119 +++++ .../modules/lambda_edge_redirect_404/main.tf | 30 ++ .../lambda_edge_redirect_404/outputs.tf | 4 + .../lambda_edge_redirect_404/variables.tf | 19 + .../lambda_edge_redirect_404/versions.tf | 11 + modules/spa-s3-cloudfront/outputs.tf | 24 + modules/spa-s3-cloudfront/providers.tf | 56 ++ modules/spa-s3-cloudfront/remote-state.tf | 41 ++ modules/spa-s3-cloudfront/variables.tf | 491 ++++++++++++++++++ modules/spa-s3-cloudfront/versions.tf | 10 + 22 files changed, 2289 insertions(+) create mode 100644 modules/spa-s3-cloudfront/README.md create mode 100644 modules/spa-s3-cloudfront/context.tf create mode 100644 modules/spa-s3-cloudfront/failover.tf create mode 100644 modules/spa-s3-cloudfront/github-actions-iam-policy.tf create mode 100644 modules/spa-s3-cloudfront/github-actions-iam-role.mixin.tf create mode 100644 modules/spa-s3-cloudfront/main.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/context.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/outputs.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/versions.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/context.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/index.js create mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/outputs.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf create mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/versions.tf create mode 100644 modules/spa-s3-cloudfront/outputs.tf create mode 100644 modules/spa-s3-cloudfront/providers.tf create mode 100644 modules/spa-s3-cloudfront/remote-state.tf create mode 100644 modules/spa-s3-cloudfront/variables.tf create mode 100644 modules/spa-s3-cloudfront/versions.tf diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md new file mode 100644 index 000000000..45d7d1b77 --- /dev/null +++ b/modules/spa-s3-cloudfront/README.md @@ -0,0 +1,239 @@ +# Component: `spa-s3-cloudfront` + +This component is responsible for provisioning: + +- S3 bucket +- CloudFront distribution for a Single Page Application +- ACM placed in us-east-1 regardless of the stack region (requirement of CloudFront) + +NOTE: The component does not use the ACM created by `dns-delegated`, because the ACM region has to be us-east-1. + +## Usage + +**Stack Level**: Regional + +Here are some example snippets for how to use this component: + +An import for all instantiations of the `spa-s3-cloudfront` component can be created at `stacks/spa/spa-defaults.yaml`: + +```yaml +components: + terraform: + spa-s3-cloudfront: + vars: + # lookup GitHub Runner IAM role via remote state + github_runners_deployment_principal_arn_enabled: true + github_runners_component_name: github-runners + github_runners_tenant_name: core + github_runners_environment_name: ue2 + github_runners_stage_name : auto + origin_force_destroy: false + origin_versioning_enabled: true + origin_block_public_acls: true + origin_block_public_policy: true + origin_ignore_public_acls: true + origin_restrict_public_buckets: true + origin_encryption_enabled: true + cloudfront_index_document: index.html + cloudfront_ipv6_enabled: false + cloudfront_compress: true + cloudfront_default_root_object: index.html + cloudfront_viewer_protocol_policy: redirect-to-https +``` + +An import for all instantiations for a specific SPA can be created at `stacks/spa/example-spa.yaml`: + +```yaml +components: + terraform: + example-spa: + component: spa-s3-cloudfront + vars: + name: example-spa + site_subdomain: example-spa + cloudfront_allowed_methods: + - GET + - HEAD + cloudfront_cached_methods: + - GET + - HEAD + cloudfront_custom_error_response: + - error_caching_min_ttl: 1 + error_code: 403 + response_code: 200 + response_page_path: /index.html + cloudfront_default_ttl: 60 + cloudfront_min_ttl: 60 + cloudfront_max_ttl: 60 +``` + +Finally, the `spa-s3-cloudfront` component can be instantiated in a stack config: + +```yaml +import: + - spa/example-spa + +components: + terraform: + example-spa: + component: spa-s3-cloudfront + settings: + spacelift: + workspace_enabled: true + vars: {} +``` + +### Failover Origins + +Failover origins are supported via `var.failover_s3_origin_name` and `var.failover_s3_origin_region`. + +### Preview Environments + +SPA Preview environments (i.e. `subdomain.example.com` mapping to a `/subdomain` path in the S3 bucket) powered by Lambda@Edge +are supported via `var.preview_environment_enabled`. See the both the variable description and inline documentation for +an extensive explanation for how these preview environments work. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | +| [aws.failover](#provider\_aws.failover) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [acm\_request\_certificate](#module\_acm\_request\_certificate) | cloudposse/acm-request-certificate/aws | 0.17.0 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | +| [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | +| [github\_runners](#module\_github\_runners) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [lambda\_edge\_preview](#module\_lambda\_edge\_preview) | ./modules/lambda-edge-preview | n/a | +| [lambda\_edge\_redirect\_404](#module\_lambda\_edge\_redirect\_404) | ./modules/lambda_edge_redirect_404 | n/a | +| [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.83.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 0.8.1 | +| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_shield_protection.shield_protection](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/shield_protection) | resource | +| [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_s3_bucket.failover_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_bucket) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [block\_origin\_public\_access\_enabled](#input\_block\_origin\_public\_access\_enabled) | When set to 'true' the s3 origin bucket will have public access block enabled. | `bool` | `true` | no | +| [cloudfront\_access\_log\_bucket\_name](#input\_cloudfront\_access\_log\_bucket\_name) | When `cloudfront_access_log_create_bucket` is `false`, this is the name of the existing S3 Bucket where
CloudFront Access Logs are to be delivered and is required. IGNORED when `cloudfront_access_log_create_bucket` is `true`. | `string` | `""` | no | +| [cloudfront\_access\_log\_bucket\_name\_rendering\_enabled](#input\_cloudfront\_access\_log\_bucket\_name\_rendering\_enabled) | If set to `true`, then the CloudFront origin access logs bucket name will be rendered by calling `format("%v-%v-%v-%v", var.namespace, var.environment, var.stage, var.cloudfront_access_log_bucket_name)`.
Otherwise, the value for `cloudfront_access_log_bucket_name` will need to be the globally unique name of the access logs bucket.

For example, if this component produces an origin bucket named `eg-ue1-devplatform-example` and `cloudfront_access_log_bucket_name` is set to
`example-cloudfront-access-logs`, then the bucket name will be rendered to be `eg-ue1-devplatform-example-cloudfront-access-logs`. | `bool` | `false` | no | +| [cloudfront\_access\_log\_create\_bucket](#input\_cloudfront\_access\_log\_create\_bucket) | When `true` and `cloudfront_access_logging_enabled` is also true, this module will create a new,
separate S3 bucket to receive CloudFront Access Logs. | `bool` | `true` | no | +| [cloudfront\_access\_log\_prefix](#input\_cloudfront\_access\_log\_prefix) | Prefix to use for CloudFront Access Log object keys. Defaults to no prefix. | `string` | `""` | no | +| [cloudfront\_access\_log\_prefix\_rendering\_enabled](#input\_cloudfront\_access\_log\_prefix\_rendering\_enabled) | Whether or not to dynamically render ${module.this.id} at the end of `var.cloudfront_access_log_prefix`. | `bool` | `false` | no | +| [cloudfront\_allowed\_methods](#input\_cloudfront\_allowed\_methods) | List of allowed methods (e.g. GET, PUT, POST, DELETE, HEAD) for AWS CloudFront. | `list(string)` |
[
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT"
]
| no | +| [cloudfront\_aws\_shield\_protection\_enabled](#input\_cloudfront\_aws\_shield\_protection\_enabled) | Enable or disable AWS Shield Advanced protection for the CloudFront distribution. If set to 'true', a subscription to AWS Shield Advanced must exist in this account. | `bool` | `false` | no | +| [cloudfront\_aws\_waf\_component\_name](#input\_cloudfront\_aws\_waf\_component\_name) | The name of the component used when deploying WAF ACL | `string` | `"waf"` | no | +| [cloudfront\_aws\_waf\_environment](#input\_cloudfront\_aws\_waf\_environment) | The environment where the WAF ACL for CloudFront distribution exists. | `string` | `null` | no | +| [cloudfront\_aws\_waf\_protection\_enabled](#input\_cloudfront\_aws\_waf\_protection\_enabled) | Enable or disable AWS WAF for the CloudFront distribution.

This assumes that the `aws-waf-acl-default-cloudfront` component has been deployed to the regional stack corresponding
to `var.waf_acl_environment`. | `bool` | `true` | no | +| [cloudfront\_cached\_methods](#input\_cloudfront\_cached\_methods) | List of cached methods (e.g. GET, PUT, POST, DELETE, HEAD). | `list(string)` |
[
"GET",
"HEAD"
]
| no | +| [cloudfront\_compress](#input\_cloudfront\_compress) | Compress content for web requests that include Accept-Encoding: gzip in the request header. | `bool` | `false` | no | +| [cloudfront\_custom\_error\_response](#input\_cloudfront\_custom\_error\_response) | List of one or more custom error response element maps. |
list(object({
error_caching_min_ttl = string
error_code = string
response_code = string
response_page_path = string
}))
| `[]` | no | +| [cloudfront\_default\_root\_object](#input\_cloudfront\_default\_root\_object) | Object that CloudFront return when requests the root URL. | `string` | `"index.html"` | no | +| [cloudfront\_default\_ttl](#input\_cloudfront\_default\_ttl) | Default amount of time (in seconds) that an object is in a CloudFront cache. | `number` | `60` | no | +| [cloudfront\_index\_document](#input\_cloudfront\_index\_document) | Amazon S3 returns this index document when requests are made to the root domain or any of the subfolders. | `string` | `"index.html"` | no | +| [cloudfront\_ipv6\_enabled](#input\_cloudfront\_ipv6\_enabled) | Set to true to enable an AAAA DNS record to be set as well as the A record. | `bool` | `true` | no | +| [cloudfront\_lambda\_function\_association](#input\_cloudfront\_lambda\_function\_association) | A config block that configures the CloudFront distribution with lambda@edge functions for specific events. |
list(object({
event_type = string
include_body = bool
lambda_arn = string
}))
| `[]` | no | +| [cloudfront\_max\_ttl](#input\_cloudfront\_max\_ttl) | Maximum amount of time (in seconds) that an object is in a CloudFront cache. | `number` | `31536000` | no | +| [cloudfront\_min\_ttl](#input\_cloudfront\_min\_ttl) | Minimum amount of time that you want objects to stay in CloudFront caches. | `number` | `0` | no | +| [cloudfront\_viewer\_protocol\_policy](#input\_cloudfront\_viewer\_protocol\_policy) | Limit the protocol users can use to access content. One of `allow-all`, `https-only`, or `redirect-to-https`. | `string` | `"redirect-to-https"` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [custom\_origins](#input\_custom\_origins) | A list of additional custom website [origins](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments) for this distribution. |
list(object({
domain_name = string
origin_id = string
origin_path = string
custom_headers = list(object({
name = string
value = string
}))
custom_origin_config = object({
http_port = number
https_port = number
origin_protocol_policy = string
origin_ssl_protocols = list(string)
origin_keepalive_timeout = number
origin_read_timeout = number
})
}))
| `[]` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_delegated\_environment\_name](#input\_dns\_delegated\_environment\_name) | The environment where `dns-delegated` component is deployed to | `string` | `"gbl"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [external\_aliases](#input\_external\_aliases) | List of FQDN's - Used to set the Alternate Domain Names (CNAMEs) setting on CloudFront. No new Route53 records will be created for these.

Setting `process_domain_validation_options` to true may cause the component to fail if an external\_alias DNS zone is not controlled by Terraform.

Setting `preview_environment_enabled` to `true` will cause this variable to be ignored. | `list(string)` | `[]` | no | +| [failover\_criteria\_status\_codes](#input\_failover\_criteria\_status\_codes) | List of HTTP Status Codes to use as the origin group failover criteria. | `list(string)` |
[
403,
404,
500,
502
]
| no | +| [failover\_s3\_origin\_environment](#input\_failover\_s3\_origin\_environment) | The [fixed name](https://github.com/cloudposse/terraform-aws-utils/blob/399951e552483a4f4c1dc7fbe2675c443f3dbd83/main.tf#L10) of the AWS Region where the
failover S3 origin exists. Setting this variable will enable use of a failover S3 origin, but it is required for the
failover S3 origin to exist beforehand. This variable is used in conjunction with `var.failover_s3_origin_format` to
build out the name of the Failover S3 origin in the specified region.

For example, if this component creates an origin of name `eg-ue1-devplatform-example` and this variable is set to `uw1`,
then it is expected that a bucket with the name `eg-uw1-devplatform-example-failover` exists in `us-west-1`. | `string` | `null` | no | +| [failover\_s3\_origin\_format](#input\_failover\_s3\_origin\_format) | If `var.failover_s3_origin_environment` is supplied, this is the format to use for the failover S3 origin bucket name when
building the name via `format([format], var.namespace, var.failover_s3_origin_environment, var.stage, var.name)`
and then looking it up via the `aws_s3_bucket` Data Source.

For example, if this component creates an origin of name `eg-ue1-devplatform-example` and `var.failover_s3_origin_environment`
is set to `uw1`, then it is expected that a bucket with the name `eg-uw1-devplatform-example-failover` exists in `us-west-1`. | `string` | `"%v-%v-%v-%v-failover"` | no | +| [forward\_cookies](#input\_forward\_cookies) | Specifies whether you want CloudFront to forward all or no cookies to the origin. Can be 'all' or 'none' | `string` | `"none"` | no | +| [forward\_header\_values](#input\_forward\_header\_values) | A list of whitelisted header values to forward to the origin (incompatible with `cache_policy_id`) | `list(string)` |
[
"Access-Control-Request-Headers",
"Access-Control-Request-Method",
"Origin"
]
| no | +| [github\_actions\_allowed\_repos](#input\_github\_actions\_allowed\_repos) | A list of the GitHub repositories that are allowed to assume this role from GitHub Actions. For example,
["cloudposse/infra-live"]. Can contain "*" as wildcard.
If org part of repo name is omitted, "cloudposse" will be assumed. | `list(string)` | `[]` | no | +| [github\_actions\_iam\_role\_attributes](#input\_github\_actions\_iam\_role\_attributes) | Additional attributes to add to the role name | `list(string)` | `[]` | no | +| [github\_actions\_iam\_role\_enabled](#input\_github\_actions\_iam\_role\_enabled) | Flag to toggle creation of an IAM Role that GitHub Actions can assume to access AWS resources | `bool` | `false` | no | +| [github\_runners\_component\_name](#input\_github\_runners\_component\_name) | The name of the component that deploys GitHub Runners, used in remote-state lookup | `string` | `"github-runners"` | no | +| [github\_runners\_deployment\_principal\_arn\_enabled](#input\_github\_runners\_deployment\_principal\_arn\_enabled) | A flag that is used to decide whether or not to include the GitHub Runner's IAM role in origin\_deployment\_principal\_arns list | `bool` | `true` | no | +| [github\_runners\_environment\_name](#input\_github\_runners\_environment\_name) | The name of the environment where the CloudTrail bucket is provisioned | `string` | `"ue2"` | no | +| [github\_runners\_stage\_name](#input\_github\_runners\_stage\_name) | The stage name where the CloudTrail bucket is provisioned | `string` | `"auto"` | no | +| [github\_runners\_tenant\_name](#input\_github\_runners\_tenant\_name) | The tenant name where the GitHub Runners are provisioned | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lambda\_edge\_redirect\_404\_enabled](#input\_lambda\_edge\_redirect\_404\_enabled) | Enable or disable SPA 404 redirects via Lambda@Edge - returns a 302 and a location of `/` if the request returned 404. | `bool` | `false` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [ordered\_cache](#input\_ordered\_cache) | An ordered list of [cache behaviors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#cache-behavior-arguments) resource for this distribution.
List in order of precedence (first match wins). This is in addition to the default cache policy.
Set `target_origin_id` to `""` to specify the S3 bucket origin created by this module. |
list(object({
target_origin_id = string
path_pattern = string

allowed_methods = list(string)
cached_methods = list(string)
compress = bool
trusted_signers = list(string)
trusted_key_groups = list(string)

cache_policy_id = string
origin_request_policy_id = string

viewer_protocol_policy = string
min_ttl = number
default_ttl = number
max_ttl = number
response_headers_policy_id = string

forward_query_string = bool
forward_header_values = list(string)
forward_cookies = string
forward_cookies_whitelisted_names = list(string)

lambda_function_association = list(object({
event_type = string
include_body = bool
lambda_arn = string
}))

function_association = list(object({
event_type = string
function_arn = string
}))
}))
| `[]` | no | +| [origin\_allow\_ssl\_requests\_only](#input\_origin\_allow\_ssl\_requests\_only) | Set to `true` in order to have the origin bucket require requests to use Secure Socket Layer (HTTPS/SSL). This will explicitly deny access to HTTP requests | `bool` | `true` | no | +| [origin\_deployment\_actions](#input\_origin\_deployment\_actions) | List of actions to permit `origin_deployment_principal_arns` to perform on bucket and bucket prefixes (see `origin_deployment_principal_arns`) | `list(string)` |
[
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:GetBucketLocation",
"s3:AbortMultipartUpload"
]
| no | +| [origin\_deployment\_principal\_arns](#input\_origin\_deployment\_principal\_arns) | List of role ARNs to grant deployment permissions to the origin Bucket. | `list(string)` | `[]` | no | +| [origin\_encryption\_enabled](#input\_origin\_encryption\_enabled) | When set to 'true' the origin Bucket will have aes256 encryption enabled by default. | `bool` | `true` | no | +| [origin\_force\_destroy](#input\_origin\_force\_destroy) | A boolean string that indicates all objects should be deleted from the origin Bucket so that the Bucket can be destroyed without error. These objects are not recoverable. | `bool` | `false` | no | +| [origin\_s3\_access\_log\_bucket\_name](#input\_origin\_s3\_access\_log\_bucket\_name) | Name of the existing S3 bucket where S3 Access Logs for the origin Bucket will be delivered. Default is not to enable S3 Access Logging for the origin Bucket. | `string` | `""` | no | +| [origin\_s3\_access\_log\_bucket\_name\_rendering\_enabled](#input\_origin\_s3\_access\_log\_bucket\_name\_rendering\_enabled) | If set to `true`, then the S3 origin access logs bucket name will be rendered by calling `format("%v-%v-%v-%v", var.namespace, var.environment, var.stage, var.origin_s3_access_log_bucket_name)`.
Otherwise, the value for `origin_s3_access_log_bucket_name` will need to be the globally unique name of the access logs bucket.

For example, if this component produces an origin bucket named `eg-ue1-devplatform-example` and `origin_s3_access_log_bucket_name` is set to
`example-s3-access-logs`, then the bucket name will be rendered to be `eg-ue1-devplatform-example-s3-access-logs`. | `bool` | `false` | no | +| [origin\_s3\_access\_log\_prefix](#input\_origin\_s3\_access\_log\_prefix) | Prefix to use for S3 Access Log object keys. Defaults to `logs/${module.this.id}` | `string` | `""` | no | +| [origin\_s3\_access\_logging\_enabled](#input\_origin\_s3\_access\_logging\_enabled) | Set `true` to deliver S3 Access Logs to the `origin_s3_access_log_bucket_name` bucket.
Defaults to `false` if `origin_s3_access_log_bucket_name` is empty (the default), `true` otherwise.
Must be set explicitly if the access log bucket is being created at the same time as this module is being invoked. | `bool` | `null` | no | +| [origin\_versioning\_enabled](#input\_origin\_versioning\_enabled) | Enable or disable versioning for the origin Bucket. Versioning is a means of keeping multiple variants of an object in the same bucket. | `bool` | `false` | no | +| [parent\_zone\_name](#input\_parent\_zone\_name) | Parent domain name of site to publish. Defaults to format(parent\_zone\_name\_pattern, stage, environment). | `string` | `""` | no | +| [preview\_environment\_enabled](#input\_preview\_environment\_enabled) | Enable or disable SPA Preview Environments via Lambda@Edge, i.e. mapping `subdomain.example.com` to the `/subdomain`
path in the origin S3 bucket.

This variable implicitly affects the following variables:

* `s3_website_enabled`
* `s3_website_password_enabled`
* `block_origin_public_access_enabled`
* `origin_allow_ssl_requests_only`
* `forward_header_values`
* `cloudfront_default_ttl`
* `cloudfront_min_ttl`
* `cloudfront_max_ttl`
* `cloudfront_lambda_function_association` | `bool` | `false` | no | +| [process\_domain\_validation\_options](#input\_process\_domain\_validation\_options) | Flag to enable/disable processing of the record to add to the DNS zone to complete certificate validation | `bool` | `true` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region. | `string` | n/a | yes | +| [s3\_object\_ownership](#input\_s3\_object\_ownership) | Specifies the S3 object ownership control on the origin bucket. Valid values are `ObjectWriter`, `BucketOwnerPreferred`, and 'BucketOwnerEnforced'. | `string` | `"ObjectWriter"` | no | +| [s3\_website\_enabled](#input\_s3\_website\_enabled) | Set to true to enable the created S3 bucket to serve as a website independently of CloudFront,
and to use that website as the origin.

Setting `preview_environment_enabled` will implicitly set this to `true`. | `bool` | `false` | no | +| [s3\_website\_password\_enabled](#input\_s3\_website\_password\_enabled) | If set to true, and `s3_website_enabled` is also true, a password will be required in the `Referrer` field of the
HTTP request in order to access the website, and CloudFront will be configured to pass this password in its requests.
This will make it much harder for people to bypass CloudFront and access the S3 website directly via its website endpoint. | `bool` | `false` | no | +| [site\_fqdn](#input\_site\_fqdn) | Fully qualified domain name of site to publish. Overrides site\_subdomain and parent\_zone\_name. | `string` | `""` | no | +| [site\_subdomain](#input\_site\_subdomain) | Subdomain to plug into site\_name\_pattern to make site FQDN. | `string` | `""` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [cloudfront\_distribution\_alias](#output\_cloudfront\_distribution\_alias) | Cloudfront Distribution Alias Record. | +| [cloudfront\_distribution\_domain\_name](#output\_cloudfront\_distribution\_domain\_name) | Cloudfront Distribution Domain Name. | +| [failover\_s3\_bucket\_name](#output\_failover\_s3\_bucket\_name) | Failover Origin bucket name, if enabled. | +| [github\_actions\_iam\_role\_arn](#output\_github\_actions\_iam\_role\_arn) | ARN of IAM role for GitHub Actions | +| [github\_actions\_iam\_role\_name](#output\_github\_actions\_iam\_role\_name) | Name of IAM role for GitHub Actions | +| [origin\_s3\_bucket\_arn](#output\_origin\_s3\_bucket\_arn) | Origin bucket ARN. | +| [origin\_s3\_bucket\_name](#output\_origin\_s3\_bucket\_name) | Origin bucket name. | + + + +## References +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spa-s3-cloudfront) - Cloud Posse's upstream component + + +[](https://cpco.io/component) diff --git a/modules/spa-s3-cloudfront/context.tf b/modules/spa-s3-cloudfront/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/spa-s3-cloudfront/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spa-s3-cloudfront/failover.tf b/modules/spa-s3-cloudfront/failover.tf new file mode 100644 index 000000000..090a95d64 --- /dev/null +++ b/modules/spa-s3-cloudfront/failover.tf @@ -0,0 +1,18 @@ +locals { + failover_enabled = local.enabled && try(length(var.failover_s3_origin_environment), 0) > 0 + failover_region = local.failover_enabled ? module.utils.region_az_alt_code_maps.from_fixed[var.failover_s3_origin_environment] : var.region + failover_bucket = local.failover_enabled ? format(var.failover_s3_origin_format, var.namespace, var.failover_s3_origin_environment, var.stage, var.name) : null +} + +module "utils" { + source = "cloudposse/utils/aws" + version = "0.8.1" +} + +data "aws_s3_bucket" "failover_bucket" { + count = local.failover_enabled ? 1 : 0 + + bucket = local.failover_bucket + + provider = aws.failover +} diff --git a/modules/spa-s3-cloudfront/github-actions-iam-policy.tf b/modules/spa-s3-cloudfront/github-actions-iam-policy.tf new file mode 100644 index 000000000..a1c4d3764 --- /dev/null +++ b/modules/spa-s3-cloudfront/github-actions-iam-policy.tf @@ -0,0 +1,29 @@ +locals { + github_actions_iam_policy = data.aws_iam_policy_document.github_actions_iam_policy.json +} + +data "aws_iam_policy_document" "github_actions_iam_policy" { + statement { + sid = "BucketActions" + effect = "Allow" + actions = [ + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:AbortMultipartUpload" + ] + resources = [module.spa_web.s3_bucket_arn] + } + + statement { + sid = "ObjectActions" + effect = "Allow" + actions = [ + "s3:GetObject", + "s3:DeleteObject", + "s3:PutObject", + "s3:PutObjectTagging", + "s3:PutObjectAcl" + ] + resources = [format("%s/*", module.spa_web.s3_bucket_arn)] + } +} diff --git a/modules/spa-s3-cloudfront/github-actions-iam-role.mixin.tf b/modules/spa-s3-cloudfront/github-actions-iam-role.mixin.tf new file mode 100644 index 000000000..4294f1aac --- /dev/null +++ b/modules/spa-s3-cloudfront/github-actions-iam-role.mixin.tf @@ -0,0 +1,72 @@ +# This mixin requires that a local variable named `github_actions_iam_policy` be defined +# and its value to be a JSON IAM Policy Document defining the permissions for the role. +# It also requires that the `github-oidc-provider` has been previously installed and the +# `github-assume-role-policy.mixin.tf` has been added to `account-map/modules/team-assume-role-policy`. + +variable "github_actions_iam_role_enabled" { + type = bool + description = <<-EOF + Flag to toggle creation of an IAM Role that GitHub Actions can assume to access AWS resources + EOF + default = false +} + +variable "github_actions_allowed_repos" { + type = list(string) + description = < 0 +} + +module "gha_role_name" { + source = "cloudposse/label/null" + version = "0.25.0" + + enabled = local.github_actions_iam_role_enabled + attributes = compact(concat(var.github_actions_iam_role_attributes, ["gha"])) + + context = module.this.context +} + +module "gha_assume_role" { + source = "../account-map/modules/team-assume-role-policy" + + trusted_github_repos = var.github_actions_allowed_repos + + context = module.gha_role_name.context +} + +resource "aws_iam_role" "github_actions" { + count = local.github_actions_iam_role_enabled ? 1 : 0 + name = module.gha_role_name.id + assume_role_policy = module.gha_assume_role.github_assume_role_policy + + inline_policy { + name = module.gha_role_name.id + policy = local.github_actions_iam_policy + } +} + +output "github_actions_iam_role_arn" { + value = one(aws_iam_role.github_actions[*].arn) + description = "ARN of IAM role for GitHub Actions" +} + +output "github_actions_iam_role_name" { + value = one(aws_iam_role.github_actions[*].name) + description = "Name of IAM role for GitHub Actions" +} diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf new file mode 100644 index 000000000..2c5785575 --- /dev/null +++ b/modules/spa-s3-cloudfront/main.tf @@ -0,0 +1,182 @@ +locals { + enabled = module.this.enabled + aws_shield_enabled = local.enabled && var.cloudfront_aws_shield_protection_enabled + aws_waf_enabled = local.enabled && var.cloudfront_aws_waf_protection_enabled + github_runners_enabled = local.enabled && var.github_runners_deployment_principal_arn_enabled + parent_zone_name = length(var.parent_zone_name) > 0 ? var.parent_zone_name : try(module.dns_delegated.outputs.default_domain_name, null) + site_fqdn = length(var.site_fqdn) > 0 ? var.site_fqdn : format("%v.%v.%v", var.site_subdomain, module.this.environment, local.parent_zone_name) + s3_access_log_bucket_name = var.origin_s3_access_log_bucket_name_rendering_enabled ? format("%[1]v-${module.this.tenant != null ? "%[2]v-" : ""}%[3]v-%[4]v-%[5]v", var.namespace, var.tenant, var.environment, var.stage, var.origin_s3_access_log_bucket_name) : var.origin_s3_access_log_bucket_name + cloudfront_access_log_bucket_name = var.cloudfront_access_log_bucket_name_rendering_enabled ? format("%[1]v-${module.this.tenant != null ? "%[2]v-" : ""}%[3]v-%[4]v-%[5]v", var.namespace, var.tenant, var.environment, var.stage, var.cloudfront_access_log_bucket_name) : var.cloudfront_access_log_bucket_name + cloudfront_access_log_prefix = var.cloudfront_access_log_prefix_rendering_enabled ? "${var.cloudfront_access_log_prefix}${module.this.id}" : var.cloudfront_access_log_prefix + origin_deployment_principal_arns = local.github_runners_enabled ? concat(var.origin_deployment_principal_arns, [module.github_runners[0].outputs.iam_role_arn]) : var.origin_deployment_principal_arns + + # Variables affected by SPA Preview Environments + # + # In order for preview environments to work, there are some specific CloudFront Distribution settings that need to be in place (in order of local variables set below this list): + # 1. A wildcard domain Route53 alias needs to be created for the CloudFront distribution. SANs for the ACM certificate need to be set accordingly. + # 2. The origin must be a custom origin pointing to the S3 website endpoint, not a S3 REST origin (the set of Lambda@Edge functions in modules/lambda-edge-preview do not support the latter). + # 3. Because of #2, the bucket in question cannot have a Public Access Block configuration blocking all public ACLs. + # 4. Because of #2 and #3, it is best practice to enable a password on the S3 website origin so that CloudFront is the single point of entry. + # 5. Object ACLs should be disabled for the origin bucket in the preview environment, otherwise CI/CD jobs uploading to the origin bucket may create object ACLs preventing the content from being served. + # 6. The statement in the bucket policy blocking non-TLS requests from CloudFront needs to be disabled. + # 7. A custom header 'x-forwarded-host' needs to be forwarded to the origin (it is injected by lambda@edge function associated with the Viewer Request event). + # 8. TTL values will be set to 0, because the preview environment is associated with development and debugging, not long term caching. + # 9. The Lambda@Edge functions created by modules/lambda-edge-preview need to be associated with the CloudFront Distribution. + # + # This isn't necessarily the only way to get preview environments to work, but these are the constraints required to achieve the currently tested implementation in modules/lambda-edge-preview. + preview_environment_enabled = local.enabled && var.preview_environment_enabled + lambda_edge_redirect_404_enabled = local.enabled && var.lambda_edge_redirect_404_enabled + preview_environment_wildcard_domain = format("%v.%v", "*", local.site_fqdn) + aliases = concat([local.site_fqdn], local.preview_environment_enabled ? [local.preview_environment_wildcard_domain] : []) + external_aliases = local.preview_environment_enabled ? [] : var.external_aliases + subject_alternative_names = local.preview_environment_enabled ? [local.preview_environment_wildcard_domain] : var.external_aliases + s3_website_enabled = var.s3_website_enabled || local.preview_environment_enabled + s3_website_password_enabled = var.s3_website_password_enabled || local.preview_environment_enabled + s3_object_ownership = local.preview_environment_enabled ? "BucketOwnerEnforced" : var.s3_object_ownership + block_origin_public_access_enabled = var.block_origin_public_access_enabled && !local.preview_environment_enabled + + # SSL Requirements by s3 bucket configuration + # | s3 website enabled | preview enabled | SSL Enabled | + # |--------------------|-----------------|-------------| + # | false | false | true | + # | true | false | false | + # | true | true | false | + # Preview must have website_enabled. + origin_allow_ssl_requests_only = var.origin_allow_ssl_requests_only && !local.s3_website_enabled + + forward_header_values = local.preview_environment_enabled ? concat(var.forward_header_values, ["x-forwarded-host"]) : var.forward_header_values + cloudfront_default_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_default_ttl + cloudfront_min_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_min_ttl + cloudfront_max_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_max_ttl + cloudfront_lambda_function_association = concat(var.cloudfront_lambda_function_association, local.preview_environment_enabled ? module.lambda_edge_preview.lambda_function_association : [], local.lambda_edge_redirect_404_enabled ? module.lambda_edge_redirect_404.lambda_function_association : []) +} + +# Create an ACM and explicitly set it to us-east-1 (requirement of CloudFront) +module "acm_request_certificate" { + source = "cloudposse/acm-request-certificate/aws" + version = "0.17.0" + providers = { + aws = aws.us-east-1 + } + + domain_name = local.site_fqdn + subject_alternative_names = local.subject_alternative_names + zone_name = local.parent_zone_name + process_domain_validation_options = var.process_domain_validation_options + ttl = 300 + + context = module.this.context +} + +module "spa_web" { + source = "cloudposse/cloudfront-s3-cdn/aws" + version = "0.83.0" + + block_origin_public_access_enabled = local.block_origin_public_access_enabled + encryption_enabled = var.origin_encryption_enabled + origin_force_destroy = var.origin_force_destroy + versioning_enabled = var.origin_versioning_enabled + web_acl_id = local.aws_waf_enabled ? module.waf.outputs.acl.arn : null + + cloudfront_access_log_create_bucket = var.cloudfront_access_log_create_bucket + cloudfront_access_log_bucket_name = local.cloudfront_access_log_bucket_name + cloudfront_access_log_prefix = local.cloudfront_access_log_prefix + + index_document = var.cloudfront_index_document + default_root_object = var.cloudfront_default_root_object + + s3_access_logging_enabled = var.origin_s3_access_logging_enabled + s3_access_log_bucket_name = local.s3_access_log_bucket_name + s3_access_log_prefix = var.origin_s3_access_log_prefix + + aliases = local.aliases + external_aliases = local.external_aliases + parent_zone_name = local.parent_zone_name + dns_alias_enabled = true + website_enabled = local.s3_website_enabled + s3_website_password_enabled = local.s3_website_password_enabled + allow_ssl_requests_only = local.origin_allow_ssl_requests_only + acm_certificate_arn = module.acm_request_certificate.arn + ipv6_enabled = var.cloudfront_ipv6_enabled + + allowed_methods = var.cloudfront_allowed_methods + cached_methods = var.cloudfront_cached_methods + custom_error_response = var.cloudfront_custom_error_response + default_ttl = local.cloudfront_default_ttl + min_ttl = local.cloudfront_min_ttl + max_ttl = local.cloudfront_max_ttl + + ordered_cache = var.ordered_cache + forward_cookies = var.forward_cookies + forward_header_values = local.forward_header_values + + compress = var.cloudfront_compress + viewer_protocol_policy = var.cloudfront_viewer_protocol_policy + + deployment_principal_arns = { for arn in local.origin_deployment_principal_arns : arn => [""] } + # Actions the deployment ARNs are allowed to perform on the S3 Origin bucket + deployment_actions = var.origin_deployment_actions + + lambda_function_association = local.cloudfront_lambda_function_association + + custom_origins = var.custom_origins + + s3_origins = local.failover_enabled ? [{ + domain_name = data.aws_s3_bucket.failover_bucket[0].bucket_domain_name + origin_id = data.aws_s3_bucket.failover_bucket[0].bucket + origin_path = null + s3_origin_config = { + origin_access_identity = null # will get translated to the origin_access_identity used by the origin created by this module. + } + }] : [] + origin_groups = local.failover_enabled ? [{ + primary_origin_id = null # will get translated to the origin id of the origin created by this module. + failover_origin_id = data.aws_s3_bucket.failover_bucket[0].bucket + failover_criteria = var.failover_criteria_status_codes + }] : [] + + s3_object_ownership = local.s3_object_ownership + + context = module.this.context +} + +resource "aws_shield_protection" "shield_protection" { + count = local.aws_shield_enabled ? 1 : 0 + + name = module.spa_web.cf_id + resource_arn = module.spa_web.cf_arn +} + +module "lambda_edge_preview" { + source = "./modules/lambda-edge-preview" + + enabled = local.preview_environment_enabled + + cloudfront_distribution_domain_name = module.spa_web.cf_domain_name + cloudfront_distribution_hosted_zone_id = module.spa_web.cf_hosted_zone_id + site_fqdn = local.site_fqdn + parent_zone_name = local.parent_zone_name + + context = module.this.context + + providers = { + aws.us-east-1 = aws.us-east-1 + } +} + +module "lambda_edge_redirect_404" { + source = "./modules/lambda_edge_redirect_404" + + enabled = local.lambda_edge_redirect_404_enabled + + cloudfront_distribution_domain_name = module.spa_web.cf_domain_name + cloudfront_distribution_hosted_zone_id = module.spa_web.cf_hosted_zone_id + site_fqdn = local.site_fqdn + parent_zone_name = local.parent_zone_name + + context = module.this.context + + providers = { + aws.us-east-1 = aws.us-east-1 + } +} diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/context.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf new file mode 100644 index 000000000..3ee3f61c6 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf @@ -0,0 +1,73 @@ +# https://levelup.gitconnected.com/preview-environments-in-aws-with-cloudfront-and-lambda-edge-7acccb0b67d1 + +locals { + lambda_runtime = "nodejs12.x" + lambda_handler = "index.handler" +} + +module "lambda_edge" { + source = "cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge" + version = "0.82.4" + + functions = { + origin_request = { + source = [{ + content = <<-EOT + exports.handler = (event, context, callback) => { + const site_fqdn = "${var.site_fqdn}"; + + const { request } = event.Records[0].cf; + const default_prefix = ""; + + console.log(event); + console.log(request); + console.log(request.headers); + const host = request.headers['x-forwarded-host'][0].value; + if (host == site_fqdn) { + request.origin.custom.path = default_prefix; // use default prefix if there is no subdomain + } else { + const subdomain = host.replace('.' + site_fqdn, ''); + request.origin.custom.path = `/$${subdomain}`; // use preview prefix + } + + return callback(null, request); + }; + EOT + filename = "index.js" + }] + runtime = local.lambda_runtime + handler = local.lambda_handler + event_type = "origin-request" + include_body = false + } + viewer_request = { + source = [{ + content = <<-EOT + exports.handler = (event, context, callback) => { + const { request } = event.Records[0].cf; + + request.headers['x-forwarded-host'] = [ + { + key: 'X-Forwarded-Host', + value: request.headers.host[0].value + } + ]; + + return callback(null, request); + }; + EOT + filename = "index.js" + }] + runtime = local.lambda_runtime + handler = local.lambda_handler + event_type = "viewer-request" + include_body = false + } + } + + providers = { + aws = aws.us-east-1 + } + + context = module.this.context +} diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/outputs.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/outputs.tf new file mode 100644 index 000000000..315e14b86 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/outputs.tf @@ -0,0 +1,4 @@ +output "lambda_function_association" { + description = "The Lambda@Edge function association configuration to pass to `var.cloudfront_lambda_function_association` in the parent module." + value = module.lambda_edge.lambda_function_association +} diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf new file mode 100644 index 000000000..92d9d1313 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf @@ -0,0 +1,19 @@ +variable "cloudfront_distribution_domain_name" { + type = string + description = "Cloudfront Distribution Domain Name." +} + +variable "cloudfront_distribution_hosted_zone_id" { + type = string + description = "The CloudFront Distribution Hosted Zone ID." +} + +variable "parent_zone_name" { + type = string + description = "The name of the Route53 Hosted Zone where aliases for the CloudFront distribution are created." +} + +variable "site_fqdn" { + type = string + description = "The fully qualified alias for the CloudFront Distribution." +} diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/versions.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/versions.tf new file mode 100644 index 000000000..19b91b119 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/versions.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + configuration_aliases = [aws.us-east-1] + } + } +} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/context.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/index.js b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/index.js new file mode 100644 index 000000000..301c8445d --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/index.js @@ -0,0 +1,119 @@ +'use strict'; + +const http = require('https'); + +const indexPage = 'index.html'; + +exports.handler = async (event, context, callback) => { + const cf = event.Records[0].cf; + const request = cf.request; + const response = cf.response; + const statusCode = response.status; + + // Only replace 403 and 404 requests typically received + // when loading a page for a SPA that uses client-side routing + const doReplace = request.method === 'GET' + && (statusCode == '403' || statusCode == '404'); + + const result = doReplace + ? await generateResponseAndLog(cf, request, indexPage) + : response; + + response.status = result.status; + response.headers = {...response.headers, ...result.headers}; + response.body = result.body; + + callback(null, response); +}; + +async function generateResponseAndLog(cf, request, indexPage){ + + const domain = cf.config.distributionDomainName; + const indexPath = `/${indexPage}`; + + const response = await generateResponse(domain, indexPath); + console.log('response: ' + JSON.stringify(response)); + return response; +} + +async function generateResponse(domain, path){ + try { + // Load HTML index from the CloudFront cache + const s3Response = await httpGet({ hostname: domain, path: path }); + + const headers = s3Response.headers || + { + 'content-type': [{ value: 'text/html;charset=UTF-8' }] + }; + + return { + status: '200', + statusDescription: 'OK', + headers: wrapAndFilterHeaders(headers), + body: s3Response.body + }; + } catch (error) { + return { + status: '500', + headers:{ + 'content-type': [{ value: 'text/plain' }] + }, + body: 'An error occurred loading the page' + }; + } +} + +function httpGet(params) { + return new Promise((resolve, reject) => { + http.get(params, (resp) => { + let result = { + headers: resp.headers, + body: '' + }; + resp.on('data', (chunk) => { result.body += chunk; }); + resp.on('end', () => { resolve(result); }); + }).on('error', (err) => { + console.log(`Couldn't fetch ${params.hostname}${params.path} : ${err.message}`); + reject(err, null); + }); + }); +} + +// Cloudfront requires header values to be wrapped in an array +function wrapAndFilterHeaders(headers){ + const allowedHeaders = [ + "content-type", + "content-length", + "date", + "last-modified", + "etag", + "cache-control", + "accept-ranges", + "server", + "age" + ]; + + const responseHeaders = {}; + + if(!headers){ + return responseHeaders; + } + + for(var propName in headers) { + // only include allowed headers + if(allowedHeaders.includes(propName.toLowerCase())){ + var header = headers[propName]; + + if (Array.isArray(header)){ + // assume already 'wrapped' format + responseHeaders[propName] = header; + } else { + // fix to required format + responseHeaders[propName] = [{ value: header }]; + } + } + + } + + return responseHeaders; +} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf new file mode 100644 index 000000000..36603d5d6 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf @@ -0,0 +1,30 @@ +# https://levelup.gitconnected.com/preview-environments-in-aws-with-cloudfront-and-lambda-edge-7acccb0b67d1 + +locals { + lambda_runtime = "nodejs12.x" + lambda_handler = "index.handler" +} + +module "lambda_edge" { + source = "cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge" + version = "0.82.4" + + functions = { + origin_response = { + source = [{ + content = file("${path.module}/index.js") + filename = "index.js" + }] + runtime = local.lambda_runtime + handler = local.lambda_handler + event_type = "origin-response" + include_body = false + } + } + + providers = { + aws = aws.us-east-1 + } + + context = module.this.context +} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/outputs.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/outputs.tf new file mode 100644 index 000000000..315e14b86 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/outputs.tf @@ -0,0 +1,4 @@ +output "lambda_function_association" { + description = "The Lambda@Edge function association configuration to pass to `var.cloudfront_lambda_function_association` in the parent module." + value = module.lambda_edge.lambda_function_association +} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf new file mode 100644 index 000000000..92d9d1313 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf @@ -0,0 +1,19 @@ +variable "cloudfront_distribution_domain_name" { + type = string + description = "Cloudfront Distribution Domain Name." +} + +variable "cloudfront_distribution_hosted_zone_id" { + type = string + description = "The CloudFront Distribution Hosted Zone ID." +} + +variable "parent_zone_name" { + type = string + description = "The name of the Route53 Hosted Zone where aliases for the CloudFront distribution are created." +} + +variable "site_fqdn" { + type = string + description = "The fully qualified alias for the CloudFront Distribution." +} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/versions.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/versions.tf new file mode 100644 index 000000000..19b91b119 --- /dev/null +++ b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/versions.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + configuration_aliases = [aws.us-east-1] + } + } +} diff --git a/modules/spa-s3-cloudfront/outputs.tf b/modules/spa-s3-cloudfront/outputs.tf new file mode 100644 index 000000000..eafbcf7a0 --- /dev/null +++ b/modules/spa-s3-cloudfront/outputs.tf @@ -0,0 +1,24 @@ +output "origin_s3_bucket_name" { + value = module.spa_web.s3_bucket + description = "Origin bucket name." +} + +output "origin_s3_bucket_arn" { + value = module.spa_web.s3_bucket_arn + description = "Origin bucket ARN." +} + +output "cloudfront_distribution_domain_name" { + value = module.spa_web.cf_domain_name + description = "Cloudfront Distribution Domain Name." +} + +output "cloudfront_distribution_alias" { + value = module.spa_web.aliases + description = "Cloudfront Distribution Alias Record." +} + +output "failover_s3_bucket_name" { + value = try(data.aws_s3_bucket.failover_bucket[0].bucket, null) + description = "Failover Origin bucket name, if enabled." +} diff --git a/modules/spa-s3-cloudfront/providers.tf b/modules/spa-s3-cloudfront/providers.tf new file mode 100644 index 000000000..fef70fbce --- /dev/null +++ b/modules/spa-s3-cloudfront/providers.tf @@ -0,0 +1,56 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +provider "aws" { + region = local.failover_region # if var.failover_s3_region is not set, this will fall back on var.region + + alias = "failover" + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +# For cloudfront, the acm has to be created in us-east-1 or it will not work +provider "aws" { + region = "us-east-1" + + alias = "us-east-1" + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/spa-s3-cloudfront/remote-state.tf b/modules/spa-s3-cloudfront/remote-state.tf new file mode 100644 index 000000000..9685e2987 --- /dev/null +++ b/modules/spa-s3-cloudfront/remote-state.tf @@ -0,0 +1,41 @@ +module "waf" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" + + bypass = !local.aws_waf_enabled + component = var.cloudfront_aws_waf_component_name + privileged = false + environment = var.cloudfront_aws_waf_environment + + defaults = { + acl = { + arn = "" + } + } + + context = module.this.context +} + +module "dns_delegated" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" + + component = "dns-delegated" + environment = var.dns_delegated_environment_name + + context = module.this.context +} + +module "github_runners" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "0.22.4" + + count = local.github_runners_enabled ? 1 : 0 + + component = "github-runners" + stage = var.github_runners_stage_name + environment = var.github_runners_environment_name + tenant = try(var.github_runners_tenant_name, var.tenant) + + context = module.this.context +} diff --git a/modules/spa-s3-cloudfront/variables.tf b/modules/spa-s3-cloudfront/variables.tf new file mode 100644 index 000000000..1e50f7725 --- /dev/null +++ b/modules/spa-s3-cloudfront/variables.tf @@ -0,0 +1,491 @@ +variable "region" { + type = string + description = "AWS Region." +} + +variable "parent_zone_name" { + type = string + default = "" + description = "Parent domain name of site to publish. Defaults to format(parent_zone_name_pattern, stage, environment)." +} + +variable "process_domain_validation_options" { + type = bool + default = true + description = "Flag to enable/disable processing of the record to add to the DNS zone to complete certificate validation" +} + +variable "site_fqdn" { + type = string + default = "" + description = "Fully qualified domain name of site to publish. Overrides site_subdomain and parent_zone_name." +} + +variable "site_subdomain" { + type = string + default = "" + description = "Subdomain to plug into site_name_pattern to make site FQDN." +} + +variable "external_aliases" { + type = list(string) + default = [] + description = <<-EOT + List of FQDN's - Used to set the Alternate Domain Names (CNAMEs) setting on CloudFront. No new Route53 records will be created for these. + + Setting `process_domain_validation_options` to true may cause the component to fail if an external_alias DNS zone is not controlled by Terraform. + + Setting `preview_environment_enabled` to `true` will cause this variable to be ignored. + EOT +} + +variable "s3_website_enabled" { + type = bool + default = false + description = <<-EOT + Set to true to enable the created S3 bucket to serve as a website independently of CloudFront, + and to use that website as the origin. + + Setting `preview_environment_enabled` will implicitly set this to `true`. + EOT +} + +variable "s3_website_password_enabled" { + type = bool + default = false + description = <<-EOT + If set to true, and `s3_website_enabled` is also true, a password will be required in the `Referrer` field of the + HTTP request in order to access the website, and CloudFront will be configured to pass this password in its requests. + This will make it much harder for people to bypass CloudFront and access the S3 website directly via its website endpoint. + EOT +} + +variable "s3_object_ownership" { + type = string + default = "ObjectWriter" + description = "Specifies the S3 object ownership control on the origin bucket. Valid values are `ObjectWriter`, `BucketOwnerPreferred`, and 'BucketOwnerEnforced'." +} + +variable "origin_s3_access_logging_enabled" { + type = bool + default = null + description = <<-EOF + Set `true` to deliver S3 Access Logs to the `origin_s3_access_log_bucket_name` bucket. + Defaults to `false` if `origin_s3_access_log_bucket_name` is empty (the default), `true` otherwise. + Must be set explicitly if the access log bucket is being created at the same time as this module is being invoked. + EOF +} + +variable "origin_s3_access_log_bucket_name" { + type = string + default = "" + description = "Name of the existing S3 bucket where S3 Access Logs for the origin Bucket will be delivered. Default is not to enable S3 Access Logging for the origin Bucket." +} + +variable "origin_s3_access_log_bucket_name_rendering_enabled" { + type = bool + description = <<-EOT + If set to `true`, then the S3 origin access logs bucket name will be rendered by calling `format("%v-%v-%v-%v", var.namespace, var.environment, var.stage, var.origin_s3_access_log_bucket_name)`. + Otherwise, the value for `origin_s3_access_log_bucket_name` will need to be the globally unique name of the access logs bucket. + + For example, if this component produces an origin bucket named `eg-ue1-devplatform-example` and `origin_s3_access_log_bucket_name` is set to + `example-s3-access-logs`, then the bucket name will be rendered to be `eg-ue1-devplatform-example-s3-access-logs`. + EOT + default = false +} + +variable "origin_s3_access_log_prefix" { + type = string + default = "" + description = "Prefix to use for S3 Access Log object keys. Defaults to `logs/$${module.this.id}`" +} + +variable "origin_force_destroy" { + type = bool + default = false + description = "A boolean string that indicates all objects should be deleted from the origin Bucket so that the Bucket can be destroyed without error. These objects are not recoverable." +} + +variable "origin_versioning_enabled" { + type = bool + default = false + description = "Enable or disable versioning for the origin Bucket. Versioning is a means of keeping multiple variants of an object in the same bucket." +} + +variable "origin_deployment_principal_arns" { + type = list(string) + description = "List of role ARNs to grant deployment permissions to the origin Bucket." + default = [] +} + +variable "origin_deployment_actions" { + type = list(string) + default = [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:GetObject", + "s3:DeleteObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:GetBucketLocation", + "s3:AbortMultipartUpload" + ] + description = "List of actions to permit `origin_deployment_principal_arns` to perform on bucket and bucket prefixes (see `origin_deployment_principal_arns`)" +} + +variable "origin_allow_ssl_requests_only" { + type = bool + default = true + description = "Set to `true` in order to have the origin bucket require requests to use Secure Socket Layer (HTTPS/SSL). This will explicitly deny access to HTTP requests" +} + +variable "block_origin_public_access_enabled" { + type = bool + default = true + description = "When set to 'true' the s3 origin bucket will have public access block enabled." +} + +variable "origin_encryption_enabled" { + type = bool + default = true + description = "When set to 'true' the origin Bucket will have aes256 encryption enabled by default." +} + +variable "cloudfront_allowed_methods" { + type = list(string) + default = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] + description = "List of allowed methods (e.g. GET, PUT, POST, DELETE, HEAD) for AWS CloudFront." +} + +variable "cloudfront_cached_methods" { + type = list(string) + default = ["GET", "HEAD"] + description = "List of cached methods (e.g. GET, PUT, POST, DELETE, HEAD)." +} + +variable "cloudfront_compress" { + type = bool + default = false + description = "Compress content for web requests that include Accept-Encoding: gzip in the request header." +} + +variable "cloudfront_custom_error_response" { + # http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/custom-error-pages.html#custom-error-pages-procedure + # https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#custom-error-response-arguments + type = list(object({ + error_caching_min_ttl = string + error_code = string + response_code = string + response_page_path = string + })) + + description = "List of one or more custom error response element maps." + default = [] +} + +variable "cloudfront_access_log_create_bucket" { + type = bool + default = true + description = <<-EOT + When `true` and `cloudfront_access_logging_enabled` is also true, this module will create a new, + separate S3 bucket to receive CloudFront Access Logs. + EOT +} + +variable "cloudfront_access_log_bucket_name" { + type = string + default = "" + description = <<-EOT + When `cloudfront_access_log_create_bucket` is `false`, this is the name of the existing S3 Bucket where + CloudFront Access Logs are to be delivered and is required. IGNORED when `cloudfront_access_log_create_bucket` is `true`. + EOT +} + +variable "cloudfront_access_log_bucket_name_rendering_enabled" { + type = bool + description = <<-EOT + If set to `true`, then the CloudFront origin access logs bucket name will be rendered by calling `format("%v-%v-%v-%v", var.namespace, var.environment, var.stage, var.cloudfront_access_log_bucket_name)`. + Otherwise, the value for `cloudfront_access_log_bucket_name` will need to be the globally unique name of the access logs bucket. + + For example, if this component produces an origin bucket named `eg-ue1-devplatform-example` and `cloudfront_access_log_bucket_name` is set to + `example-cloudfront-access-logs`, then the bucket name will be rendered to be `eg-ue1-devplatform-example-cloudfront-access-logs`. + EOT + default = false +} + +variable "cloudfront_access_log_prefix" { + type = string + default = "" + description = "Prefix to use for CloudFront Access Log object keys. Defaults to no prefix." +} + +variable "cloudfront_access_log_prefix_rendering_enabled" { + type = bool + default = false + description = "Whether or not to dynamically render $${module.this.id} at the end of `var.cloudfront_access_log_prefix`." +} + +variable "cloudfront_aws_shield_protection_enabled" { + description = "Enable or disable AWS Shield Advanced protection for the CloudFront distribution. If set to 'true', a subscription to AWS Shield Advanced must exist in this account." + type = bool + default = false +} + +variable "cloudfront_aws_waf_protection_enabled" { + description = <<-EOT + Enable or disable AWS WAF for the CloudFront distribution. + + This assumes that the `aws-waf-acl-default-cloudfront` component has been deployed to the regional stack corresponding + to `var.waf_acl_environment`. + EOT + type = bool + default = true +} + +variable "cloudfront_aws_waf_environment" { + type = string + description = "The environment where the WAF ACL for CloudFront distribution exists." + default = null +} + +variable "cloudfront_aws_waf_component_name" { + type = string + description = "The name of the component used when deploying WAF ACL" + default = "waf" +} + +variable "cloudfront_default_root_object" { + type = string + default = "index.html" + description = "Object that CloudFront return when requests the root URL." +} + +variable "cloudfront_default_ttl" { + type = number + default = 60 + description = "Default amount of time (in seconds) that an object is in a CloudFront cache." +} + +variable "cloudfront_min_ttl" { + type = number + default = 0 + description = "Minimum amount of time that you want objects to stay in CloudFront caches." +} + +variable "cloudfront_max_ttl" { + type = number + default = 31536000 + description = "Maximum amount of time (in seconds) that an object is in a CloudFront cache." +} + +variable "cloudfront_index_document" { + type = string + default = "index.html" + description = "Amazon S3 returns this index document when requests are made to the root domain or any of the subfolders." +} + +variable "cloudfront_ipv6_enabled" { + type = bool + default = true + description = "Set to true to enable an AAAA DNS record to be set as well as the A record." +} + +variable "cloudfront_viewer_protocol_policy" { + type = string + description = "Limit the protocol users can use to access content. One of `allow-all`, `https-only`, or `redirect-to-https`." + default = "redirect-to-https" +} + +variable "cloudfront_lambda_function_association" { + type = list(object({ + event_type = string + include_body = bool + lambda_arn = string + })) + + description = "A config block that configures the CloudFront distribution with lambda@edge functions for specific events." + default = [] +} + +variable "custom_origins" { + type = list(object({ + domain_name = string + origin_id = string + origin_path = string + custom_headers = list(object({ + name = string + value = string + })) + custom_origin_config = object({ + http_port = number + https_port = number + origin_protocol_policy = string + origin_ssl_protocols = list(string) + origin_keepalive_timeout = number + origin_read_timeout = number + }) + })) + default = [] + description = <<-EOT + A list of additional custom website [origins](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments) for this distribution. + EOT +} + +variable "dns_delegated_environment_name" { + description = "The environment where `dns-delegated` component is deployed to" + type = string + default = "gbl" +} + +variable "failover_criteria_status_codes" { + type = list(string) + description = "List of HTTP Status Codes to use as the origin group failover criteria." + default = [ + 403, + 404, + 500, + 502 + ] +} + +variable "failover_s3_origin_format" { + type = string + description = <<-EOT + If `var.failover_s3_origin_environment` is supplied, this is the format to use for the failover S3 origin bucket name when + building the name via `format([format], var.namespace, var.failover_s3_origin_environment, var.stage, var.name)` + and then looking it up via the `aws_s3_bucket` Data Source. + + For example, if this component creates an origin of name `eg-ue1-devplatform-example` and `var.failover_s3_origin_environment` + is set to `uw1`, then it is expected that a bucket with the name `eg-uw1-devplatform-example-failover` exists in `us-west-1`. + EOT + default = "%v-%v-%v-%v-failover" +} + +variable "failover_s3_origin_environment" { + type = string + description = <<-EOT + The [fixed name](https://github.com/cloudposse/terraform-aws-utils/blob/399951e552483a4f4c1dc7fbe2675c443f3dbd83/main.tf#L10) of the AWS Region where the + failover S3 origin exists. Setting this variable will enable use of a failover S3 origin, but it is required for the + failover S3 origin to exist beforehand. This variable is used in conjunction with `var.failover_s3_origin_format` to + build out the name of the Failover S3 origin in the specified region. + + For example, if this component creates an origin of name `eg-ue1-devplatform-example` and this variable is set to `uw1`, + then it is expected that a bucket with the name `eg-uw1-devplatform-example-failover` exists in `us-west-1`. + EOT + default = null +} + +variable "forward_cookies" { + type = string + default = "none" + description = "Specifies whether you want CloudFront to forward all or no cookies to the origin. Can be 'all' or 'none'" +} + +variable "forward_header_values" { + type = list(string) + description = "A list of whitelisted header values to forward to the origin (incompatible with `cache_policy_id`)" + default = ["Access-Control-Request-Headers", "Access-Control-Request-Method", "Origin"] +} + +variable "ordered_cache" { + type = list(object({ + target_origin_id = string + path_pattern = string + + allowed_methods = list(string) + cached_methods = list(string) + compress = bool + trusted_signers = list(string) + trusted_key_groups = list(string) + + cache_policy_id = string + origin_request_policy_id = string + + viewer_protocol_policy = string + min_ttl = number + default_ttl = number + max_ttl = number + response_headers_policy_id = string + + forward_query_string = bool + forward_header_values = list(string) + forward_cookies = string + forward_cookies_whitelisted_names = list(string) + + lambda_function_association = list(object({ + event_type = string + include_body = bool + lambda_arn = string + })) + + function_association = list(object({ + event_type = string + function_arn = string + })) + })) + default = [] + description = <<-EOT + An ordered list of [cache behaviors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#cache-behavior-arguments) resource for this distribution. + List in order of precedence (first match wins). This is in addition to the default cache policy. + Set `target_origin_id` to `""` to specify the S3 bucket origin created by this module. + EOT +} + +variable "preview_environment_enabled" { + type = bool + description = <<-EOT + Enable or disable SPA Preview Environments via Lambda@Edge, i.e. mapping `subdomain.example.com` to the `/subdomain` + path in the origin S3 bucket. + + This variable implicitly affects the following variables: + + * `s3_website_enabled` + * `s3_website_password_enabled` + * `block_origin_public_access_enabled` + * `origin_allow_ssl_requests_only` + * `forward_header_values` + * `cloudfront_default_ttl` + * `cloudfront_min_ttl` + * `cloudfront_max_ttl` + * `cloudfront_lambda_function_association` + EOT + default = false +} + +variable "lambda_edge_redirect_404_enabled" { + type = bool + description = <<-EOT + Enable or disable SPA 404 redirects via Lambda@Edge - returns a 302 and a location of `/` if the request returned 404. + EOT + default = false +} + +variable "github_runners_deployment_principal_arn_enabled" { + type = bool + description = "A flag that is used to decide whether or not to include the GitHub Runner's IAM role in origin_deployment_principal_arns list" + default = true +} + +variable "github_runners_component_name" { + type = string + description = "The name of the component that deploys GitHub Runners, used in remote-state lookup" + default = "github-runners" +} + +variable "github_runners_environment_name" { + type = string + description = "The name of the environment where the CloudTrail bucket is provisioned" + default = "ue2" +} + +variable "github_runners_stage_name" { + type = string + description = "The stage name where the CloudTrail bucket is provisioned" + default = "auto" +} + +variable "github_runners_tenant_name" { + type = string + description = "The tenant name where the GitHub Runners are provisioned" + default = null +} diff --git a/modules/spa-s3-cloudfront/versions.tf b/modules/spa-s3-cloudfront/versions.tf new file mode 100644 index 000000000..cc73ffd35 --- /dev/null +++ b/modules/spa-s3-cloudfront/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From b73d1474a891bad49ccbbd5ddab7f1801ba3787c Mon Sep 17 00:00:00 2001 From: Ray Botha Date: Tue, 7 Feb 2023 12:14:51 -0800 Subject: [PATCH 030/501] fix dd-forwarder: datadog service config depends on lambda arn config (#531) Co-authored-by: Benjamin Smith --- modules/datadog-lambda-forwarder/main.tf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/datadog-lambda-forwarder/main.tf b/modules/datadog-lambda-forwarder/main.tf index 7ef6c30f5..4e323bb58 100644 --- a/modules/datadog-lambda-forwarder/main.tf +++ b/modules/datadog-lambda-forwarder/main.tf @@ -110,4 +110,6 @@ resource "datadog_integration_aws_log_collection" "main" { count = var.lambda_arn_enabled ? 1 : 0 account_id = module.datadog-integration.outputs.aws_account_id services = var.log_collection_services + + depends_on = [module.datadog_lambda_forwarder] } From e1c15ee78043426721589c5d4a0850bd02913f88 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 7 Feb 2023 13:31:57 -0800 Subject: [PATCH 031/501] Upstream `dynamodb` (#512) --- modules/dynamodb/README.md | 5 +++-- modules/dynamodb/default.auto.tfvars | 5 ----- modules/dynamodb/main.tf | 4 ++-- modules/dynamodb/providers.tf | 18 ++++++++++++++++-- modules/dynamodb/versions.tf | 2 +- 5 files changed, 22 insertions(+), 12 deletions(-) delete mode 100644 modules/dynamodb/default.auto.tfvars diff --git a/modules/dynamodb/README.md b/modules/dynamodb/README.md index 4da60de42..fb7017a2d 100644 --- a/modules/dynamodb/README.md +++ b/modules/dynamodb/README.md @@ -34,7 +34,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | ## Providers @@ -44,7 +44,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [dynamodb\_table](#module\_dynamodb\_table) | cloudposse/dynamodb/aws | 0.29.2 | +| [dynamodb\_table](#module\_dynamodb\_table) | cloudposse/dynamodb/aws | 0.31.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -80,6 +80,7 @@ No resources. | [hash\_key\_type](#input\_hash\_key\_type) | Hash Key type, which must be a scalar type: `S`, `N`, or `B` for String, Number or Binary data, respectively. | `string` | `"S"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/dynamodb/default.auto.tfvars b/modules/dynamodb/default.auto.tfvars deleted file mode 100644 index 99105f049..000000000 --- a/modules/dynamodb/default.auto.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# This file is included by default in terraform plans - -enabled = true - -name = "dynamodb" diff --git a/modules/dynamodb/main.tf b/modules/dynamodb/main.tf index 93e4b376e..d7f7e21f1 100644 --- a/modules/dynamodb/main.tf +++ b/modules/dynamodb/main.tf @@ -6,7 +6,7 @@ locals { module "dynamodb_table" { source = "cloudposse/dynamodb/aws" - version = "0.29.2" + version = "0.31.0" billing_mode = var.billing_mode replicas = var.replicas @@ -42,4 +42,4 @@ module "dynamodb_table" { enable_point_in_time_recovery = var.point_in_time_recovery_enabled context = module.this.context -} \ No newline at end of file +} diff --git a/modules/dynamodb/providers.tf b/modules/dynamodb/providers.tf index c6e854450..08ee01b2a 100644 --- a/modules/dynamodb/providers.tf +++ b/modules/dynamodb/providers.tf @@ -1,6 +1,14 @@ provider "aws" { - region = var.region - profile = coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } } module "iam_roles" { @@ -13,3 +21,9 @@ variable "import_profile_name" { default = null description = "AWS Profile name to use when importing a resource" } + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/dynamodb/versions.tf b/modules/dynamodb/versions.tf index d5cde7755..cc73ffd35 100644 --- a/modules/dynamodb/versions.tf +++ b/modules/dynamodb/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.0" + version = ">= 4.9.0" } } } From 9c996c960771aca2ed8d366ffadaa9222c7396b5 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 8 Feb 2023 08:33:58 -0800 Subject: [PATCH 032/501] Upstream datadog logs archive (#552) --- modules/datadog-logs-archive/README.md | 14 +++++++------- modules/datadog-logs-archive/asm.tf | 19 ------------------- modules/datadog-logs-archive/main.tf | 16 +++++++++++----- .../datadog-logs-archive/provider-datadog.tf | 8 ++++++++ modules/datadog-logs-archive/providers.tf | 6 ------ modules/datadog-logs-archive/ssm.tf | 11 ----------- modules/datadog-logs-archive/variables.tf | 19 ------------------- modules/datadog-logs-archive/versions.tf | 2 +- 8 files changed, 27 insertions(+), 68 deletions(-) delete mode 100644 modules/datadog-logs-archive/asm.tf delete mode 100644 modules/datadog-logs-archive/ssm.tf diff --git a/modules/datadog-logs-archive/README.md b/modules/datadog-logs-archive/README.md index 293f6e132..6c4e26a5d 100644 --- a/modules/datadog-logs-archive/README.md +++ b/modules/datadog-logs-archive/README.md @@ -1,12 +1,12 @@ # Component: `datadog-logs-archive` -This component is responsible for provisioning Datadog Log Archives. It creates a single log archive pipeline for each AWS account. If the `catchall` flag is set, it creates a catchall archive within the same S3 bucket. +This component is responsible for provisioning Datadog Log Archives. It creates a single log archive pipeline for each AWS account. If the `catchall` flag is set, it creates a catchall archive within the same S3 bucket. Each log archive filters for the tag `env:$env` where $env is the environment/account name (ie sbx, prd, tools, etc), as well as any tags identified in the additional_tags key. The `catchall` archive, as the name implies, filters for '*'. A second bucket is created for cloudtrail, and a cloudtrail is configured to monitor the log archive bucket and log activity to the cloudtrail bucket. To forward these cloudtrail logs to datadog, the cloudtrail bucket's id must be added to the s3_buckets key for our datadog-lambda-forwarder component. -Both buckets support object lock, with overrideable defaults of COMPLIANCE mode with a duration of 7 days. +Both buckets support object lock, with overrideable defaults of COMPLIANCE mode with a duration of 7 days. ## Prerequisites @@ -22,8 +22,8 @@ Because of the protections for S3 buckets, if we want to destroy/replace our buc #### Two step process to destroy via terraform * first set `s3_force_destroy` var to true and apply * next set `enabled` to false and apply or use tf destroy - - + + ## Usage **Stack Level**: Global @@ -39,9 +39,9 @@ components: workspace_enabled: true vars: enabled: true - additional_query_tags: - - "forwardername:tzl-*-dev-datadog-lambda-forwarder-logs" - - "account:852653222113" + # additional_query_tags: + # - "forwardername:*-dev-datadog-lambda-forwarder-logs" + # - "account:123456789012" ``` diff --git a/modules/datadog-logs-archive/asm.tf b/modules/datadog-logs-archive/asm.tf deleted file mode 100644 index a4e64b0b6..000000000 --- a/modules/datadog-logs-archive/asm.tf +++ /dev/null @@ -1,19 +0,0 @@ -data "aws_secretsmanager_secret" "datadog_api_key" { - count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 - name = var.datadog_api_secret_key -} - -data "aws_secretsmanager_secret_version" "datadog_api_key" { - count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 - secret_id = data.aws_secretsmanager_secret.datadog_api_key[0].id -} - -data "aws_secretsmanager_secret" "datadog_app_key" { - count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 - name = var.datadog_app_secret_key -} - -data "aws_secretsmanager_secret_version" "datadog_app_key" { - count = local.enabled && var.secrets_store_type == "ASM" ? 1 : 0 - secret_id = data.aws_secretsmanager_secret.datadog_app_key[0].id -} diff --git a/modules/datadog-logs-archive/main.tf b/modules/datadog-logs-archive/main.tf index 102dce9b0..66e6d593e 100644 --- a/modules/datadog-logs-archive/main.tf +++ b/modules/datadog-logs-archive/main.tf @@ -16,20 +16,26 @@ locals { } ] - non_catchall_ids = [for x in jsondecode(join("", data.http.current_order.*.body)).data : x.id if x.attributes.name != "catchall"] - catchall_id = [for x in jsondecode(join("", data.http.current_order.*.body)).data : x.id if x.attributes.name == "catchall"] + # in case enabled: false and we have no current order to lookup + data_current_order_body = one(data.http.current_order.*.response_body) == null ? {} : jsondecode(data.http.current_order[0].response_body) + # in case there is no response (valid http request but no existing data) + current_order_data = lookup(local.data_current_order_body, "data", null) + + non_catchall_ids = local.enabled ? [for x in local.current_order_data : x.id if x.attributes.name != "catchall"] : [] + catchall_id = local.enabled ? [for x in local.current_order_data : x.id if x.attributes.name == "catchall"] : [] ordered_ids = concat(local.non_catchall_ids, local.catchall_id) - policy = jsondecode(data.aws_iam_policy_document.default[0].json) + policy = local.enabled ? jsondecode(data.aws_iam_policy_document.default[0].json) : null } # We use the http data source due to lack of a data source for datadog_logs_archive_order +# While the data source does exist, it doesn't provide useful information, nor how to lookup the id of a log archive order # This fetches the current order from DD's api so we can shuffle it around if needed to # keep the catchall in last place. data "http" "current_order" { count = local.enabled ? 1 : 0 - url = "https://api.datadoghq.com/api/v2/logs/config/archives" + url = format("https://api.%s/api/v2/logs/config/archives", module.datadog_configuration.datadog_site) depends_on = [datadog_logs_archive.logs_archive, datadog_logs_archive.catchall_archive] request_headers = { Accept = "application/json", @@ -134,7 +140,7 @@ module "bucket_policy" { source = "cloudposse/iam-policy/aws" version = "0.3.0" - iam_policy_statements = lookup(local.policy, "Statement") + iam_policy_statements = try(lookup(local.policy, "Statement"), null) context = module.this.context } diff --git a/modules/datadog-logs-archive/provider-datadog.tf b/modules/datadog-logs-archive/provider-datadog.tf index 886e23a30..963dce116 100644 --- a/modules/datadog-logs-archive/provider-datadog.tf +++ b/modules/datadog-logs-archive/provider-datadog.tf @@ -2,9 +2,17 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" region = var.region context = module.this.context + enabled = true } locals { datadog_api_key = module.datadog_configuration.datadog_api_key datadog_app_key = module.datadog_configuration.datadog_app_key } + +provider "datadog" { + api_key = local.datadog_api_key + app_key = local.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} diff --git a/modules/datadog-logs-archive/providers.tf b/modules/datadog-logs-archive/providers.tf index 24cf03434..08ee01b2a 100755 --- a/modules/datadog-logs-archive/providers.tf +++ b/modules/datadog-logs-archive/providers.tf @@ -1,9 +1,3 @@ -provider "datadog" { - api_key = local.datadog_api_key - app_key = local.datadog_app_key - validate = local.enabled -} - provider "aws" { region = var.region diff --git a/modules/datadog-logs-archive/ssm.tf b/modules/datadog-logs-archive/ssm.tf deleted file mode 100644 index a78029cb9..000000000 --- a/modules/datadog-logs-archive/ssm.tf +++ /dev/null @@ -1,11 +0,0 @@ -data "aws_ssm_parameter" "datadog_api_key" { - count = local.enabled && var.secrets_store_type == "SSM" ? 1 : 0 - name = format("/%s", var.datadog_api_secret_key) - with_decryption = true -} - -data "aws_ssm_parameter" "datadog_app_key" { - count = local.enabled && var.secrets_store_type == "SSM" ? 1 : 0 - name = format("/%s", var.datadog_app_secret_key) - with_decryption = true -} diff --git a/modules/datadog-logs-archive/variables.tf b/modules/datadog-logs-archive/variables.tf index 60148a363..d2e59e024 100644 --- a/modules/datadog-logs-archive/variables.tf +++ b/modules/datadog-logs-archive/variables.tf @@ -9,25 +9,6 @@ variable "additional_query_tags" { default = [] } - -variable "secrets_store_type" { - type = string - description = "Secret store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" - default = "SSM" -} - -variable "datadog_api_secret_key" { - type = string - description = "The key of the Datadog API secret" - default = "datadog/datadog_api_key" -} - -variable "datadog_app_secret_key" { - type = string - description = "The key of the Datadog Application secret" - default = "datadog/datadog_app_key" -} - variable "catchall_enabled" { type = bool description = "Set to true to enable a catchall for logs unmatched by any queries. This should only be used in one environment/account" diff --git a/modules/datadog-logs-archive/versions.tf b/modules/datadog-logs-archive/versions.tf index 4a385964f..372817512 100644 --- a/modules/datadog-logs-archive/versions.tf +++ b/modules/datadog-logs-archive/versions.tf @@ -8,7 +8,7 @@ terraform { } datadog = { source = "datadog/datadog" - version = ">= 3.3.0" + version = ">= 3.19" } http = { source = "hashicorp/http" From 1345d7cdae8e574fd8b20f41a0604232885df269 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 13 Feb 2023 08:58:54 -0800 Subject: [PATCH 033/501] Upstream `ACM` and `eks/Platform` for release_engineering (#555) --- modules/acm/README.md | 5 +- modules/acm/outputs.tf | 5 + modules/acm/versions.tf | 2 +- modules/eks/platform/README.md | 115 +++++++++++ modules/eks/platform/context.tf | 279 +++++++++++++++++++++++++++ modules/eks/platform/main.tf | 39 ++++ modules/eks/platform/outputs.tf | 0 modules/eks/platform/providers.tf | 29 +++ modules/eks/platform/remote-state.tf | 19 ++ modules/eks/platform/variables.tf | 33 ++++ modules/eks/platform/versions.tf | 14 ++ 11 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 modules/eks/platform/README.md create mode 100644 modules/eks/platform/context.tf create mode 100644 modules/eks/platform/main.tf create mode 100644 modules/eks/platform/outputs.tf create mode 100644 modules/eks/platform/providers.tf create mode 100644 modules/eks/platform/remote-state.tf create mode 100644 modules/eks/platform/variables.tf create mode 100644 modules/eks/platform/versions.tf diff --git a/modules/acm/README.md b/modules/acm/README.md index ee178632d..a5ee83f98 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -52,13 +52,13 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.9.0 | ## Modules @@ -118,6 +118,7 @@ components: | Name | Description | |------|-------------| | [arn](#output\_arn) | The ARN of the certificate | +| [domain\_name](#output\_domain\_name) | Certificate domain name | | [domain\_validation\_options](#output\_domain\_validation\_options) | CNAME records that are added to the DNS zone to complete certificate validation | | [id](#output\_id) | The ID of the certificate | diff --git a/modules/acm/outputs.tf b/modules/acm/outputs.tf index b6d753a08..875f2357f 100644 --- a/modules/acm/outputs.tf +++ b/modules/acm/outputs.tf @@ -12,3 +12,8 @@ output "domain_validation_options" { value = module.acm.domain_validation_options description = "CNAME records that are added to the DNS zone to complete certificate validation" } + +output "domain_name" { + value = local.enabled ? var.domain_name : null + description = "Certificate domain name" +} diff --git a/modules/acm/versions.tf b/modules/acm/versions.tf index e89eb16ed..cc73ffd35 100644 --- a/modules/acm/versions.tf +++ b/modules/acm/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.9.0" } } } diff --git a/modules/eks/platform/README.md b/modules/eks/platform/README.md new file mode 100644 index 000000000..e6926914f --- /dev/null +++ b/modules/eks/platform/README.md @@ -0,0 +1,115 @@ +# Component: `platform` + +This component maps another components' outputs into SSM parameter store to declare +platform context used by CI/CD workflows. + +## Usage + +**Stack Level**: Regional + +Once the catalog file is created, the file can be imported as follows. + +```yaml +import: + - catalog/eks/platform + ... +``` + +The default catalog values `e.g. stacks/catalog/eks/platform.yaml` + +```yaml +components: + terraform: + eks/platform: + metadata: + component: eks/platform + backend: + s3: + workspace_key_prefix: platform + settings: + spacelift: + depends_on: + - eks/cluster + - eks/alb-controller-ingress-group + vars: + enabled: true + name: "platform" + eks_component_name: eks/cluster + ssm_platform_path: /platform/%s/%s + platform_environment: default + references: + default_alb_ingress_group: + component: eks/alb-controller-ingress-group + output: group_name +``` + +That would read `group_name` from `eks/alb-controller-ingress-group` component outputs and +put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [helm](#requirement\_helm) | >= 2.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [remote](#module\_remote) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/eks"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [platform\_environment](#input\_platform\_environment) | Platform environment | `string` | `"default"` | no | +| [references](#input\_references) | Platform mapping from remote components outputs |
map(object({
component = string
output = string
}))
| `{}` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [ssm\_platform\_path](#input\_ssm\_platform\_path) | Format SSM path to store platform configs | `string` | `"/platform/%s/%s"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +No outputs. + + +[](https://cpco.io/component) diff --git a/modules/eks/platform/context.tf b/modules/eks/platform/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/platform/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/platform/main.tf b/modules/eks/platform/main.tf new file mode 100644 index 000000000..7c795fcdf --- /dev/null +++ b/modules/eks/platform/main.tf @@ -0,0 +1,39 @@ +locals { + enabled = module.this.enabled + partition = join("", data.aws_partition.current[*].partition) + metadata = { + kube_version = { + component = var.eks_component_name + output = "eks_cluster_version" + } + } +} + +data "aws_partition" "current" { + count = local.enabled ? 1 : 0 +} + +module "store_write" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.10.0" + + parameter_write = concat([for k, v in var.references : + { + name = format("%s/%s", format(var.ssm_platform_path, module.eks.outputs.eks_cluster_id, var.platform_environment), k) + value = lookup(module.remote[k].outputs, v.output) + type = "SecureString" + overwrite = true + description = "Platform config for ${var.platform_environment} at ${module.eks.outputs.eks_cluster_id} cluster" + } + ], + [for k, v in local.metadata : + { + name = format("%s/%s", format(var.ssm_platform_path, module.eks.outputs.eks_cluster_id, "_metadata"), k) + value = lookup(module.remote[k].outputs, v.output) + type = "SecureString" + overwrite = true + description = "Platform metadata for ${module.eks.outputs.eks_cluster_id} cluster" + } + ]) + context = module.this.context +} diff --git a/modules/eks/platform/outputs.tf b/modules/eks/platform/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/modules/eks/platform/providers.tf b/modules/eks/platform/providers.tf new file mode 100644 index 000000000..c2419aabb --- /dev/null +++ b/modules/eks/platform/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/eks/platform/remote-state.tf b/modules/eks/platform/remote-state.tf new file mode 100644 index 000000000..1c2e7e8fd --- /dev/null +++ b/modules/eks/platform/remote-state.tf @@ -0,0 +1,19 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.eks_component_name + + context = module.this.context +} + + +module "remote" { + for_each = merge(var.references, local.metadata) + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = each.value["component"] + + context = module.this.context +} diff --git a/modules/eks/platform/variables.tf b/modules/eks/platform/variables.tf new file mode 100644 index 000000000..3c3cbf9d3 --- /dev/null +++ b/modules/eks/platform/variables.tf @@ -0,0 +1,33 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "references" { + description = "Platform mapping from remote components outputs" + default = {} + type = map(object({ + component = string + output = string + })) +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/eks" +} + +variable "ssm_platform_path" { + type = string + description = "Format SSM path to store platform configs" + default = "/platform/%s/%s" +} + +variable "platform_environment" { + type = string + description = "Platform environment" + default = "default" +} + + diff --git a/modules/eks/platform/versions.tf b/modules/eks/platform/versions.tf new file mode 100644 index 000000000..47f2ceea4 --- /dev/null +++ b/modules/eks/platform/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + } +} From d295909f2e1fa60493f18e04efd867b9387ce995 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 13 Feb 2023 13:23:37 -0800 Subject: [PATCH 034/501] upstream `lambda` (#557) --- modules/lambda/README.md | 159 ++++++++++ modules/lambda/context.tf | 279 ++++++++++++++++++ modules/lambda/lambdas/.gitignore | 3 + .../lambda/lambdas/hello-world-go/Makefile | 18 ++ modules/lambda/lambdas/hello-world-go/go.mod | 5 + .../lambdas/hello-world-go/hello_world.go | 16 + modules/lambda/main.tf | 81 +++++ modules/lambda/outputs.tf | 29 ++ modules/lambda/providers.tf | 29 ++ modules/lambda/variables.tf | 273 +++++++++++++++++ modules/lambda/versions.tf | 10 + 11 files changed, 902 insertions(+) create mode 100644 modules/lambda/README.md create mode 100644 modules/lambda/context.tf create mode 100644 modules/lambda/lambdas/.gitignore create mode 100644 modules/lambda/lambdas/hello-world-go/Makefile create mode 100644 modules/lambda/lambdas/hello-world-go/go.mod create mode 100644 modules/lambda/lambdas/hello-world-go/hello_world.go create mode 100644 modules/lambda/main.tf create mode 100644 modules/lambda/outputs.tf create mode 100644 modules/lambda/providers.tf create mode 100644 modules/lambda/variables.tf create mode 100644 modules/lambda/versions.tf diff --git a/modules/lambda/README.md b/modules/lambda/README.md new file mode 100644 index 000000000..d744bf607 --- /dev/null +++ b/modules/lambda/README.md @@ -0,0 +1,159 @@ +# Component: `lambda` + +This component is responsible for provisioning Lambda functions. + +## Usage + +**Stack Level**: Regional + +Stack configuration for defaults: +```yaml +components: + terraform: + lambda-defaults: + metadata: + type: abstract + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true +``` + +Sample App Yaml Entry: +```yaml +import: + - catalog/lambda/defaults + +components: + terraform: + lambda/hello-world-go: + metadata: + component: lambda + inherits: + - lambda-defaults + vars: + name: hello-world-go + function_name: main + description: Hello Lambda from GoLang! + handler: hello_world # in go this is the compiled binary + memory_size: 256 + # https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html + runtime: go1.x + package_type: Zip + policy_json: null + s3_bucket_name: lambda-source # lambda main.tf calculates the rest of the bucket_name + s3_key: hello-world-go.zip +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [label](#module\_label) | cloudposse/label/null | 0.25.0 | +| [lambda](#module\_lambda) | cloudposse/lambda-function/aws | 0.4.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [architectures](#input\_architectures) | Instruction set architecture for your Lambda function. Valid values are ["x86\_64"] and ["arm64"].
Default is ["x86\_64"]. Removing this attribute, function's architecture stay the same. | `list(string)` | `null` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [cloudwatch\_event\_rules](#input\_cloudwatch\_event\_rules) | Creates EventBridge (CloudWatch Events) rules for invoking the Lambda Function along with the required permissions. | `map(any)` | `{}` | no | +| [cloudwatch\_lambda\_insights\_enabled](#input\_cloudwatch\_lambda\_insights\_enabled) | Enable CloudWatch Lambda Insights for the Lambda Function. | `bool` | `false` | no | +| [cloudwatch\_log\_subscription\_filters](#input\_cloudwatch\_log\_subscription\_filters) | CloudWatch Logs subscription filter resources. Currently supports only Lambda functions as destinations. | `map(any)` | `{}` | no | +| [cloudwatch\_logs\_kms\_key\_arn](#input\_cloudwatch\_logs\_kms\_key\_arn) | The ARN of the KMS Key to use when encrypting log data. | `string` | `null` | no | +| [cloudwatch\_logs\_retention\_in\_days](#input\_cloudwatch\_logs\_retention\_in\_days) | Specifies the number of days you want to retain log events in the specified log group. Possible values are:
1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653, and 0. If you select 0, the events in the
log group are always retained and never expire. | `number` | `null` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [custom\_iam\_policy\_arns](#input\_custom\_iam\_policy\_arns) | ARNs of custom policies to be attached to the lambda role | `set(string)` | `[]` | no | +| [dead\_letter\_config\_target\_arn](#input\_dead\_letter\_config\_target\_arn) | ARN of an SNS topic or SQS queue to notify when an invocation fails. If this option is used, the function's IAM role
must be granted suitable access to write to the target object, which means allowing either the sns:Publish or
sqs:SendMessage action on this ARN, depending on which service is targeted." | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input\_description) | Description of what the Lambda Function does. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [event\_source\_mappings](#input\_event\_source\_mappings) | Creates event source mappings to allow the Lambda function to get events from Kinesis, DynamoDB and SQS. The IAM role
of this Lambda function will be enhanced with necessary minimum permissions to get those events. | `any` | `{}` | no | +| [filename](#input\_filename) | The path to the function's deployment package within the local filesystem. If defined, The s3\_-prefixed options and image\_uri cannot be used. | `string` | `null` | no | +| [function\_name](#input\_function\_name) | Unique name for the Lambda Function. | `string` | n/a | yes | +| [handler](#input\_handler) | The function entrypoint in your code. | `string` | `null` | no | +| [iam\_policy\_description](#input\_iam\_policy\_description) | Description of the IAM policy for the Lambda IAM role | `string` | `"Minimum SSM read permissions for Lambda IAM Role"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [ignore\_external\_function\_updates](#input\_ignore\_external\_function\_updates) | Ignore updates to the Lambda Function executed externally to the Terraform lifecycle. Set this to `true` if you're
using CodeDeploy, aws CLI or other external tools to update the Lambda Function code." | `bool` | `false` | no | +| [image\_config](#input\_image\_config) | The Lambda OCI [image configurations](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function#image_config)
block with three (optional) arguments:
- *entry\_point* - The ENTRYPOINT for the docker image (type `list(string)`).
- *command* - The CMD for the docker image (type `list(string)`).
- *working\_directory* - The working directory for the docker image (type `string`). | `any` | `{}` | no | +| [image\_uri](#input\_image\_uri) | The ECR image URI containing the function's deployment package. Conflicts with `filename`, `s3_bucket_name`, `s3_key`, and `s3_object_version`. | `string` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kms\_key\_arn](#input\_kms\_key\_arn) | Amazon Resource Name (ARN) of the AWS Key Management Service (KMS) key that is used to encrypt environment variables.
If this configuration is not provided when environment variables are in use, AWS Lambda uses a default service key.
If this configuration is provided when environment variables are not in use, the AWS Lambda API does not save this
configuration and Terraform will show a perpetual difference of adding the key. To fix the perpetual difference,
remove this configuration. | `string` | `""` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lambda\_at\_edge\_enabled](#input\_lambda\_at\_edge\_enabled) | Enable Lambda@Edge for your Node.js or Python functions. The required trust relationship and publishing of function versions will be configured in this module. | `bool` | `false` | no | +| [lambda\_environment](#input\_lambda\_environment) | Environment (e.g. ENV variables) configuration for the Lambda function enable you to dynamically pass settings to your function code and libraries. |
object({
variables = map(string)
})
| `null` | no | +| [layers](#input\_layers) | List of Lambda Layer Version ARNs (maximum of 5) to attach to the Lambda Function. | `list(string)` | `[]` | no | +| [memory\_size](#input\_memory\_size) | Amount of memory in MB the Lambda Function can use at runtime. | `number` | `128` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [package\_type](#input\_package\_type) | The Lambda deployment package type. Valid values are `Zip` and `Image`. | `string` | `"Zip"` | no | +| [permissions\_boundary](#input\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the role | `string` | `""` | no | +| [policy\_json](#input\_policy\_json) | IAM policy to attach to the Lambda IAM role | `string` | `null` | no | +| [publish](#input\_publish) | Whether to publish creation/change as new Lambda Function Version. | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [reserved\_concurrent\_executions](#input\_reserved\_concurrent\_executions) | The amount of reserved concurrent executions for this lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `-1` | no | +| [runtime](#input\_runtime) | The runtime environment for the Lambda function you are uploading. | `string` | `null` | no | +| [s3\_bucket\_name](#input\_s3\_bucket\_name) | The name suffix of the S3 bucket containing the function's deployment package. Conflicts with filename and image\_uri.
This bucket must reside in the same AWS region where you are creating the Lambda function. | `string` | `null` | no | +| [s3\_key](#input\_s3\_key) | The S3 key of an object containing the function's deployment package. Conflicts with filename and image\_uri. | `string` | `null` | no | +| [s3\_object\_version](#input\_s3\_object\_version) | The object version containing the function's deployment package. Conflicts with filename and image\_uri. | `string` | `null` | no | +| [sns\_subscriptions](#input\_sns\_subscriptions) | Creates subscriptions to SNS topics which trigger the Lambda Function. Required Lambda invocation permissions will be generated. | `map(any)` | `{}` | no | +| [source\_code\_hash](#input\_source\_code\_hash) | Used to trigger updates. Must be set to a base64-encoded SHA256 hash of the package file specified with either
filename or s3\_key. The usual way to set this is filebase64sha256('file.zip') where 'file.zip' is the local filename
of the lambda function source archive. | `string` | `""` | no | +| [ssm\_parameter\_names](#input\_ssm\_parameter\_names) | List of AWS Systems Manager Parameter Store parameter names. The IAM role of this Lambda function will be enhanced
with read permissions for those parameters. Parameters must start with a forward slash and can be encrypted with the
default KMS key. | `list(string)` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | The amount of time the Lambda Function has to run in seconds. | `number` | `3` | no | +| [tracing\_config\_mode](#input\_tracing\_config\_mode) | Tracing config mode of the Lambda function. Can be either PassThrough or Active. | `string` | `null` | no | +| [vpc\_config](#input\_vpc\_config) | Provide this to allow your function to access your VPC (if both 'subnet\_ids' and 'security\_group\_ids' are empty then
vpc\_config is considered to be empty or unset, see https://docs.aws.amazon.com/lambda/latest/dg/vpc.html for details). |
object({
security_group_ids = list(string)
subnet_ids = list(string)
})
| `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | ARN of the lambda function | +| [function\_name](#output\_function\_name) | Lambda function name | +| [invoke\_arn](#output\_invoke\_arn) | Invoke ARN of the lambda function | +| [qualified\_arn](#output\_qualified\_arn) | ARN identifying your Lambda Function Version (if versioning is enabled via publish = true) | +| [role\_arn](#output\_role\_arn) | Lambda IAM role ARN | +| [role\_name](#output\_role\_name) | Lambda IAM role name | + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/lambda/context.tf b/modules/lambda/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/lambda/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/lambda/lambdas/.gitignore b/modules/lambda/lambdas/.gitignore new file mode 100644 index 000000000..e76f4fc4d --- /dev/null +++ b/modules/lambda/lambdas/.gitignore @@ -0,0 +1,3 @@ +*/*.zip + +*/hello_world diff --git a/modules/lambda/lambdas/hello-world-go/Makefile b/modules/lambda/lambdas/hello-world-go/Makefile new file mode 100644 index 000000000..a4a7b2ea3 --- /dev/null +++ b/modules/lambda/lambdas/hello-world-go/Makefile @@ -0,0 +1,18 @@ +BUCKET ?= # vst-platform-usw2-dev-lambda-source +AWS_PROFILE ?= # vst-platform-gbl-dev-admin + +all: deps build zip s3-cp + @exit 0 + +## Install dependencies (if any) +deps: + go get github.com/aws/aws-lambda-go/lambda + +build: + GOOS=linux go build hello_world.go + +zip: + @zip hello-world-go.zip hello_world + +s3-cp: + @aws --profile $(AWS_PROFILE) s3 cp hello-world-go.zip s3://$(BUCKET)/hello-world-go.zip --sse AES256 diff --git a/modules/lambda/lambdas/hello-world-go/go.mod b/modules/lambda/lambdas/hello-world-go/go.mod new file mode 100644 index 000000000..53ef7d720 --- /dev/null +++ b/modules/lambda/lambdas/hello-world-go/go.mod @@ -0,0 +1,5 @@ +module github.com/vstream/infra/components/terraform/lambda/lambdas/hello-world-go + +go 1.17 + +require github.com/aws/aws-lambda-go v1.34.1 // indirect diff --git a/modules/lambda/lambdas/hello-world-go/hello_world.go b/modules/lambda/lambdas/hello-world-go/hello_world.go new file mode 100644 index 000000000..f80e2e75e --- /dev/null +++ b/modules/lambda/lambdas/hello-world-go/hello_world.go @@ -0,0 +1,16 @@ +// main.go +// https://github.com/aws/aws-lambda-go +package main + +import ( + "github.com/aws/aws-lambda-go/lambda" +) + +func hello() (string, error) { + return "Hello ƛ!", nil +} + +func main() { + // Make the handler available for Remote Procedure Call by AWS Lambda + lambda.Start(hello) +} diff --git a/modules/lambda/main.tf b/modules/lambda/main.tf new file mode 100644 index 000000000..97e9a6532 --- /dev/null +++ b/modules/lambda/main.tf @@ -0,0 +1,81 @@ +locals { + enabled = module.this.enabled + iam_policy_enabled = local.enabled && var.policy_json != null + s3_bucket_full_name = format("%s-%s-%s-%s-%s", module.this.namespace, module.this.tenant, module.this.environment, module.this.stage, var.s3_bucket_name) +} + + + +module "label" { + source = "cloudposse/label/null" + version = "0.25.0" + + attributes = [var.function_name] + + context = module.this.context +} + +resource "aws_iam_policy" "default" { + count = local.iam_policy_enabled ? 1 : 0 + + name = module.label.id + path = "/" + description = format("%s Lambda policy", module.label.id) + policy = var.policy_json + + tags = module.this.tags +} + +resource "aws_iam_role_policy_attachment" "default" { + count = local.iam_policy_enabled ? 1 : 0 + + role = module.lambda.role_name + policy_arn = aws_iam_policy.default[0].arn +} + +module "lambda" { + source = "cloudposse/lambda-function/aws" + version = "0.4.1" + + function_name = module.label.id + description = var.description + handler = var.handler + lambda_environment = var.lambda_environment + image_uri = var.image_uri + image_config = var.image_config + + filename = var.filename + s3_bucket = local.s3_bucket_full_name + s3_key = var.s3_key + s3_object_version = var.s3_object_version + + architectures = var.architectures + cloudwatch_event_rules = var.cloudwatch_event_rules + cloudwatch_lambda_insights_enabled = var.cloudwatch_lambda_insights_enabled + cloudwatch_logs_retention_in_days = var.cloudwatch_logs_retention_in_days + cloudwatch_logs_kms_key_arn = var.cloudwatch_logs_kms_key_arn + cloudwatch_log_subscription_filters = var.cloudwatch_log_subscription_filters + ignore_external_function_updates = var.ignore_external_function_updates + event_source_mappings = var.event_source_mappings + kms_key_arn = var.kms_key_arn + lambda_at_edge_enabled = var.lambda_at_edge_enabled + layers = var.layers + memory_size = var.memory_size + package_type = var.package_type + permissions_boundary = var.permissions_boundary + publish = var.publish + reserved_concurrent_executions = var.reserved_concurrent_executions + runtime = var.runtime + sns_subscriptions = var.sns_subscriptions + source_code_hash = var.source_code_hash + ssm_parameter_names = var.ssm_parameter_names + timeout = var.timeout + tracing_config_mode = var.tracing_config_mode + vpc_config = var.vpc_config + custom_iam_policy_arns = var.custom_iam_policy_arns + dead_letter_config_target_arn = var.dead_letter_config_target_arn + iam_policy_description = var.iam_policy_description + + context = module.this.context +} + diff --git a/modules/lambda/outputs.tf b/modules/lambda/outputs.tf new file mode 100644 index 000000000..e62082216 --- /dev/null +++ b/modules/lambda/outputs.tf @@ -0,0 +1,29 @@ +output "arn" { + description = "ARN of the lambda function" + value = module.lambda.arn +} + +output "invoke_arn" { + description = "Invoke ARN of the lambda function" + value = module.lambda.invoke_arn +} + +output "qualified_arn" { + description = "ARN identifying your Lambda Function Version (if versioning is enabled via publish = true)" + value = module.lambda.qualified_arn +} + +output "function_name" { + description = "Lambda function name" + value = module.lambda.function_name +} + +output "role_name" { + description = "Lambda IAM role name" + value = module.lambda.role_name +} + +output "role_arn" { + description = "Lambda IAM role ARN" + value = module.lambda.role_arn +} diff --git a/modules/lambda/providers.tf b/modules/lambda/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/lambda/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/lambda/variables.tf b/modules/lambda/variables.tf new file mode 100644 index 000000000..746d98b98 --- /dev/null +++ b/modules/lambda/variables.tf @@ -0,0 +1,273 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "function_name" { + type = string + description = "Unique name for the Lambda Function." +} + +variable "architectures" { + type = list(string) + description = < Date: Mon, 13 Feb 2023 14:42:33 -0800 Subject: [PATCH 035/501] upstream `lambda` (#558) --- modules/lambda/README.md | 32 +++++++++++++------ modules/lambda/component.yaml | 14 ++++++++ .../lambda/lambdas/hello-world-go/Makefile | 18 ----------- modules/lambda/lambdas/hello-world-go/go.mod | 5 --- .../lambdas/hello-world-go/hello_world.go | 16 ---------- .../lambdas/hello-world-python/lambda.py | 5 +++ modules/lambda/main.tf | 10 +++++- modules/lambda/providers-archive.tf | 1 + modules/lambda/variables.tf | 10 ++++++ modules/lambda/versions.tf | 4 +++ 10 files changed, 66 insertions(+), 49 deletions(-) create mode 100644 modules/lambda/component.yaml delete mode 100644 modules/lambda/lambdas/hello-world-go/Makefile delete mode 100644 modules/lambda/lambdas/hello-world-go/go.mod delete mode 100644 modules/lambda/lambdas/hello-world-go/hello_world.go create mode 100644 modules/lambda/lambdas/hello-world-python/lambda.py create mode 100644 modules/lambda/providers-archive.tf diff --git a/modules/lambda/README.md b/modules/lambda/README.md index d744bf607..6e41472db 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -27,23 +27,33 @@ import: components: terraform: - lambda/hello-world-go: + lambda/hello-world-py: metadata: component: lambda inherits: - - lambda-defaults + - lambda/defaults vars: - name: hello-world-go + name: hello-world-py function_name: main - description: Hello Lambda from GoLang! - handler: hello_world # in go this is the compiled binary + description: Hello Lambda from Python! + handler: lambda.lambda_handler # in go this is the compiled binary, python it's filename.function memory_size: 256 # https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html - runtime: go1.x - package_type: Zip + runtime: python3.9 + package_type: Zip # `Zip` or `Image` policy_json: null - s3_bucket_name: lambda-source # lambda main.tf calculates the rest of the bucket_name - s3_key: hello-world-go.zip + + # Filename example + filename: lambdas/hello-world-python/output.zip # generated by zip variable. + zip: + enabled: true + input_dir: hello-world-python + output: hello-world-python/output.zip + + # S3 Source Example + # s3_bucket_name: lambda-source # lambda main.tf calculates the rest of the bucket_name + # s3_key: hello-world-go.zip + ``` @@ -53,12 +63,14 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | +| [archive](#requirement\_archive) | >= 2.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | ## Providers | Name | Version | |------|---------| +| [archive](#provider\_archive) | >= 2.3.0 | | [aws](#provider\_aws) | >= 4.9.0 | ## Modules @@ -76,6 +88,7 @@ components: |------|------| | [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [archive_file.lambdazip](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | ## Inputs @@ -139,6 +152,7 @@ components: | [timeout](#input\_timeout) | The amount of time the Lambda Function has to run in seconds. | `number` | `3` | no | | [tracing\_config\_mode](#input\_tracing\_config\_mode) | Tracing config mode of the Lambda function. Can be either PassThrough or Active. | `string` | `null` | no | | [vpc\_config](#input\_vpc\_config) | Provide this to allow your function to access your VPC (if both 'subnet\_ids' and 'security\_group\_ids' are empty then
vpc\_config is considered to be empty or unset, see https://docs.aws.amazon.com/lambda/latest/dg/vpc.html for details). |
object({
security_group_ids = list(string)
subnet_ids = list(string)
})
| `null` | no | +| [zip](#input\_zip) | Zip Configuration for local file deployments |
object({
enabled = optional(bool, false)
output = optional(string, "output.zip")
input_dir = optional(string, null)
})
| `{}` | no | ## Outputs diff --git a/modules/lambda/component.yaml b/modules/lambda/component.yaml new file mode 100644 index 000000000..6d8efc69f --- /dev/null +++ b/modules/lambda/component.yaml @@ -0,0 +1,14 @@ +# 'lambda' component vendoring config + +# 'component.yaml' in the component folder is processed by the 'atmos' commands +# 'atmos vendor pull -c lambda' or 'atmos vendor pull --component lambda' + +apiVersion: atmos/v1 +kind: ComponentVendorConfig +spec: + source: + uri: github.com/cloudposse/terraform-aws-components.git//modules/lambda?ref={{ .Version }} + version: 1.122.0 + included_paths: + - "**/**" + excluded_paths: [] diff --git a/modules/lambda/lambdas/hello-world-go/Makefile b/modules/lambda/lambdas/hello-world-go/Makefile deleted file mode 100644 index a4a7b2ea3..000000000 --- a/modules/lambda/lambdas/hello-world-go/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -BUCKET ?= # vst-platform-usw2-dev-lambda-source -AWS_PROFILE ?= # vst-platform-gbl-dev-admin - -all: deps build zip s3-cp - @exit 0 - -## Install dependencies (if any) -deps: - go get github.com/aws/aws-lambda-go/lambda - -build: - GOOS=linux go build hello_world.go - -zip: - @zip hello-world-go.zip hello_world - -s3-cp: - @aws --profile $(AWS_PROFILE) s3 cp hello-world-go.zip s3://$(BUCKET)/hello-world-go.zip --sse AES256 diff --git a/modules/lambda/lambdas/hello-world-go/go.mod b/modules/lambda/lambdas/hello-world-go/go.mod deleted file mode 100644 index 53ef7d720..000000000 --- a/modules/lambda/lambdas/hello-world-go/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/vstream/infra/components/terraform/lambda/lambdas/hello-world-go - -go 1.17 - -require github.com/aws/aws-lambda-go v1.34.1 // indirect diff --git a/modules/lambda/lambdas/hello-world-go/hello_world.go b/modules/lambda/lambdas/hello-world-go/hello_world.go deleted file mode 100644 index f80e2e75e..000000000 --- a/modules/lambda/lambdas/hello-world-go/hello_world.go +++ /dev/null @@ -1,16 +0,0 @@ -// main.go -// https://github.com/aws/aws-lambda-go -package main - -import ( - "github.com/aws/aws-lambda-go/lambda" -) - -func hello() (string, error) { - return "Hello ƛ!", nil -} - -func main() { - // Make the handler available for Remote Procedure Call by AWS Lambda - lambda.Start(hello) -} diff --git a/modules/lambda/lambdas/hello-world-python/lambda.py b/modules/lambda/lambdas/hello-world-python/lambda.py new file mode 100644 index 000000000..2990b3670 --- /dev/null +++ b/modules/lambda/lambdas/hello-world-python/lambda.py @@ -0,0 +1,5 @@ +def lambda_handler(event, context): + message = 'Hello {} {}!'.format(event['first_name'], event['last_name']) + return { + 'message' : message + } diff --git a/modules/lambda/main.tf b/modules/lambda/main.tf index 97e9a6532..043303c12 100644 --- a/modules/lambda/main.tf +++ b/modules/lambda/main.tf @@ -1,7 +1,7 @@ locals { enabled = module.this.enabled iam_policy_enabled = local.enabled && var.policy_json != null - s3_bucket_full_name = format("%s-%s-%s-%s-%s", module.this.namespace, module.this.tenant, module.this.environment, module.this.stage, var.s3_bucket_name) + s3_bucket_full_name = var.s3_bucket_name != null ? format("%s-%s-%s-%s-%s", module.this.namespace, module.this.tenant, module.this.environment, module.this.stage, var.s3_bucket_name) : null } @@ -33,6 +33,14 @@ resource "aws_iam_role_policy_attachment" "default" { policy_arn = aws_iam_policy.default[0].arn } +data "archive_file" "lambdazip" { + count = var.zip.enabled ? 1 : 0 + type = "zip" + output_path = "${path.module}/lambdas/${var.zip.output}" + + source_dir = "${path.module}/lambdas/${var.zip.input_dir}" +} + module "lambda" { source = "cloudposse/lambda-function/aws" version = "0.4.1" diff --git a/modules/lambda/providers-archive.tf b/modules/lambda/providers-archive.tf new file mode 100644 index 000000000..f0dc6f82d --- /dev/null +++ b/modules/lambda/providers-archive.tf @@ -0,0 +1 @@ +provider "archive" {} diff --git a/modules/lambda/variables.tf b/modules/lambda/variables.tf index 746d98b98..88f59b33e 100644 --- a/modules/lambda/variables.tf +++ b/modules/lambda/variables.tf @@ -271,3 +271,13 @@ variable "policy_json" { description = "IAM policy to attach to the Lambda IAM role" default = null } + +variable "zip" { + type = object({ + enabled = optional(bool, false) + output = optional(string, "output.zip") + input_dir = optional(string, null) + }) + description = "Zip Configuration for local file deployments" + default = {} +} diff --git a/modules/lambda/versions.tf b/modules/lambda/versions.tf index 0a8009b67..37fe7029f 100644 --- a/modules/lambda/versions.tf +++ b/modules/lambda/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = ">= 4.9.0" } + archive = { + source = "hashicorp/archive" + version = ">= 2.3.0" + } } } From 3ca10e823509f8c51d746e7e71e385b48e8b1ddc Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 14 Feb 2023 09:34:07 -0800 Subject: [PATCH 036/501] `aws-backup` upstream (#559) --- modules/aws-backup/README.md | 172 +++++++++++++++++++------ modules/aws-backup/context.tf | 109 +++++++++++++--- modules/aws-backup/default.auto.tfvars | 1 - modules/aws-backup/main.tf | 19 ++- modules/aws-backup/outputs.tf | 5 - modules/aws-backup/providers.tf | 15 ++- modules/aws-backup/variables.tf | 11 -- modules/aws-backup/versions.tf | 4 +- 8 files changed, 251 insertions(+), 85 deletions(-) delete mode 100644 modules/aws-backup/default.auto.tfvars diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index 05695b5d1..fafb61fb9 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -8,39 +8,134 @@ This component is responsible for provisioning an AWS Backup Plan. It creates a Here's an example snippet for how to use this component. +### Component Abstraction and Separation + +By separating the "common" settings from the component, we can first provision the IAM Role and AWS Backup Vault to prepare resources for future use without incuring cost. + +For example, `stacks/catalog/aws-backup/common`: + ```yaml +# This configuration creates the AWS Backup Vault and IAM Role, and does not incur any cost on its own. +# See: https://aws.amazon.com/backup/pricing/ components: terraform: aws-backup: + metadata: + type: abstract + settings: + spacelift: + workspace_enabled: true + vars: {} + + aws-backup/common: + metadata: + component: aws-backup + inherits: + - aws-backup vars: - # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html - schedule: cron(0 0 * * ? *) # Daily At 12:00 AM UTC - start_window: 60 # Minutes - completion_window: 240 # Minutes - cold_storage_after: null # Days - delete_after: 14 # Days - destination_vault_arn: null # Copy to another Region's Vault - copy_action_cold_storage_after: null # Copy to another Region's Vault Cold Storage Config (Days) - copy_action_delete_after: null # Copy to another Region's Vault Persistence Config (Days) - backup_resources: [] - selection_tags: - - type: "STRINGEQUALS" - key: "aws-backup/resource_schedule" - value: "daily-14day-backup" + enabled: true + iam_role_enabled: true # this will be reused + vault_enabled: true # this will be reused + plan_enabled: false ``` +Then if we would like to deploy the component into a given stacks we can import the following to deploy our backup plans. + Since most of these values are shared and common, we can put them in a `catalog/aws-backup/` yaml file and share them across environments. This makes deploying the same configuration to multiple environments easy. -Deploying to a new stack (environment) then only requires: +`stacks/catalog/aws-backup/defaults`: + ```yaml import: - - catalog/aws-backup/aws-backup-nonprod + - catalog/aws-backup/common components: terraform: - aws-backup: {} + aws-backup/plan-defaults: + metadata: + component: aws-backup + type: abstract + settings: + spacelift: + workspace_enabled: true + depends_on: + - aws-backup/common + vars: + enabled: true + iam_role_enabled: false # reuse from aws-backup-vault + vault_enabled: false # reuse from aws-backup-vault + plan_enabled: true + plan_name_suffix: aws-backup-defaults + # in minutes + start_window: 60 + completion_window: 240 + # in days + cold_storage_after: null + delete_after: 30 # 1 month + copy_action_cold_storage_after: null + copy_action_delete_after: null + + aws-backup/daily-plan: + metadata: + component: aws-backup + inherits: + - aws-backup/plan-defaults + vars: + plan_name_suffix: aws-backup-daily + # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html + schedule: cron(0 0 ? * * *) # Daily at midnight (UTC) + selection_tags: + - type: STRINGEQUALS + key: aws-backup/efs + value: daily + - type: STRINGEQUALS + key: aws-backup/rds + value: daily + + aws-backup/weekly-plan: + metadata: + component: aws-backup + inherits: + - aws-backup/plan-defaults + vars: + plan_name_suffix: aws-backup-weekly + # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html + schedule: cron(0 0 ? * 1 *) # Weekly on first day of week at midnight (UTC) + selection_tags: + - type: STRINGEQUALS + key: aws-backup/efs + value: weekly + - type: STRINGEQUALS + key: aws-backup/rds + value: weekly + + aws-backup/monthly-plan: + metadata: + component: aws-backup + inherits: + - aws-backup/plan-defaults + vars: + plan_name_suffix: aws-backup-monthly + # delete monthly snapshots after 60 days + delete_after: 60 + # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html + schedule: cron(0 0 1 * ? *) # Monthly on 1st day of the month (doesn't matter which) at midnight UTC + selection_tags: + - type: STRINGEQUALS + key: aws-backup/efs + value: monthly + - type: STRINGEQUALS + key: aws-backup/rds + value: monthly +``` + +Deploying to a new stack (environment) then only requires: + +```yaml +import: + - catalog/aws-backup/defaults ``` The above configuration can be used to deploy a new backup to a new region. @@ -93,8 +188,8 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 2.0 | +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | ## Providers @@ -104,9 +199,9 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [backup](#module\_backup) | cloudposse/backup/aws | 0.8.1 | +| [backup](#module\_backup) | cloudposse/backup/aws | 0.14.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [this](#module\_this) | cloudposse/label/null | 0.24.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -116,39 +211,41 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | -| [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [backup\_resources](#input\_backup\_resources) | An array of strings that either contain Amazon Resource Names (ARNs) or match patterns of resources to assign to a backup plan | `list(string)` | `[]` | no | | [cold\_storage\_after](#input\_cold\_storage\_after) | Specifies the number of days after creation that a recovery point is moved to cold storage | `number` | `null` | no | | [completion\_window](#input\_completion\_window) | The amount of time AWS Backup attempts a backup before canceling the job and returning an error. Must be at least 60 minutes greater than `start_window` | `number` | `null` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [copy\_action\_cold\_storage\_after](#input\_copy\_action\_cold\_storage\_after) | For copy operation, specifies the number of days after creation that a recovery point is moved to cold storage | `number` | `null` | no | | [copy\_action\_delete\_after](#input\_copy\_action\_delete\_after) | For copy operation, specifies the number of days after creation that a recovery point is deleted. Must be 90 days greater than `copy_action_cold_storage_after` | `number` | `null` | no | | [delete\_after](#input\_delete\_after) | Specifies the number of days after creation that a recovery point is deleted. Must be 90 days greater than `cold_storage_after` | `number` | `null` | no | -| [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [destination\_vault\_arn](#input\_destination\_vault\_arn) | An Amazon Resource Name (ARN) that uniquely identifies the destination backup vault for the copied backup | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [iam\_role\_enabled](#input\_iam\_role\_enabled) | Whether or not to create a new IAM Role and Policy Attachment | `bool` | `true` | no | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | The server-side encryption key that is used to protect your backups | `string` | `null` | no | -| [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | -| [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | -| [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [plan\_enabled](#input\_plan\_enabled) | Whether or not to create a new Plan | `bool` | `true` | no | | [plan\_name\_suffix](#input\_plan\_name\_suffix) | The string appended to the plan name | `string` | `null` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [schedule](#input\_schedule) | A CRON expression specifying when AWS Backup initiates a backup job | `string` | `null` | no | | [selection\_tags](#input\_selection\_tags) | An array of tag condition objects used to filter resources based on tags for assigning to a backup plan | `list(map(string))` | `[]` | no | -| [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [start\_window](#input\_start\_window) | The amount of time in minutes before beginning a backup. Minimum value is 60 minutes | `number` | `null` | no | -| [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | -| [target\_iam\_role\_name](#input\_target\_iam\_role\_name) | Override target IAM Name | `string` | `null` | no | -| [target\_vault\_name](#input\_target\_vault\_name) | Override target Vault Name | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [vault\_enabled](#input\_vault\_enabled) | Whether or not a new Vault should be created | `bool` | `true` | no | ## Outputs @@ -160,7 +257,6 @@ No resources. | [backup\_selection\_id](#output\_backup\_selection\_id) | Backup Selection ID | | [backup\_vault\_arn](#output\_backup\_vault\_arn) | Backup Vault ARN | | [backup\_vault\_id](#output\_backup\_vault\_id) | Backup Vault ID | -| [backup\_vault\_recovery\_points](#output\_backup\_vault\_recovery\_points) | Backup Vault recovery points | diff --git a/modules/aws-backup/context.tf b/modules/aws-backup/context.tf index 81f99b4e3..5e0ef8856 100644 --- a/modules/aws-backup/context.tf +++ b/modules/aws-backup/context.tf @@ -8,6 +8,8 @@ # Cloud Posse's standard configuration inputs suitable for passing # to Cloud Posse modules. # +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# # Modules should access the whole context as `module.this.context` # to get the input variables with nulls for defaults, # for example `context = module.this.context`, @@ -20,10 +22,11 @@ module "this" { source = "cloudposse/label/null" - version = "0.24.1" # requires Terraform >= 0.13.0 + version = "0.25.0" # requires Terraform >= 0.13.0 enabled = var.enabled namespace = var.namespace + tenant = var.tenant environment = var.environment stage = var.stage name = var.name @@ -36,6 +39,8 @@ module "this" { id_length_limit = var.id_length_limit label_key_case = var.label_key_case label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags context = var.context } @@ -47,6 +52,7 @@ variable "context" { default = { enabled = true namespace = null + tenant = null environment = null stage = null name = null @@ -59,6 +65,15 @@ variable "context" { id_length_limit = null label_key_case = null label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] } description = <<-EOT Single object for setting entire context at once. @@ -88,32 +103,42 @@ variable "enabled" { variable "namespace" { type = string default = null - description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" } variable "environment" { type = string default = null - description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" } variable "stage" { type = string default = null - description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" } variable "name" { type = string default = null - description = "Solution name, e.g. 'app' or 'jenkins'" + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT } variable "delimiter" { type = string default = null description = <<-EOT - Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. + Delimiter to be used between ID elements. Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. EOT } @@ -121,36 +146,64 @@ variable "delimiter" { variable "attributes" { type = list(string) default = [] - description = "Additional attributes (e.g. `1`)" + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT } variable "tags" { type = map(string) default = {} - description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT } variable "additional_tag_map" { type = map(string) default = {} - description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT } variable "label_order" { type = list(string) default = null description = <<-EOT - The naming order of the id output and Name tag. + The order in which the labels (ID elements) appear in the `id`. Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 5 elements, but at least one must be present. - EOT + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT } variable "regex_replace_chars" { type = string default = null description = <<-EOT - Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. EOT } @@ -161,7 +214,7 @@ variable "id_length_limit" { description = <<-EOT Limit `id` to this many characters (minimum 6). Set to `0` for unlimited length. - Set to `null` for default, which is `0`. + Set to `null` for keep the existing setting, which defaults to `0`. Does not affect `id_full`. EOT validation { @@ -174,7 +227,8 @@ variable "label_key_case" { type = string default = null description = <<-EOT - The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. Possible values: `lower`, `title`, `upper`. Default value: `title`. EOT @@ -189,8 +243,11 @@ variable "label_value_case" { type = string default = null description = <<-EOT - The letter case of output label values (also used in `tags` and `id`). + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. Default value: `lower`. EOT @@ -199,4 +256,24 @@ variable "label_value_case" { error_message = "Allowed values: `lower`, `title`, `upper`, `none`." } } + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + #### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/aws-backup/default.auto.tfvars b/modules/aws-backup/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/aws-backup/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/aws-backup/main.tf b/modules/aws-backup/main.tf index 30a49558c..5045ba198 100644 --- a/modules/aws-backup/main.tf +++ b/modules/aws-backup/main.tf @@ -1,8 +1,6 @@ module "backup" { source = "cloudposse/backup/aws" - version = "0.8.1" - - context = module.this.context + version = "0.14.0" plan_name_suffix = var.plan_name_suffix vault_enabled = var.vault_enabled @@ -12,19 +10,18 @@ module "backup" { backup_resources = var.backup_resources selection_tags = var.selection_tags - schedule = var.schedule - start_window = var.start_window - completion_window = var.completion_window - cold_storage_after = var.cold_storage_after - delete_after = var.delete_after - kms_key_arn = var.kms_key_arn - target_iam_role_name = var.target_iam_role_name + schedule = var.schedule + start_window = var.start_window + completion_window = var.completion_window + cold_storage_after = var.cold_storage_after + delete_after = var.delete_after + kms_key_arn = var.kms_key_arn # Copy config to new region destination_vault_arn = var.destination_vault_arn copy_action_cold_storage_after = var.copy_action_cold_storage_after copy_action_delete_after = var.copy_action_delete_after - target_vault_name = var.target_vault_name + context = module.this.context } diff --git a/modules/aws-backup/outputs.tf b/modules/aws-backup/outputs.tf index dfaae3046..77d3b198c 100644 --- a/modules/aws-backup/outputs.tf +++ b/modules/aws-backup/outputs.tf @@ -8,11 +8,6 @@ output "backup_vault_arn" { description = "Backup Vault ARN" } -output "backup_vault_recovery_points" { - value = module.backup.backup_vault_recovery_points - description = "Backup Vault recovery points" -} - output "backup_plan_arn" { value = module.backup.backup_plan_arn description = "Backup Plan ARN" diff --git a/modules/aws-backup/providers.tf b/modules/aws-backup/providers.tf index 74cd8f825..08ee01b2a 100755 --- a/modules/aws-backup/providers.tf +++ b/modules/aws-backup/providers.tf @@ -1,7 +1,14 @@ provider "aws" { region = var.region - profile = coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } } module "iam_roles" { @@ -14,3 +21,9 @@ variable "import_profile_name" { default = null description = "AWS Profile name to use when importing a resource" } + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/aws-backup/variables.tf b/modules/aws-backup/variables.tf index 359fc1848..b22993433 100644 --- a/modules/aws-backup/variables.tf +++ b/modules/aws-backup/variables.tf @@ -75,12 +75,6 @@ variable "plan_name_suffix" { default = null } -variable "target_vault_name" { - type = string - description = "Override target Vault Name" - default = null -} - variable "vault_enabled" { type = bool description = "Whether or not a new Vault should be created" @@ -99,8 +93,3 @@ variable "iam_role_enabled" { default = true } -variable "target_iam_role_name" { - type = string - description = "Override target IAM Name" - default = null -} diff --git a/modules/aws-backup/versions.tf b/modules/aws-backup/versions.tf index 5b2c49b90..cc73ffd35 100644 --- a/modules/aws-backup/versions.tf +++ b/modules/aws-backup/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 0.13.0" + required_version = ">= 1.0.0" required_providers { aws = { source = "hashicorp/aws" - version = ">= 2.0" + version = ">= 4.9.0" } } } From 476af01d759209e7b6c3a029e313ffa7f2800dd2 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 15 Feb 2023 00:45:11 +0300 Subject: [PATCH 037/501] [eks/argocd] Upstream ArgoCD (#560) --- modules/eks/argocd/README.md | 197 +++++++++++++ modules/eks/argocd/context.tf | 279 ++++++++++++++++++ modules/eks/argocd/data.tf | 57 ++++ modules/eks/argocd/main.tf | 261 ++++++++++++++++ modules/eks/argocd/outputs.tf | 4 + modules/eks/argocd/provider-helm.tf | 158 ++++++++++ modules/eks/argocd/providers.tf | 41 +++ modules/eks/argocd/remote-state.tf | 42 +++ .../resources/argo-horizontal-color.png | Bin 0 -> 63751 bytes .../resources/argocd-apps-values.yaml.tpl | 23 ++ .../argocd-notifications-values.yaml.tpl | 40 +++ .../argocd/resources/argocd-values.yaml.tpl | 139 +++++++++ .../eks/argocd/resources/kustomize/.gitignore | 1 + .../resources/kustomize/kustomization.yaml | 11 + .../eks/argocd/resources/kustomize/patch.yaml | 3 + .../argocd/resources/kustomize/post-render.sh | 26 ++ modules/eks/argocd/variables-argocd-apps.tf | 30 ++ .../argocd/variables-argocd-notifications.tf | 102 +++++++ modules/eks/argocd/variables-argocd.tf | 202 +++++++++++++ modules/eks/argocd/variables-helm.tf | 93 ++++++ modules/eks/argocd/versions.tf | 18 ++ 21 files changed, 1727 insertions(+) create mode 100644 modules/eks/argocd/README.md create mode 100644 modules/eks/argocd/context.tf create mode 100644 modules/eks/argocd/data.tf create mode 100644 modules/eks/argocd/main.tf create mode 100644 modules/eks/argocd/outputs.tf create mode 100644 modules/eks/argocd/provider-helm.tf create mode 100644 modules/eks/argocd/providers.tf create mode 100644 modules/eks/argocd/remote-state.tf create mode 100644 modules/eks/argocd/resources/argo-horizontal-color.png create mode 100644 modules/eks/argocd/resources/argocd-apps-values.yaml.tpl create mode 100644 modules/eks/argocd/resources/argocd-notifications-values.yaml.tpl create mode 100644 modules/eks/argocd/resources/argocd-values.yaml.tpl create mode 100644 modules/eks/argocd/resources/kustomize/.gitignore create mode 100644 modules/eks/argocd/resources/kustomize/kustomization.yaml create mode 100644 modules/eks/argocd/resources/kustomize/patch.yaml create mode 100755 modules/eks/argocd/resources/kustomize/post-render.sh create mode 100644 modules/eks/argocd/variables-argocd-apps.tf create mode 100644 modules/eks/argocd/variables-argocd-notifications.tf create mode 100644 modules/eks/argocd/variables-argocd.tf create mode 100644 modules/eks/argocd/variables-helm.tf create mode 100644 modules/eks/argocd/versions.tf diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md new file mode 100644 index 000000000..56230951e --- /dev/null +++ b/modules/eks/argocd/README.md @@ -0,0 +1,197 @@ +# Component: `argocd` + +This component is responsible for provisioning [Argo CD](https://argoproj.github.io/cd/). + +Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. + +> :warning::warning::warning: ArgoCD CRDs must be installed separately from this component/helm release. :warning::warning::warning: +```shell +kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=" + +# Eg. version v2.4.9 +kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=v2.4.9" +``` + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component: + +```yaml +components: + terraform: + argocd: + settings: + spacelift: + workspace_enabled: true + depends_on: + - argocd-applicationset + - tenant-gbl-corp-argocd-depoy-non-prod + vars: + enabled: true + alb_group_name: argocd + alb_name: argocd + alb_logs_prefix: argocd + certificate_issuer: selfsigning-issuer + github_organization: MyOrg + oidc_enabled: false + saml_enabled: true + ssm_store_account: corp + ssm_store_account_region: us-west-2 + saml_admin_role: ArgoCD-non-prod-admin + saml_readonly_role: ArgoCD-non-prod-observer + argocd_repo_name: argocd-deploy-non-prod + chart_values: {} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [helm](#requirement\_helm) | >= 2.6.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [aws.config\_secrets](#provider\_aws.config\_secrets) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.3.0 | +| [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.3.0 | +| [argocd\_repo](#module\_argocd\_repo) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [iam\_roles\_config\_secrets](#module\_iam\_roles\_config\_secrets) | ../../account-map/modules/iam-roles | n/a | +| [saml\_sso\_providers](#module\_saml\_sso\_providers) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_ssm_parameter.github_deploy_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.oidc_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.oidc_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameters_by_path.argocd_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_group\_name](#input\_alb\_group\_name) | A name used in annotations to reuse an ALB (e.g. `argocd`) or to generate a new one | `string` | `null` | no | +| [alb\_logs\_bucket](#input\_alb\_logs\_bucket) | The name of the bucket for ALB access logs. The bucket must have policy allowing the ELB logging principal | `string` | `""` | no | +| [alb\_logs\_prefix](#input\_alb\_logs\_prefix) | `alb_logs_bucket` s3 bucket prefix | `string` | `""` | no | +| [alb\_name](#input\_alb\_name) | The name of the ALB (e.g. `argocd`) provisioned by `alb-controller`. Works together with `var.alb_group_name` | `string` | `null` | no | +| [argo\_enable\_workflows\_auth](#input\_argo\_enable\_workflows\_auth) | Allow argo-workflows to use Dex instance for SAML auth | `bool` | `false` | no | +| [argo\_workflows\_name](#input\_argo\_workflows\_name) | Name of argo-workflows instance | `string` | `"argo-workflows"` | no | +| [argocd\_apps\_chart](#input\_argocd\_apps\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"argocd-apps"` | no | +| [argocd\_apps\_chart\_description](#input\_argocd\_apps\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"A Helm chart for managing additional Argo CD Applications and Projects"` | no | +| [argocd\_apps\_chart\_repository](#input\_argocd\_apps\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://argoproj.github.io/argo-helm"` | no | +| [argocd\_apps\_chart\_version](#input\_argocd\_apps\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"0.0.3"` | no | +| [argocd\_apps\_enabled](#input\_argocd\_apps\_enabled) | Enable argocd apps | `bool` | `true` | no | +| [argocd\_create\_namespaces](#input\_argocd\_create\_namespaces) | ArgoCD create namespaces policy | `bool` | `false` | no | +| [argocd\_rbac\_groups](#input\_argocd\_rbac\_groups) | List of ArgoCD Group Role Assignment strings to be added to the argocd-rbac configmap policy.csv item.
e.g.
[
{
group: idp-group-name,
role: argocd-role-name
},
]
becomes: `g, idp-group-name, role:argocd-role-name`
See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. |
list(object({
group = string,
role = string
}))
| `[]` | no | +| [argocd\_rbac\_policies](#input\_argocd\_rbac\_policies) | List of ArgoCD RBAC Permission strings to be added to the argocd-rbac configmap policy.csv item.

See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. | `list(string)` | `[]` | no | +| [argocd\_repositories](#input\_argocd\_repositories) | Map of objects defining an `argocd_repo` to configure. The key is the name of the ArgoCD repository. |
map(object({
environment = string # The environment where the `argocd_repo` component is deployed.
stage = string # The stage where the `argocd_repo` component is deployed.
tenant = string # The tenant where the `argocd_repo` component is deployed.
}))
| `{}` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [certificate\_issuer](#input\_certificate\_issuer) | Certificate manager cluster issuer | `string` | `"letsencrypt-staging"` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"argo-cd"` | no | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://argoproj.github.io/argo-helm"` | no | +| [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"5.19.12"` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `false` | no | +| [datadog\_notifications\_enabled](#input\_datadog\_notifications\_enabled) | Whether or not to notify Datadog of deployments via the Datadog Events API. | `bool` | `false` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [forecastle\_enabled](#input\_forecastle\_enabled) | Toggles Forecastle integration in the deployed chart | `bool` | `false` | no | +| [github\_notifications\_enabled](#input\_github\_notifications\_enabled) | Whether or not to enable GitHub deployment and commit status notifications. | `bool` | `false` | no | +| [github\_organization](#input\_github\_organization) | GitHub Organization | `string` | n/a | yes | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [host](#input\_host) | Host name to use for ingress and ALB | `string` | `""` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | `"argocd"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [notifications\_default\_triggers](#input\_notifications\_default\_triggers) | Default notification Triggers to configure.

See: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/triggers/#default-triggers
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/790438efebf423c2d56cb4b93471f4adb3fcd448/charts/argo-cd/values.yaml#L2841) | `map(list(string))` | `{}` | no | +| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = optional(number)
installationID = optional(number)
privateKey = optional(string)
}))
})
| `{}` | no | +| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
}))
| `{}` | no | +| [notifications\_triggers](#input\_notifications\_triggers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
map(list(
object({
oncePer = optional(string)
send = list(string)
when = string
})
))
| `{}` | no | +| [oidc\_enabled](#input\_oidc\_enabled) | Toggles OIDC integration in the deployed chart | `bool` | `false` | no | +| [oidc\_issuer](#input\_oidc\_issuer) | OIDC issuer URL | `string` | `""` | no | +| [oidc\_name](#input\_oidc\_name) | Name of the OIDC resource | `string` | `""` | no | +| [oidc\_rbac\_scopes](#input\_oidc\_rbac\_scopes) | OIDC RBAC scopes to request | `string` | `"[argocd_realm_access]"` | no | +| [oidc\_requested\_scopes](#input\_oidc\_requested\_scopes) | Set of OIDC scopes to request | `string` | `"[\"openid\", \"profile\", \"email\", \"groups\"]"` | no | +| [rbac\_enabled](#input\_rbac\_enabled) | Enable Service Account for pods. | `bool` | `true` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region. | `string` | n/a | yes | +| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| `null` | no | +| [saml\_enabled](#input\_saml\_enabled) | Toggles SAML integration in the deployed chart | `bool` | `false` | no | +| [saml\_okta\_app\_name](#input\_saml\_okta\_app\_name) | Name of the Okta SAML Integration | `string` | `"ArgoCD"` | no | +| [saml\_rbac\_scopes](#input\_saml\_rbac\_scopes) | SAML RBAC scopes to request | `string` | `"[email,groups]"` | no | +| [saml\_sso\_providers](#input\_saml\_sso\_providers) | SAML SSO providers components |
map(object({
component = string
}))
| `{}` | no | +| [slack\_notifications\_enabled](#input\_slack\_notifications\_enabled) | Whether or not to enable Slack notifications. | `bool` | `false` | no | +| [slack\_notifications\_icon](#input\_slack\_notifications\_icon) | URI of custom image to use as the Slack notifications icon. | `string` | `null` | no | +| [slack\_notifications\_username](#input\_slack\_notifications\_username) | Custom username to use for Slack notifications. | `string` | `null` | no | +| [ssm\_oidc\_client\_id](#input\_ssm\_oidc\_client\_id) | The SSM Parameter Store path for the ID of the IdP client | `string` | `"/argocd/oidc/client_id"` | no | +| [ssm\_oidc\_client\_secret](#input\_ssm\_oidc\_client\_secret) | The SSM Parameter Store path for the secret of the IdP client | `string` | `"/argocd/oidc/client_secret"` | no | +| [ssm\_store\_account](#input\_ssm\_store\_account) | Account storing SSM parameters | `string` | n/a | yes | +| [ssm\_store\_account\_region](#input\_ssm\_store\_account\_region) | AWS region storing SSM parameters | `string` | n/a | yes | +| [ssm\_store\_account\_tenant](#input\_ssm\_store\_account\_tenant) | Tenant of the account storing SSM parameters.

If the tenant label is not used, leave this as null. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `300` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [metadata](#output\_metadata) | Block status of the deployed release | + + +## References + +- [Argo CD](https://argoproj.github.io/cd/) +- [Argo CD Docs](https://argo-cd.readthedocs.io/en/stable/) +- [Argo Helm Chart](https://github.com/argoproj/argo-helm/blob/master/charts/argo-cd/) + +[](https://cpco.io/component) diff --git a/modules/eks/argocd/context.tf b/modules/eks/argocd/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/argocd/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/argocd/data.tf b/modules/eks/argocd/data.tf new file mode 100644 index 000000000..7803bfb55 --- /dev/null +++ b/modules/eks/argocd/data.tf @@ -0,0 +1,57 @@ +locals { + kubernetes_host = local.enabled ? data.aws_eks_cluster.kubernetes[0].endpoint : "" + kubernetes_token = local.enabled ? data.aws_eks_cluster_auth.kubernetes[0].token : "" + kubernetes_cluster_ca_certificate = local.enabled ? base64decode(data.aws_eks_cluster.kubernetes[0].certificate_authority[0].data) : "" + oidc_client_id = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_id[0].value : "" + oidc_client_secret = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_secret[0].value : "" + + # saml_certificate = base64encode(format("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", module.okta_saml_apps.outputs.certificates[var.saml_okta_app_name])) + # + # saml_sso_url = sensitive(local.saml_enabled ? module.okta_saml_apps.outputs.sso_urls[var.saml_okta_app_name] : "") + # saml_ca = sensitive(local.saml_enabled ? local.saml_certificate : "") +} + +# NOTE: OIDC parameters are global, hence why they use a separate AWS provider + +data "aws_ssm_parameter" "oidc_client_id" { + count = local.oidc_enabled_count + name = var.ssm_oidc_client_id + with_decryption = true + + provider = aws.config_secrets +} + +data "aws_ssm_parameter" "oidc_client_secret" { + count = local.oidc_enabled_count + name = var.ssm_oidc_client_secret + with_decryption = true + + provider = aws.config_secrets +} + +data "aws_eks_cluster" "kubernetes" { + count = local.count_enabled + name = module.eks.outputs.eks_cluster_id +} + +data "aws_eks_cluster_auth" "kubernetes" { + count = local.count_enabled + name = module.eks.outputs.eks_cluster_id +} + +data "aws_ssm_parameter" "github_deploy_key" { + for_each = local.enabled ? var.argocd_repositories : {} + + name = local.enabled ? format( + module.argocd_repo[each.key].outputs.deploy_keys_ssm_path_format, + format( + "${module.this.tenant != null ? "%[1]s/" : ""}%[2]s-%[3]s", + module.this.tenant, + module.this.environment, + module.this.stage + ) + ) : null + with_decryption = true + + provider = aws.config_secrets +} diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf new file mode 100644 index 000000000..e35cc86c1 --- /dev/null +++ b/modules/eks/argocd/main.tf @@ -0,0 +1,261 @@ +locals { + enabled = module.this.enabled + kubernetes_namespace = var.kubernetes_namespace + count_enabled = local.enabled ? 1 : 0 + oidc_enabled = local.enabled && var.oidc_enabled + oidc_enabled_count = local.oidc_enabled ? 1 : 0 + saml_enabled = local.enabled && var.saml_enabled + argocd_repositories = local.enabled ? { + for k, v in var.argocd_repositories : k => { + clone_url = module.argocd_repo[k].outputs.repository_ssh_clone_url + github_deploy_key = data.aws_ssm_parameter.github_deploy_key[k].value + } + } : {} + credential_templates = flatten([ + for k, v in local.argocd_repositories : [ + { + name = "configs.credentialTemplates.${k}.url" + value = v.clone_url + type = "string" + }, + { + name = "configs.credentialTemplates.${k}.sshPrivateKey" + value = v.github_deploy_key + type = "string" + }, + ] + ]) + regional_service_discovery_domain = "${module.this.environment}.${module.dns_gbl_delegated.outputs.default_domain_name}" + host = var.host != "" ? var.host : format("%s.%s", coalesce(var.alb_name, var.name), local.regional_service_discovery_domain) + enable_argo_workflows_auth = local.saml_enabled && var.argo_enable_workflows_auth + enable_argo_workflows_auth_count = local.enable_argo_workflows_auth ? 1 : 0 + argo_workflows_host = "${var.argo_workflows_name}.${local.regional_service_discovery_domain}" + + oidc_config_map = local.oidc_enabled ? { + server : { + config : { + "oidc.config" = <<-EOT + name: ${var.oidc_name} + issuer: ${var.oidc_issuer} + clientID: ${local.oidc_client_id} + clientSecret: ${local.oidc_client_secret} + requestedScopes: ${var.oidc_requested_scopes} + EOT + } + } + } : {} + + saml_config_map = local.saml_enabled ? { + configs : { + params : { + "dexserver.disable.tls" = true + } + cm : { + "url" = "https://${local.host}" + "dex.config" = join("\n", [ + local.dex_config_connectors + ]) + } + } + } : {} + + dex_config_connectors = yamlencode({ + connectors = [for name, config in(local.enabled ? var.saml_sso_providers : {}) : + { + type = "saml" + id = "saml" + name = name + config = { + ssoURL = module.saml_sso_providers[name].outputs.url + caData = base64encode(format("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", module.saml_sso_providers[name].outputs.ca)) + redirectURI = format("https://%s/api/dex/callback", local.host) + entityIssuer = format("https://%s/api/dex/callback", local.host) + usernameAttr = "name" + emailAttr = "email" + ssoIssuer = module.saml_sso_providers[name].outputs.issuer + } + } + ] + }) + + post_render_script = local.enable_argo_workflows_auth ? "./resources/kustomize/post-render.sh" : null + kustomize_files_values = local.enable_argo_workflows_auth ? { + __ignore = { + kustomize_files = { for f in fileset("./resources/kustomize", "[^_]*.{sh,yaml}") : f => filesha256("./resources/kustomize/${f}") } + } + } : {} +} + +data "aws_ssm_parameters_by_path" "argocd_notifications" { + for_each = local.notifications_notifiers_ssm_path + path = each.value + with_decryption = true +} + +locals { + notifications_notifiers_ssm_path = { for key, value in var.notifications_notifiers : + key => format("%s/%s/", var.notifications_notifiers.ssm_path_prefix, key) + } + + notifications_notifiers_ssm_configs = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : + key => nonsensitive(zipmap( + [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], + value.values + )) + } + + notifications_notifiers_variables = { + for key, value in var.notifications_notifiers : + key => { for param_name, param_value in value : param_name => param_value if param_value != null } + if key != "ssm_path_prefix" + } + + notifications_notifiers = { + for key, value in local.notifications_notifiers_variables : + replace(key, "_", ".") => yamlencode(merge(local.notifications_notifiers_ssm_configs[key], value)) + } +} + +module "argocd" { + source = "cloudposse/helm-release/aws" + version = "0.3.0" + + name = "argocd" # avoids hitting length restrictions on IAM Role names + chart = var.chart + repository = var.chart_repository + description = var.chart_description + chart_version = var.chart_version + kubernetes_namespace = local.kubernetes_namespace + create_namespace = var.create_namespace + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + postrender_binary_path = local.post_render_script + + eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer + + service_account_name = module.this.name + service_account_namespace = var.kubernetes_namespace + + set_sensitive = local.credential_templates + + values = compact([ + # standard k8s object settings + yamlencode({ + fullnameOverride = module.this.name, + serviceAccount = { + name = module.this.name + }, + resources = var.resources + rbac = { + create = var.rbac_enabled + } + }), + # argocd-specific settings + templatefile( + "${path.module}/resources/argocd-values.yaml.tpl", + { + # admin_enabled = !(local.oidc_enabled || local.saml_enabled) + admin_enabled = true + alb_group_name = var.alb_group_name == null ? "" : var.alb_group_name + alb_logs_bucket = var.alb_logs_bucket + alb_logs_prefix = var.alb_logs_prefix + alb_name = var.alb_name == null ? "" : var.alb_name + application_repos = { for k, v in local.argocd_repositories : k => v.clone_url } + argocd_host = local.host + cert_issuer = var.certificate_issuer + forecastle_enabled = var.forecastle_enabled + ingress_host = local.host + name = module.this.name + oidc_enabled = local.oidc_enabled + oidc_rbac_scopes = var.oidc_rbac_scopes + organization = var.github_organization + saml_enabled = local.saml_enabled + saml_rbac_scopes = var.saml_rbac_scopes + rbac_policies = var.argocd_rbac_policies + rbac_groups = var.argocd_rbac_groups + enable_argo_workflows_auth = local.enable_argo_workflows_auth + } + ), + # argocd-notifications specific settings + templatefile( + "${path.module}/resources/argocd-notifications-values.yaml.tpl", + { + argocd_host = "https://${local.host}" + slack_notifications_enabled = var.slack_notifications_enabled + slack_notifications_username = var.slack_notifications_username + slack_notifications_icon = var.slack_notifications_icon + github_notifications_enabled = var.github_notifications_enabled + datadog_notifications_enabled = var.datadog_notifications_enabled + } + ), + yamlencode( + { + notifications = { + templates = { for key, value in var.notifications_templates : replace(key, "_", ".") => yamlencode(value) } + } + } + ), + yamlencode( + { + notifications = { + triggers = { for key, value in var.notifications_triggers : + # replace(key, "_", ".") => merge(yamlencode(value), data.aws_ssm_parameters_by_path.argocd_notifications[0].values) + replace(key, "_", ".") => yamlencode(value) + } + } + } + ), + yamlencode( + { + notifications = { + notifiers = local.notifications_notifiers + } + } + ), + yamlencode(merge( + local.oidc_config_map, + local.saml_config_map, + )), + yamlencode(local.kustomize_files_values), + yamlencode(var.chart_values) + ]) + + context = module.this.context +} + +module "argocd_apps" { + source = "cloudposse/helm-release/aws" + version = "0.3.0" + + name = "" # avoids hitting length restrictions on IAM Role names + chart = var.argocd_apps_chart + repository = var.argocd_apps_chart_repository + description = var.argocd_apps_chart_description + chart_version = var.argocd_apps_chart_version + kubernetes_namespace = var.kubernetes_namespace + create_namespace = var.create_namespace + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + enabled = local.enabled && var.argocd_apps_enabled + values = compact([ + templatefile( + "${path.module}/resources/argocd-apps-values.yaml.tpl", + { + application_repos = { for k, v in local.argocd_repositories : k => v.clone_url } + create_namespaces = var.argocd_create_namespaces + namespace = local.kubernetes_namespace + tenant = module.this.tenant + environment = var.environment + stage = var.stage + } + ), + ]) + + depends_on = [ + module.argocd + ] +} diff --git a/modules/eks/argocd/outputs.tf b/modules/eks/argocd/outputs.tf new file mode 100644 index 000000000..26b6561e7 --- /dev/null +++ b/modules/eks/argocd/outputs.tf @@ -0,0 +1,4 @@ +output "metadata" { + value = module.argocd.metadata + description = "Block status of the deployed release" +} diff --git a/modules/eks/argocd/provider-helm.tf b/modules/eks/argocd/provider-helm.tf new file mode 100644 index 000000000..20e4d3837 --- /dev/null +++ b/modules/eks/argocd/provider-helm.tf @@ -0,0 +1,158 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = true + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data + eks_cluster_id = module.eks.outputs.eks_cluster_id + eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/argocd/providers.tf b/modules/eks/argocd/providers.tf new file mode 100644 index 000000000..018e689a0 --- /dev/null +++ b/modules/eks/argocd/providers.tf @@ -0,0 +1,41 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +provider "aws" { + alias = "config_secrets" + region = var.ssm_store_account_region + profile = coalesce(var.import_profile_name, module.iam_roles_config_secrets.terraform_profile_name) +} + +module "iam_roles_config_secrets" { + source = "../../account-map/modules/iam-roles" + stage = var.ssm_store_account + tenant = var.ssm_store_account_tenant + context = module.this.context +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/eks/argocd/remote-state.tf b/modules/eks/argocd/remote-state.tf new file mode 100644 index 000000000..9024293ae --- /dev/null +++ b/modules/eks/argocd/remote-state.tf @@ -0,0 +1,42 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.eks_component_name + + context = module.this.context +} + +module "dns_gbl_delegated" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + environment = "gbl" + component = "dns-delegated" + + context = module.this.context +} + +module "saml_sso_providers" { + for_each = local.enabled ? var.saml_sso_providers : {} + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = each.value.component + + context = module.this.context +} + +module "argocd_repo" { + for_each = local.enabled ? var.argocd_repositories : {} + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = each.key + environment = each.value.environment + stage = each.value.stage + tenant = each.value.tenant + + context = module.this.context +} diff --git a/modules/eks/argocd/resources/argo-horizontal-color.png b/modules/eks/argocd/resources/argo-horizontal-color.png new file mode 100644 index 0000000000000000000000000000000000000000..1154626e3a43734324e55722c11f8fd60d9b7c35 GIT binary patch literal 63751 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 z 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 Q4E0x1Ks+gyGk*Vn02#U2RR910 literal 0 HcmV?d00001 diff --git a/modules/eks/argocd/resources/argocd-apps-values.yaml.tpl b/modules/eks/argocd/resources/argocd-apps-values.yaml.tpl new file mode 100644 index 000000000..e1cb7f900 --- /dev/null +++ b/modules/eks/argocd/resources/argocd-apps-values.yaml.tpl @@ -0,0 +1,23 @@ +applications: +%{ for name, url in application_repos ~} +- name: ${name} + namespace: ${namespace} + additionalLabels: {} + additionalAnnotations: {} + project: default + source: + repoURL: ${url} + targetRevision: HEAD + path: ./%{ if tenant != null }${tenant}/%{ endif }${environment}-${stage}/${namespace} + directory: + recurse: false + destination: + server: https://kubernetes.default.svc + namespace: ${namespace} + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=${create_namespaces} +%{ endfor ~} diff --git a/modules/eks/argocd/resources/argocd-notifications-values.yaml.tpl b/modules/eks/argocd/resources/argocd-notifications-values.yaml.tpl new file mode 100644 index 000000000..3a0facdb4 --- /dev/null +++ b/modules/eks/argocd/resources/argocd-notifications-values.yaml.tpl @@ -0,0 +1,40 @@ +notifications: + secret: + # create: false # Do not create an argocd-notifications-secret — this secret should instead be created via sops-secrets-operator + create: true + + argocdUrl: ${argocd_host} + + notifiers: + %{ if slack_notifications_enabled == true } + service.slack: | + token: $slack-token + username: "${slack_notifications_username}" + icon: "${slack_notifications_icon}" + %{ endif } + %{ if github_notifications_enabled == true } + # The webhook service notification configuration for GitHub cannot be consolidated into a single service because at least + # one of the notification templates requires the use of more than one GitHub endpoint via the webhook service. Since the + # webhook service configuration is a map, with each endpoint having its own key, we must also configure each key below, + # even if the configuration itself is exactly the same. + # See: https://github.com/argoproj/notifications-engine/blob/32519f8f68ec85d8ac3741d4ad52f7f5476ce5e7/pkg/services/webhook.go#L23 + service.webhook.github-commit-status: | + url: "https://api.github.com" + headers: + - name: "Authorization" + value: "token $github-token" + service.webhook.github-deployment: | + url: "https://api.github.com" + headers: + - name: "Authorization" + value: "token $github-token" + %{ endif } + %{ if datadog_notifications_enabled == true } + service.webhook.datadog: | + url: "https://api.datadoghq.com/api/v1/events" + headers: + - name: "DD-API-KEY" + value: "$datadog-api-key" + - name: "Content-Type" + value: "application/json" + %{ endif } diff --git a/modules/eks/argocd/resources/argocd-values.yaml.tpl b/modules/eks/argocd/resources/argocd-values.yaml.tpl new file mode 100644 index 000000000..1347a2c42 --- /dev/null +++ b/modules/eks/argocd/resources/argocd-values.yaml.tpl @@ -0,0 +1,139 @@ +global: + image: + imagePullPolicy: IfNotPresent + +crds: + install: true + +dex: + image: + imagePullPolicy: IfNotPresent + tag: v2.30.2 +%{ if enable_argo_workflows_auth ~} + env: + - name: ARGO_WORKFLOWS_SSO_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: argo-workflows-sso + key: client-secret +%{ endif ~} + +controller: + replicas: 1 + +server: + replicas: 2 + + ingress: + enabled: true + annotations: + cert-manager.io/cluster-issuer: ${cert_issuer} + external-dns.alpha.kubernetes.io/hostname: ${ingress_host} + external-dns.alpha.kubernetes.io/ttl: "60" + kubernetes.io/ingress.class: alb +%{ if alb_group_name != "" ~} + alb.ingress.kubernetes.io/group.name: ${alb_group_name} +%{ endif ~} +%{ if alb_name != "" ~} + alb.ingress.kubernetes.io/load-balancer-name: ${alb_name} +%{ endif ~} + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/backend-protocol: HTTPS + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80},{"HTTPS":443}]' + alb.ingress.kubernetes.io/ssl-redirect: '443' + alb.ingress.kubernetes.io/load-balancer-attributes: + routing.http.drop_invalid_header_fields.enabled=true, +%{ if alb_logs_bucket != "" ~} + access_logs.s3.enabled=true, + access_logs.s3.bucket=${alb_logs_bucket}, + access_logs.s3.prefix=${alb_logs_prefix} +%{ endif ~} +%{ if forecastle_enabled == true ~} + forecastle.stakater.com/appName: "ArgoCD" + forecastle.stakater.com/expose: "true" + forecastle.stakater.com/group: "portal" + forecastle.stakater.com/icon: https://argoproj.github.io/argo-cd/assets/logo.png + forecastle.stakater.com/instance: default +%{ endif ~} + hosts: + - ${argocd_host} + extraPaths: + # Must use implementation specific wildcard paths + # https://github.com/kubernetes-sigs/aws-load-balancer-controller/issues/1702#issuecomment-736890777 + - path: /* + pathType: ImplementationSpecific + backend: + service: + name: ${name}-server + port: + name: https + tls: + - hosts: + - ${argocd_host} + secretName: argocd-tls + https: false + + service: + type: NodePort + + config: + url: https://${argocd_host} + admin.enabled: "${admin_enabled}" + + # https://github.com/argoproj/argo-cd/issues/7835 + kustomize.buildOptions: --enable-helm + +# overridden in main.tf +# oidc.conf : ~ +# dex.config: ~ + + repositories: | +%{ for name, url in application_repos ~} + - url: ${url} + sshPrivateKeySecret: + name: argocd-repo-creds-${name} + key: sshPrivateKey +%{ endfor ~} + resource.customizations: | + admissionregistration.k8s.io/MutatingWebhookConfiguration: + ignoreDifferences: | + jsonPointers: + - /webhooks/0/clientConfig/caBundle + argoproj.io/Application: + health.lua: | + hs = {} + hs.status = "Progressing" + hs.message = "" + if obj.status ~= nil then + if obj.status.health ~= nil then + hs.status = obj.status.health.status + if obj.status.health.message ~= nil then + hs.message = obj.status.health.message + end + end + end + return hs + + rbacConfig: + policy.csv: | +%{ for policy in rbac_policies ~} + ${policy} +%{ endfor ~} +%{for item in rbac_groups ~} + g, ${item.group}, role:${item.role} +%{ endfor ~} + +%{ if oidc_enabled == true ~} + scopes: '${oidc_rbac_scopes}' +%{ endif ~} +%{ if saml_enabled == true ~} + scopes: '${saml_rbac_scopes}' +%{ endif ~} + + policy.default: role:readonly + +repoServer: + replicas: 2 + +applicationSet: + replicas: 2 diff --git a/modules/eks/argocd/resources/kustomize/.gitignore b/modules/eks/argocd/resources/kustomize/.gitignore new file mode 100644 index 000000000..5d23e393d --- /dev/null +++ b/modules/eks/argocd/resources/kustomize/.gitignore @@ -0,0 +1 @@ +_*.yaml \ No newline at end of file diff --git a/modules/eks/argocd/resources/kustomize/kustomization.yaml b/modules/eks/argocd/resources/kustomize/kustomization.yaml new file mode 100644 index 000000000..5894a3624 --- /dev/null +++ b/modules/eks/argocd/resources/kustomize/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - _all.yaml + +patches: + - path: patch.yaml + target: + kind: Deployment + labelSelector: "app.kubernetes.io/part-of=argocd,app.kubernetes.io/component=dex-server" \ No newline at end of file diff --git a/modules/eks/argocd/resources/kustomize/patch.yaml b/modules/eks/argocd/resources/kustomize/patch.yaml new file mode 100644 index 000000000..3a4ce55dd --- /dev/null +++ b/modules/eks/argocd/resources/kustomize/patch.yaml @@ -0,0 +1,3 @@ +- op: add + path: /metadata/annotations/secret.reloader.stakater.com~1reload + value: argo-workflows-sso \ No newline at end of file diff --git a/modules/eks/argocd/resources/kustomize/post-render.sh b/modules/eks/argocd/resources/kustomize/post-render.sh new file mode 100755 index 000000000..8ae41288f --- /dev/null +++ b/modules/eks/argocd/resources/kustomize/post-render.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +KUSTOMIZE_VERSION=4.5.3 +KUSTOMIZE_REPO_COMMIT=d2e59002aeb1faa724c6fa6e8218df2ad12631f8 +KUSTOMIZE_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/kubernetes-sigs/kustomize/$KUSTOMIZE_REPO_COMMIT/hack/install_kustomize.sh" + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +ALL_YAML="$SCRIPT_DIR/_all.yaml" +KUSTOMIZE_INSTALL_DIR="$(mktemp -d)" + +on_exit() +{ + test -e "$ALL_YAML" && rm "$ALL_YAML" +} + +trap on_exit EXIT + +curl -s "$KUSTOMIZE_INSTALL_SCRIPT_URL" | bash /dev/stdin "$KUSTOMIZE_VERSION" "$KUSTOMIZE_INSTALL_DIR" > /dev/null + +cd "$SCRIPT_DIR" + +cat <&0 > "$ALL_YAML" + +"$KUSTOMIZE_INSTALL_DIR/kustomize" build --reorder none . \ No newline at end of file diff --git a/modules/eks/argocd/variables-argocd-apps.tf b/modules/eks/argocd/variables-argocd-apps.tf new file mode 100644 index 000000000..39e67baad --- /dev/null +++ b/modules/eks/argocd/variables-argocd-apps.tf @@ -0,0 +1,30 @@ +variable "argocd_apps_chart_description" { + type = string + description = "Set release description attribute (visible in the history)." + default = "A Helm chart for managing additional Argo CD Applications and Projects" +} + +variable "argocd_apps_chart" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." + default = "argocd-apps" +} + +variable "argocd_apps_chart_repository" { + type = string + description = "Repository URL where to locate the requested chart." + default = "https://argoproj.github.io/argo-helm" +} + +variable "argocd_apps_chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." + default = "0.0.3" +} + +variable "argocd_apps_enabled" { + type = bool + description = "Enable argocd apps" + default = true +} + diff --git a/modules/eks/argocd/variables-argocd-notifications.tf b/modules/eks/argocd/variables-argocd-notifications.tf new file mode 100644 index 000000000..748ee606e --- /dev/null +++ b/modules/eks/argocd/variables-argocd-notifications.tf @@ -0,0 +1,102 @@ +variable "notifications_templates" { + type = map(object({ + message = string + alertmanager = optional(object({ + labels = map(string) + annotations = map(string) + generatorURL = string + })) + github = optional(object({ + status = object({ + state = string + label = string + targetURL = string + }) + })) + })) + default = {} + description = <<-EOT + Notification Templates to configure. + + See: https://argocd-notifications.readthedocs.io/en/stable/templates/ + See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) + EOT +} + +variable "notifications_triggers" { + type = map(list( + object({ + oncePer = optional(string) + send = list(string) + when = string + }) + )) + default = {} + description = <<-EOT + Notification Triggers to configure. + + See: https://argocd-notifications.readthedocs.io/en/stable/triggers/ + See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) + EOT +} + +variable "notifications_notifiers" { + type = object({ + ssm_path_prefix = optional(string, "/argocd/notifications/notifiers") + service_github = optional(object({ + appID = optional(number) + installationID = optional(number) + privateKey = optional(string) + })) + }) + default = {} + description = <<-EOT + Notification Triggers to configure. + + See: https://argocd-notifications.readthedocs.io/en/stable/triggers/ + See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) + EOT +} + + +variable "notifications_default_triggers" { + type = map(list(string)) + default = {} + description = <<-EOT + Default notification Triggers to configure. + + See: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/triggers/#default-triggers + See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/790438efebf423c2d56cb4b93471f4adb3fcd448/charts/argo-cd/values.yaml#L2841) + EOT +} + + +variable "slack_notifications_enabled" { + type = bool + default = false + description = "Whether or not to enable Slack notifications." +} + +variable "slack_notifications_username" { + type = string + default = null + description = "Custom username to use for Slack notifications." +} + +variable "slack_notifications_icon" { + type = string + default = null + description = "URI of custom image to use as the Slack notifications icon." +} + +variable "github_notifications_enabled" { + type = bool + default = false + description = "Whether or not to enable GitHub deployment and commit status notifications." +} + +variable "datadog_notifications_enabled" { + type = bool + default = false + description = "Whether or not to notify Datadog of deployments via the Datadog Events API." +} diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf new file mode 100644 index 000000000..1db151129 --- /dev/null +++ b/modules/eks/argocd/variables-argocd.tf @@ -0,0 +1,202 @@ +// ArgoCD variables + +variable "alb_group_name" { + type = string + description = "A name used in annotations to reuse an ALB (e.g. `argocd`) or to generate a new one" + default = null +} + +variable "alb_name" { + type = string + description = "The name of the ALB (e.g. `argocd`) provisioned by `alb-controller`. Works together with `var.alb_group_name`" + default = null +} + +variable "alb_logs_bucket" { + type = string + description = "The name of the bucket for ALB access logs. The bucket must have policy allowing the ELB logging principal" + default = "" +} + +variable "alb_logs_prefix" { + type = string + description = "`alb_logs_bucket` s3 bucket prefix" + default = "" +} + +variable "certificate_issuer" { + type = string + description = "Certificate manager cluster issuer" + default = "letsencrypt-staging" +} + +variable "argocd_create_namespaces" { + type = bool + description = "ArgoCD create namespaces policy" + default = false +} + +variable "argocd_repositories" { + type = map(object({ + environment = string # The environment where the `argocd_repo` component is deployed. + stage = string # The stage where the `argocd_repo` component is deployed. + tenant = string # The tenant where the `argocd_repo` component is deployed. + })) + description = "Map of objects defining an `argocd_repo` to configure. The key is the name of the ArgoCD repository." + default = {} +} + +variable "github_organization" { + type = string + description = "GitHub Organization" +} + +variable "ssm_store_account" { + type = string + description = "Account storing SSM parameters" +} + +variable "ssm_store_account_tenant" { + type = string + description = <<-EOT + Tenant of the account storing SSM parameters. + + If the tenant label is not used, leave this as null. + EOT + default = null +} + +variable "ssm_store_account_region" { + type = string + description = "AWS region storing SSM parameters" +} + +variable "ssm_oidc_client_id" { + type = string + description = "The SSM Parameter Store path for the ID of the IdP client" + default = "/argocd/oidc/client_id" +} + +variable "ssm_oidc_client_secret" { + type = string + description = "The SSM Parameter Store path for the secret of the IdP client" + default = "/argocd/oidc/client_secret" +} + +variable "host" { + type = string + description = "Host name to use for ingress and ALB" + default = "" +} + +variable "forecastle_enabled" { + type = bool + description = "Toggles Forecastle integration in the deployed chart" + default = false +} + +variable "oidc_enabled" { + type = bool + description = "Toggles OIDC integration in the deployed chart" + default = false +} + +variable "oidc_issuer" { + type = string + description = "OIDC issuer URL" + default = "" +} + +variable "oidc_name" { + type = string + description = "Name of the OIDC resource" + default = "" +} + +variable "oidc_rbac_scopes" { + type = string + description = "OIDC RBAC scopes to request" + default = "[argocd_realm_access]" +} + +variable "oidc_requested_scopes" { + type = string + description = "Set of OIDC scopes to request" + default = "[\"openid\", \"profile\", \"email\", \"groups\"]" +} + +variable "saml_enabled" { + type = bool + description = "Toggles SAML integration in the deployed chart" + default = false +} + +variable "saml_okta_app_name" { + type = string + description = "Name of the Okta SAML Integration" + default = "ArgoCD" +} + +variable "saml_rbac_scopes" { + type = string + description = "SAML RBAC scopes to request" + default = "[email,groups]" +} + +variable "argo_enable_workflows_auth" { + type = bool + default = false + description = "Allow argo-workflows to use Dex instance for SAML auth" +} + +variable "argo_workflows_name" { + type = string + default = "argo-workflows" + description = "Name of argo-workflows instance" +} + +variable "argocd_rbac_policies" { + type = list(string) + default = [] + description = <<-EOT + List of ArgoCD RBAC Permission strings to be added to the argocd-rbac configmap policy.csv item. + + See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. + EOT +} + +variable "argocd_rbac_groups" { + type = list(object({ + group = string, + role = string + })) + default = [] + description = <<-EOT + List of ArgoCD Group Role Assignment strings to be added to the argocd-rbac configmap policy.csv item. + e.g. + [ + { + group: idp-group-name, + role: argocd-role-name + }, + ] + becomes: `g, idp-group-name, role:argocd-role-name` + See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. + EOT +} + +variable "eks_component_name" { + type = string + default = "eks/cluster" + description = "The name of the eks component" +} + +variable "saml_sso_providers" { + type = map(object({ + component = string + })) + + default = {} + description = "SAML SSO providers components" +} + diff --git a/modules/eks/argocd/variables-helm.tf b/modules/eks/argocd/variables-helm.tf new file mode 100644 index 000000000..ddbafbd81 --- /dev/null +++ b/modules/eks/argocd/variables-helm.tf @@ -0,0 +1,93 @@ +// Standard Helm Chart variables + +variable "region" { + description = "AWS Region." + type = string +} + +variable "chart_description" { + type = string + description = "Set release description attribute (visible in the history)." + default = null +} + +variable "chart" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." + default = "argo-cd" +} + +variable "chart_repository" { + type = string + description = "Repository URL where to locate the requested chart." + default = "https://argoproj.github.io/argo-helm" +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." + default = "5.19.12" +} + +variable "resources" { + type = object({ + limits = object({ + cpu = string + memory = string + }) + requests = object({ + cpu = string + memory = string + }) + }) + default = null + description = "The cpu and memory of the deployment's limits and requests." +} + +variable "create_namespace" { + type = bool + description = "Create the namespace if it does not yet exist. Defaults to `false`." + default = false +} + +variable "kubernetes_namespace" { + type = string + description = "The namespace to install the release into." + default = "argocd" +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = 300 +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails." + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used." + default = true +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`." + default = true +} + +variable "chart_values" { + type = any + description = "Additional values to yamlencode as `helm_release` values." + default = {} +} + +variable "rbac_enabled" { + type = bool + default = true + description = "Enable Service Account for pods." +} diff --git a/modules/eks/argocd/versions.tf b/modules/eks/argocd/versions.tf new file mode 100644 index 000000000..23781035b --- /dev/null +++ b/modules/eks/argocd/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.6.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.9.0" + } + } +} From 119cf02135a60df734ba874595ff927d851fed34 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 14 Feb 2023 15:00:37 -0800 Subject: [PATCH 038/501] upstream `opsgenie-team` (#561) --- modules/opsgenie-team/README.md | 47 +++++++--- modules/opsgenie-team/datadog-integration.tf | 94 +++++++++++++++++++ modules/opsgenie-team/introspection.mixin.tf | 26 ----- modules/opsgenie-team/main.tf | 30 +++--- .../modules/escalation/README.md | 8 +- .../opsgenie-team/modules/escalation/main.tf | 6 +- .../modules/escalation/opsgenie.context.tf | 11 +++ .../modules/escalation/variables.tf | 2 +- .../modules/escalation/versions.tf | 4 +- .../modules/integration/README.md | 2 + .../opsgenie-team/modules/routing/README.md | 8 +- modules/opsgenie-team/modules/routing/main.tf | 45 ++++++++- .../modules/routing/opsgenie.context.tf | 11 +++ .../modules/routing/variables.tf | 12 ++- modules/opsgenie-team/opsgenie.context.tf | 11 +++ modules/opsgenie-team/providers.tf | 3 +- modules/opsgenie-team/versions.tf | 6 +- 17 files changed, 257 insertions(+), 69 deletions(-) create mode 100644 modules/opsgenie-team/datadog-integration.tf delete mode 100644 modules/opsgenie-team/introspection.mixin.tf create mode 100644 modules/opsgenie-team/modules/escalation/opsgenie.context.tf create mode 100644 modules/opsgenie-team/modules/routing/opsgenie.context.tf create mode 100644 modules/opsgenie-team/opsgenie.context.tf diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index 43d82e7d4..a3e1d196c 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -4,6 +4,13 @@ This component is responsible for provisioning Opsgenie teams and related servic ## Usage +#### Pre-requisites +You need an API Key stored in `/opsgenie/opsgenie_api_key` of SSM, this is configurable using the `ssm_parameter_name_format` and `ssm_path` variables. + +Generate an API Key by going [here](https://id.atlassian.com/manage-profile/security/api-tokens) and Clicking **Create API Token**. + +#### Getting Started + **Stack Level**: Global Here's an example snippet for how to use this component. @@ -12,7 +19,7 @@ This component should only be applied once as the resources it creates are regio ```yaml # 9-5 Mon-Fri -business_hours: &buisness_hours +business_hours: &business_hours type: "weekday-and-time-of-day" restrictions: - start_hour: 9 @@ -31,6 +38,7 @@ waking_hours: &waking_hours end_hour: 17 end_min: 00 +# This is a partial incident mapping, we use this as a base to add P1 & P2 below. This is not a complete mapping as there is no P0 priority_level_to_incident: &priority_level_to_incident enabled: true type: incident @@ -72,7 +80,7 @@ components: opsgenie-team-defaults: metadata: type: abstract - component: opsgenie + component: opsgenie-team vars: schedules: @@ -81,6 +89,8 @@ components: description: "London Schedule" timezone: "Europe/London" + # Routing Rules determine how alerts are routed to the team, + # this includes priority changes, incident mappings, and schedules. routing_rules: london_schedule: enabled: false @@ -99,6 +109,8 @@ components: expected_value: P2 # Since Incidents require a service, we create a rule for every `routing_rule` type `incident` for every service on the team. + # This is done behind the scenes by the `opsgenie-team` component. + # These rules below map P1 & P2 to incidents, using yaml anchors from above. p1: *p1_is_incident p2: *p2_is_incident @@ -124,7 +136,7 @@ components: enabled: true name: otherteam_escalation description: Other team escalation - rule: + rules: condition: if-not-acked notify_type: default delay: 60 @@ -136,7 +148,7 @@ components: enabled: true name: yaep_escalation description: Yet another escalation policy - rule: + rules: condition: if-not-acked notify_type: default delay: 90 @@ -148,14 +160,13 @@ components: enabled: true name: schedule_escalation description: Schedule escalation policy - rule: + rules: condition: if-not-acked notify_type: default delay: 30 recipients: - type: schedule name: secondary_on_call - ``` The API keys relating to the Opsgenie Integrations are stored in SSM Parameter Store and can be accessed via chamber. @@ -187,7 +198,7 @@ The problem is there are 3 different api endpoints in use ### There isn’t a resource for datadog to create an opsgenie integration so this has to be done manually via ClickOps - - Track the issue: https://github.com/DataDog/terraform-provider-datadog/issues/836 + - Track the issue: x ### No Resource to create Slack Integration @@ -239,14 +250,16 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [datadog](#requirement\_datadog) | >= 3.3.0 | | [opsgenie](#requirement\_opsgenie) | >= 0.6.7 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.9.0 | +| [datadog](#provider\_datadog) | >= 3.3.0 | | [opsgenie](#provider\_opsgenie) | >= 0.6.7 | ## Modules @@ -256,7 +269,6 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [escalation](#module\_escalation) | ./modules/escalation | n/a | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [integration](#module\_integration) | ./modules/integration | n/a | -| [introspection](#module\_introspection) | cloudposse/label/null | 0.25.0 | | [members\_merge](#module\_members\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.1 | | [routing](#module\_routing) | ./modules/routing | n/a | | [schedule](#module\_schedule) | cloudposse/incident-management/opsgenie//modules/schedule | 0.16.0 | @@ -268,7 +280,15 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | Name | Type | |------|------| +| [datadog_integration_opsgenie_service_object.fake_service_name](https://registry.terraform.io/providers/datadog/datadog/latest/docs/resources/integration_opsgenie_service_object) | resource | +| [aws_secretsmanager_secret.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | +| [aws_secretsmanager_secret.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | +| [aws_secretsmanager_secret_version.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | +| [aws_secretsmanager_secret_version.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | +| [aws_ssm_parameter.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.opsgenie_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.opsgenie_team_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [opsgenie_team.existing](https://registry.terraform.io/providers/opsgenie/opsgenie/latest/docs/data-sources/team) | data source | | [opsgenie_user.team_members](https://registry.terraform.io/providers/opsgenie/opsgenie/latest/docs/data-sources/user) | data source | @@ -280,6 +300,10 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_only\_integrations\_enabled](#input\_create\_only\_integrations\_enabled) | Whether to reuse all existing resources and only create new integrations | `bool` | `false` | no | +| [datadog\_api\_secret\_key](#input\_datadog\_api\_secret\_key) | The key of the Datadog API secret | `string` | `"datadog/datadog_api_key"` | no | +| [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The key of the Datadog Application secret | `string` | `"datadog/datadog_app_key"` | no | +| [datadog\_integration\_enabled](#input\_datadog\_integration\_enabled) | Whether to enable Datadog integration with opsgenie (datadog side) | `bool` | `true` | no | +| [datadog\_secrets\_store\_type](#input\_datadog\_secrets\_store\_type) | Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM` | `string` | `"SSM"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -300,7 +324,6 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [required\_tags](#input\_required\_tags) | List of required tag names | `list(string)` | `[]` | no | | [routing\_rules](#input\_routing\_rules) | Routing Rules for the team | `any` | `null` | no | | [schedules](#input\_schedules) | Schedules to create for the team | `map(any)` | `{}` | no | | [services](#input\_services) | Services to create and register to the team. | `map(any)` | `{}` | no | @@ -309,6 +332,8 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [team](#input\_team) | Configure the team inputs | `map(any)` | `{}` | no | +| [team\_name](#input\_team\_name) | Current OpsGenie Team Name | `string` | `null` | no | +| [team\_naming\_format](#input\_team\_naming\_format) | OpsGenie Team Naming Format | `string` | `"%s_%s"` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | ## Outputs diff --git a/modules/opsgenie-team/datadog-integration.tf b/modules/opsgenie-team/datadog-integration.tf new file mode 100644 index 000000000..0e61bafc0 --- /dev/null +++ b/modules/opsgenie-team/datadog-integration.tf @@ -0,0 +1,94 @@ +variable "datadog_integration_enabled" { + type = bool + default = true + description = "Whether to enable Datadog integration with opsgenie (datadog side)" +} + +data "aws_ssm_parameter" "opsgenie_team_api_key" { + count = local.enabled && var.datadog_integration_enabled ? 1 : 0 + name = module.integration["datadog"].ssm_path + with_decryption = true + depends_on = [module.integration] +} + +resource "datadog_integration_opsgenie_service_object" "fake_service_name" { + count = local.enabled && var.datadog_integration_enabled ? 1 : 0 + name = local.team_name + opsgenie_api_key = data.aws_ssm_parameter.opsgenie_team_api_key[0].value + region = "us" + depends_on = [module.integration] +} + + + +// Provider Configuration + +provider "datadog" { + api_key = local.datadog_api_key + app_key = local.datadog_app_key + validate = local.enabled +} + +locals { + asm_enabled = local.enabled && var.datadog_secrets_store_type == "ASM" + ssm_enabled = local.enabled && var.datadog_secrets_store_type == "SSM" + + # https://docs.datadoghq.com/account_management/api-app-keys/ + datadog_api_key = local.enabled ? (local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string : data.aws_ssm_parameter.datadog_api_key[0].value) : null + datadog_app_key = local.enabled ? (local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string : data.aws_ssm_parameter.datadog_app_key[0].value) : null +} + +variable "datadog_secrets_store_type" { + type = string + description = "Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" + default = "SSM" +} + +variable "datadog_api_secret_key" { + type = string + description = "The key of the Datadog API secret" + default = "datadog/datadog_api_key" +} + +variable "datadog_app_secret_key" { + type = string + description = "The key of the Datadog Application secret" + default = "datadog/datadog_app_key" +} + +// ASM + +data "aws_secretsmanager_secret" "datadog_api_key" { + count = local.asm_enabled ? 1 : 0 + name = var.datadog_api_secret_key +} + +data "aws_secretsmanager_secret_version" "datadog_api_key" { + count = local.asm_enabled ? 1 : 0 + secret_id = data.aws_secretsmanager_secret.datadog_api_key[0].id +} + +data "aws_secretsmanager_secret" "datadog_app_key" { + count = local.asm_enabled ? 1 : 0 + name = var.datadog_app_secret_key +} + +data "aws_secretsmanager_secret_version" "datadog_app_key" { + count = local.asm_enabled ? 1 : 0 + secret_id = data.aws_secretsmanager_secret.datadog_app_key[0].id +} + + +// SSM + +data "aws_ssm_parameter" "datadog_api_key" { + count = local.ssm_enabled ? 1 : 0 + name = format("/%s", var.datadog_api_secret_key) + with_decryption = true +} + +data "aws_ssm_parameter" "datadog_app_key" { + count = local.ssm_enabled ? 1 : 0 + name = format("/%s", var.datadog_app_secret_key) + with_decryption = true +} diff --git a/modules/opsgenie-team/introspection.mixin.tf b/modules/opsgenie-team/introspection.mixin.tf deleted file mode 100644 index 593327249..000000000 --- a/modules/opsgenie-team/introspection.mixin.tf +++ /dev/null @@ -1,26 +0,0 @@ -locals { - # Throw an error if lookup fails - # tflint-ignore: terraform_unused_declarations - check_required_tags = module.this.enabled ? [ - for k in var.required_tags : - lookup(module.this.tags, k) - ] : [] -} - -variable "required_tags" { - type = list(string) - description = "List of required tag names" - default = [] -} - -# introspection module will contain the additional tags -module "introspection" { - source = "cloudposse/label/null" - version = "0.25.0" - - tags = merge(var.tags, { - "Component" = basename(abspath(path.module)) - }) - - context = module.this.context -} diff --git a/modules/opsgenie-team/main.tf b/modules/opsgenie-team/main.tf index 6021eceb6..9b98fdfd8 100644 --- a/modules/opsgenie-team/main.tf +++ b/modules/opsgenie-team/main.tf @@ -44,7 +44,7 @@ module "members_merge" { local.members, ] - # context = module.introspection.context + # context = module.this.context } module "team" { @@ -59,7 +59,7 @@ module "team" { members = try(module.members_merge[0].merged, []) }, var.team) - context = module.introspection.context + context = module.this.context } module "integration" { @@ -93,7 +93,7 @@ module "integration" { # Allow underscores in the identifier regex_replace_chars = "/[^a-zA-Z0-9-_]/" - context = module.introspection.context + context = module.this.context depends_on = [module.team] } @@ -113,7 +113,7 @@ module "service" { description = lookup(each.value, "description", null) } - context = module.introspection.context + context = module.this.context depends_on = [module.team] } @@ -132,13 +132,13 @@ module "schedule" { enabled = local.create_all_enabled schedule = { - name = try(each.key, null) + name = try(format(var.team_naming_format, local.team_name, each.key), null) description = try(each.value.description, null) timezone = try(each.value.timezone, null) owner_team_id = local.team_id } - context = module.introspection.context + context = module.this.context depends_on = [ module.team, @@ -160,11 +160,12 @@ module "routing" { team_name = local.team_name name = each.key - criteria = try(each.value.criteria, null) - type = try(each.value.type, null) - notify = try(each.value.notify, null) - order = try(each.value.order, null) - priority = try(each.value.priority, null) + is_default = try(each.value.is_default, null) + criteria = try(each.value.criteria, null) + type = try(each.value.type, null) + notify = try(each.value.notify, null) + order = try(each.value.order, null) + priority = try(each.value.priority, null) # We send the map of services services = var.services @@ -178,7 +179,7 @@ module "routing" { # Allow underscores in the name regex_replace_chars = "/[^a-zA-Z0-9-_]/" - context = module.introspection.context + context = module.this.context depends_on = [ module.team, @@ -210,7 +211,10 @@ module "escalation" { repeat = try(each.value.repeat, null) } - context = module.introspection.context + context = module.this.context + + team_name = local.team_name + team_naming_format = var.team_naming_format depends_on = [ module.team, diff --git a/modules/opsgenie-team/modules/escalation/README.md b/modules/opsgenie-team/modules/escalation/README.md index 298860692..d97a36c3c 100644 --- a/modules/opsgenie-team/modules/escalation/README.md +++ b/modules/opsgenie-team/modules/escalation/README.md @@ -32,14 +32,14 @@ module "escalation" { | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13.0 | -| [opsgenie](#requirement\_opsgenie) | >= 0.4 | +| [terraform](#requirement\_terraform) | >= 1.0 | +| [opsgenie](#requirement\_opsgenie) | >= 0.6.7 | ## Providers | Name | Version | |------|---------| -| [opsgenie](#provider\_opsgenie) | >= 0.4 | +| [opsgenie](#provider\_opsgenie) | >= 0.6.7 | ## Modules @@ -78,6 +78,8 @@ module "escalation" { | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [team\_name](#input\_team\_name) | Current OpsGenie Team Name | `string` | `null` | no | +| [team\_naming\_format](#input\_team\_naming\_format) | OpsGenie Team Naming Format | `string` | `"%s_%s"` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | ## Outputs diff --git a/modules/opsgenie-team/modules/escalation/main.tf b/modules/opsgenie-team/modules/escalation/main.tf index 81ac73655..a62909395 100644 --- a/modules/opsgenie-team/modules/escalation/main.tf +++ b/modules/opsgenie-team/modules/escalation/main.tf @@ -11,7 +11,7 @@ locals { ])) lookup_schedules = distinct(flatten([ for rule in var.escalation.rules : - rule.recipient.name + format(var.team_naming_format, var.team_name, rule.recipient.name) if rule.recipient.type == "schedule" ])) } @@ -35,7 +35,7 @@ data "opsgenie_schedule" "recipient" { resource "opsgenie_escalation" "this" { count = module.this.enabled ? 1 : 0 - name = var.escalation.name + name = format(var.team_naming_format, var.team_name, var.escalation.name) description = try(var.escalation.description, var.escalation.name) owner_team_id = try(var.escalation.owner_team_id, null) @@ -49,7 +49,7 @@ resource "opsgenie_escalation" "this" { # In spite of the docs, only one recipient can be used per escalation resource with multiple rules recipient { - id = rules.value.recipient.type == "team" ? data.opsgenie_team.recipient[rules.value.recipient.name].id : rules.value.recipient.type == "schedule" ? data.opsgenie_schedule.recipient[rules.value.recipient.name].id : data.opsgenie_user.recipient[rules.value.recipient.name].id + id = rules.value.recipient.type == "team" ? data.opsgenie_team.recipient[rules.value.recipient.name].id : rules.value.recipient.type == "schedule" ? data.opsgenie_schedule.recipient[format(var.team_naming_format, var.team_name, rules.value.recipient.name)].id : data.opsgenie_user.recipient[rules.value.recipient.name].id type = rules.value.recipient.type } } diff --git a/modules/opsgenie-team/modules/escalation/opsgenie.context.tf b/modules/opsgenie-team/modules/escalation/opsgenie.context.tf new file mode 100644 index 000000000..a6fa39d3b --- /dev/null +++ b/modules/opsgenie-team/modules/escalation/opsgenie.context.tf @@ -0,0 +1,11 @@ +variable "team_name" { + type = string + default = null + description = "Current OpsGenie Team Name" +} + +variable "team_naming_format" { + type = string + default = "%s_%s" + description = "OpsGenie Team Naming Format" +} diff --git a/modules/opsgenie-team/modules/escalation/variables.tf b/modules/opsgenie-team/modules/escalation/variables.tf index b6d512a0d..6db977f7e 100644 --- a/modules/opsgenie-team/modules/escalation/variables.tf +++ b/modules/opsgenie-team/modules/escalation/variables.tf @@ -2,4 +2,4 @@ variable "escalation" { default = {} type = any description = "Opsgenie Escalation configuration" -} \ No newline at end of file +} diff --git a/modules/opsgenie-team/modules/escalation/versions.tf b/modules/opsgenie-team/modules/escalation/versions.tf index 240bbd512..87be4ce66 100644 --- a/modules/opsgenie-team/modules/escalation/versions.tf +++ b/modules/opsgenie-team/modules/escalation/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 0.13.0" + required_version = ">= 1.0" required_providers { opsgenie = { source = "opsgenie/opsgenie" - version = ">= 0.4" + version = ">= 0.6.7" } } } diff --git a/modules/opsgenie-team/modules/integration/README.md b/modules/opsgenie-team/modules/integration/README.md index ec6c772d5..227810bbe 100644 --- a/modules/opsgenie-team/modules/integration/README.md +++ b/modules/opsgenie-team/modules/integration/README.md @@ -1,5 +1,7 @@ ## Integration +This module creates an OpsGenie integrations for a team. By Default, it creates a Datadog integration. + ## Requirements diff --git a/modules/opsgenie-team/modules/routing/README.md b/modules/opsgenie-team/modules/routing/README.md index b12efbdfb..bf6f14519 100644 --- a/modules/opsgenie-team/modules/routing/README.md +++ b/modules/opsgenie-team/modules/routing/README.md @@ -1,5 +1,8 @@ ## Routing +This module creates team routing rules, these are the initial rules that are applied to an alert to determine who gets notified. +This module also creates incident service rules, which determine if an alert is considered a service incident or not. + ## Requirements @@ -19,6 +22,7 @@ | Name | Source | Version | |------|--------|---------| | [service\_incident\_rule](#module\_service\_incident\_rule) | cloudposse/incident-management/opsgenie//modules/service_incident_rule | 0.16.0 | +| [serviceless\_incident\_rule](#module\_serviceless\_incident\_rule) | cloudposse/incident-management/opsgenie//modules/service_incident_rule | 0.16.0 | | [team\_routing\_rule](#module\_team\_routing\_rule) | cloudposse/incident-management/opsgenie//modules/team_routing_rule | 0.16.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -44,6 +48,7 @@ | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [incident\_properties](#input\_incident\_properties) | Properties to override on the incident routing rule | `map(any)` | n/a | yes | +| [is\_default](#input\_is\_default) | Set this alerting route as the default route | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | @@ -57,7 +62,8 @@ | [services](#input\_services) | Team services to associate with incident routing rules | `map(any)` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [team\_name](#input\_team\_name) | Name of the team to assign this integration to | `string` | n/a | yes | +| [team\_name](#input\_team\_name) | Current OpsGenie Team Name | `string` | `null` | no | +| [team\_naming\_format](#input\_team\_naming\_format) | OpsGenie Team Naming Format | `string` | `"%s_%s"` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [time\_restriction](#input\_time\_restriction) | Time restriction of alert routing rule | `any` | `null` | no | | [timezone](#input\_timezone) | Timezone for this alerting route | `string` | `null` | no | diff --git a/modules/opsgenie-team/modules/routing/main.tf b/modules/opsgenie-team/modules/routing/main.tf index 412d73be6..fc41bee23 100644 --- a/modules/opsgenie-team/modules/routing/main.tf +++ b/modules/opsgenie-team/modules/routing/main.tf @@ -46,7 +46,8 @@ module "team_routing_rule" { notify = [{ type = var.notify.type - id = join("", data.opsgenie_schedule.notification_schedule.*.id) + name = try(format(var.team_naming_format, var.team_name, var.notify.name), null) + id = try(join("", data.opsgenie_schedule.notification_schedule.*.id), "") }] criteria = { @@ -61,11 +62,16 @@ module "team_routing_rule" { context = module.this.context } +locals { + default_service = { for k, v in var.services : k => v if k == "default_service" } + services = { for k, v in var.services : k => v if k != "default_service" } +} + module "service_incident_rule" { source = "cloudposse/incident-management/opsgenie//modules/service_incident_rule" version = "0.16.0" - for_each = local.service_incident_rule_enabled ? var.services : {} + for_each = local.service_incident_rule_enabled ? local.services : {} service_incident_rule = { service_id = data.opsgenie_service.incident_service[each.key].id @@ -96,3 +102,38 @@ module "service_incident_rule" { context = module.this.context } + + +module "serviceless_incident_rule" { + source = "cloudposse/incident-management/opsgenie//modules/service_incident_rule" + version = "0.16.0" + + depends_on = [data.opsgenie_service.incident_service] + + for_each = local.service_incident_rule_enabled ? local.default_service : {} + + service_incident_rule = { + service_id = data.opsgenie_service.incident_service[each.key].id + + incident_rule = { + condition_match_type = var.criteria.type + conditions = try(var.criteria.conditions, null) + + incident_properties = { + message = try(var.incident_properties.message, "{{message}}") + tags = try(var.incident_properties.tags, []) + details = try(var.incident_properties.details, {}) + + priority = var.priority + + stakeholder_properties = { + message = try(var.incident_properties.message, "{{message}}") + description = try(var.incident_properties.description, null) + enable = try(var.incident_properties.update_stakeholders, true) + } + } + } + } + + context = module.this.context +} diff --git a/modules/opsgenie-team/modules/routing/opsgenie.context.tf b/modules/opsgenie-team/modules/routing/opsgenie.context.tf new file mode 100644 index 000000000..a6fa39d3b --- /dev/null +++ b/modules/opsgenie-team/modules/routing/opsgenie.context.tf @@ -0,0 +1,11 @@ +variable "team_name" { + type = string + default = null + description = "Current OpsGenie Team Name" +} + +variable "team_naming_format" { + type = string + default = "%s_%s" + description = "OpsGenie Team Naming Format" +} diff --git a/modules/opsgenie-team/modules/routing/variables.tf b/modules/opsgenie-team/modules/routing/variables.tf index a5c503624..3ab43c5f2 100644 --- a/modules/opsgenie-team/modules/routing/variables.tf +++ b/modules/opsgenie-team/modules/routing/variables.tf @@ -1,8 +1,3 @@ -variable "team_name" { - type = string - description = "Name of the team to assign this integration to" -} - variable "criteria" { type = object({ type = string, @@ -68,3 +63,10 @@ variable "time_restriction" { default = null description = "Time restriction of alert routing rule" } + +variable "is_default" { + type = bool + default = false + description = "Set this alerting route as the default route" + +} diff --git a/modules/opsgenie-team/opsgenie.context.tf b/modules/opsgenie-team/opsgenie.context.tf new file mode 100644 index 000000000..a6fa39d3b --- /dev/null +++ b/modules/opsgenie-team/opsgenie.context.tf @@ -0,0 +1,11 @@ +variable "team_name" { + type = string + default = null + description = "Current OpsGenie Team Name" +} + +variable "team_naming_format" { + type = string + default = "%s_%s" + description = "OpsGenie Team Naming Format" +} diff --git a/modules/opsgenie-team/providers.tf b/modules/opsgenie-team/providers.tf index 63ca6fe51..393058066 100644 --- a/modules/opsgenie-team/providers.tf +++ b/modules/opsgenie-team/providers.tf @@ -28,11 +28,12 @@ variable "import_role_arn" { } data "aws_ssm_parameter" "opsgenie_api_key" { + count = local.enabled ? 1 : 0 name = format(var.ssm_parameter_name_format, var.ssm_path, "opsgenie_api_key") with_decryption = true } provider "opsgenie" { - api_key = data.aws_ssm_parameter.opsgenie_api_key.value + api_key = join("", data.aws_ssm_parameter.opsgenie_api_key[*].value) } diff --git a/modules/opsgenie-team/versions.tf b/modules/opsgenie-team/versions.tf index f013c9ee4..9e545cdee 100644 --- a/modules/opsgenie-team/versions.tf +++ b/modules/opsgenie-team/versions.tf @@ -4,11 +4,15 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.9.0" } opsgenie = { source = "opsgenie/opsgenie" version = ">= 0.6.7" } + datadog = { + source = "datadog/datadog" + version = ">= 3.3.0" + } } } From 2a3b07060e3aefbb86f5f8f3670f7c51199a60c0 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Thu, 16 Feb 2023 20:53:05 +0300 Subject: [PATCH 039/501] [sso-saml-provider] Upstream SSO SAML provider component (#562) --- modules/sso-saml-provider/README.md | 22 ++ modules/sso-saml-provider/context.tf | 279 +++++++++++++++++++++++++ modules/sso-saml-provider/main.tf | 16 ++ modules/sso-saml-provider/outputs.tf | 18 ++ modules/sso-saml-provider/providers.tf | 28 +++ modules/sso-saml-provider/variables.tf | 9 + modules/sso-saml-provider/versions.tf | 10 + 7 files changed, 382 insertions(+) create mode 100644 modules/sso-saml-provider/README.md create mode 100644 modules/sso-saml-provider/context.tf create mode 100644 modules/sso-saml-provider/main.tf create mode 100644 modules/sso-saml-provider/outputs.tf create mode 100644 modules/sso-saml-provider/providers.tf create mode 100644 modules/sso-saml-provider/variables.tf create mode 100644 modules/sso-saml-provider/versions.tf diff --git a/modules/sso-saml-provider/README.md b/modules/sso-saml-provider/README.md new file mode 100644 index 000000000..dffff96d5 --- /dev/null +++ b/modules/sso-saml-provider/README.md @@ -0,0 +1,22 @@ +# Component: `sso-saml-provider` + +This component reads sso credentials from SSM Parameter store and provides them as outputs + +## Usage + +**Stack Level**: Regional + +Use this in the catalog or use these variables to overwrite the catalog values. + +```yaml +components: + terraform: + sso-saml-provider: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + ssm_path_prefix: "/sso/saml/google" +``` + diff --git a/modules/sso-saml-provider/context.tf b/modules/sso-saml-provider/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/sso-saml-provider/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/sso-saml-provider/main.tf b/modules/sso-saml-provider/main.tf new file mode 100644 index 000000000..01167147f --- /dev/null +++ b/modules/sso-saml-provider/main.tf @@ -0,0 +1,16 @@ +locals { + enabled = module.this.enabled + + url = try(module.store_read.map[format("%s/%s", var.ssm_path_prefix, "url")], "") + ca = try(module.store_read.map[format("%s/%s", var.ssm_path_prefix, "ca")], "") + issuer = try(module.store_read.map[format("%s/%s", var.ssm_path_prefix, "issuer")], "") +} + +module "store_read" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.10.0" + + context = module.this.context + + parameter_read = formatlist("%s/%s", var.ssm_path_prefix, ["url", "ca", "issuer"]) +} diff --git a/modules/sso-saml-provider/outputs.tf b/modules/sso-saml-provider/outputs.tf new file mode 100644 index 000000000..b95c47afe --- /dev/null +++ b/modules/sso-saml-provider/outputs.tf @@ -0,0 +1,18 @@ +output "url" { + value = local.enabled ? local.url : null + description = "Identity Provider Single Sign-On URL" + sensitive = true +} + +output "ca" { + value = local.enabled ? local.ca : null + description = "Raw signing certificate" + sensitive = true +} + +output "issuer" { + value = local.enabled ? local.issuer : null + description = "Identity Provider Single Sign-On Issuer URL" + sensitive = true +} + diff --git a/modules/sso-saml-provider/providers.tf b/modules/sso-saml-provider/providers.tf new file mode 100644 index 000000000..efa9ede5d --- /dev/null +++ b/modules/sso-saml-provider/providers.tf @@ -0,0 +1,28 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/sso-saml-provider/variables.tf b/modules/sso-saml-provider/variables.tf new file mode 100644 index 000000000..a2f76a6b9 --- /dev/null +++ b/modules/sso-saml-provider/variables.tf @@ -0,0 +1,9 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "ssm_path_prefix" { + type = string + description = "Top level SSM path prefix (without leading or trailing slash)" +} diff --git a/modules/sso-saml-provider/versions.tf b/modules/sso-saml-provider/versions.tf new file mode 100644 index 000000000..f33ede77f --- /dev/null +++ b/modules/sso-saml-provider/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} From d9d613764a5c649d90f0cdf423e82387f225adf6 Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Sat, 18 Feb 2023 09:27:49 -0700 Subject: [PATCH 040/501] feat: updates spacelift to support policies outside of the comp folder (#522) Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- modules/spacelift/README.md | 3 ++- modules/spacelift/main.tf | 2 +- modules/spacelift/variables.tf | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index 9aaf8d776..f4470b5bc 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -399,7 +399,8 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [policies\_available](#input\_policies\_available) | List of available default policies to create in Spacelift (these policies will not be attached to Spacelift stacks by default, use `var.policies_enabled`) | `list(string)` |
[
"git_push.proposed-run",
"git_push.tracked-run",
"plan.default",
"trigger.dependencies",
"trigger.retries"
]
| no | | [policies\_by\_id\_enabled](#input\_policies\_by\_id\_enabled) | List of existing policy IDs to attach to all Spacelift stacks. These policies must already exist in Spacelift | `list(string)` | `[]` | no | -| [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of custom policy names to attach to all Spacelift stacks. These policies must exist in `components/terraform/spacelift/rego-policies` | `list(string)` | `[]` | no | +| [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of existing policy names to attach to all Spacelift stacks. These policies must exist at `modules/spacelift/rego-policies` OR `var.policies_by_name_path`. | `list(string)` | `[]` | no | +| [policies\_by\_name\_path](#input\_policies\_by\_name\_path) | Path to the catalog of external Rego policies. The Rego files must exist in the caller's code at the path. The module will create Spacelift policies from the external Rego definitions | `string` | `""` | no | | [policies\_enabled](#input\_policies\_enabled) | DEPRECATED: Use `policies_by_id_enabled` instead. List of default policies created by this stack to attach to all Spacelift stacks | `list(string)` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [runner\_image](#input\_runner\_image) | Full address & tag of the Spacelift runner image (e.g. on ECR) | `string` | n/a | yes | diff --git a/modules/spacelift/main.tf b/modules/spacelift/main.tf index 2587e8e27..2b0cd90b9 100644 --- a/modules/spacelift/main.tf +++ b/modules/spacelift/main.tf @@ -31,7 +31,7 @@ module "spacelift" { policies_enabled = var.policies_enabled policies_by_id_enabled = var.policies_by_id_enabled policies_by_name_enabled = var.policies_by_name_enabled - policies_by_name_path = format("%s/rego-policies", path.module) + policies_by_name_path = var.policies_by_name_path != "" ? var.policies_by_name_path : format("%s/rego-policies", path.module) administrative_push_policy_enabled = var.administrative_push_policy_enabled administrative_trigger_policy_enabled = var.administrative_trigger_policy_enabled diff --git a/modules/spacelift/variables.tf b/modules/spacelift/variables.tf index 5e87cc8e0..fbef64eba 100644 --- a/modules/spacelift/variables.tf +++ b/modules/spacelift/variables.tf @@ -86,10 +86,16 @@ variable "policies_by_id_enabled" { variable "policies_by_name_enabled" { type = list(string) - description = "List of custom policy names to attach to all Spacelift stacks. These policies must exist in `components/terraform/spacelift/rego-policies`" + description = "List of existing policy names to attach to all Spacelift stacks. These policies must exist at `modules/spacelift/rego-policies` OR `var.policies_by_name_path`." default = [] } +variable "policies_by_name_path" { + type = string + description = "Path to the catalog of external Rego policies. The Rego files must exist in the caller's code at the path. The module will create Spacelift policies from the external Rego definitions" + default = "" +} + variable "administrative_stack_drift_detection_enabled" { type = bool description = "Flag to enable/disable administrative stack drift detection" From 477c5e7e1d27e736f01443f45a82f26de5bef419 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 21 Feb 2023 08:45:19 -0800 Subject: [PATCH 041/501] update dd agent docs (#565) --- modules/datadog-agent/README.md | 41 ++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/modules/datadog-agent/README.md b/modules/datadog-agent/README.md index 42ff86c3e..b6fdc8c05 100644 --- a/modules/datadog-agent/README.md +++ b/modules/datadog-agent/README.md @@ -2,15 +2,6 @@ This component installs the `datadog-agent` for EKS clusters. -Note that pending https://tanzle.atlassian.net/browse/SRE-268 & https://cloudposse.atlassian.net/browse/MEROPE-381 , failed Terraform applies for this component may leave state & live release config inconsistent resulting in out-of-sync configuration but a no-change plan. - -If you're getting a "No changes" plan when you know the live release config doesn't match the new values, force a taint/recreate of the Helm release with a Spacelift task for the stack like this: `terraform apply -replace='module.datadog_agent.helm_release.this[0]' -auto-approve`. - -Locally this looks like -```shell -atmos terraform deploy datadog-agent -s ${region}-${stage} -replace='module.datadog_agent.helm_release.this[0]' -``` - ## Usage **Stack Level**: Regional @@ -26,21 +17,48 @@ components: workspace_enabled: true vars: enabled: true + eks_component_name: eks/cluster name: "datadog" description: "Datadog Kubernetes Agent" kubernetes_namespace: "monitoring" create_namespace: true repository: "https://helm.datadoghq.com" chart: "datadog" - chart_version: "3.0.0" - timeout: 600 + chart_version: "3.0.3" + timeout: 1200 wait: true atomic: true cleanup_on_fail: true + cluster_checks_enabled: false + helm_manifest_experiment_enabled: false + secrets_store_type: SSM + tags: + team: sre + service: datadog-agent + app: monitoring + # datadog-agent shouldn't be deployed to the Fargate nodes + values: + agents: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: eks.amazonaws.com/compute-type + operator: NotIn + values: + - fargate + datadog: + env: + - name: DD_EC2_PREFER_IMDSV2 # this merges ec2 instances and the node in the hostmap section + value: "true" + ``` Deploy this to a particular environment such as dev, prod, etc. +This will add cluster checks to a specific environment. + ```yaml components: terraform: @@ -51,6 +69,7 @@ components: - catalog/cluster-checks/defaults/*.yaml - catalog/cluster-checks/dev/*.yaml datadog_cluster_check_config_parameters: {} + # add additional tags to all data coming in from this agent. datadog_tags: - "env:dev" - "region:us-west-2" From c6ac4deadd620414ce9b0125eb405f802c8943b0 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Tue, 21 Feb 2023 20:33:27 +0200 Subject: [PATCH 042/501] Add Redshift component (#563) Co-authored-by: cloudpossebot --- modules/redshift/README.md | 150 +++++++++++++++ modules/redshift/context.tf | 279 ++++++++++++++++++++++++++++ modules/redshift/main.tf | 74 ++++++++ modules/redshift/outputs.tf | 44 +++++ modules/redshift/providers.tf | 29 +++ modules/redshift/remote-state.tf | 8 + modules/redshift/systems-manager.tf | 108 +++++++++++ modules/redshift/variables.tf | 102 ++++++++++ modules/redshift/versions.tf | 14 ++ 9 files changed, 808 insertions(+) create mode 100644 modules/redshift/README.md create mode 100644 modules/redshift/context.tf create mode 100644 modules/redshift/main.tf create mode 100644 modules/redshift/outputs.tf create mode 100644 modules/redshift/providers.tf create mode 100644 modules/redshift/remote-state.tf create mode 100644 modules/redshift/systems-manager.tf create mode 100644 modules/redshift/variables.tf create mode 100644 modules/redshift/versions.tf diff --git a/modules/redshift/README.md b/modules/redshift/README.md new file mode 100644 index 000000000..c5b956515 --- /dev/null +++ b/modules/redshift/README.md @@ -0,0 +1,150 @@ +# Component: `redshift` + +This component is responsible for provisioning a RedShift instance. It seeds relevant database information (hostnames, username, password, etc.) into AWS SSM Parameter Store. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + redshift: + vars: + enabled: true + name: redshift + database_name: redshift + publicly_accessible: false + node_type: dc2.large + number_of_nodes: 1 + cluster_type: single-node + ssm_enabled: true + log_exports: + - userlog + - connectionlog + - useractivitylog + admin_user: redshift + custom_sg_enabled: true + custom_sg_rules: + - type: ingress + key: postgres + description: Allow inbound traffic to the redshift cluster + from_port: 5439 + to_port: 5439 + protocol: tcp + cidr_blocks: + - 10.0.0.0/8 + +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 4.17 | +| [random](#requirement\_random) | >= 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.17 | +| [random](#provider\_random) | >= 3.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [redshift\_cluster](#module\_redshift\_cluster) | cloudposse/redshift-cluster/aws | 1.0.0 | +| [redshift\_sg](#module\_redshift\_sg) | cloudposse/security-group/aws | 2.0.0-rc1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ssm_parameter.redshift_database_hostname](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.redshift_database_name](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.redshift_database_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.redshift_database_port](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.redshift_database_user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [random_password.admin_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | +| [random_pet.admin_user](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_password](#input\_admin\_password) | Password for the master DB user. Required unless a snapshot\_identifier is provided | `string` | `null` | no | +| [admin\_user](#input\_admin\_user) | Username for the master DB user. Required unless a snapshot\_identifier is provided | `string` | `null` | no | +| [allow\_version\_upgrade](#input\_allow\_version\_upgrade) | Whether or not to enable major version upgrades which are applied during the maintenance window to the Amazon Redshift engine that is running on the cluster | `bool` | `false` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [cluster\_type](#input\_cluster\_type) | The cluster type to use. Either `single-node` or `multi-node` | `string` | `"single-node"` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [custom\_sg\_allow\_all\_egress](#input\_custom\_sg\_allow\_all\_egress) | Whether to allow all egress traffic or not | `bool` | `true` | no | +| [custom\_sg\_enabled](#input\_custom\_sg\_enabled) | Whether to use custom security group or not | `bool` | `false` | no | +| [custom\_sg\_rules](#input\_custom\_sg\_rules) | An array of custom security groups to create and assign to the cluster. |
list(object({
key = string
type = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
| `[]` | no | +| [database\_name](#input\_database\_name) | The name of the first database to be created when the cluster is created | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [engine\_version](#input\_engine\_version) | The version of the Amazon Redshift engine to use. See https://docs.aws.amazon.com/redshift/latest/mgmt/cluster-versions.html | `string` | `"1.0"` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [node\_type](#input\_node\_type) | The node type to be provisioned for the cluster. See https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#working-with-clusters-overview | `string` | `"dc2.large"` | no | +| [number\_of\_nodes](#input\_number\_of\_nodes) | The number of compute nodes in the cluster. This parameter is required when the ClusterType parameter is specified as multi-node | `number` | `1` | no | +| [port](#input\_port) | The port number on which the cluster accepts incoming connections | `number` | `5439` | no | +| [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS region | `string` | n/a | yes | +| [security\_group\_ids](#input\_security\_group\_ids) | An array of security group IDs to associate with the endpoint. | `list(string)` | `null` | no | +| [ssm\_enabled](#input\_ssm\_enabled) | If `true` create SSM keys for the database user and password. | `bool` | `false` | no | +| [ssm\_key\_format](#input\_ssm\_key\_format) | SSM path format. The values will will be used in the following order: `var.ssm_key_prefix`, `var.name`, `var.ssm_key_*` | `string` | `"/%v/%v/%v"` | no | +| [ssm\_key\_hostname](#input\_ssm\_key\_hostname) | The SSM key to save the hostname. See `var.ssm_path_format`. | `string` | `"admin/db_hostname"` | no | +| [ssm\_key\_password](#input\_ssm\_key\_password) | The SSM key to save the password. See `var.ssm_path_format`. | `string` | `"admin/db_password"` | no | +| [ssm\_key\_port](#input\_ssm\_key\_port) | The SSM key to save the port. See `var.ssm_path_format`. | `string` | `"admin/db_port"` | no | +| [ssm\_key\_prefix](#input\_ssm\_key\_prefix) | SSM path prefix. Omit the leading forward slash `/`. | `string` | `"redshift"` | no | +| [ssm\_key\_user](#input\_ssm\_key\_user) | The SSM key to save the user. See `var.ssm_path_format`. | `string` | `"admin/db_user"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [use\_private\_subnets](#input\_use\_private\_subnets) | Whether to use private or public subnets for the Redshift cluster | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | Amazon Resource Name (ARN) of cluster | +| [cluster\_identifier](#output\_cluster\_identifier) | The Cluster Identifier | +| [cluster\_security\_groups](#output\_cluster\_security\_groups) | The security groups associated with the cluster | +| [database\_name](#output\_database\_name) | The name of the default database in the Cluster | +| [dns\_name](#output\_dns\_name) | The DNS name of the cluster | +| [endpoint](#output\_endpoint) | The connection endpoint | +| [id](#output\_id) | The Redshift Cluster ID | +| [port](#output\_port) | The Port the cluster responds on | +| [redshift\_database\_ssm\_key\_prefix](#output\_redshift\_database\_ssm\_key\_prefix) | SSM prefix | +| [vpc\_security\_group\_ids](#output\_vpc\_security\_group\_ids) | The VPC security group IDs associated with the cluster | + + + +## References +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/redshift) - Cloud Posse's upstream component + + +[](https://cpco.io/component) diff --git a/modules/redshift/context.tf b/modules/redshift/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/redshift/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/redshift/main.tf b/modules/redshift/main.tf new file mode 100644 index 000000000..b62e45445 --- /dev/null +++ b/modules/redshift/main.tf @@ -0,0 +1,74 @@ +locals { + enabled = module.this.enabled + subnet_ids = var.use_private_subnets ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids + admin_user = var.admin_user != null && var.admin_user != "" ? var.admin_user : join("", random_pet.admin_user.*.id) + admin_password = var.admin_password != null && var.admin_password != "" ? var.admin_password : join("", random_password.admin_password.*.result) + database_name = var.database_name == null ? module.this.id : var.database_name +} + +resource "random_pet" "admin_user" { + count = local.enabled && (var.admin_user == null || var.admin_user == "") ? 1 : 0 + + length = 2 + separator = "_" + + keepers = { + db_name = var.database_name + } +} + +resource "random_password" "admin_password" { + count = local.enabled && (var.admin_password == null || var.admin_password == "") ? 1 : 0 + + length = 33 + # Leave special characters out to avoid quoting and other issues. + # Special characters have no additional security compared to increasing length. + special = false + override_special = "!#$%^&*()<>-_" + + keepers = { + db_name = var.database_name + } +} + +module "redshift_cluster" { + source = "cloudposse/redshift-cluster/aws" + version = "1.0.0" + + subnet_ids = local.subnet_ids + vpc_security_group_ids = coalesce(var.security_group_ids, module.redshift_sg[*].id, []) + + port = var.port + admin_user = local.admin_user + admin_password = local.admin_password + database_name = local.database_name + node_type = var.node_type + number_of_nodes = var.number_of_nodes + cluster_type = var.cluster_type + engine_version = var.engine_version + publicly_accessible = var.publicly_accessible + allow_version_upgrade = var.allow_version_upgrade + + context = module.this.context +} + +module "redshift_sg" { + count = local.enabled && var.custom_sg_enabled ? 1 : 0 + + source = "cloudposse/security-group/aws" + version = "2.0.0-rc1" + + create_before_destroy = true + preserve_security_group_id = true + + attributes = ["redshift"] + + # Allow unlimited egress + allow_all_egress = var.custom_sg_allow_all_egress + + rules = var.custom_sg_rules + + vpc_id = module.vpc.outputs.vpc_id + + context = module.this.context +} diff --git a/modules/redshift/outputs.tf b/modules/redshift/outputs.tf new file mode 100644 index 000000000..185282e9b --- /dev/null +++ b/modules/redshift/outputs.tf @@ -0,0 +1,44 @@ +output "id" { + description = "The Redshift Cluster ID" + value = local.enabled ? module.redshift_cluster.id : null +} + +output "arn" { + description = "Amazon Resource Name (ARN) of cluster" + value = local.enabled ? module.redshift_cluster.arn : null +} + +output "cluster_identifier" { + description = "The Cluster Identifier" + value = local.enabled ? module.redshift_cluster.cluster_identifier : null +} + +output "port" { + description = "The Port the cluster responds on" + value = local.enabled ? module.redshift_cluster.port : null +} + +output "dns_name" { + description = "The DNS name of the cluster" + value = local.enabled ? module.redshift_cluster.dns_name : null +} + +output "vpc_security_group_ids" { + description = "The VPC security group IDs associated with the cluster" + value = local.enabled ? module.redshift_cluster.vpc_security_group_ids : null +} + +output "cluster_security_groups" { + description = "The security groups associated with the cluster" + value = local.enabled ? module.redshift_cluster.cluster_security_groups : null +} + +output "endpoint" { + description = "The connection endpoint" + value = local.enabled ? module.redshift_cluster.endpoint : null +} + +output "database_name" { + description = "The name of the default database in the Cluster" + value = local.enabled ? module.redshift_cluster.database_name : null +} diff --git a/modules/redshift/providers.tf b/modules/redshift/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/redshift/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/redshift/remote-state.tf b/modules/redshift/remote-state.tf new file mode 100644 index 000000000..3e0ccd51e --- /dev/null +++ b/modules/redshift/remote-state.tf @@ -0,0 +1,8 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "vpc" + + context = module.this.context +} diff --git a/modules/redshift/systems-manager.tf b/modules/redshift/systems-manager.tf new file mode 100644 index 000000000..ce2b541a6 --- /dev/null +++ b/modules/redshift/systems-manager.tf @@ -0,0 +1,108 @@ +# AWS KMS alias used for encryption/decryption of SSM secure strings +variable "kms_alias_name_ssm" { + type = string + default = "alias/aws/ssm" + description = "KMS alias name for SSM" +} + +variable "ssm_enabled" { + type = bool + default = false + description = "If `true` create SSM keys for the database user and password." +} + +variable "ssm_key_format" { + type = string + default = "/%v/%v/%v" + description = "SSM path format. The values will will be used in the following order: `var.ssm_key_prefix`, `var.name`, `var.ssm_key_*`" +} + +variable "ssm_key_prefix" { + type = string + default = "redshift" + description = "SSM path prefix. Omit the leading forward slash `/`." +} + +variable "ssm_key_user" { + type = string + default = "admin/db_user" + description = "The SSM key to save the user. See `var.ssm_path_format`." +} + +variable "ssm_key_password" { + type = string + default = "admin/db_password" + description = "The SSM key to save the password. See `var.ssm_path_format`." +} + +variable "ssm_key_hostname" { + type = string + default = "admin/db_hostname" + description = "The SSM key to save the hostname. See `var.ssm_path_format`." +} + +variable "ssm_key_port" { + type = string + default = "admin/db_port" + description = "The SSM key to save the port. See `var.ssm_path_format`." +} + +locals { + ssm_enabled = local.enabled && var.ssm_enabled +} + +resource "aws_ssm_parameter" "redshift_database_name" { + count = local.ssm_enabled ? 1 : 0 + + name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_port) + value = local.database_name + description = "Redshift DB port" + type = "String" + overwrite = true +} + +resource "aws_ssm_parameter" "redshift_database_user" { + count = local.ssm_enabled ? 1 : 0 + + name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_user) + value = local.admin_user + description = "Redshift DB user" + type = "String" + overwrite = true +} + +resource "aws_ssm_parameter" "redshift_database_password" { + count = local.ssm_enabled ? 1 : 0 + + name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_password) + value = local.admin_password + description = "Redshift DB password" + type = "SecureString" + key_id = var.kms_alias_name_ssm + overwrite = true +} + +resource "aws_ssm_parameter" "redshift_database_hostname" { + count = local.ssm_enabled ? 1 : 0 + + name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_hostname) + value = module.redshift_cluster.endpoint + description = "Redshift DB hostname" + type = "String" + overwrite = true +} + +resource "aws_ssm_parameter" "redshift_database_port" { + count = local.ssm_enabled ? 1 : 0 + + name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_port) + value = var.port + description = "Redshift DB port" + type = "String" + overwrite = true +} + +output "redshift_database_ssm_key_prefix" { + value = local.ssm_enabled ? format(var.ssm_key_format, var.ssm_key_prefix, var.name, "") : null + description = "SSM prefix" +} diff --git a/modules/redshift/variables.tf b/modules/redshift/variables.tf new file mode 100644 index 000000000..949886068 --- /dev/null +++ b/modules/redshift/variables.tf @@ -0,0 +1,102 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "port" { + type = number + default = 5439 + description = "The port number on which the cluster accepts incoming connections" +} + +variable "admin_user" { + type = string + default = null + description = "Username for the master DB user. Required unless a snapshot_identifier is provided" +} + +variable "admin_password" { + type = string + default = null + description = "Password for the master DB user. Required unless a snapshot_identifier is provided" +} + +variable "database_name" { + type = string + default = null + description = "The name of the first database to be created when the cluster is created" +} + +variable "node_type" { + type = string + default = "dc2.large" + description = "The node type to be provisioned for the cluster. See https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#working-with-clusters-overview" +} + +variable "number_of_nodes" { + type = number + default = 1 + description = "The number of compute nodes in the cluster. This parameter is required when the ClusterType parameter is specified as multi-node" +} + +variable "cluster_type" { + type = string + default = "single-node" + description = "The cluster type to use. Either `single-node` or `multi-node`" +} + +variable "engine_version" { + type = string + default = "1.0" + description = "The version of the Amazon Redshift engine to use. See https://docs.aws.amazon.com/redshift/latest/mgmt/cluster-versions.html" +} + +variable "publicly_accessible" { + type = bool + default = false + description = "If true, the cluster can be accessed from a public network" +} + +variable "allow_version_upgrade" { + type = bool + default = false + description = "Whether or not to enable major version upgrades which are applied during the maintenance window to the Amazon Redshift engine that is running on the cluster" +} + +variable "use_private_subnets" { + type = bool + default = true + description = "Whether to use private or public subnets for the Redshift cluster" +} + +variable "security_group_ids" { + type = list(string) + default = null + description = "An array of security group IDs to associate with the endpoint." +} + +variable "custom_sg_enabled" { + type = bool + default = false + description = "Whether to use custom security group or not" +} + +variable "custom_sg_allow_all_egress" { + type = bool + default = true + description = "Whether to allow all egress traffic or not" +} + +variable "custom_sg_rules" { + type = list(object({ + key = string + type = string + from_port = number + to_port = number + protocol = string + cidr_blocks = list(string) + description = string + })) + default = [] + description = "An array of custom security groups to create and assign to the cluster." +} diff --git a/modules/redshift/versions.tf b/modules/redshift/versions.tf new file mode 100644 index 000000000..0fe97e02d --- /dev/null +++ b/modules/redshift/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.17" + } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } + } +} From 15cdbd4897936a1dadea3ec0a6b8aa33d29cb8f8 Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Wed, 22 Feb 2023 19:13:34 -0600 Subject: [PATCH 043/501] SSO upgrades and Support for Assume Role from Identity Users (#567) Co-authored-by: cloudpossebot Co-authored-by: Benjamin Smith --- .../modules/roles-to-principals/main.tf | 13 ++++++--- modules/aws-sso/README.md | 23 +++++++++------- modules/aws-sso/main.tf | 6 ++--- modules/aws-sso/outputs.tf | 9 +++++++ modules/aws-sso/policy-AdminstratorAccess.tf | 15 ++++++----- .../policy-BillingAdministratorAccess.tf | 1 + .../aws-sso/policy-BillingReadOnlyAccess.tf | 1 + .../aws-sso/policy-DNSAdministratorAccess.tf | 15 ++++++----- ....tf => policy-Identity-role-TeamAccess.tf} | 27 ++++++++++--------- modules/aws-sso/policy-PoweruserAccess.tf | 1 + modules/aws-sso/policy-ReadOnlyAccess.tf | 1 + .../aws-sso/policy-TerraformUpdateAccess.tf | 15 ++++++----- modules/aws-sso/remote-state.tf | 4 +-- modules/aws-sso/variables.tf | 9 ++++--- 14 files changed, 84 insertions(+), 56 deletions(-) create mode 100644 modules/aws-sso/outputs.tf rename modules/aws-sso/{policy-Identity-role-RoleAccess.tf => policy-Identity-role-TeamAccess.tf} (57%) diff --git a/modules/account-map/modules/roles-to-principals/main.tf b/modules/account-map/modules/roles-to-principals/main.tf index 31db906db..4a22086a3 100644 --- a/modules/account-map/modules/roles-to-principals/main.tf +++ b/modules/account-map/modules/roles-to-principals/main.tf @@ -32,9 +32,16 @@ locals { )]))) # Support for AWS SSO Permission Sets - permission_set_arn_like = distinct(compact(flatten([for acct, v in var.permission_set_map : formatlist( - # arn:aws:iam::550826706431:role/aws-reserved/sso.amazonaws.com/ap-southeast-1/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc + permission_set_arn_like = distinct(compact(flatten([for acct, v in var.permission_set_map : concat(formatlist( + # arn:aws:iam::12345:role/aws-reserved/sso.amazonaws.com/ap-southeast-1/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc # AWS SSO Sometimes includes `/region/`, but not always. format("arn:%s:iam::%s:role/aws-reserved/sso.amazonaws.com*/AWSReservedSSO_%%s_*", local.aws_partition, module.account_map.outputs.full_account_map[acct]), - v)]))) + v), + formatlist( + # Support assume role from the allowed identity account SSO users + # arn:aws:iam::12345:role/aws-reserved/sso.amazonaws.com/ap-southeast-1/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc + # AWS SSO Sometimes includes `/region/`, but not always. + format("arn:%s:iam::%s:role/aws-reserved/sso.amazonaws.com*/AWSReservedSSO_%%s_*", local.aws_partition, module.account_map.outputs.full_account_map[module.account_map.outputs.identity_account_account_name]), + v), + )]))) } diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index b21fa7ab4..2d212d487 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -47,7 +47,7 @@ The `account_assignments` setting configures access to permission sets for users The `identity_roles_accessible` element provides a list of role names corresponding to roles created in the `iam-primary-roles` component. For each names role, a corresponding permission set will be created which allows the user to assume that role. The permission set name is generated in Terraform from the role name using this statement: ``` -format("Identity%sRoleAccess", title(role)) +format("Identity%sTeamAccess", title(role)) ``` #### Example @@ -92,7 +92,7 @@ components: permission_sets: - AdministratorAccess - ReadOnlyAccess - identity_roles_accessible: + aws_teams_accessible: - "admin" - "ops" - "poweruser" @@ -123,10 +123,10 @@ components: |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 0.6.2 | +| [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 0.7.1 | | [role\_prefix](#module\_role\_prefix) | cloudposse/label/null | 0.25.0 | -| [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 0.6.2 | -| [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 0.6.2 | +| [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 0.7.1 | +| [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 0.7.1 | | [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -135,7 +135,7 @@ components: | Name | Type | |------|------| | [aws_iam_policy_document.TerraformUpdateAccess](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.assume_identity_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.assume_aws_team](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.dns_administrator_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | @@ -146,15 +146,16 @@ components: | [account\_assignments](#input\_account\_assignments) | Enables access to permission sets for users and groups in accounts, in the following structure:
yaml
:
groups:
:
permission_sets:
-
users:
:
permission_sets:
-
|
map(map(map(object({
permission_sets = list(string)
}
))))
| `{}` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [aws\_teams\_accessible](#input\_aws\_teams\_accessible) | List of IAM roles (e.g. ["admin", "terraform"]) for which to create permission
sets that allow the user to assume that role. Named like
admin -> IdentityAdminTeamAccess | `set(string)` | `[]` | no | +| [aws\_teams\_stage\_name](#input\_aws\_teams\_stage\_name) | The name of the stage where the IAM primary roles are provisioned | `string` | `"identity"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [global\_environment\_name](#input\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no | -| [iam\_primary\_roles\_stage\_name](#input\_iam\_primary\_roles\_stage\_name) | The name of the stage where the IAM primary roles are provisioned | `string` | `"identity"` | no | +| [global\_stage\_name](#input\_global\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [identity\_roles\_accessible](#input\_identity\_roles\_accessible) | List of IAM roles (e.g. ["admin", "terraform"]) for which to create permission
sets that allow the user to assume that role. Named like
admin -> IdentityAdminRoleAccess | `set(string)` | `[]` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | @@ -165,7 +166,6 @@ components: | [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [root\_account\_stage\_name](#input\_root\_account\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | @@ -173,7 +173,10 @@ components: ## Outputs -No outputs. +| Name | Description | +|------|-------------| +| [permission\_sets](#output\_permission\_sets) | Permission sets | +| [sso\_account\_assignments](#output\_sso\_account\_assignments) | SSO account assignments | ## References diff --git a/modules/aws-sso/main.tf b/modules/aws-sso/main.tf index 4765f5b25..cb6377bde 100644 --- a/modules/aws-sso/main.tf +++ b/modules/aws-sso/main.tf @@ -1,6 +1,6 @@ module "permission_sets" { source = "cloudposse/sso/aws//modules/permission-sets" - version = "0.6.2" + version = "0.7.1" permission_sets = concat( local.administrator_access_permission_set, @@ -18,7 +18,7 @@ module "permission_sets" { module "sso_account_assignments" { source = "cloudposse/sso/aws//modules/account-assignments" - version = "0.6.2" + version = "0.7.1" account_assignments = local.account_assignments context = module.this.context @@ -26,7 +26,7 @@ module "sso_account_assignments" { module "sso_account_assignments_root" { source = "cloudposse/sso/aws//modules/account-assignments" - version = "0.6.2" + version = "0.7.1" providers = { aws = aws.root diff --git a/modules/aws-sso/outputs.tf b/modules/aws-sso/outputs.tf new file mode 100644 index 000000000..dbb76ef5b --- /dev/null +++ b/modules/aws-sso/outputs.tf @@ -0,0 +1,9 @@ +output "permission_sets" { + value = module.permission_sets.permission_sets + description = "Permission sets" +} + +output "sso_account_assignments" { + value = module.sso_account_assignments.assignments + description = "SSO account assignments" +} diff --git a/modules/aws-sso/policy-AdminstratorAccess.tf b/modules/aws-sso/policy-AdminstratorAccess.tf index c88ad4890..afd5463ae 100644 --- a/modules/aws-sso/policy-AdminstratorAccess.tf +++ b/modules/aws-sso/policy-AdminstratorAccess.tf @@ -1,11 +1,12 @@ locals { administrator_access_permission_set = [{ - name = "AdministratorAccess", - description = "Allow Full Admininstrator access to the account", - relay_state = "", - session_duration = "", - tags = {}, - inline_policy = "" - policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/AdministratorAccess"] + name = "AdministratorAccess", + description = "Allow Full Administrator access to the account", + relay_state = "", + session_duration = "", + tags = {}, + inline_policy = "" + policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/AdministratorAccess"] + customer_managed_policy_attachments = [] }] } diff --git a/modules/aws-sso/policy-BillingAdministratorAccess.tf b/modules/aws-sso/policy-BillingAdministratorAccess.tf index 417d72f79..0e854f192 100644 --- a/modules/aws-sso/policy-BillingAdministratorAccess.tf +++ b/modules/aws-sso/policy-BillingAdministratorAccess.tf @@ -10,5 +10,6 @@ locals { "arn:${local.aws_partition}:iam::aws:policy/job-function/Billing", "arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess", ] + customer_managed_policy_attachments = [] }] } diff --git a/modules/aws-sso/policy-BillingReadOnlyAccess.tf b/modules/aws-sso/policy-BillingReadOnlyAccess.tf index e9dced8aa..732e90aa6 100644 --- a/modules/aws-sso/policy-BillingReadOnlyAccess.tf +++ b/modules/aws-sso/policy-BillingReadOnlyAccess.tf @@ -10,5 +10,6 @@ locals { "arn:${local.aws_partition}:iam::aws:policy/AWSBillingReadOnlyAccess", "arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess", ] + customer_managed_policy_attachments = [] }] } diff --git a/modules/aws-sso/policy-DNSAdministratorAccess.tf b/modules/aws-sso/policy-DNSAdministratorAccess.tf index c3d810f48..98f04f057 100644 --- a/modules/aws-sso/policy-DNSAdministratorAccess.tf +++ b/modules/aws-sso/policy-DNSAdministratorAccess.tf @@ -27,12 +27,13 @@ data "aws_iam_policy_document" "dns_administrator_access" { locals { dns_administrator_access_permission_set = [{ - name = "DNSRecordAdministratorAccess", - description = "Allow DNS Record Admininstrator access to the account, but not zone administration", - relay_state = "https://console.aws.amazon.com/route53/", - session_duration = "", - tags = {}, - inline_policy = data.aws_iam_policy_document.dns_administrator_access.json, - policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess"] + name = "DNSRecordAdministratorAccess", + description = "Allow DNS Record Admininstrator access to the account, but not zone administration", + relay_state = "https://console.aws.amazon.com/route53/", + session_duration = "", + tags = {}, + inline_policy = data.aws_iam_policy_document.dns_administrator_access.json, + policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess"] + customer_managed_policy_attachments = [] }] } diff --git a/modules/aws-sso/policy-Identity-role-RoleAccess.tf b/modules/aws-sso/policy-Identity-role-TeamAccess.tf similarity index 57% rename from modules/aws-sso/policy-Identity-role-RoleAccess.tf rename to modules/aws-sso/policy-Identity-role-TeamAccess.tf index 1a2174a36..6028088bf 100644 --- a/modules/aws-sso/policy-Identity-role-RoleAccess.tf +++ b/modules/aws-sso/policy-Identity-role-TeamAccess.tf @@ -1,23 +1,23 @@ # This file generates a permission set for each role specified in var.target_identity_roles -# which is named "IdentityRoleAccess" and grants access to only that role, +# which is named "IdentityTeamAccess" and grants access to only that role, # plus ViewOnly access because it is difficult to navigate without any access at all. locals { - identity_account = module.account_map.outputs.full_account_map[var.iam_primary_roles_stage_name] + identity_account = module.account_map.outputs.full_account_map[module.account_map.outputs.identity_account_account_name] } module "role_prefix" { source = "cloudposse/label/null" version = "0.25.0" - stage = var.iam_primary_roles_stage_name + stage = var.aws_teams_stage_name context = module.this.context } -data "aws_iam_policy_document" "assume_identity_role" { - for_each = local.enabled ? var.identity_roles_accessible : [] +data "aws_iam_policy_document" "assume_aws_team" { + for_each = local.enabled ? var.aws_teams_accessible : [] statement { sid = "RoleAssumeRole" @@ -53,13 +53,14 @@ data "aws_iam_policy_document" "assume_identity_role" { } locals { - identity_access_permission_sets = [for role in var.identity_roles_accessible : { - name = format("Identity%sRoleAccess", title(role)), - description = "Allow user to assume %s role in Identity account, which allows access to other accounts", - relay_state = "", - session_duration = "", - tags = {}, - inline_policy = data.aws_iam_policy_document.assume_identity_role[role].json - policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/job-function/ViewOnlyAccess"] + identity_access_permission_sets = [for role in var.aws_teams_accessible : { + name = format("Identity%sTeamAccess", title(role)), + description = format("Allow user to assume the %s Team role in the Identity account, which allows access to other accounts", title(role)) + relay_state = "", + session_duration = "", + tags = {}, + inline_policy = data.aws_iam_policy_document.assume_aws_team[role].json + policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/job-function/ViewOnlyAccess"] + customer_managed_policy_attachments = [] }] } diff --git a/modules/aws-sso/policy-PoweruserAccess.tf b/modules/aws-sso/policy-PoweruserAccess.tf index 58374a961..8b21d0cf9 100644 --- a/modules/aws-sso/policy-PoweruserAccess.tf +++ b/modules/aws-sso/policy-PoweruserAccess.tf @@ -10,5 +10,6 @@ locals { "arn:${local.aws_partition}:iam::aws:policy/PowerUserAccess", "arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess", ] + customer_managed_policy_attachments = [] }] } diff --git a/modules/aws-sso/policy-ReadOnlyAccess.tf b/modules/aws-sso/policy-ReadOnlyAccess.tf index cc03f8499..e9ce242a4 100644 --- a/modules/aws-sso/policy-ReadOnlyAccess.tf +++ b/modules/aws-sso/policy-ReadOnlyAccess.tf @@ -10,5 +10,6 @@ locals { "arn:${local.aws_partition}:iam::aws:policy/ReadOnlyAccess", "arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess", ] + customer_managed_policy_attachments = [] }] } diff --git a/modules/aws-sso/policy-TerraformUpdateAccess.tf b/modules/aws-sso/policy-TerraformUpdateAccess.tf index cd0fb915b..3cbb73123 100644 --- a/modules/aws-sso/policy-TerraformUpdateAccess.tf +++ b/modules/aws-sso/policy-TerraformUpdateAccess.tf @@ -19,12 +19,13 @@ data "aws_iam_policy_document" "TerraformUpdateAccess" { locals { terraform_update_access_permission_set = [{ - name = "TerraformUpdateAccess", - description = "Allow access to Terraform state sufficient to make changes", - relay_state = "", - session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles - tags = {}, - inline_policy = data.aws_iam_policy_document.TerraformUpdateAccess.json, - policy_attachments = [] + name = "TerraformUpdateAccess", + description = "Allow access to Terraform state sufficient to make changes", + relay_state = "", + session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles + tags = {}, + inline_policy = data.aws_iam_policy_document.TerraformUpdateAccess.json, + policy_attachments = [] + customer_managed_policy_attachments = [] }] } diff --git a/modules/aws-sso/remote-state.tf b/modules/aws-sso/remote-state.tf index b7e892a43..511171b50 100644 --- a/modules/aws-sso/remote-state.tf +++ b/modules/aws-sso/remote-state.tf @@ -4,7 +4,7 @@ module "account_map" { component = "account-map" environment = var.global_environment_name - stage = var.root_account_stage_name + stage = var.global_stage_name privileged = var.privileged context = module.this.context @@ -16,7 +16,7 @@ module "tfstate" { component = "tfstate-backend" environment = var.tfstate_environment_name - stage = var.root_account_stage_name + stage = var.global_stage_name privileged = var.privileged context = module.this.context diff --git a/modules/aws-sso/variables.tf b/modules/aws-sso/variables.tf index 1a4006d55..cc82a245e 100644 --- a/modules/aws-sso/variables.tf +++ b/modules/aws-sso/variables.tf @@ -13,7 +13,8 @@ variable "global_environment_name" { description = "Global environment name" default = "gbl" } -variable "root_account_stage_name" { + +variable "global_stage_name" { type = string description = "The name of the stage where `account_map` is provisioned" default = "root" @@ -49,18 +50,18 @@ variable "account_assignments" { default = {} } -variable "iam_primary_roles_stage_name" { +variable "aws_teams_stage_name" { type = string description = "The name of the stage where the IAM primary roles are provisioned" default = "identity" } -variable "identity_roles_accessible" { +variable "aws_teams_accessible" { type = set(string) description = <<-EOT List of IAM roles (e.g. ["admin", "terraform"]) for which to create permission sets that allow the user to assume that role. Named like - admin -> IdentityAdminRoleAccess + admin -> IdentityAdminTeamAccess EOT default = [] } From 790a2981e4d5a3cf8415662ab03a5a952a5e0f1d Mon Sep 17 00:00:00 2001 From: nitrocode <7775707+nitrocode@users.noreply.github.com> Date: Wed, 22 Feb 2023 22:33:08 -0600 Subject: [PATCH 044/501] Add spacelift-policy component (#556) Co-authored-by: Andriy Knysh --- modules/spacelift-policy/README.md | 155 ++++++++++ modules/spacelift-policy/context.tf | 279 ++++++++++++++++++ modules/spacelift-policy/main.tf | 113 +++++++ modules/spacelift-policy/outputs.tf | 4 + .../example.trigger.administrative.rego | 21 ++ .../policies/plan.autodeployupdates.rego | 21 ++ modules/spacelift-policy/providers.tf | 5 + modules/spacelift-policy/variables.tf | 30 ++ modules/spacelift-policy/versions.tf | 14 + 9 files changed, 642 insertions(+) create mode 100644 modules/spacelift-policy/README.md create mode 100644 modules/spacelift-policy/context.tf create mode 100644 modules/spacelift-policy/main.tf create mode 100644 modules/spacelift-policy/outputs.tf create mode 100644 modules/spacelift-policy/policies/example.trigger.administrative.rego create mode 100644 modules/spacelift-policy/policies/plan.autodeployupdates.rego create mode 100644 modules/spacelift-policy/providers.tf create mode 100644 modules/spacelift-policy/variables.tf create mode 100644 modules/spacelift-policy/versions.tf diff --git a/modules/spacelift-policy/README.md b/modules/spacelift-policy/README.md new file mode 100644 index 000000000..2a94f1114 --- /dev/null +++ b/modules/spacelift-policy/README.md @@ -0,0 +1,155 @@ +# Component: `spacelift-policy` + +This component is responsible for provisioning Spacelift policies. + +## Usage + +**Stack Level**: Global + +NOTE: The input `labels` will be applied to every policy. To overwrite (not append) the `labels` key can be used per policy as well. + +```yaml +components: + terraform: + spacelift-policy/defaults: + metadata: + type: abstract + component: spacelift-policy + settings: + spacelift: + workspace_enabled: true + administrative: true + autodeploy: true + vars: + enabled: true + space_id: root + spacelift_api_endpoint: https://TODO.app.spacelift.io + + # policies to attach to all admin stacks + spacelift-policy/admin: + metadata: + component: spacelift-policy + inherits: + - spacelift-policy/defaults + settings: + spacelift: + labels: + - folder:admin + vars: + labels: + - 'autoattach:folder:admin' + policy_version: 0.52.0 + policies: + global-admin-git-push-policy: + name: Global Administrator Git Push Policy + type: GIT_PUSH + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/git_push.administrative.rego + global-admin-trigger-policy: + name: Global Administrator Trigger Policy + type: TRIGGER + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/trigger.administrative.rego + + # example: from local path + # test-policy-from-path: + # name: Path policy + # type: TRIGGER + # body_path: policies/example.trigger.administrative.rego + + # policies to attach to all non-admin stacks + spacelift-policy/non-admin: + metadata: + component: spacelift-policy + inherits: + - spacelift-policy/defaults + vars: + labels: + - 'autoattach:folder:non-admin' + policy_version: 0.52.0 + policies: + git-push-proposed-run-policy: + name: GIT_PUSH Proposed Run Policy + type: GIT_PUSH + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/git_push.proposed-run.rego + git-push-tracked-run-policy: + name: GIT_PUSH Tracked Run Policy + type: GIT_PUSH + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/git_push.tracked-run.rego + plan-default-policy: + name: PLAN Default Policy + type: PLAN + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/plan.default.rego + trigger-dependencies-policy: + name: TRIGGER Dependencies Policy + type: TRIGGER + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/trigger.dependencies.rego +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [http](#requirement\_http) | >= 3.0 | +| [spacelift](#requirement\_spacelift) | >= 0.1.31 | + +## Providers + +| Name | Version | +|------|---------| +| [http](#provider\_http) | >= 3.0 | +| [spacelift](#provider\_spacelift) | >= 0.1.31 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [spacelift_policy.default](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/policy) | resource | +| [http_http.default](https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels](#input\_labels) | List of global labels to add to each policy. These values can be overridden in `var.policies`'s per policy `labels` key. | `list(string)` | `[]` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [policies](#input\_policies) | The map of required policies to add. | `any` | n/a | yes | +| [policy\_version](#input\_policy\_version) | The optional global policy version injected using a %s in each `body_url`. This can be pinned to a version tag or a branch. | `string` | `"master"` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [space\_id](#input\_space\_id) | The global `space_id` to assign to each policy. This value can be overridden in `var.policies`'s per policy `space_id` key. | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [policies](#output\_policies) | All calculated policies | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spacelift-policy) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/spacelift-policy/context.tf b/modules/spacelift-policy/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/spacelift-policy/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spacelift-policy/main.tf b/modules/spacelift-policy/main.tf new file mode 100644 index 000000000..b665cb5b8 --- /dev/null +++ b/modules/spacelift-policy/main.tf @@ -0,0 +1,113 @@ +locals { + enabled = module.this.enabled + + # Get all policies without a URL + # { k = { "body": "https://", etc } } + policies_with_body = { + for k, v in var.policies : + # merge them with existing data structure + k => merge(v, { + # append body_append to each body if one exists + "body" = format( + join("\n", [ + "# NOTE: source of policy is in the stack YAML", + "", + "%s", + ]), + lookup(v, "body", file("${path.module}/${lookup(v, "body_path")}")), + ) + }) + if lookup(v, "body", null) != null || lookup(v, "body_path", null) != null + } + + # Get all policies with a URL + # { k = "https://" } + policies_with_body_url = { + for k, v in var.policies : + k => try( + format(v["body_url"], var.policy_version), + v["body_url"], + ) + if lookup(v, "body_url", null) != null + } + + # After downloading the bodies from the URLs + # { k = { "body" = "...", etc } } + policies_with_body_url_downloaded = { + for k, v in local.policies_with_body_url : + # merge them with existing data structure + k => merge(var.policies[k], { + # append body_append to each body if one exists + "body" = format( + join("\n", [ + "# NOTE: source url of policy: %s", + "", + "%s", + ]), + v, + data.http.default[k]["body"], + ) + }) if local.enabled + } + + # TODO: get local policies + + # Merge all the policies together and create policies from this object + all_policies = merge( + local.policies_with_body, + local.policies_with_body_url_downloaded, + ) + + # keep the object keys consistent to avoid terraform errors + policies = { + for k, v in local.all_policies : + k => merge( + # remove optional keys + { + for vk, vv in v : + vk => vv + if !contains([ + "name", + "body_append", + "body_url", + "body_path", + "labels", + "space_id", + ], vk) + }, + # these were previously set in the spacelift_policy resource and moved here + # to avoid terraform errors around inconsistent object keys + { + name = lookup(v, "name", title(join(" ", split("-", k)))) + labels = lookup(v, "labels", var.labels) + space_id = lookup(v, "space_id", var.space_id) + body = lookup(v, "body_append", "") == "" ? v["body"] : format( + join("\n", [ + "%s", + "", + "# NOTE: below is appended to the original policy", + "", + "%s", + ]), + v["body"], + lookup(v, "body_append", "") + ) + }, + ) + } +} + +data "http" "default" { + for_each = local.enabled ? local.policies_with_body_url : {} + url = each.value +} + +resource "spacelift_policy" "default" { + for_each = local.enabled ? local.policies : {} + + name = lookup(each.value, "name") + body = lookup(each.value, "body") + type = upper(lookup(each.value, "type")) + labels = lookup(each.value, "labels") + space_id = lookup(each.value, "space_id") +} diff --git a/modules/spacelift-policy/outputs.tf b/modules/spacelift-policy/outputs.tf new file mode 100644 index 000000000..62b96771a --- /dev/null +++ b/modules/spacelift-policy/outputs.tf @@ -0,0 +1,4 @@ +output "policies" { + description = "All calculated policies" + value = local.policies +} diff --git a/modules/spacelift-policy/policies/example.trigger.administrative.rego b/modules/spacelift-policy/policies/example.trigger.administrative.rego new file mode 100644 index 000000000..2a9760596 --- /dev/null +++ b/modules/spacelift-policy/policies/example.trigger.administrative.rego @@ -0,0 +1,21 @@ +# Local example policy taken from https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/trigger.administrative.rego + +# https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsany_prefix_match + +package spacelift + +# Trigger the stack after it gets created in the `administrative` stack +trigger[stack.id] { + stack := input.stacks[_] + # compare a plaintext string (stack.id) to a checksum + strings.any_suffix_match(crypto.sha256(stack.id), id_shas_of_created_stacks) +} + +id_shas_of_created_stacks[change.entity.data.values.id] { + change := input.run.changes[_] + change.action == "added" + change.entity.type == "spacelift_stack" + change.phase == "apply" # The change has actually been applied, not just planned +} + +sample { true } diff --git a/modules/spacelift-policy/policies/plan.autodeployupdates.rego b/modules/spacelift-policy/policies/plan.autodeployupdates.rego new file mode 100644 index 000000000..43a78ceba --- /dev/null +++ b/modules/spacelift-policy/policies/plan.autodeployupdates.rego @@ -0,0 +1,21 @@ +package spacelift + +# This policy allows autodeploy if there are only new resources or updates. +# It requires manual intervention (approval) if any of the resources will be deleted. + +# Usage: +# settings: +# spacelift: +# autodeploy: true +# policies_by_name_enabled: +# - plan.autodeployupdates + +warn[sprintf(message, [action, resource.address])] { + message := "action '%s' requires human review (%s)" + review := {"delete"} + + resource := input.terraform.resource_changes[_] + action := resource.change.actions[_] + + review[action] +} diff --git a/modules/spacelift-policy/providers.tf b/modules/spacelift-policy/providers.tf new file mode 100644 index 000000000..341f51d33 --- /dev/null +++ b/modules/spacelift-policy/providers.tf @@ -0,0 +1,5 @@ +# Purposely did not add spacelift inputs since this is a sensitive change and +# it should not be an easy thing to plan this locally. Best to use the exported +# inputs when this is necessary to plan locally. + +provider "spacelift" {} diff --git a/modules/spacelift-policy/variables.tf b/modules/spacelift-policy/variables.tf new file mode 100644 index 000000000..1fe8defee --- /dev/null +++ b/modules/spacelift-policy/variables.tf @@ -0,0 +1,30 @@ +# This input is unused however, this is added by default to every component by atmos +# and this is defined to avoid any `var.region` warnings. +# tflint-ignore: terraform_unused_declarations +variable "region" { + type = string + description = "AWS Region" +} + +variable "policy_version" { + type = string + description = "The optional global policy version injected using a %s in each `body_url`. This can be pinned to a version tag or a branch." + default = "master" +} + +variable "policies" { + type = any + description = "The map of required policies to add." +} + +variable "labels" { + type = list(string) + description = "List of global labels to add to each policy. These values can be overridden in `var.policies`'s per policy `labels` key." + default = [] +} + +variable "space_id" { + type = string + description = "The global `space_id` to assign to each policy. This value can be overridden in `var.policies`'s per policy `space_id` key." + default = "root" +} diff --git a/modules/spacelift-policy/versions.tf b/modules/spacelift-policy/versions.tf new file mode 100644 index 000000000..136ee3428 --- /dev/null +++ b/modules/spacelift-policy/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + spacelift = { + source = "spacelift-io/spacelift" + version = ">= 0.1.31" + } + http = { + source = "hashicorp/http" + version = ">= 3.0" + } + } +} From 7d4b35effe0dbea953674eb916069ac1d875adeb Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Fri, 24 Feb 2023 20:55:25 +0300 Subject: [PATCH 045/501] Fix ArgoCD minor issues (#571) Co-authored-by: cloudpossebot --- modules/argocd-repo/templates/applicationset.yaml.tpl | 6 +++--- modules/eks/argocd/README.md | 11 ++++------- modules/eks/argocd/main.tf | 8 +++++++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index 11f695dea..550bfe29f 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -8,23 +8,24 @@ metadata: argocd-autopilot.argoproj-labs.io/default-dest-server: https://kubernetes.default.svc argocd.argoproj.io/sync-options: PruneLast=true argocd.argoproj.io/sync-wave: "-2" +%{if slack_channel != "" && slack_channel != null ~} notifications.argoproj.io/subscribe.on-deployed.slack: ${slack_channel} notifications.argoproj.io/subscribe.on-health-degraded.slack: ${slack_channel} notifications.argoproj.io/subscribe.on-sync-failed.slack: ${slack_channel} notifications.argoproj.io/subscribe.on-sync-running.slack: ${slack_channel} notifications.argoproj.io/subscribe.on-sync-status-unknown.slack: ${slack_channel} notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ${slack_channel} + notifications.argoproj.io/subscribe.on-deleted.slack: ${slack_channel} +%{ endif ~} notifications.argoproj.io/subscribe.on-deployed.datadog: "" notifications.argoproj.io/subscribe.on-health-degraded.datadog: "" notifications.argoproj.io/subscribe.on-sync-failed.datadog: "" notifications.argoproj.io/subscribe.on-sync-running.datadog: "" notifications.argoproj.io/subscribe.on-sync-status-unknown.datadog: "" notifications.argoproj.io/subscribe.on-sync-succeeded.datadog: "" - notifications.argoproj.io/subscribe.on-deleted.slack: ${slack_channel} notifications.argoproj.io/subscribe.on-deployed.github-deployment: "" notifications.argoproj.io/subscribe.on-deployed.github-commit-status: "" notifications.argoproj.io/subscribe.on-deleted.github-deployment: "" - creationTimestamp: null name: ${name} namespace: ${namespace} spec: @@ -48,7 +49,6 @@ kind: ApplicationSet metadata: annotations: argocd.argoproj.io/sync-wave: "0" - creationTimestamp: null name: ${name} namespace: ${namespace} spec: diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 56230951e..7753b7ac8 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -4,13 +4,8 @@ This component is responsible for provisioning [Argo CD](https://argoproj.github Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. -> :warning::warning::warning: ArgoCD CRDs must be installed separately from this component/helm release. :warning::warning::warning: -```shell -kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=" - -# Eg. version v2.4.9 -kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=v2.4.9" -``` +> :warning::warning::warning: Initial install needs run `deploy` two times because first run will create ArgoCD CRDs +> and second run will finish ArgoCD configuration. :warning::warning::warning: ## Usage @@ -61,6 +56,7 @@ components: |------|---------| | [aws](#provider\_aws) | >= 4.0 | | [aws.config\_secrets](#provider\_aws.config\_secrets) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.9.0 | ## Modules @@ -87,6 +83,7 @@ components: | [aws_ssm_parameter.oidc_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.oidc_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameters_by_path.argocd_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | +| [kubernetes_resources.example](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/resources) | data source | ## Inputs diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index e35cc86c1..98a576705 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -225,6 +225,12 @@ module "argocd" { context = module.this.context } +data "kubernetes_resources" "example" { + api_version = "apiextensions.k8s.io/v1" + kind = "CustomResourceDefinition" + field_selector = "metadata.name==applications.argoproj.io" +} + module "argocd_apps" { source = "cloudposse/helm-release/aws" version = "0.3.0" @@ -240,7 +246,7 @@ module "argocd_apps" { atomic = var.atomic cleanup_on_fail = var.cleanup_on_fail timeout = var.timeout - enabled = local.enabled && var.argocd_apps_enabled + enabled = local.enabled && var.argocd_apps_enabled && length(data.kubernetes_resources.example.objects) > 0 values = compact([ templatefile( "${path.module}/resources/argocd-apps-values.yaml.tpl", From 62bbaa518b5b60676d909e78f82b186899edfb60 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Fri, 24 Feb 2023 23:59:12 +0300 Subject: [PATCH 046/501] [account-map] Update remote config module version (#572) Co-authored-by: cloudpossebot --- modules/account-map/README.md | 13 +++++++------ modules/account-map/modules/iam-roles/main.tf | 2 +- modules/account-map/remote-state.tf | 2 +- modules/account-map/versions.tf | 4 ++++ 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index c939de919..f810124fd 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -18,7 +18,7 @@ components: enabled: true # Set profiles_enabled to false unless we are using AWS config profiles for Terraform access. # When profiles_enabled is false, role_arn must be provided instead of profile in each terraform component provider. - # This is automatically handled by the component's `provider.tf` file in conjunction with + # This is automatically handled by the component's `provider.tf` file in conjunction with # the `account-map/modules/iam-roles` module. profiles_enabled: false root_account_aws_name: "aws-root" @@ -26,16 +26,16 @@ components: identity_account_account_name: identity dns_account_account_name: dns audit_account_account_name: audit - + # The following variables contain `format()` strings that take the labels from `null-label` # as arguments in the standard order. The default values are shown here, assuming - # the `null-label.label_order` is + # the `null-label.label_order` is # ["namespace", "tenant", "environment", "stage", "name", "attributes"] # Note that you can rearrange the order of the labels in the template by # using [explicit argument indexes](https://pkg.go.dev/fmt#hdr-Explicit_argument_indexes) just like in `go`. # `iam_role_arn_template_template` is the template for the template [sic] used to render Role ARNs. - # The template is first used to render a template for the account that takes only the role name. + # The template is first used to render a template for the account that takes only the role name. # Then that rendered template is used to create the final Role ARN for the account. iam_role_arn_template_template: "arn:%s:iam::%s:role/%s-%s-%s-%s-%%s" # `profile_template` is the template used to render AWS Profile names. @@ -50,19 +50,20 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | +| [local](#requirement\_local) | >= 1.3 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [local](#provider\_local) | n/a | +| [local](#provider\_local) | >= 1.3 | ## Modules | Name | Source | Version | |------|--------|---------| -| [accounts](#module\_accounts) | cloudposse/stack-config/yaml//modules/remote-state | 1.0.0 | +| [accounts](#module\_accounts) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/account-map/modules/iam-roles/main.tf b/modules/account-map/modules/iam-roles/main.tf index 57ffce386..bab29e15c 100644 --- a/modules/account-map/modules/iam-roles/main.tf +++ b/modules/account-map/modules/iam-roles/main.tf @@ -10,7 +10,7 @@ module "always" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "account-map" privileged = var.privileged diff --git a/modules/account-map/remote-state.tf b/modules/account-map/remote-state.tf index a70b8025a..1112153a5 100644 --- a/modules/account-map/remote-state.tf +++ b/modules/account-map/remote-state.tf @@ -1,6 +1,6 @@ module "accounts" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.0.0" + version = "1.4.1" component = "account" privileged = true diff --git a/modules/account-map/versions.tf b/modules/account-map/versions.tf index cc73ffd35..2fdade250 100644 --- a/modules/account-map/versions.tf +++ b/modules/account-map/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = ">= 4.9.0" } + local = { + source = "hashicorp/local" + version = ">= 1.3" + } } } From b2f03bf4bf9a27c75690739ebe6088628d18a8fc Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Mon, 27 Feb 2023 07:56:20 -0600 Subject: [PATCH 047/501] github-runners add support for runner groups (#569) Co-authored-by: cloudpossebot --- modules/github-runners/README.md | 11 ++++++----- modules/github-runners/main.tf | 1 + modules/github-runners/templates/user-data.sh | 2 +- modules/github-runners/variables.tf | 6 ++++++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index e2f6dc170..8dbbc7599 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -13,16 +13,16 @@ components: terraform: github-runners: vars: + cpu_utilization_high_threshold_percent: 5 + cpu_utilization_low_threshold_percent: 1 + default_cooldown: 300 github_scope: company instance_type: "t3.small" - min_size: 1 max_size: 10 - default_cooldown: 300 + min_size: 1 + runner_group: default scale_down_cooldown_seconds: 2700 wait_for_capacity_timeout: 10m - cpu_utilization_high_threshold_percent: 5 - cpu_utilization_low_threshold_percent: 1 - spot_maxprice: 0.02 mixed_instances_policy: instances_distribution: on_demand_allocation_strategy: "prioritized" @@ -151,6 +151,7 @@ chamber write github/runners/ registration-token ghp_secretstring | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [runner\_group](#input\_runner\_group) | GitHub runner group | `string` | `"default"` | no | | [runner\_labels](#input\_runner\_labels) | List of labels to add to the GitHub Runner (e.g. 'Amazon Linux 2'). | `list(string)` | `[]` | no | | [runner\_role\_additional\_policy\_arns](#input\_runner\_role\_additional\_policy\_arns) | List of policy ARNs that will be attached to the runners' default role on creation in addition to the defaults | `list(string)` | `[]` | no | | [runner\_version](#input\_runner\_version) | GitHub runner release version | `string` | `"2.288.1"` | no | diff --git a/modules/github-runners/main.tf b/modules/github-runners/main.tf index faab3f2c2..2a4131c5b 100644 --- a/modules/github-runners/main.tf +++ b/modules/github-runners/main.tf @@ -73,6 +73,7 @@ data "cloudinit_config" "config" { pre_install = var.userdata_pre_install post_install = var.userdata_post_install runner_version = var.runner_version + runner_group = var.runner_group }) } } diff --git a/modules/github-runners/templates/user-data.sh b/modules/github-runners/templates/user-data.sh index 440b6b86d..d640d1acf 100644 --- a/modules/github-runners/templates/user-data.sh +++ b/modules/github-runners/templates/user-data.sh @@ -64,4 +64,4 @@ export USER=root ${post_install} ls -la /tmp -/tmp/create-latest-svc.sh ${github_scope} "" $NODE_NAME $USER $LABELS +/tmp/create-latest-svc.sh ${github_scope} "" $NODE_NAME $USER $LABELS ${runner_group} diff --git a/modules/github-runners/variables.tf b/modules/github-runners/variables.tf index 52a874e8e..864901c9a 100644 --- a/modules/github-runners/variables.tf +++ b/modules/github-runners/variables.tf @@ -137,6 +137,12 @@ variable "runner_labels" { description = "List of labels to add to the GitHub Runner (e.g. 'Amazon Linux 2')." } +variable "runner_group" { + type = string + default = "default" + description = "GitHub runner group" +} + variable "runner_role_additional_policy_arns" { type = list(string) default = [] From 3d6dc3006700fc907026cb231e4c77d0ce378dfa Mon Sep 17 00:00:00 2001 From: Michael Pursifull Date: Mon, 27 Feb 2023 11:36:24 -0600 Subject: [PATCH 048/501] Set spacelift-worker-pool ami explicitly to x86_64 (#577) --- modules/spacelift-worker-pool/data.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/spacelift-worker-pool/data.tf b/modules/spacelift-worker-pool/data.tf index 7cf475a29..b069e0725 100644 --- a/modules/spacelift-worker-pool/data.tf +++ b/modules/spacelift-worker-pool/data.tf @@ -31,4 +31,9 @@ data "aws_ami" "spacelift" { name = "virtualization-type" values = ["hvm"] } + + filter { + name = "architecture" + values = ["x86_64"] + } } From f05425838f02b67c3aca1aa758c6d98c372da735 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 27 Feb 2023 12:38:59 -0800 Subject: [PATCH 049/501] Update `eks/cluster` (#578) Co-authored-by: cloudpossebot --- modules/eks/cluster/README.md | 1 + modules/eks/cluster/main.tf | 1 + modules/eks/cluster/variables.tf | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 43cf6b9f2..ab46350c9 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -100,6 +100,7 @@ components: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [addons](#input\_addons) | Manages [`aws_eks_addon`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
list(object({
addon_name = string
addon_version = string
resolve_conflicts = string
service_account_role_arn = string
}))
| `[]` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 8a1a80d95..c92eeab1e 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -106,6 +106,7 @@ module "eks_cluster" { public_access_cidrs = var.public_access_cidrs subnet_ids = var.cluster_private_subnets_only ? local.private_subnet_ids : concat(local.private_subnet_ids, local.public_subnet_ids) vpc_id = local.vpc_id + addons = var.addons kubernetes_config_map_ignore_role_changes = false diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 161e01601..c129f2bac 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -385,3 +385,14 @@ variable "fargate_profile_iam_role_permissions_boundary" { description = "If provided, all Fargate Profiles IAM roles will be created with this permissions boundary attached" default = null } + +variable "addons" { + type = list(object({ + addon_name = string + addon_version = string + resolve_conflicts = string + service_account_role_arn = string + })) + description = "Manages [`aws_eks_addon`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources" + default = [] +} From a8598e08f5160ba28b1249700102ac07a2a044a4 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 28 Feb 2023 10:45:23 -0800 Subject: [PATCH 050/501] update `account` readme.md (#570) --- modules/account/README.md | 140 ++++++++++++++++++++++++-------------- 1 file changed, 90 insertions(+), 50 deletions(-) diff --git a/modules/account/README.md b/modules/account/README.md index 7fa7d663e..c1b7e8b7d 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -8,99 +8,139 @@ In addition, it enables [AWS IAM Access Analyzer](https://docs.aws.amazon.com/IA **Stack Level**: Global -**IMPORTANT**: Account names must not contain dashes. Doing so will lead to unpredictable resource names as a `-` is the default delimiter. Additionally, account names must be lower case alpha-numeric with no special characters. +**IMPORTANT**: Account Name building blocks (such as tenant, stage, environment) must not contain dashes. Doing so will lead to unpredictable resource names as a `-` is the default delimiter. Additionally, account names must be lower case alpha-numeric with no special characters. +For example: + +| Key | Value | Correctness | +|------------------|-----------------|-------------| +| **Tenant** | foo | ✅ | +| **Tenant** | foo-bar | ❌ | +| **Environment** | use1 | ✅ | +| **Environment** | us-east-1 | ❌ | +| **Account Name** | `core-identity` | ✅ | Here is an example snippet for how to use this component. Include this snippet in the stack configuration for the management account -(typically `root`) in the management tenant/OU (usually something like `mgmt` or `core`) in the global region (`gbl`). You can insert +(typically `root`) in the management tenant/OU (usually something like `mgmt` or `core`) in the global region (`gbl`). You can insert the content directly, or create a `stacks/catalog/account.yaml` file and import it from there. ```yaml components: terraform: account: + settings: + spacelift: + workspace_enabled: false + backend: + s3: + role_arn: null vars: + enabled: true account_email_format: aws+%s@example.net - account_iam_user_access_to_billing: DENY + account_iam_user_access_to_billing: ALLOW organization_enabled: true aws_service_access_principals: - cloudtrail.amazonaws.com + - guardduty.amazonaws.com + - ipam.amazonaws.com - ram.amazonaws.com + - securityhub.amazonaws.com + - servicequotas.amazonaws.com + - sso.amazonaws.com + - securityhub.amazonaws.com + - auditmanager.amazonaws.com enabled_policy_types: - SERVICE_CONTROL_POLICY - TAG_POLICY organization_config: - root_account_stage_name: root + root_account: + name: core-root + stage: root + tenant: core + tags: + eks: false accounts: [] organization: - service_control_policies: [] + service_control_policies: + - DenyEC2InstancesWithoutEncryptionInTransit organizational_units: - - name: data + - name: core accounts: - - name: proddata + - name: core-artifacts + tenant: core + stage: artifacts tags: - eks: true - - name: devdata + eks: false + - name: core-audit + tenant: core + stage: audit tags: - eks: true - - name: stagedata + eks: false + - name: core-auto + tenant: core + stage: auto tags: eks: true - service_control_policies: - - DenyLeavingOrganization - - name: platform - accounts: - - name: prodplatform + - name: core-corp + tenant: core + stage: corp tags: eks: true - - name: devplatform + - name: core-dns + tenant: core + stage: dns tags: - eks: true - - name: stageplatform + eks: false + - name: core-identity + tenant: core + stage: identity tags: - eks: true + eks: false + - name: core-network + tenant: core + stage: network + tags: + eks: false + - name: core-security + tenant: core + stage: security + tags: + eks: false service_control_policies: - DenyLeavingOrganization - - name: mgmt + - name: plat accounts: - - name: demo + - name: plat-dev + tenant: plat + stage: dev tags: eks: true - - name: audit - tags: - eks: false - - name: corp + - name: plat-sandbox + tenant: plat + stage: sandbox tags: eks: true - - name: security + - name: plat-staging + tenant: plat + stage: staging tags: - eks: false - - name: identity - tags: - eks: false - - name: network - tags: - eks: false - - name: dns - tags: - eks: false - - name: automation + eks: true + - name: plat-prod + tenant: plat + stage: prod tags: eks: true service_control_policies: - DenyLeavingOrganization service_control_policies_config_paths: - # These paths specify where to find the service control policies identified by SID in the service_control_policies sections above. - # The number such as "0.12.0" is the release number/tag of the service control policies repository, and you may want to - # update it to reflect the latest release. - - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/organization-policies.yaml" - - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/ec2-policies.yaml" - - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/cloudwatch-logs-policies.yaml" - - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/deny-all-policies.yaml" - - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/iam-policies.yaml" - - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/kms-policies.yaml" - - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/route53-policies.yaml" - - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/s3-policies.yaml" - + # These paths specify where to find the service control policies identified by SID in the service_control_policies sections above. + - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/cloudwatch-logs-policies.yaml" + - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/deny-all-policies.yaml" + - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/iam-policies.yaml" + - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/kms-policies.yaml" + - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/organization-policies.yaml" + - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/route53-policies.yaml" + - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/s3-policies.yaml" + - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/ec2-policies.yaml" ``` From a100fa3336fb4f0bc2b7a34a6fa9683104f21e39 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 28 Feb 2023 10:46:09 -0800 Subject: [PATCH 051/501] datadog monitors improvements (#579) --- .../catalog/monitors/aurora.yaml | 3 +- .../datadog-monitor/catalog/monitors/ec2.yaml | 4 +- .../datadog-monitor/catalog/monitors/efs.yaml | 16 ++--- .../datadog-monitor/catalog/monitors/elb.yaml | 4 +- .../catalog/monitors/host.yaml | 10 +-- .../datadog-monitor/catalog/monitors/k8s.yaml | 68 ++++++++++--------- .../catalog/monitors/rabbitmq.yaml | 9 ++- .../datadog-monitor/catalog/monitors/rds.yaml | 11 ++- 8 files changed, 69 insertions(+), 56 deletions(-) diff --git a/modules/datadog-monitor/catalog/monitors/aurora.yaml b/modules/datadog-monitor/catalog/monitors/aurora.yaml index 12b9cfb5a..c1b004600 100644 --- a/modules/datadog-monitor/catalog/monitors/aurora.yaml +++ b/modules/datadog-monitor/catalog/monitors/aurora.yaml @@ -5,8 +5,9 @@ aurora-replica-lag: name: "(RDS) ${tenant} ${stage} - Aurora Replica Lag Detected" type: metric alert query: | - min(last_15m):min:aws.rds.aurora_replica_lag{stage:${ stage }} by {dbinstanceidentifier} > 1000 + min(last_15m):min:aws.rds.aurora_replica_lag{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment} > 1000 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} ({dbinstanceidentifier}) Replica lag has been greater than half a second for more than 15 minutes {{/is_warning}} diff --git a/modules/datadog-monitor/catalog/monitors/ec2.yaml b/modules/datadog-monitor/catalog/monitors/ec2.yaml index 3c297366d..25cd04e8f 100644 --- a/modules/datadog-monitor/catalog/monitors/ec2.yaml +++ b/modules/datadog-monitor/catalog/monitors/ec2.yaml @@ -5,9 +5,9 @@ ec2-failed-status-check: name: "(EC2) ${tenant} ${ stage } - Failed Status Check" type: metric alert query: | - avg(last_10m):avg:aws.ec2.status_check_failed{stage:${ stage }} by {instance_id} > 0 + avg(last_10m):avg:aws.ec2.status_check_failed{stage:${ stage }} by {instance_id,stage,tenant,environment} > 0 message: | - ({stage} {region}) {instance_id} failed a status check + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{instance_id}} failed a status check escalation_message: "" tags: managed-by: Terraform diff --git a/modules/datadog-monitor/catalog/monitors/efs.yaml b/modules/datadog-monitor/catalog/monitors/efs.yaml index 80880d944..9976bac92 100644 --- a/modules/datadog-monitor/catalog/monitors/efs.yaml +++ b/modules/datadog-monitor/catalog/monitors/efs.yaml @@ -5,9 +5,9 @@ efs-throughput-utilization-check: name: "(EFS) ${tenant} ${ stage } - % Throughput Utilization" type: metric alert query: | - avg(last_1h):(sum:aws.efs.metered_iobytes{stage:${ stage }} by {filesystemid} * 100 / 1048576) / (sum:aws.efs.permitted_throughput{stage:${ stage }} by {filesystemid} / 1048576) > 75 + avg(last_1h):(sum:aws.efs.metered_iobytes{stage:${ stage }} by {filesystemid} * 100 / 1048576) / (sum:aws.efs.permitted_throughput{stage:${ stage }} by {filesystemid,stage,tenant,environment} / 1048576) > 75 message: | - ({stage} {region}) {filesystemid} Throughput Utilization is too high + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{filesystemid}} Throughput Utilization is too high escalation_message: "" tags: managed-by: Terraform @@ -39,9 +39,9 @@ efs-burst-balance: name: "(EFS) ${tenant} ${ stage } - Burst Balance Low (< 100 GB)" type: metric alert query: | - min(last_1h):avg:aws.efs.burst_credit_balance{stage:${ stage }} by {filesystemid} < 100000000000 + min(last_1h):avg:aws.efs.burst_credit_balance{stage:${ stage }} by {filesystemid,stage,tenant,environment} < 100000000000 message: | - ({stage} {region}) {filesystemid} EFS Burst Balance for {filesystemid} dipped below 100 GB. + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{filesystemid}} EFS Burst Balance for {{filesystemid}} dipped below 100 GB. escalation_message: "" tags: managed-by: Terraform @@ -71,9 +71,9 @@ efs-io-percent-limit: name: "(EFS) ${tenant} ${ stage } - I/O limit has been reached (> 90%)" type: metric alert query: | - max(last_1h):avg:aws.efs.percent_iolimit{stage:${ stage }} by {filesystemid} > 90 + max(last_1h):avg:aws.efs.percent_iolimit{stage:${ stage }} by {filesystemid,stage,tenant,environment} > 90 message: | - ({stage} {region}) {filesystemid} EFS I/O limit has been reached for fs {filesystemid}. + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{filesystemid}} EFS I/O limit has been reached for fs {{filesystemid}}. escalation_message: "" tags: managed-by: Terraform @@ -102,9 +102,9 @@ efs-client-connection-anomaly: name: "(EFS) ${tenant} ${ stage } - Client Connection Anomaly" type: metric alert query: | - avg(last_4h):anomalies(avg:aws.efs.client_connections{stage:${ stage }} by {aws_account,filesystemid,name}.as_count(), 'basic', 2, direction='both', alert_window='last_15m', interval=60, count_default_zero='true') >= 1 + avg(last_4h):anomalies(avg:aws.efs.client_connections{stage:${ stage }} by {aws_account,filesystemid,name,stage,tenant,environment}.as_count(), 'basic', 2, direction='both', alert_window='last_15m', interval=60, count_default_zero='true') >= 1 message: | - ({stage} {region}) [{name}] EFS Client Connection Anomoly for filesystem {filesystemid}. + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{name}}] EFS Client Connection Anomoly for filesystem {{filesystemid}}. escalation_message: "" tags: managed-by: Terraform diff --git a/modules/datadog-monitor/catalog/monitors/elb.yaml b/modules/datadog-monitor/catalog/monitors/elb.yaml index e252c7071..3d644c152 100644 --- a/modules/datadog-monitor/catalog/monitors/elb.yaml +++ b/modules/datadog-monitor/catalog/monitors/elb.yaml @@ -2,9 +2,9 @@ elb-lb-httpcode-5xx-notify: name: "(ELB) ${tenant} ${ stage } HTTP 5XX client error detected" type: query alert query: | - avg(last_15m):max:aws.elb.httpcode_elb_5xx{${context_dd_tags}} by {env,host} > 50 + avg(last_15m):max:aws.elb.httpcode_elb_5xx{${context_dd_tags}} by {env,host,stage,tenant,environment} > 50 message: | - [${ stage }] [ {{ env }} ] lb:[ {{host}} ] + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) lb:[ {{host}} ] {{#is_warning}} Number of HTTP 5XX client error codes generated by the load balancer > {{warn_threshold}}% {{/is_warning}} diff --git a/modules/datadog-monitor/catalog/monitors/host.yaml b/modules/datadog-monitor/catalog/monitors/host.yaml index edc61520b..866ff9813 100644 --- a/modules/datadog-monitor/catalog/monitors/host.yaml +++ b/modules/datadog-monitor/catalog/monitors/host.yaml @@ -4,7 +4,7 @@ host-io-wait-times: name: "(Host) ${tenant} ${ stage } - I/O Wait Times" type: metric alert - query: "avg(last_10m):avg:system.cpu.iowait{stage:${ stage }} by {host} > 50" + query: "avg(last_10m):avg:system.cpu.iowait{stage:${ stage }} by {host,stage,tenant,environment} > 50" message: |- The I/O wait time for ({{host.name}} {{host.ip}}) is very high escalation_message: "" @@ -30,7 +30,7 @@ host-io-wait-times: host-disk-use: name: "(Host) ${tenant} ${ stage } - Host Disk Usage" type: metric alert - query: "avg(last_30m):(avg:system.disk.total{stage:${ stage }} by {host} - avg:system.disk.free{stage:${ stage }} by {host}) / avg:system.disk.total{stage:${ stage }} by {host} * 100 > 90" + query: "avg(last_30m):(avg:system.disk.total{stage:${ stage }} by {host,stage,tenant,environment} - avg:system.disk.free{stage:${ stage }} by {host}) / avg:system.disk.total{stage:${ stage }} by {host} * 100 > 90" message: |- Disk Usage has been above threshold over 30 minutes on ({{host.name}} {{host.ip}}) escalation_message: "" @@ -60,7 +60,7 @@ host-disk-use: host-high-mem-use: name: "(Host) ${tenant} ${ stage } - Memory Utilization" type: query alert - query: "avg(last_15m):avg:system.mem.pct_usable{stage:${ stage }} by {host} < 0.1" + query: "avg(last_15m):avg:system.mem.pct_usable{stage:${ stage }} by {host,stage,tenant,environment} < 0.1" message: |- Running out of free memory on ({{host.name}} {{host.ip}}) escalation_message: "" @@ -90,9 +90,9 @@ host-high-mem-use: host-high-load-avg: name: "(Host) ${tenant} ${ stage } - High System Load Average" type: metric alert - query: "avg(last_30m):avg:system.load.norm.5{stage:${ stage }} by {host} > 0.8" + query: "avg(last_30m):avg:system.load.norm.5{stage:${ stage }} by {host,stage,tenant,environment} > 0.8" message: |- - Load average is high on ({{host.name}} {{host.ip}}) + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) Load average is high on ({{host.name}} {{host.ip}}) escalation_message: "" tags: managed-by: Terraform diff --git a/modules/datadog-monitor/catalog/monitors/k8s.yaml b/modules/datadog-monitor/catalog/monitors/k8s.yaml index 5ff999311..234c36be9 100644 --- a/modules/datadog-monitor/catalog/monitors/k8s.yaml +++ b/modules/datadog-monitor/catalog/monitors/k8s.yaml @@ -5,9 +5,9 @@ k8s-deployment-replica-pod-down: name: "(k8s) ${tenant} ${ stage } - Deployment Replica Pod is down" type: query alert query: | - avg(last_15m):avg:kubernetes_state.deployment.replicas_desired{stage:${ stage }} by {cluster_name,deployment} - avg:kubernetes_state.deployment.replicas_ready{stage:${ stage }} by {cluster_name,deployment} >= 2 + avg(last_15m):avg:kubernetes_state.deployment.replicas_desired{stage:${ stage }} by {cluster_name,deployment,stage,tenant,environment} - avg:kubernetes_state.deployment.replicas_ready{stage:${ stage }} by {cluster_name,deployment,stage,tenant,environment} >= 2 message: | - ({{cluster_name.name}}) More than one Deployments Replica's pods are down on {{deployment.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] More than one Deployments Replica's pods are down on {{deployment.name}} escalation_message: "" tags: managed-by: Terraform @@ -31,9 +31,9 @@ k8s-pod-restarting: name: "(k8s) ${tenant} ${ stage } - Pods are restarting multiple times" type: query alert query: | - change(sum(last_5m),last_5m):exclude_null(avg:kubernetes.containers.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod_name}) > 5 + change(sum(last_5m),last_5m):exclude_null(avg:kubernetes.containers.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod_name,stage,tenant,environment}) > 5 message: | - ({{cluster_name.name}}) pod {{pod_name.name}} is restarting multiple times on {{kube_namespace.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] pod {{pod_name.name}} is restarting multiple times on {{kube_namespace.name}} escalation_message: "" tags: managed-by: Terraform @@ -58,9 +58,9 @@ k8s-statefulset-replica-down: name: "(k8s) ${tenant} ${ stage } - StatefulSet Replica Pod is down" type: query alert query: | - max(last_15m):sum:kubernetes_state.statefulset.replicas_desired{stage:${ stage }} by {cluster_name,kube_namespace,statefulset} - sum:kubernetes_state.statefulset.replicas_ready{stage:${ stage }} by {cluster_name,kube_namespace,statefulset} >= 2 + max(last_15m):sum:kubernetes_state.statefulset.replicas_desired{stage:${ stage }} by {cluster_name,kube_namespace,statefulset,stage,tenant,environment} - sum:kubernetes_state.statefulset.replicas_ready{stage:${ stage }} by {cluster_name,kube_namespace,statefulset,stage,tenant,environment} >= 2 message: | - ({{cluster_name.name}} {{statefulset.name}}) More than one StatefulSet Replica's pods are down on {{kube_namespace.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}} {{statefulset.name}}] More than one StatefulSet Replica's pods are down on {{kube_namespace.name}} escalation_message: "" tags: managed-by: Terraform @@ -86,9 +86,9 @@ k8s-daemonset-pod-down: name: "(k8s) ${tenant} ${ stage } - DaemonSet Pod is down" type: query alert query: | - max(last_15m):sum:kubernetes_state.daemonset.desired{stage:${ stage }} by {cluster_name,kube_namespace,daemonset} - sum:kubernetes_state.daemonset.ready{stage:${ stage }} by {cluster_name,kube_namespace,daemonset} >= 1 + max(last_15m):sum:kubernetes_state.daemonset.desired{stage:${ stage }} by {cluster_name,kube_namespace,daemonset,stage,tenant,environment} - sum:kubernetes_state.daemonset.ready{stage:${ stage }} by {cluster_name,kube_namespace,daemonset,stage,tenant,environment} >= 1 message: | - ({{cluster_name.name}} {{daemonset.name}}) One or more DaemonSet pods are down on {{kube_namespace.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}} {{daemonset.name}}] One or more DaemonSet pods are down on {{kube_namespace.name}} escalation_message: "" tags: managed-by: Terraform @@ -112,9 +112,9 @@ k8s-crashloopBackOff: name: "(k8s) ${tenant} ${ stage } - CrashloopBackOff detected" type: query alert query: | - max(last_10m):max:kubernetes_state.container.status_report.count.waiting{stage:${ stage },reason:crashloopbackoff} by {cluster_name,kube_namespace,pod_name} >= 1 + max(last_10m):max:kubernetes_state.container.status_report.count.waiting{stage:${ stage },reason:crashloopbackoff} by {cluster_name,kube_namespace,pod_name,stage,tenant,environment} >= 1 message: | - ({{cluster_name.name}}) pod {{pod_name.name}} is CrashloopBackOff on {{kube_namespace.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] pod {{pod_name.name}} is CrashloopBackOff on {{kube_namespace.name}} escalation_message: "" tags: managed-by: Terraform @@ -138,9 +138,9 @@ k8s-multiple-pods-failing: name: "(k8s) ${tenant} ${ stage } - Multiple Pods are failing" type: query alert query: | - change(avg(last_5m),last_5m):sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:failed} by {cluster_name,kube_namespace} > 10 + change(avg(last_5m),last_5m):sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:failed} by {cluster_name,kube_namespace,stage,tenant,environment} > 10 message: | - ({{cluster_name.name}}) More than ten pods are failing on {{kube_namespace.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] More than ten pods are failing on {{kube_namespace.name}} escalation_message: "" tags: managed-by: Terraform @@ -165,9 +165,9 @@ k8s-unavailable-deployment-replica: name: "(k8s) ${tenant} ${ stage } - Unavailable Deployment Replica(s) detected" type: metric alert query: | - max(last_10m):max:kubernetes_state.deployment.replicas_unavailable{stage:${ stage }} by {cluster_name,kube_namespace} > 0 + max(last_10m):max:kubernetes_state.deployment.replicas_unavailable{stage:${ stage }} by {cluster_name,kube_namespace,stage,tenant,environment} > 0 message: | - ({{cluster_name.name}}) Detected unavailable Deployment replicas for longer than 10 minutes on {{kube_namespace.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] Detected unavailable Deployment replicas for longer than 10 minutes on {{kube_namespace.name}} escalation_message: "" tags: managed-by: Terraform @@ -196,9 +196,9 @@ k8s-unavailable-statefulset-replica: name: "(k8s) ${tenant} ${ stage } - Unavailable Statefulset Replica(s) detected" type: metric alert query: | - max(last_10m):max:kubernetes_state.statefulset.replicas_unavailable{stage:${ stage }} by {cluster_name,kube_namespace} > 0 + max(last_10m):max:kubernetes_state.statefulset.replicas_unavailable{stage:${ stage }} by {cluster_name,kube_namespace,stage,tenant,environment} > 0 message: | - ({{cluster_name.name}}) Detected unavailable Statefulset replicas for longer than 10 minutes on {{kube_namespace.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] Detected unavailable Statefulset replicas for longer than 10 minutes on {{kube_namespace.name}} escalation_message: "" tags: managed-by: Terraform @@ -227,9 +227,9 @@ k8s-node-status-unschedulable: name: "(k8s) ${tenant} ${ stage } - Detected Unschedulable Node(s)" type: query alert query: | - max(last_15m):sum:kubernetes_state.node.status{stage:${ stage },status:schedulable} by {cluster_name} * 100 / sum:kubernetes_state.node.status{stage:${ stage }} by {cluster_name} < 80 + max(last_15m):sum:kubernetes_state.node.status{stage:${ stage },status:schedulable} by {cluster_name} * 100 / sum:kubernetes_state.node.status{stage:${ stage }} by {cluster_name,stage,tenant,environment} < 80 message: | - More than 20% of nodes are unschedulable on ({{cluster_name}} cluster). \n Keep in mind that this might be expected based on your infrastructure. + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] More than 20% of nodes are unschedulable on the cluster. \n Keep in mind that this might be expected based on your infrastructure. escalation_message: "" tags: managed-by: Terraform @@ -258,9 +258,9 @@ k8s-imagepullbackoff: name: "(k8s) ${tenant} ${ stage } - ImagePullBackOff detected" type: "query alert" query: | - max(last_10m):max:kubernetes_state.container.status_report.count.waiting{reason:imagepullbackoff,stage:${ stage }} by {kube_cluster_name,kube_namespace,pod_name} >= 1 + max(last_10m):max:kubernetes_state.container.status_report.count.waiting{reason:imagepullbackoff,stage:${ stage }} by {kube_cluster_name,kube_namespace,pod_name,stage,tenant,environment} >= 1 message: | - Pod {{pod_name.name}} is ImagePullBackOff on namespace {{kube_namespace.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] Pod {{pod_name.name}} is ImagePullBackOff on namespace {{kube_namespace.name}} escalation_message: "" tags: managed-by: Terraform @@ -289,9 +289,9 @@ k8s-high-cpu-usage: name: "(k8s) ${tenant} ${ stage } - High CPU Usage Detected" type: metric alert query: | - avg(last_10m):avg:system.cpu.system{stage:${ stage }} by {host} > 90 + avg(last_10m):avg:system.cpu.system{stage:${ stage }} by {host,stage,tenant,environment} > 90 message: | - ({{host.cluster_name}}) High CPU usage for the last 10 minutes on {{host.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}} {{host.cluster_name}}] High CPU usage for the last 10 minutes on {{host.name}} escalation_message: "" tags: managed-by: Terraform @@ -320,9 +320,9 @@ k8s-high-disk-usage: name: "(k8s) ${tenant} ${ stage } - High Disk Usage Detected" type: metric alert query: | - min(last_5m):min:system.disk.used{stage:${ stage }} by {host,cluster_name} / avg:system.disk.total{stage:${ stage }} by {host,cluster_name} * 100 > 90 + min(last_5m):min:system.disk.used{stage:${ stage }} by {host,cluster_name,stage,tenant,environment} / avg:system.disk.total{stage:${ stage }} by {host,cluster_name,stage,tenant,environment} * 100 > 90 message: | - ({{cluster_name.name}}) High disk usage detected on {{host.name}} + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] High disk usage detected on {{host.name}} escalation_message: "" tags: managed-by: Terraform @@ -351,8 +351,9 @@ k8s-high-memory-usage: name: "(k8s) ${tenant} ${ stage } - High Memory Usage Detected" type: metric alert query: | - avg(last_10m):avg:kubernetes.memory.usage_pct{stage:${ stage }} by {cluster_name} > 90 + avg(last_10m):avg:kubernetes.memory.usage_pct{stage:${ stage }} by {cluster_name,stage,tenant,environment} > 90 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] High memory usage detected on {{host.name}} {{#is_warning}} {{cluster_name.name}} memory usage greater than 80% for 10 minutes {{/is_warning}} @@ -387,8 +388,9 @@ k8s-high-filesystem-usage: name: "(k8s) ${tenant} ${ stage } - High Filesystem Usage Detected" type: metric alert query: | - avg(last_10m):avg:kubernetes.filesystem.usage_pct{stage:${ stage }} by {cluster_name} > 90 + avg(last_10m):avg:kubernetes.filesystem.usage_pct{stage:${ stage }} by {cluster_name,stage,tenant,environment} > 90 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] {{#is_warning}} {{cluster_name.name}} filesystem usage greater than 80% for 10 minutes {{/is_warning}} @@ -423,8 +425,9 @@ k8s-network-tx-errors: name: "(k8s) ${tenant} ${ stage } - High Network TX (send) Errors" type: metric alert query: | - avg(last_10m):avg:kubernetes.network.tx_errors{stage:${ stage }} by {cluster_name} > 100 + avg(last_10m):avg:kubernetes.network.tx_errors{stage:${ stage }} by {cluster_name,stage,tenant,environment} > 100 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] {{#is_warning}} {{cluster_name.name}} network TX (send) errors occurring 10 times per second {{/is_warning}} @@ -459,8 +462,9 @@ k8s-network-rx-errors: name: "(k8s) ${tenant} ${ stage } - High Network RX (receive) Errors" type: metric alert query: | - avg(last_10m):avg:kubernetes.network.rx_errors{stage:${ stage }} by {cluster_name} > 100 + avg(last_10m):avg:kubernetes.network.rx_errors{stage:${ stage }} by {cluster_name,stage,tenant,environment} > 100 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] {{#is_warning}} {{cluster_name.name}} network RX (receive) errors occurring 10 times per second {{/is_warning}} @@ -495,9 +499,9 @@ k8s-increased-pod-crash: name: "(k8s) ${tenant} ${ stage } - Increased Pod Crashes" type: query alert query: | - avg(last_5m):avg:kubernetes_state.container.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod} - hour_before(avg:kubernetes_state.container.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod}) > 3 + avg(last_5m):avg:kubernetes_state.container.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod,stage,tenant,environment} - hour_before(avg:kubernetes_state.container.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod,stage,tenant,environment}) > 3 message: |- - ({{cluster_name.name}} {{kube_namespace.name}} {{pod.name}}) has crashed repeatedly over the last hour + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}} {{kube_namespace.name}} {{pod.name}}] has crashed repeatedly over the last hour escalation_message: "" tags: managed-by: Terraform @@ -526,9 +530,9 @@ k8s-pending-pods: name: "(k8s) ${tenant} ${ stage } - Pending Pods" type: metric alert query: | - min(last_30m):sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:running} by {cluster_name} - sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:running} by {cluster_name} + sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:pending} by {cluster_name}.fill(zero) >= 1 + min(last_30m):sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:running} by {cluster_name,stage,tenant,environment} - sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:running} by {cluster_name,stage,tenant,environment} + sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:pending} by {cluster_name,stage,tenant,environment}.fill(zero) >= 1 message: |- - ({{cluster_name.name}}) There has been at least 1 pod Pending for 30 minutes. + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] There has been at least 1 pod Pending for 30 minutes. There are currently ({{value}}) pods Pending. escalation_message: "" tags: diff --git a/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml b/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml index 9bccec922..6c3a9d5b4 100644 --- a/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml +++ b/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml @@ -2,8 +2,9 @@ rabbitmq-messages-unacknowledged-rate-too-high: name: "[RabbitMQ] ${tenant} ${ stage } - Messages unacknowledged rate is higher than usual on: {{broker.name}}" type: "query alert" query: | - avg(last_4h):anomalies(avg:aws.amazonmq.message_unacknowledged_count{stage:${ stage }} by {broker,queue}, 'agile', 2, direction='above', alert_window='last_15m', interval=60, count_default_zero='true', seasonality='hourly') >= 1 + avg(last_4h):anomalies(avg:aws.amazonmq.message_unacknowledged_count{stage:${ stage }} by {broker,queue,stage,tenant,environment}, 'agile', 2, direction='above', alert_window='last_15m', interval=60, count_default_zero='true', seasonality='hourly') >= 1 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) The rate at which messages are being delivered without receiving acknowledgement is higher than usual. There may be errors or performance issues downstream.\n Broker: {{broker.name}}\n @@ -37,8 +38,9 @@ rabbitmq-memory-utilization: name: "[RabbitMQ] ${tenant} ${ stage } - Memory Utilization: {{broker.name}}" type: "query alert" query: | - avg(last_10m):avg:aws.amazonmq.rabbit_mqmem_used{stage:${ stage }} by {broker,node} / avg:aws.amazonmq.rabbit_mqmem_limit{stage:${ stage }} by {broker,node} > 0.50 + avg(last_10m):avg:aws.amazonmq.rabbit_mqmem_used{stage:${ stage }} by {broker,node,stage,tenant,environment} / avg:aws.amazonmq.rabbit_mqmem_limit{stage:${ stage }} by {broker,node,stage,tenant,environment} > 0.50 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) Memory Percentage of a node in Rabbit MQ Cluster is high Broker: {{broker.name}} Node: {{node.name}} @@ -70,8 +72,9 @@ rabbitmq-disk-utilization: name: "[RabbitMQ] ${tenant} ${ stage } - Disk Utilization: {{broker.name}}" type: "query alert" query: | - avg(last_10m):avg:aws.amazonmq.rabbit_mqdisk_free{stage:${ stage }} by {broker,node} < 100000000000 + avg(last_10m):avg:aws.amazonmq.rabbit_mqdisk_free{stage:${ stage }} by {broker,node,stage,tenant,environment} < 100000000000 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) Free Disk Space of a node in Rabbit MQ Cluster is Less than 100 GB Broker: {{broker.name}} Node: {{node.name}} diff --git a/modules/datadog-monitor/catalog/monitors/rds.yaml b/modules/datadog-monitor/catalog/monitors/rds.yaml index e92f8966f..57b23c16d 100644 --- a/modules/datadog-monitor/catalog/monitors/rds.yaml +++ b/modules/datadog-monitor/catalog/monitors/rds.yaml @@ -5,8 +5,9 @@ rds-cpuutilization: name: "(RDS) ${tenant} ${ stage } - CPU Utilization above 90%" type: metric alert query: | - avg(last_15m):avg:aws.rds.cpuutilization{stage:${ stage }} by {dbinstanceidentifier} > 90 + avg(last_15m):avg:aws.rds.cpuutilization{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment} > 90 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} ({dbinstanceidentifier}) CPU Utilization above 85% {{/is_warning}} @@ -41,8 +42,9 @@ rds-disk-queue-depth: name: "(RDS) ${tenant} ${ stage } - Disk queue depth above 64" type: metric alert query: | - avg(last_15m):avg:aws.rds.disk_queue_depth{stage:${ stage }} by {dbinstanceidentifier} > 64 + avg(last_15m):avg:aws.rds.disk_queue_depth{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment} > 64 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} ({dbinstanceidentifier}) Disk queue depth above 48 {{/is_warning}} @@ -79,6 +81,7 @@ rds-freeable-memory: query: | avg(last_5m):avg:aws.rds.freeable_memory{stage:${ stage }} < 256000000 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} ({dbinstanceidentifier}) Freeable memory below 512 MB {{/is_warning}} @@ -113,8 +116,9 @@ rds-swap-usage: name: "(RDS) ${tenant} ${ stage } - Swap usage above 256 MB" type: metric alert query: | - avg(last_15m):avg:aws.rds.swap_usage{stage:${ stage }} by {dbinstanceidentifier} > 256000000 + avg(last_15m):avg:aws.rds.swap_usage{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment} > 256000000 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} ({dbinstanceidentifier}) Swap usage above 128 MB {{/is_warning}} @@ -151,6 +155,7 @@ rds-database-connections: query: | avg(last_4h):anomalies(avg:aws.rds.database_connections{stage:${ stage }}, 'basic', 2, direction='both', alert_window='last_15m', interval=60, count_default_zero='true') >= 1 message: | + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} ({dbinstanceidentifier}) Anomaly of a large variance in RDS connection count {{/is_warning}} From 84929261b9e308fe7ccd4c556bf90394928db228 Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Tue, 28 Feb 2023 12:47:23 -0600 Subject: [PATCH 052/501] `spacelift` add missing `var.region` (#574) Co-authored-by: cloudpossebot Co-authored-by: Benjamin Smith --- modules/spacelift/README.md | 1 + modules/spacelift/variables.tf | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index f4470b5bc..bd797476c 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -403,6 +403,7 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | [policies\_by\_name\_path](#input\_policies\_by\_name\_path) | Path to the catalog of external Rego policies. The Rego files must exist in the caller's code at the path. The module will create Spacelift policies from the external Rego definitions | `string` | `""` | no | | [policies\_enabled](#input\_policies\_enabled) | DEPRECATED: Use `policies_by_id_enabled` instead. List of default policies created by this stack to attach to all Spacelift stacks | `list(string)` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | | [runner\_image](#input\_runner\_image) | Full address & tag of the Spacelift runner image (e.g. on ECR) | `string` | n/a | yes | | [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | | [spacelift\_component\_path](#input\_spacelift\_component\_path) | The Spacelift Component Path | `string` | `"components/terraform"` | no | diff --git a/modules/spacelift/variables.tf b/modules/spacelift/variables.tf index fbef64eba..f90999116 100644 --- a/modules/spacelift/variables.tf +++ b/modules/spacelift/variables.tf @@ -1,3 +1,8 @@ +variable "region" { + type = string + description = "AWS Region" +} + variable "runner_image" { type = string description = "Full address & tag of the Spacelift runner image (e.g. on ECR)" From 77f89a23cf1fb1eb23fd1bedeb9d51b6a916471d Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 1 Mar 2023 11:09:44 -0800 Subject: [PATCH 053/501] `datadog-monitors`: Team Grouping (#580) --- .../catalog/monitors/aurora.yaml | 2 +- .../datadog-monitor/catalog/monitors/ec2.yaml | 2 +- .../datadog-monitor/catalog/monitors/efs.yaml | 8 ++--- .../datadog-monitor/catalog/monitors/elb.yaml | 2 +- .../catalog/monitors/host.yaml | 8 ++--- .../datadog-monitor/catalog/monitors/k8s.yaml | 36 +++++++++---------- .../catalog/monitors/rabbitmq.yaml | 6 ++-- .../datadog-monitor/catalog/monitors/rds.yaml | 6 ++-- 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/modules/datadog-monitor/catalog/monitors/aurora.yaml b/modules/datadog-monitor/catalog/monitors/aurora.yaml index c1b004600..81e887166 100644 --- a/modules/datadog-monitor/catalog/monitors/aurora.yaml +++ b/modules/datadog-monitor/catalog/monitors/aurora.yaml @@ -5,7 +5,7 @@ aurora-replica-lag: name: "(RDS) ${tenant} ${stage} - Aurora Replica Lag Detected" type: metric alert query: | - min(last_15m):min:aws.rds.aurora_replica_lag{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment} > 1000 + min(last_15m):min:aws.rds.aurora_replica_lag{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment,team} > 1000 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} diff --git a/modules/datadog-monitor/catalog/monitors/ec2.yaml b/modules/datadog-monitor/catalog/monitors/ec2.yaml index 25cd04e8f..f8fea361c 100644 --- a/modules/datadog-monitor/catalog/monitors/ec2.yaml +++ b/modules/datadog-monitor/catalog/monitors/ec2.yaml @@ -5,7 +5,7 @@ ec2-failed-status-check: name: "(EC2) ${tenant} ${ stage } - Failed Status Check" type: metric alert query: | - avg(last_10m):avg:aws.ec2.status_check_failed{stage:${ stage }} by {instance_id,stage,tenant,environment} > 0 + avg(last_10m):avg:aws.ec2.status_check_failed{stage:${ stage }} by {instance_id,stage,tenant,environment,team} > 0 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{instance_id}} failed a status check escalation_message: "" diff --git a/modules/datadog-monitor/catalog/monitors/efs.yaml b/modules/datadog-monitor/catalog/monitors/efs.yaml index 9976bac92..bece80607 100644 --- a/modules/datadog-monitor/catalog/monitors/efs.yaml +++ b/modules/datadog-monitor/catalog/monitors/efs.yaml @@ -5,7 +5,7 @@ efs-throughput-utilization-check: name: "(EFS) ${tenant} ${ stage } - % Throughput Utilization" type: metric alert query: | - avg(last_1h):(sum:aws.efs.metered_iobytes{stage:${ stage }} by {filesystemid} * 100 / 1048576) / (sum:aws.efs.permitted_throughput{stage:${ stage }} by {filesystemid,stage,tenant,environment} / 1048576) > 75 + avg(last_1h):(sum:aws.efs.metered_iobytes{stage:${ stage }} by {filesystemid} * 100 / 1048576) / (sum:aws.efs.permitted_throughput{stage:${ stage }} by {filesystemid,stage,tenant,environment,team} / 1048576) > 75 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{filesystemid}} Throughput Utilization is too high escalation_message: "" @@ -39,7 +39,7 @@ efs-burst-balance: name: "(EFS) ${tenant} ${ stage } - Burst Balance Low (< 100 GB)" type: metric alert query: | - min(last_1h):avg:aws.efs.burst_credit_balance{stage:${ stage }} by {filesystemid,stage,tenant,environment} < 100000000000 + min(last_1h):avg:aws.efs.burst_credit_balance{stage:${ stage }} by {filesystemid,stage,tenant,environment,team} < 100000000000 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{filesystemid}} EFS Burst Balance for {{filesystemid}} dipped below 100 GB. escalation_message: "" @@ -71,7 +71,7 @@ efs-io-percent-limit: name: "(EFS) ${tenant} ${ stage } - I/O limit has been reached (> 90%)" type: metric alert query: | - max(last_1h):avg:aws.efs.percent_iolimit{stage:${ stage }} by {filesystemid,stage,tenant,environment} > 90 + max(last_1h):avg:aws.efs.percent_iolimit{stage:${ stage }} by {filesystemid,stage,tenant,environment,team} > 90 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{filesystemid}} EFS I/O limit has been reached for fs {{filesystemid}}. escalation_message: "" @@ -102,7 +102,7 @@ efs-client-connection-anomaly: name: "(EFS) ${tenant} ${ stage } - Client Connection Anomaly" type: metric alert query: | - avg(last_4h):anomalies(avg:aws.efs.client_connections{stage:${ stage }} by {aws_account,filesystemid,name,stage,tenant,environment}.as_count(), 'basic', 2, direction='both', alert_window='last_15m', interval=60, count_default_zero='true') >= 1 + avg(last_4h):anomalies(avg:aws.efs.client_connections{stage:${ stage }} by {aws_account,filesystemid,name,stage,tenant,environment,team}.as_count(), 'basic', 2, direction='both', alert_window='last_15m', interval=60, count_default_zero='true') >= 1 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{name}}] EFS Client Connection Anomoly for filesystem {{filesystemid}}. escalation_message: "" diff --git a/modules/datadog-monitor/catalog/monitors/elb.yaml b/modules/datadog-monitor/catalog/monitors/elb.yaml index 3d644c152..a54ef0c77 100644 --- a/modules/datadog-monitor/catalog/monitors/elb.yaml +++ b/modules/datadog-monitor/catalog/monitors/elb.yaml @@ -2,7 +2,7 @@ elb-lb-httpcode-5xx-notify: name: "(ELB) ${tenant} ${ stage } HTTP 5XX client error detected" type: query alert query: | - avg(last_15m):max:aws.elb.httpcode_elb_5xx{${context_dd_tags}} by {env,host,stage,tenant,environment} > 50 + avg(last_15m):max:aws.elb.httpcode_elb_5xx{${context_dd_tags}} by {env,host,stage,tenant,environment,team} > 50 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) lb:[ {{host}} ] {{#is_warning}} diff --git a/modules/datadog-monitor/catalog/monitors/host.yaml b/modules/datadog-monitor/catalog/monitors/host.yaml index 866ff9813..020536227 100644 --- a/modules/datadog-monitor/catalog/monitors/host.yaml +++ b/modules/datadog-monitor/catalog/monitors/host.yaml @@ -4,7 +4,7 @@ host-io-wait-times: name: "(Host) ${tenant} ${ stage } - I/O Wait Times" type: metric alert - query: "avg(last_10m):avg:system.cpu.iowait{stage:${ stage }} by {host,stage,tenant,environment} > 50" + query: "avg(last_10m):avg:system.cpu.iowait{stage:${ stage }} by {host,stage,tenant,environment,team} > 50" message: |- The I/O wait time for ({{host.name}} {{host.ip}}) is very high escalation_message: "" @@ -30,7 +30,7 @@ host-io-wait-times: host-disk-use: name: "(Host) ${tenant} ${ stage } - Host Disk Usage" type: metric alert - query: "avg(last_30m):(avg:system.disk.total{stage:${ stage }} by {host,stage,tenant,environment} - avg:system.disk.free{stage:${ stage }} by {host}) / avg:system.disk.total{stage:${ stage }} by {host} * 100 > 90" + query: "avg(last_30m):(avg:system.disk.total{stage:${ stage }} by {host,stage,tenant,environment,team} - avg:system.disk.free{stage:${ stage }} by {host}) / avg:system.disk.total{stage:${ stage }} by {host} * 100 > 90" message: |- Disk Usage has been above threshold over 30 minutes on ({{host.name}} {{host.ip}}) escalation_message: "" @@ -60,7 +60,7 @@ host-disk-use: host-high-mem-use: name: "(Host) ${tenant} ${ stage } - Memory Utilization" type: query alert - query: "avg(last_15m):avg:system.mem.pct_usable{stage:${ stage }} by {host,stage,tenant,environment} < 0.1" + query: "avg(last_15m):avg:system.mem.pct_usable{stage:${ stage }} by {host,stage,tenant,environment,team} < 0.1" message: |- Running out of free memory on ({{host.name}} {{host.ip}}) escalation_message: "" @@ -90,7 +90,7 @@ host-high-mem-use: host-high-load-avg: name: "(Host) ${tenant} ${ stage } - High System Load Average" type: metric alert - query: "avg(last_30m):avg:system.load.norm.5{stage:${ stage }} by {host,stage,tenant,environment} > 0.8" + query: "avg(last_30m):avg:system.load.norm.5{stage:${ stage }} by {host,stage,tenant,environment,team} > 0.8" message: |- ({{tenant.name}}-{{environment.name}}-{{stage.name}}) Load average is high on ({{host.name}} {{host.ip}}) escalation_message: "" diff --git a/modules/datadog-monitor/catalog/monitors/k8s.yaml b/modules/datadog-monitor/catalog/monitors/k8s.yaml index 234c36be9..f0d6f1dc6 100644 --- a/modules/datadog-monitor/catalog/monitors/k8s.yaml +++ b/modules/datadog-monitor/catalog/monitors/k8s.yaml @@ -5,7 +5,7 @@ k8s-deployment-replica-pod-down: name: "(k8s) ${tenant} ${ stage } - Deployment Replica Pod is down" type: query alert query: | - avg(last_15m):avg:kubernetes_state.deployment.replicas_desired{stage:${ stage }} by {cluster_name,deployment,stage,tenant,environment} - avg:kubernetes_state.deployment.replicas_ready{stage:${ stage }} by {cluster_name,deployment,stage,tenant,environment} >= 2 + avg(last_15m):avg:kubernetes_state.deployment.replicas_desired{stage:${ stage }} by {cluster_name,deployment,stage,tenant,environment,team} - avg:kubernetes_state.deployment.replicas_ready{stage:${ stage }} by {cluster_name,deployment,stage,tenant,environment,team} >= 2 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] More than one Deployments Replica's pods are down on {{deployment.name}} escalation_message: "" @@ -31,7 +31,7 @@ k8s-pod-restarting: name: "(k8s) ${tenant} ${ stage } - Pods are restarting multiple times" type: query alert query: | - change(sum(last_5m),last_5m):exclude_null(avg:kubernetes.containers.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod_name,stage,tenant,environment}) > 5 + change(sum(last_5m),last_5m):exclude_null(avg:kubernetes.containers.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod_name,stage,tenant,environment,team}) > 5 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] pod {{pod_name.name}} is restarting multiple times on {{kube_namespace.name}} escalation_message: "" @@ -58,7 +58,7 @@ k8s-statefulset-replica-down: name: "(k8s) ${tenant} ${ stage } - StatefulSet Replica Pod is down" type: query alert query: | - max(last_15m):sum:kubernetes_state.statefulset.replicas_desired{stage:${ stage }} by {cluster_name,kube_namespace,statefulset,stage,tenant,environment} - sum:kubernetes_state.statefulset.replicas_ready{stage:${ stage }} by {cluster_name,kube_namespace,statefulset,stage,tenant,environment} >= 2 + max(last_15m):sum:kubernetes_state.statefulset.replicas_desired{stage:${ stage }} by {cluster_name,kube_namespace,statefulset,stage,tenant,environment,team} - sum:kubernetes_state.statefulset.replicas_ready{stage:${ stage }} by {cluster_name,kube_namespace,statefulset,stage,tenant,environment,team} >= 2 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}} {{statefulset.name}}] More than one StatefulSet Replica's pods are down on {{kube_namespace.name}} escalation_message: "" @@ -86,7 +86,7 @@ k8s-daemonset-pod-down: name: "(k8s) ${tenant} ${ stage } - DaemonSet Pod is down" type: query alert query: | - max(last_15m):sum:kubernetes_state.daemonset.desired{stage:${ stage }} by {cluster_name,kube_namespace,daemonset,stage,tenant,environment} - sum:kubernetes_state.daemonset.ready{stage:${ stage }} by {cluster_name,kube_namespace,daemonset,stage,tenant,environment} >= 1 + max(last_15m):sum:kubernetes_state.daemonset.desired{stage:${ stage }} by {cluster_name,kube_namespace,daemonset,stage,tenant,environment,team} - sum:kubernetes_state.daemonset.ready{stage:${ stage }} by {cluster_name,kube_namespace,daemonset,stage,tenant,environment,team} >= 1 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}} {{daemonset.name}}] One or more DaemonSet pods are down on {{kube_namespace.name}} escalation_message: "" @@ -112,7 +112,7 @@ k8s-crashloopBackOff: name: "(k8s) ${tenant} ${ stage } - CrashloopBackOff detected" type: query alert query: | - max(last_10m):max:kubernetes_state.container.status_report.count.waiting{stage:${ stage },reason:crashloopbackoff} by {cluster_name,kube_namespace,pod_name,stage,tenant,environment} >= 1 + max(last_10m):max:kubernetes_state.container.status_report.count.waiting{stage:${ stage },reason:crashloopbackoff} by {cluster_name,kube_namespace,pod_name,stage,tenant,environment,team} >= 1 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] pod {{pod_name.name}} is CrashloopBackOff on {{kube_namespace.name}} escalation_message: "" @@ -138,7 +138,7 @@ k8s-multiple-pods-failing: name: "(k8s) ${tenant} ${ stage } - Multiple Pods are failing" type: query alert query: | - change(avg(last_5m),last_5m):sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:failed} by {cluster_name,kube_namespace,stage,tenant,environment} > 10 + change(avg(last_5m),last_5m):sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:failed} by {cluster_name,kube_namespace,stage,tenant,environment,team} > 10 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] More than ten pods are failing on {{kube_namespace.name}} escalation_message: "" @@ -165,7 +165,7 @@ k8s-unavailable-deployment-replica: name: "(k8s) ${tenant} ${ stage } - Unavailable Deployment Replica(s) detected" type: metric alert query: | - max(last_10m):max:kubernetes_state.deployment.replicas_unavailable{stage:${ stage }} by {cluster_name,kube_namespace,stage,tenant,environment} > 0 + max(last_10m):max:kubernetes_state.deployment.replicas_unavailable{stage:${ stage }} by {cluster_name,kube_namespace,stage,tenant,environment,team} > 0 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] Detected unavailable Deployment replicas for longer than 10 minutes on {{kube_namespace.name}} escalation_message: "" @@ -196,7 +196,7 @@ k8s-unavailable-statefulset-replica: name: "(k8s) ${tenant} ${ stage } - Unavailable Statefulset Replica(s) detected" type: metric alert query: | - max(last_10m):max:kubernetes_state.statefulset.replicas_unavailable{stage:${ stage }} by {cluster_name,kube_namespace,stage,tenant,environment} > 0 + max(last_10m):max:kubernetes_state.statefulset.replicas_unavailable{stage:${ stage }} by {cluster_name,kube_namespace,stage,tenant,environment,team} > 0 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] Detected unavailable Statefulset replicas for longer than 10 minutes on {{kube_namespace.name}} escalation_message: "" @@ -227,7 +227,7 @@ k8s-node-status-unschedulable: name: "(k8s) ${tenant} ${ stage } - Detected Unschedulable Node(s)" type: query alert query: | - max(last_15m):sum:kubernetes_state.node.status{stage:${ stage },status:schedulable} by {cluster_name} * 100 / sum:kubernetes_state.node.status{stage:${ stage }} by {cluster_name,stage,tenant,environment} < 80 + max(last_15m):sum:kubernetes_state.node.status{stage:${ stage },status:schedulable} by {cluster_name} * 100 / sum:kubernetes_state.node.status{stage:${ stage }} by {cluster_name,stage,tenant,environment,team} < 80 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] More than 20% of nodes are unschedulable on the cluster. \n Keep in mind that this might be expected based on your infrastructure. escalation_message: "" @@ -258,7 +258,7 @@ k8s-imagepullbackoff: name: "(k8s) ${tenant} ${ stage } - ImagePullBackOff detected" type: "query alert" query: | - max(last_10m):max:kubernetes_state.container.status_report.count.waiting{reason:imagepullbackoff,stage:${ stage }} by {kube_cluster_name,kube_namespace,pod_name,stage,tenant,environment} >= 1 + max(last_10m):max:kubernetes_state.container.status_report.count.waiting{reason:imagepullbackoff,stage:${ stage }} by {kube_cluster_name,kube_namespace,pod_name,stage,tenant,environment,team} >= 1 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] Pod {{pod_name.name}} is ImagePullBackOff on namespace {{kube_namespace.name}} escalation_message: "" @@ -289,7 +289,7 @@ k8s-high-cpu-usage: name: "(k8s) ${tenant} ${ stage } - High CPU Usage Detected" type: metric alert query: | - avg(last_10m):avg:system.cpu.system{stage:${ stage }} by {host,stage,tenant,environment} > 90 + avg(last_10m):avg:system.cpu.system{stage:${ stage }} by {host,stage,tenant,environment,team} > 90 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}} {{host.cluster_name}}] High CPU usage for the last 10 minutes on {{host.name}} escalation_message: "" @@ -320,7 +320,7 @@ k8s-high-disk-usage: name: "(k8s) ${tenant} ${ stage } - High Disk Usage Detected" type: metric alert query: | - min(last_5m):min:system.disk.used{stage:${ stage }} by {host,cluster_name,stage,tenant,environment} / avg:system.disk.total{stage:${ stage }} by {host,cluster_name,stage,tenant,environment} * 100 > 90 + min(last_5m):min:system.disk.used{stage:${ stage }} by {host,cluster_name,stage,tenant,environment,team} / avg:system.disk.total{stage:${ stage }} by {host,cluster_name,stage,tenant,environment,team} * 100 > 90 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] High disk usage detected on {{host.name}} escalation_message: "" @@ -351,7 +351,7 @@ k8s-high-memory-usage: name: "(k8s) ${tenant} ${ stage } - High Memory Usage Detected" type: metric alert query: | - avg(last_10m):avg:kubernetes.memory.usage_pct{stage:${ stage }} by {cluster_name,stage,tenant,environment} > 90 + avg(last_10m):avg:kubernetes.memory.usage_pct{stage:${ stage }} by {cluster_name,stage,tenant,environment,team} > 90 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] High memory usage detected on {{host.name}} {{#is_warning}} @@ -388,7 +388,7 @@ k8s-high-filesystem-usage: name: "(k8s) ${tenant} ${ stage } - High Filesystem Usage Detected" type: metric alert query: | - avg(last_10m):avg:kubernetes.filesystem.usage_pct{stage:${ stage }} by {cluster_name,stage,tenant,environment} > 90 + avg(last_10m):avg:kubernetes.filesystem.usage_pct{stage:${ stage }} by {cluster_name,stage,tenant,environment,team} > 90 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] {{#is_warning}} @@ -425,7 +425,7 @@ k8s-network-tx-errors: name: "(k8s) ${tenant} ${ stage } - High Network TX (send) Errors" type: metric alert query: | - avg(last_10m):avg:kubernetes.network.tx_errors{stage:${ stage }} by {cluster_name,stage,tenant,environment} > 100 + avg(last_10m):avg:kubernetes.network.tx_errors{stage:${ stage }} by {cluster_name,stage,tenant,environment,team} > 100 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] {{#is_warning}} @@ -462,7 +462,7 @@ k8s-network-rx-errors: name: "(k8s) ${tenant} ${ stage } - High Network RX (receive) Errors" type: metric alert query: | - avg(last_10m):avg:kubernetes.network.rx_errors{stage:${ stage }} by {cluster_name,stage,tenant,environment} > 100 + avg(last_10m):avg:kubernetes.network.rx_errors{stage:${ stage }} by {cluster_name,stage,tenant,environment,team} > 100 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] {{#is_warning}} @@ -499,7 +499,7 @@ k8s-increased-pod-crash: name: "(k8s) ${tenant} ${ stage } - Increased Pod Crashes" type: query alert query: | - avg(last_5m):avg:kubernetes_state.container.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod,stage,tenant,environment} - hour_before(avg:kubernetes_state.container.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod,stage,tenant,environment}) > 3 + avg(last_5m):avg:kubernetes_state.container.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod,stage,tenant,environment,team} - hour_before(avg:kubernetes_state.container.restarts{stage:${ stage }} by {cluster_name,kube_namespace,pod,stage,tenant,environment,team}) > 3 message: |- ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}} {{kube_namespace.name}} {{pod.name}}] has crashed repeatedly over the last hour escalation_message: "" @@ -530,7 +530,7 @@ k8s-pending-pods: name: "(k8s) ${tenant} ${ stage } - Pending Pods" type: metric alert query: | - min(last_30m):sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:running} by {cluster_name,stage,tenant,environment} - sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:running} by {cluster_name,stage,tenant,environment} + sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:pending} by {cluster_name,stage,tenant,environment}.fill(zero) >= 1 + min(last_30m):sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:running} by {cluster_name,stage,tenant,environment,team} - sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:running} by {cluster_name,stage,tenant,environment,team} + sum:kubernetes_state.pod.status_phase{stage:${ stage },phase:pending} by {cluster_name,stage,tenant,environment,team}.fill(zero) >= 1 message: |- ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{cluster_name.name}}] There has been at least 1 pod Pending for 30 minutes. There are currently ({{value}}) pods Pending. diff --git a/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml b/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml index 6c3a9d5b4..2e6299c2b 100644 --- a/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml +++ b/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml @@ -2,7 +2,7 @@ rabbitmq-messages-unacknowledged-rate-too-high: name: "[RabbitMQ] ${tenant} ${ stage } - Messages unacknowledged rate is higher than usual on: {{broker.name}}" type: "query alert" query: | - avg(last_4h):anomalies(avg:aws.amazonmq.message_unacknowledged_count{stage:${ stage }} by {broker,queue,stage,tenant,environment}, 'agile', 2, direction='above', alert_window='last_15m', interval=60, count_default_zero='true', seasonality='hourly') >= 1 + avg(last_4h):anomalies(avg:aws.amazonmq.message_unacknowledged_count{stage:${ stage }} by {broker,queue,stage,tenant,environment,team}, 'agile', 2, direction='above', alert_window='last_15m', interval=60, count_default_zero='true', seasonality='hourly') >= 1 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) The rate at which messages are being delivered without receiving acknowledgement is higher than usual. @@ -38,7 +38,7 @@ rabbitmq-memory-utilization: name: "[RabbitMQ] ${tenant} ${ stage } - Memory Utilization: {{broker.name}}" type: "query alert" query: | - avg(last_10m):avg:aws.amazonmq.rabbit_mqmem_used{stage:${ stage }} by {broker,node,stage,tenant,environment} / avg:aws.amazonmq.rabbit_mqmem_limit{stage:${ stage }} by {broker,node,stage,tenant,environment} > 0.50 + avg(last_10m):avg:aws.amazonmq.rabbit_mqmem_used{stage:${ stage }} by {broker,node,stage,tenant,environment,team} / avg:aws.amazonmq.rabbit_mqmem_limit{stage:${ stage }} by {broker,node,stage,tenant,environment,team} > 0.50 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) Memory Percentage of a node in Rabbit MQ Cluster is high @@ -72,7 +72,7 @@ rabbitmq-disk-utilization: name: "[RabbitMQ] ${tenant} ${ stage } - Disk Utilization: {{broker.name}}" type: "query alert" query: | - avg(last_10m):avg:aws.amazonmq.rabbit_mqdisk_free{stage:${ stage }} by {broker,node,stage,tenant,environment} < 100000000000 + avg(last_10m):avg:aws.amazonmq.rabbit_mqdisk_free{stage:${ stage }} by {broker,node,stage,tenant,environment,team} < 100000000000 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) Free Disk Space of a node in Rabbit MQ Cluster is Less than 100 GB diff --git a/modules/datadog-monitor/catalog/monitors/rds.yaml b/modules/datadog-monitor/catalog/monitors/rds.yaml index 57b23c16d..d6828923b 100644 --- a/modules/datadog-monitor/catalog/monitors/rds.yaml +++ b/modules/datadog-monitor/catalog/monitors/rds.yaml @@ -5,7 +5,7 @@ rds-cpuutilization: name: "(RDS) ${tenant} ${ stage } - CPU Utilization above 90%" type: metric alert query: | - avg(last_15m):avg:aws.rds.cpuutilization{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment} > 90 + avg(last_15m):avg:aws.rds.cpuutilization{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment,team} > 90 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} @@ -42,7 +42,7 @@ rds-disk-queue-depth: name: "(RDS) ${tenant} ${ stage } - Disk queue depth above 64" type: metric alert query: | - avg(last_15m):avg:aws.rds.disk_queue_depth{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment} > 64 + avg(last_15m):avg:aws.rds.disk_queue_depth{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment,team} > 64 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} @@ -116,7 +116,7 @@ rds-swap-usage: name: "(RDS) ${tenant} ${ stage } - Swap usage above 256 MB" type: metric alert query: | - avg(last_15m):avg:aws.rds.swap_usage{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment} > 256000000 + avg(last_15m):avg:aws.rds.swap_usage{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment,team} > 256000000 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} From a954fbb5796f16a256a7b82f0fb4e0e44347d9a6 Mon Sep 17 00:00:00 2001 From: Kevin Mahoney Date: Thu, 2 Mar 2023 12:49:12 -0500 Subject: [PATCH 054/501] `datadog-lambda-forwarder`: if s3_buckets not set, module fails (#581) Co-authored-by: cloudpossebot --- modules/datadog-lambda-forwarder/README.md | 5 +++-- modules/datadog-lambda-forwarder/main.tf | 3 ++- modules/datadog-lambda-forwarder/variables.tf | 10 ++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 0b3a5d91a..8d263ce0d 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -64,7 +64,7 @@ components: |------|--------|---------| | [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.1.0 | +| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.2.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [log\_group\_prefix](#module\_log\_group\_prefix) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -129,7 +129,8 @@ components: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [s3\_bucket\_kms\_arns](#input\_s3\_bucket\_kms\_arns) | List of KMS key ARNs for s3 bucket encryption | `list(string)` | `[]` | no | -| [s3\_buckets](#input\_s3\_buckets) | The names and ARNs of S3 buckets to forward logs to Datadog | `list(string)` | `null` | no | +| [s3\_buckets](#input\_s3\_buckets) | The names of S3 buckets to forward logs to Datadog | `list(string)` | `[]` | no | +| [s3\_buckets\_with\_prefixes](#input\_s3\_buckets\_with\_prefixes) | The names S3 buckets and prefix to forward logs to Datadog | `map(object({ bucket_name : string, bucket_prefix : string }))` | `{}` | no | | [security\_group\_ids](#input\_security\_group\_ids) | List of security group IDs to use when the Lambda Function runs in a VPC | `list(string)` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [subnet\_ids](#input\_subnet\_ids) | List of subnet IDs to use when deploying the Lambda Function in a VPC | `list(string)` | `null` | no | diff --git a/modules/datadog-lambda-forwarder/main.tf b/modules/datadog-lambda-forwarder/main.tf index 4e323bb58..90249f919 100644 --- a/modules/datadog-lambda-forwarder/main.tf +++ b/modules/datadog-lambda-forwarder/main.tf @@ -40,7 +40,7 @@ module "log_group_prefix" { module "datadog_lambda_forwarder" { source = "cloudposse/datadog-lambda-forwarder/aws" - version = "1.1.0" + version = "1.2.0" cloudwatch_forwarder_log_groups = local.cloudwatch_forwarder_log_groups dd_api_key_kms_ciphertext_blob = var.dd_api_key_kms_ciphertext_blob @@ -72,6 +72,7 @@ module "datadog_lambda_forwarder" { lambda_runtime = var.lambda_runtime s3_bucket_kms_arns = var.s3_bucket_kms_arns s3_buckets = var.s3_buckets + s3_buckets_with_prefixes = var.s3_buckets_with_prefixes security_group_ids = var.security_group_ids subnet_ids = var.subnet_ids tracing_config_mode = var.tracing_config_mode diff --git a/modules/datadog-lambda-forwarder/variables.tf b/modules/datadog-lambda-forwarder/variables.tf index 2e5d3be3d..f7be4190b 100644 --- a/modules/datadog-lambda-forwarder/variables.tf +++ b/modules/datadog-lambda-forwarder/variables.tf @@ -90,8 +90,14 @@ variable "kms_key_id" { variable "s3_buckets" { type = list(string) - description = "The names and ARNs of S3 buckets to forward logs to Datadog" - default = null + description = "The names of S3 buckets to forward logs to Datadog" + default = [] +} + +variable "s3_buckets_with_prefixes" { + type = map(object({ bucket_name : string, bucket_prefix : string })) + description = "The names S3 buckets and prefix to forward logs to Datadog" + default = {} } variable "s3_bucket_kms_arns" { From 5b011fec1cf2f4c27ddfbed8743ba4e7254e85a2 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 2 Mar 2023 10:07:25 -0800 Subject: [PATCH 055/501] bugfix: rds anomalies monitor not sending team information (#583) --- modules/datadog-monitor/catalog/monitors/rds.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/datadog-monitor/catalog/monitors/rds.yaml b/modules/datadog-monitor/catalog/monitors/rds.yaml index d6828923b..eb8765575 100644 --- a/modules/datadog-monitor/catalog/monitors/rds.yaml +++ b/modules/datadog-monitor/catalog/monitors/rds.yaml @@ -153,7 +153,7 @@ rds-database-connections: name: "(RDS) ${tenant} ${ stage } - Anomaly of a large variance in RDS connection count" type: metric alert query: | - avg(last_4h):anomalies(avg:aws.rds.database_connections{stage:${ stage }}, 'basic', 2, direction='both', alert_window='last_15m', interval=60, count_default_zero='true') >= 1 + avg(last_4h):anomalies(avg:aws.rds.database_connections{stage:${ stage }} by {dbinstanceidentifier,stage,tenant,environment,team}, 'basic', 2, direction='both', interval=60, alert_window='last_15m', count_default_zero='true') >= 1 message: | ({{tenant.name}}-{{environment.name}}-{{stage.name}}) {{#is_warning}} From 1d4f5f7aadc3df2f780f28e4cf35326f10175195 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Sun, 5 Mar 2023 13:23:46 -0700 Subject: [PATCH 056/501] Upgrade Remote State to `1.4.1` (#585) --- .../account-map/modules/roles-to-principals/main.tf | 2 +- .../modules/team-assume-role-policy/README.md | 2 +- .../github-assume-role-policy.mixin.tf | 2 +- modules/acm/README.md | 2 +- modules/acm/remote-state.tf | 2 +- modules/alb/README.md | 4 ++-- modules/alb/remote-state.tf | 4 ++-- modules/aurora-mysql-resources/README.md | 2 +- modules/aurora-mysql-resources/remote-state.tf | 2 +- modules/aurora-mysql/README.md | 10 +++++----- modules/aurora-mysql/remote-state.tf | 10 +++++----- modules/aurora-postgres-resources/README.md | 2 +- modules/aurora-postgres-resources/remote-state.tf | 2 +- modules/aurora-postgres/README.md | 8 ++++---- modules/aurora-postgres/remote-state.tf | 8 ++++---- modules/aws-sso/README.md | 4 ++-- modules/aws-sso/remote-state.tf | 4 ++-- modules/aws-team-roles/README.md | 2 +- modules/aws-team-roles/remote-state.tf | 2 +- modules/aws-teams/README.md | 4 ++-- modules/aws-teams/remote-state.tf | 4 ++-- modules/bastion/README.md | 2 +- modules/bastion/remote-state.tf | 2 +- modules/cloudtrail/README.md | 2 +- modules/cloudtrail/remote-state.tf | 2 +- modules/datadog-agent/README.md | 2 +- modules/datadog-agent/remote-state.tf | 2 +- .../modules/datadog_keys/main.tf | 2 +- modules/datadog-lambda-forwarder/README.md | 2 +- modules/datadog-lambda-forwarder/remote-state.tf | 2 +- modules/datadog-private-location-ecs/README.md | 4 ++-- modules/datadog-private-location-ecs/remote-state.tf | 4 ++-- .../datadog-synthetics-private-location/README.md | 2 +- .../remote-state.tf | 2 +- modules/dms/replication-instance/README.md | 2 +- modules/dms/replication-instance/remote-state.tf | 2 +- modules/dms/replication-task/README.md | 6 +++--- modules/dms/replication-task/remote-state.tf | 6 +++--- modules/dns-delegated/README.md | 4 ++-- modules/dns-delegated/remote-state.tf | 4 ++-- modules/documentdb/README.md | 8 ++++---- modules/documentdb/remote-state.tf | 8 ++++---- modules/ec2-client-vpn/README.md | 2 +- modules/ec2-client-vpn/remote-state.tf | 2 +- modules/ecs-service/README.md | 6 +++--- modules/ecs-service/remote-state.tf | 6 +++--- modules/ecs/README.md | 4 ++-- modules/ecs/remote-state.tf | 4 ++-- modules/eks/actions-runner-controller/README.md | 2 +- .../eks/actions-runner-controller/remote-state.tf | 2 +- modules/eks/alb-controller-ingress-class/README.md | 2 +- .../eks/alb-controller-ingress-class/remote-state.tf | 2 +- modules/eks/alb-controller-ingress-group/README.md | 8 ++++---- .../eks/alb-controller-ingress-group/remote-state.tf | 8 ++++---- modules/eks/alb-controller/README.md | 2 +- modules/eks/alb-controller/remote-state.tf | 2 +- modules/eks/aws-node-termination-handler/README.md | 2 +- .../eks/aws-node-termination-handler/remote-state.tf | 2 +- modules/eks/cert-manager/README.md | 4 ++-- modules/eks/cert-manager/remote-state.tf | 4 ++-- modules/eks/cluster/README.md | 10 +++++----- modules/eks/cluster/remote-state.tf | 10 +++++----- modules/eks/echo-server/README.md | 2 +- modules/eks/echo-server/remote-state.tf | 2 +- modules/eks/efs-controller/README.md | 4 ++-- modules/eks/efs-controller/remote-state.tf | 4 ++-- modules/eks/efs/README.md | 6 +++--- modules/eks/efs/remote-state.tf | 6 +++--- modules/eks/eks-without-spotinst/README.md | 10 +++++----- modules/eks/eks-without-spotinst/remote-state.tf | 10 +++++----- modules/eks/external-dns/README.md | 4 ++-- modules/eks/external-dns/remote-state.tf | 4 ++-- modules/eks/idp-roles/README.md | 2 +- modules/eks/idp-roles/remote-state.tf | 2 +- modules/eks/karpenter-provisioner/README.md | 4 ++-- modules/eks/karpenter-provisioner/remote-state.tf | 4 ++-- modules/eks/karpenter/README.md | 2 +- modules/eks/karpenter/remote-state.tf | 2 +- modules/eks/metrics-server/README.md | 2 +- modules/eks/metrics-server/remote-state.tf | 2 +- modules/eks/redis-operator/README.md | 2 +- modules/eks/redis-operator/remote-state.tf | 2 +- modules/eks/redis/README.md | 2 +- modules/eks/redis/remote-state.tf | 2 +- modules/eks/reloader/README.md | 2 +- modules/eks/reloader/remote-state.tf | 2 +- modules/elasticache-redis/README.md | 8 ++++---- modules/elasticache-redis/remote-state.tf | 8 ++++---- modules/elasticsearch/README.md | 4 ++-- modules/elasticsearch/remote-state.tf | 4 ++-- modules/github-runners/README.md | 4 ++-- modules/github-runners/remote-state.tf | 4 ++-- modules/global-accelerator-endpoint-group/README.md | 2 +- .../remote-state.tf | 2 +- modules/global-accelerator/README.md | 2 +- modules/global-accelerator/remote-state.tf | 2 +- modules/mq-broker/README.md | 4 ++-- modules/mq-broker/remote-state.tf | 4 ++-- modules/mwaa/README.md | 4 ++-- modules/mwaa/remote-state.tf | 4 ++-- modules/rds/README.md | 6 +++--- modules/rds/remote-state.tf | 6 +++--- modules/s3-bucket/README.md | 2 +- modules/s3-bucket/remote-state.tf | 2 +- modules/ses/README.md | 2 +- modules/ses/remote-state.tf | 2 +- modules/sftp/README.md | 2 +- modules/sftp/remote-state.tf | 2 +- modules/snowflake-account/README.md | 2 +- modules/snowflake-account/remote-state.tf | 2 +- modules/snowflake-database/README.md | 2 +- modules/snowflake-database/remote-state.tf | 2 +- modules/spa-s3-cloudfront/README.md | 6 +++--- modules/spa-s3-cloudfront/remote-state.tf | 6 +++--- modules/spacelift-worker-pool/README.md | 6 +++--- modules/spacelift-worker-pool/remote-state.tf | 6 +++--- modules/tgw/cross-region-hub-connector/README.md | 6 +++--- .../tgw/cross-region-hub-connector/remote-state.tf | 6 +++--- modules/tgw/cross-region-spoke/README.md | 12 ++++++------ modules/tgw/cross-region-spoke/remote-state.tf | 12 ++++++------ modules/tgw/hub/README.md | 6 +++--- modules/tgw/hub/remote-state.tf | 6 +++--- modules/tgw/spoke/README.md | 2 +- modules/tgw/spoke/remote-state.tf | 2 +- modules/vpc-peering/README.md | 2 +- modules/vpc-peering/remote-state.tf | 2 +- modules/vpc/README.md | 2 +- modules/vpc/remote-state.tf | 2 +- 128 files changed, 246 insertions(+), 246 deletions(-) diff --git a/modules/account-map/modules/roles-to-principals/main.tf b/modules/account-map/modules/roles-to-principals/main.tf index 4a22086a3..42fe98e0c 100644 --- a/modules/account-map/modules/roles-to-principals/main.tf +++ b/modules/account-map/modules/roles-to-principals/main.tf @@ -10,7 +10,7 @@ module "always" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "account-map" privileged = var.privileged diff --git a/modules/account-map/modules/team-assume-role-policy/README.md b/modules/account-map/modules/team-assume-role-policy/README.md index bc15cfdaa..e199cf58f 100644 --- a/modules/account-map/modules/team-assume-role-policy/README.md +++ b/modules/account-map/modules/team-assume-role-policy/README.md @@ -47,7 +47,7 @@ No requirements. |------|--------|---------| | [allowed\_role\_map](#module\_allowed\_role\_map) | ../../../account-map/modules/roles-to-principals | n/a | | [denied\_role\_map](#module\_denied\_role\_map) | ../../../account-map/modules/roles-to-principals | n/a | -| [github\_oidc\_provider](#module\_github\_oidc\_provider) | cloudposse/stack-config/yaml//modules/remote-state | 1.0.0 | +| [github\_oidc\_provider](#module\_github\_oidc\_provider) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf b/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf index 88fa9de73..03b2be3d6 100644 --- a/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf +++ b/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf @@ -60,7 +60,7 @@ module "github_oidc_provider" { count = local.github_oidc_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.0.0" + version = "1.4.1" component = "github-oidc-provider" environment = var.global_environment_name diff --git a/modules/acm/README.md b/modules/acm/README.md index a5ee83f98..ac6407386 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -66,7 +66,7 @@ components: |------|--------|---------| | [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.16.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/acm/remote-state.tf b/modules/acm/remote-state.tf index f00321dc2..f6e7dc3de 100644 --- a/modules/acm/remote-state.tf +++ b/modules/acm/remote-state.tf @@ -1,6 +1,6 @@ module "private_ca" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" count = local.private_ca_enabled ? 1 : 0 diff --git a/modules/alb/README.md b/modules/alb/README.md index b0298df12..734726c3e 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -36,8 +36,8 @@ No providers. |------|--------|---------| | [alb](#module\_alb) | cloudposse/alb/aws | 1.4.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [remote\_dns](#module\_remote\_dns) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [remote\_vpc](#module\_remote\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [remote\_dns](#module\_remote\_dns) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [remote\_vpc](#module\_remote\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/alb/remote-state.tf b/modules/alb/remote-state.tf index 6fb1e0e68..fb2c54aa4 100644 --- a/modules/alb/remote-state.tf +++ b/modules/alb/remote-state.tf @@ -1,6 +1,6 @@ module "remote_vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "remote_vpc" { module "remote_dns" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "dns-delegated" diff --git a/modules/aurora-mysql-resources/README.md b/modules/aurora-mysql-resources/README.md index 3daec0034..25131baa2 100644 --- a/modules/aurora-mysql-resources/README.md +++ b/modules/aurora-mysql-resources/README.md @@ -71,7 +71,7 @@ components: |------|--------|---------| | [additional\_grants](#module\_additional\_grants) | ./modules/mysql-user | n/a | | [additional\_users](#module\_additional\_users) | ./modules/mysql-user | n/a | -| [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aurora-mysql-resources/remote-state.tf b/modules/aurora-mysql-resources/remote-state.tf index d52bbe6cd..921a71b2e 100644 --- a/modules/aurora-mysql-resources/remote-state.tf +++ b/modules/aurora-mysql-resources/remote-state.tf @@ -1,6 +1,6 @@ module "aurora_mysql" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.aurora_mysql_component_name diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index 8747e5925..73ef2ba1b 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -171,15 +171,15 @@ Reploying the component should show no changes. For example, `atmos terraform ap |------|--------|---------| | [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/rds-cluster/aws | 1.3.1 | | [cluster](#module\_cluster) | cloudposse/label/null | 0.25.0 | -| [dns-delegated](#module\_dns-delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [dns-delegated](#module\_dns-delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | | [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | -| [primary\_cluster](#module\_primary\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [primary\_cluster](#module\_primary\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/aurora-mysql/remote-state.tf b/modules/aurora-mysql/remote-state.tf index d4287a49e..c48a82448 100644 --- a/modules/aurora-mysql/remote-state.tf +++ b/modules/aurora-mysql/remote-state.tf @@ -4,7 +4,7 @@ locals { module "dns-delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "dns-delegated" environment = "gbl" @@ -14,7 +14,7 @@ module "dns-delegated" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" for_each = var.eks_component_names @@ -25,7 +25,7 @@ module "eks" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "vpc" @@ -34,7 +34,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" for_each = local.accounts_with_vpc @@ -49,7 +49,7 @@ module "vpc_ingress" { module "primary_cluster" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" count = local.remote_read_replica_enabled ? 1 : 0 diff --git a/modules/aurora-postgres-resources/README.md b/modules/aurora-postgres-resources/README.md index 6e8bc0e9f..36eb14e82 100644 --- a/modules/aurora-postgres-resources/README.md +++ b/modules/aurora-postgres-resources/README.md @@ -47,7 +47,7 @@ components: |------|--------|---------| | [additional\_grants](#module\_additional\_grants) | ./modules/postgresql-user | n/a | | [additional\_users](#module\_additional\_users) | ./modules/postgresql-user | n/a | -| [aurora\_postgres](#module\_aurora\_postgres) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [aurora\_postgres](#module\_aurora\_postgres) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aurora-postgres-resources/remote-state.tf b/modules/aurora-postgres-resources/remote-state.tf index 6ffd77e85..2f18e4fcf 100644 --- a/modules/aurora-postgres-resources/remote-state.tf +++ b/modules/aurora-postgres-resources/remote-state.tf @@ -1,6 +1,6 @@ module "aurora_postgres" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.aurora_postgres_component_name diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index fda87583a..f1a115217 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -103,14 +103,14 @@ components: |------|--------|---------| | [aurora\_postgres\_cluster](#module\_aurora\_postgres\_cluster) | cloudposse/rds-cluster/aws | 1.3.2 | | [cluster](#module\_cluster) | cloudposse/label/null | 0.25.0 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | | [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/aurora-postgres/remote-state.tf b/modules/aurora-postgres/remote-state.tf index 0551f6d60..1ee16d716 100644 --- a/modules/aurora-postgres/remote-state.tf +++ b/modules/aurora-postgres/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" for_each = { for i, account in var.allow_ingress_from_vpc_accounts : @@ -27,7 +27,7 @@ module "vpc_ingress" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" for_each = local.eks_security_group_enabled ? var.eks_component_names : toset([]) component = each.value @@ -38,7 +38,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "dns-delegated" environment = var.dns_gbl_delegated_environment_name diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index 2d212d487..202b6b2c5 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -121,13 +121,13 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 0.7.1 | | [role\_prefix](#module\_role\_prefix) | cloudposse/label/null | 0.25.0 | | [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 0.7.1 | | [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 0.7.1 | -| [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/aws-sso/remote-state.tf b/modules/aws-sso/remote-state.tf index 511171b50..838324082 100644 --- a/modules/aws-sso/remote-state.tf +++ b/modules/aws-sso/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "account-map" environment = var.global_environment_name @@ -12,7 +12,7 @@ module "account_map" { module "tfstate" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "tfstate-backend" environment = var.tfstate_environment_name diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index d56ee37ab..420226cc6 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -125,7 +125,7 @@ components: | Name | Source | Version | |------|--------|---------| | [assume\_role](#module\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | -| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.0.0 | +| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aws-team-roles/remote-state.tf b/modules/aws-team-roles/remote-state.tf index 7f95d1ba6..a81c9b931 100644 --- a/modules/aws-team-roles/remote-state.tf +++ b/modules/aws-team-roles/remote-state.tf @@ -1,6 +1,6 @@ module "aws_saml" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.0.0" + version = "1.4.1" component = "aws-saml" privileged = true diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index 7c492d155..fa4e184ca 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -147,9 +147,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.0.0 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [assume\_role](#module\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | -| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.0.0 | +| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aws-teams/remote-state.tf b/modules/aws-teams/remote-state.tf index 8672fe26d..a2a12059c 100644 --- a/modules/aws-teams/remote-state.tf +++ b/modules/aws-teams/remote-state.tf @@ -1,6 +1,6 @@ module "aws_saml" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.0.0" + version = "1.4.1" component = "aws-saml" privileged = true @@ -16,7 +16,7 @@ module "aws_saml" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.0.0" + version = "1.4.1" component = "account-map" environment = var.account_map_environment_name diff --git a/modules/bastion/README.md b/modules/bastion/README.md index f9a9e089c..7b75057af 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -76,7 +76,7 @@ components: | [sg](#module\_sg) | cloudposse/security-group/aws | 2.0.0 | | [ssm\_tls\_ssh\_key\_pair](#module\_ssm\_tls\_ssh\_key\_pair) | cloudposse/ssm-tls-ssh-key-pair/aws | 0.10.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/bastion/remote-state.tf b/modules/bastion/remote-state.tf index 1b1079219..3e0ccd51e 100644 --- a/modules/bastion/remote-state.tf +++ b/modules/bastion/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" diff --git a/modules/cloudtrail/README.md b/modules/cloudtrail/README.md index adc078cec..433cf8ff4 100644 --- a/modules/cloudtrail/README.md +++ b/modules/cloudtrail/README.md @@ -46,7 +46,7 @@ components: | Name | Source | Version | |------|--------|---------| | [cloudtrail](#module\_cloudtrail) | cloudposse/cloudtrail/aws | 0.21.0 | -| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.0 | +| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_cloudtrail](#module\_kms\_key\_cloudtrail) | cloudposse/kms-key/aws | 0.12.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/cloudtrail/remote-state.tf b/modules/cloudtrail/remote-state.tf index 590119fc5..ba51272a3 100644 --- a/modules/cloudtrail/remote-state.tf +++ b/modules/cloudtrail/remote-state.tf @@ -1,6 +1,6 @@ module "cloudtrail_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.0" + version = "1.4.1" component = "cloudtrail-bucket" environment = var.cloudtrail_bucket_environment_name diff --git a/modules/datadog-agent/README.md b/modules/datadog-agent/README.md index b6fdc8c05..633374c62 100644 --- a/modules/datadog-agent/README.md +++ b/modules/datadog-agent/README.md @@ -163,7 +163,7 @@ https-checks: | [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.7.0 | | [datadog\_cluster\_check\_yaml\_config](#module\_datadog\_cluster\_check\_yaml\_config) | cloudposse/config/yaml | 1.0.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [values\_merge](#module\_values\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.1 | diff --git a/modules/datadog-agent/remote-state.tf b/modules/datadog-agent/remote-state.tf index 6ef90fd26..ac55ba94c 100644 --- a/modules/datadog-agent/remote-state.tf +++ b/modules/datadog-agent/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/datadog-configuration/modules/datadog_keys/main.tf b/modules/datadog-configuration/modules/datadog_keys/main.tf index 7eb556f04..cc4c03d3b 100644 --- a/modules/datadog-configuration/modules/datadog_keys/main.tf +++ b/modules/datadog-configuration/modules/datadog_keys/main.tf @@ -26,7 +26,7 @@ locals { } module "datadog_configuration" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "datadog-configuration" diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 8d263ce0d..50aee326e 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -62,7 +62,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.2.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | diff --git a/modules/datadog-lambda-forwarder/remote-state.tf b/modules/datadog-lambda-forwarder/remote-state.tf index 341a4fc52..157792473 100644 --- a/modules/datadog-lambda-forwarder/remote-state.tf +++ b/modules/datadog-lambda-forwarder/remote-state.tf @@ -1,6 +1,6 @@ module "datadog-integration" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "datadog-integration" diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index 0216b816f..a52f2ab7f 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -80,11 +80,11 @@ components: | [container\_definition](#module\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.66.2 | -| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [roles\_to\_principals](#module\_roles\_to\_principals) | ../account-map/modules/roles-to-principals | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/datadog-private-location-ecs/remote-state.tf b/modules/datadog-private-location-ecs/remote-state.tf index 97fe10aab..d732e096c 100644 --- a/modules/datadog-private-location-ecs/remote-state.tf +++ b/modules/datadog-private-location-ecs/remote-state.tf @@ -11,7 +11,7 @@ locals { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" @@ -20,7 +20,7 @@ module "vpc" { module "ecs_cluster" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "ecs" diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index bd7dd3f2c..800a89558 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -146,7 +146,7 @@ Environment variables: |------|--------|---------| | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/helm-release/aws | 0.7.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/datadog-synthetics-private-location/remote-state.tf b/modules/datadog-synthetics-private-location/remote-state.tf index 6ef90fd26..ac55ba94c 100644 --- a/modules/datadog-synthetics-private-location/remote-state.tf +++ b/modules/datadog-synthetics-private-location/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/dms/replication-instance/README.md b/modules/dms/replication-instance/README.md index 6f206b6a5..cbb09f1e5 100644 --- a/modules/dms/replication-instance/README.md +++ b/modules/dms/replication-instance/README.md @@ -62,7 +62,7 @@ No providers. | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 1.0.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/dms/replication-instance/remote-state.tf b/modules/dms/replication-instance/remote-state.tf index 1b1079219..3e0ccd51e 100644 --- a/modules/dms/replication-instance/remote-state.tf +++ b/modules/dms/replication-instance/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" diff --git a/modules/dms/replication-task/README.md b/modules/dms/replication-task/README.md index 681ec311c..8f0a78e36 100644 --- a/modules/dms/replication-task/README.md +++ b/modules/dms/replication-task/README.md @@ -53,9 +53,9 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [dms\_endpoint\_source](#module\_dms\_endpoint\_source) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [dms\_endpoint\_target](#module\_dms\_endpoint\_target) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [dms\_replication\_instance](#module\_dms\_replication\_instance) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [dms\_endpoint\_source](#module\_dms\_endpoint\_source) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dms\_endpoint\_target](#module\_dms\_endpoint\_target) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dms\_replication\_instance](#module\_dms\_replication\_instance) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [dms\_replication\_task](#module\_dms\_replication\_task) | cloudposse/dms/aws//modules/dms-replication-task | 0.1.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/dms/replication-task/remote-state.tf b/modules/dms/replication-task/remote-state.tf index 2710ac5eb..72b46e4bf 100644 --- a/modules/dms/replication-task/remote-state.tf +++ b/modules/dms/replication-task/remote-state.tf @@ -1,6 +1,6 @@ module "dms_replication_instance" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.replication_instance_component_name @@ -9,7 +9,7 @@ module "dms_replication_instance" { module "dms_endpoint_source" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.source_endpoint_component_name @@ -18,7 +18,7 @@ module "dms_endpoint_source" { module "dms_endpoint_target" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.target_endpoint_component_name diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index 057179941..9e0e1a383 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -105,10 +105,10 @@ NOTE: With each of these workarounds, you may have an issue connecting to the se |------|--------|---------| | [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.17.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.0 | +| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 1.1.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/dns-delegated/remote-state.tf b/modules/dns-delegated/remote-state.tf index bf10b0363..02dc50a8d 100644 --- a/modules/dns-delegated/remote-state.tf +++ b/modules/dns-delegated/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.0" + version = "1.4.1" for_each = local.private_enabled ? local.vpc_environment_names : toset([]) @@ -12,7 +12,7 @@ module "vpc" { module "private_ca" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.0" + version = "1.4.1" count = local.private_ca_enabled && local.certificate_enabled ? 1 : 0 diff --git a/modules/documentdb/README.md b/modules/documentdb/README.md index d1bd87639..55c3a998a 100644 --- a/modules/documentdb/README.md +++ b/modules/documentdb/README.md @@ -44,13 +44,13 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [documentdb\_cluster](#module\_documentdb\_cluster) | cloudposse/documentdb-cluster/aws | 0.14.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/documentdb/remote-state.tf b/modules/documentdb/remote-state.tf index 838f7bd7d..e4fe005b9 100644 --- a/modules/documentdb/remote-state.tf +++ b/modules/documentdb/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" component = "eks" @@ -24,7 +24,7 @@ module "eks" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" component = "dns-delegated" @@ -33,7 +33,7 @@ module "dns_delegated" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" stack_config_local_path = "../../../stacks" component = "dns-delegated" diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index 90563a9aa..a9d5ea7b5 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -103,7 +103,7 @@ No providers. | [ec2\_client\_vpn](#module\_ec2\_client\_vpn) | cloudposse/ec2-client-vpn/aws | 0.14.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/ec2-client-vpn/remote-state.tf b/modules/ec2-client-vpn/remote-state.tf index 437923524..3e0ccd51e 100644 --- a/modules/ec2-client-vpn/remote-state.tf +++ b/modules/ec2-client-vpn/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" component = "vpc" diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 0a775d9f7..b804f45f8 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -173,16 +173,16 @@ components: | [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.66.4 | | [ecs\_cloudwatch\_autoscaling](#module\_ecs\_cloudwatch\_autoscaling) | cloudposse/ecs-cloudwatch-autoscaling/aws | 0.7.3 | | [ecs\_cloudwatch\_sns\_alarms](#module\_ecs\_cloudwatch\_sns\_alarms) | cloudposse/ecs-cloudwatch-sns-alarms/aws | 0.12.3 | -| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | | [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [logs](#module\_logs) | cloudposse/cloudwatch-logs/aws | 0.6.6 | -| [rds](#module\_rds) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [rds](#module\_rds) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [roles\_to\_principals](#module\_roles\_to\_principals) | ../account-map/modules/roles-to-principals | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vanity\_alias](#module\_vanity\_alias) | cloudposse/route53-alias/aws | 0.13.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/ecs-service/remote-state.tf b/modules/ecs-service/remote-state.tf index b21680fda..56a58b272 100644 --- a/modules/ecs-service/remote-state.tf +++ b/modules/ecs-service/remote-state.tf @@ -45,7 +45,7 @@ locals { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" @@ -55,7 +55,7 @@ module "vpc" { module "rds" { count = local.enabled && var.use_rds_client_sg ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "rds" @@ -64,7 +64,7 @@ module "rds" { module "ecs_cluster" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "ecs" diff --git a/modules/ecs/README.md b/modules/ecs/README.md index 067bb848e..ff758e68b 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -57,11 +57,11 @@ components: |------|--------|---------| | [alb](#module\_alb) | cloudposse/alb/aws | 1.5.0 | | [cluster](#module\_cluster) | cloudposse/ecs-cluster/aws | 0.2.2 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [target\_group\_label](#module\_target\_group\_label) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/ecs/remote-state.tf b/modules/ecs/remote-state.tf index d8a969005..1323cd151 100644 --- a/modules/ecs/remote-state.tf +++ b/modules/ecs/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.dns_delegated_component_name stage = var.dns_delegated_stage_name diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 61e9e0992..90e912762 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -206,7 +206,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller |------|--------|---------| | [actions\_runner](#module\_actions\_runner) | cloudposse/helm-release/aws | 0.7.0 | | [actions\_runner\_controller](#module\_actions\_runner\_controller) | cloudposse/helm-release/aws | 0.7.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/actions-runner-controller/remote-state.tf b/modules/eks/actions-runner-controller/remote-state.tf index 90c6ab1a8..ac55ba94c 100644 --- a/modules/eks/actions-runner-controller/remote-state.tf +++ b/modules/eks/actions-runner-controller/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/alb-controller-ingress-class/README.md b/modules/eks/alb-controller-ingress-class/README.md index af925ccee..6d6e07868 100644 --- a/modules/eks/alb-controller-ingress-class/README.md +++ b/modules/eks/alb-controller-ingress-class/README.md @@ -42,7 +42,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/alb-controller-ingress-class/remote-state.tf b/modules/eks/alb-controller-ingress-class/remote-state.tf index 90c6ab1a8..ac55ba94c 100644 --- a/modules/eks/alb-controller-ingress-class/remote-state.tf +++ b/modules/eks/alb-controller-ingress-class/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index c9c48434c..1d5e41867 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -53,13 +53,13 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [load\_balancer\_name](#module\_load\_balancer\_name) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/eks/alb-controller-ingress-group/remote-state.tf b/modules/eks/alb-controller-ingress-group/remote-state.tf index 8511e7f56..738022a1d 100644 --- a/modules/eks/alb-controller-ingress-group/remote-state.tf +++ b/modules/eks/alb-controller-ingress-group/remote-state.tf @@ -1,6 +1,6 @@ module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "dns-delegated" environment = var.dns_delegated_environment_name @@ -10,7 +10,7 @@ module "dns_delegated" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.eks_component_name @@ -19,7 +19,7 @@ module "eks" { module "global_accelerator" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" for_each = local.global_accelerator_enabled ? toset(["true"]) : [] @@ -31,7 +31,7 @@ module "global_accelerator" { module "waf" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" for_each = local.waf_enabled ? toset(["true"]) : [] diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 189e0d045..33aa003d7 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -68,7 +68,7 @@ components: | Name | Source | Version | |------|--------|---------| | [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.7.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/alb-controller/remote-state.tf b/modules/eks/alb-controller/remote-state.tf index 90c6ab1a8..ac55ba94c 100644 --- a/modules/eks/alb-controller/remote-state.tf +++ b/modules/eks/alb-controller/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 757cd43a1..82a9b174d 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -59,7 +59,7 @@ components: | Name | Source | Version | |------|--------|---------| | [aws\_node\_termination\_handler](#module\_aws\_node\_termination\_handler) | cloudposse/helm-release/aws | 0.5.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/aws-node-termination-handler/remote-state.tf b/modules/eks/aws-node-termination-handler/remote-state.tf index 6ef90fd26..ac55ba94c 100644 --- a/modules/eks/aws-node-termination-handler/remote-state.tf +++ b/modules/eks/aws-node-termination-handler/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index 6fb90563a..a33f333d7 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -71,8 +71,8 @@ The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` |------|--------|---------| | [cert\_manager](#module\_cert\_manager) | cloudposse/helm-release/aws | 0.7.0 | | [cert\_manager\_issuer](#module\_cert\_manager\_issuer) | cloudposse/helm-release/aws | 0.7.0 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/cert-manager/remote-state.tf b/modules/eks/cert-manager/remote-state.tf index 1901569b7..3cd649b37 100644 --- a/modules/eks/cert-manager/remote-state.tf +++ b/modules/eks/cert-manager/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "dns-delegated" environment = "gbl" diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index ab46350c9..9250822cb 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -69,17 +69,17 @@ components: | Name | Source | Version | |------|--------|---------| -| [delegated\_roles](#module\_delegated\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [delegated\_roles](#module\_delegated\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.5.0 | | [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.1.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [karpenter\_label](#module\_karpenter\_label) | cloudposse/label/null | 0.25.0 | | [region\_node\_group](#module\_region\_node\_group) | ./modules/node_group_by_region | n/a | -| [team\_roles](#module\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [team\_roles](#module\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf index bac0ec31e..33afd7f60 100644 --- a/modules/eks/cluster/remote-state.tf +++ b/modules/eks/cluster/remote-state.tf @@ -4,7 +4,7 @@ locals { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "vpc" @@ -13,7 +13,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" for_each = local.accounts_with_vpc @@ -27,7 +27,7 @@ module "vpc_ingress" { module "team_roles" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "aws-teams" @@ -40,7 +40,7 @@ module "team_roles" { module "delegated_roles" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "aws-team-roles" @@ -54,7 +54,7 @@ module "delegated_roles" { # to it rather than overwrite it (specifically the aws-auth configMap) module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index dc94bec58..b6ad9be06 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -86,7 +86,7 @@ components: | Name | Source | Version | |------|--------|---------| | [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.7.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/echo-server/remote-state.tf b/modules/eks/echo-server/remote-state.tf index 90c6ab1a8..ac55ba94c 100644 --- a/modules/eks/echo-server/remote-state.tf +++ b/modules/eks/echo-server/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/efs-controller/README.md b/modules/eks/efs-controller/README.md index 4c5d146a8..65cb93046 100644 --- a/modules/eks/efs-controller/README.md +++ b/modules/eks/efs-controller/README.md @@ -58,9 +58,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [efs](#module\_efs) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [efs](#module\_efs) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [efs\_controller](#module\_efs\_controller) | cloudposse/helm-release/aws | 0.5.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/efs-controller/remote-state.tf b/modules/eks/efs-controller/remote-state.tf index 9e0f8f81b..0a78c6203 100644 --- a/modules/eks/efs-controller/remote-state.tf +++ b/modules/eks/efs-controller/remote-state.tf @@ -1,6 +1,6 @@ module "efs" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.efs_component_name @@ -9,7 +9,7 @@ module "efs" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/efs/README.md b/modules/eks/efs/README.md index 0cd19fa8a..2264c217e 100644 --- a/modules/eks/efs/README.md +++ b/modules/eks/efs/README.md @@ -38,12 +38,12 @@ components: | Name | Source | Version | |------|--------|---------| | [efs](#module\_efs) | cloudposse/efs/aws | 0.32.7 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [gbl\_dns\_delegated](#module\_gbl\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [gbl\_dns\_delegated](#module\_gbl\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [kms\_key\_efs](#module\_kms\_key\_efs) | cloudposse/kms-key/aws | 0.12.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/eks/efs/remote-state.tf b/modules/eks/efs/remote-state.tf index 4827a69f8..1c7fa8b1f 100644 --- a/modules/eks/efs/remote-state.tf +++ b/modules/eks/efs/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" for_each = local.eks_security_group_enabled ? var.eks_component_names : toset([]) @@ -20,7 +20,7 @@ module "eks" { module "gbl_dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "dns-delegated" environment = "gbl" diff --git a/modules/eks/eks-without-spotinst/README.md b/modules/eks/eks-without-spotinst/README.md index 82e37ac70..38e444248 100644 --- a/modules/eks/eks-without-spotinst/README.md +++ b/modules/eks/eks-without-spotinst/README.md @@ -74,15 +74,15 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [delegated\_roles](#module\_delegated\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | +| [delegated\_roles](#module\_delegated\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 0.44.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [primary\_roles](#module\_primary\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | +| [primary\_roles](#module\_primary\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [region\_node\_group](#module\_region\_node\_group) | ./modules/node_group_by_region | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/eks/eks-without-spotinst/remote-state.tf b/modules/eks/eks-without-spotinst/remote-state.tf index 1560c930d..46e18213f 100644 --- a/modules/eks/eks-without-spotinst/remote-state.tf +++ b/modules/eks/eks-without-spotinst/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" for_each = toset(var.allow_ingress_from_vpc_stages) @@ -21,7 +21,7 @@ module "vpc_ingress" { module "primary_roles" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" component = "iam-primary-roles" @@ -34,7 +34,7 @@ module "primary_roles" { module "delegated_roles" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" component = "iam-delegated-roles" @@ -48,7 +48,7 @@ module "delegated_roles" { # to it rather than overwrite it (specifically the aws-auth configMap) module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.0" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index b1076e38a..67cb4abd7 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -65,8 +65,8 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [external\_dns](#module\_external\_dns) | cloudposse/helm-release/aws | 0.7.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/external-dns/remote-state.tf b/modules/eks/external-dns/remote-state.tf index a6d442848..0b00224ea 100644 --- a/modules/eks/external-dns/remote-state.tf +++ b/modules/eks/external-dns/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "dns-delegated" environment = var.dns_gbl_delegated_environment_name diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index d59e8f6bd..8fd9d9bd9 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -40,7 +40,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [idp\_roles](#module\_idp\_roles) | cloudposse/helm-release/aws | 0.6.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/idp-roles/remote-state.tf b/modules/eks/idp-roles/remote-state.tf index 89a89a442..af8f64247 100644 --- a/modules/eks/idp-roles/remote-state.tf +++ b/modules/eks/idp-roles/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.0" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/karpenter-provisioner/README.md b/modules/eks/karpenter-provisioner/README.md index 1afbedbd4..dbaf1d86e 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/modules/eks/karpenter-provisioner/README.md @@ -114,10 +114,10 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/eks/karpenter-provisioner/remote-state.tf b/modules/eks/karpenter-provisioner/remote-state.tf index c8c7bd15f..b217d4bca 100644 --- a/modules/eks/karpenter-provisioner/remote-state.tf +++ b/modules/eks/karpenter-provisioner/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "vpc" diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 2cd9c3c91..1d2e32f9e 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -307,7 +307,7 @@ For more details, refer to: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.7.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/karpenter/remote-state.tf b/modules/eks/karpenter/remote-state.tf index 90c6ab1a8..ac55ba94c 100644 --- a/modules/eks/karpenter/remote-state.tf +++ b/modules/eks/karpenter/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index e8ebba173..f0d96ef7d 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -58,7 +58,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [metrics\_server](#module\_metrics\_server) | cloudposse/helm-release/aws | 0.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/metrics-server/remote-state.tf b/modules/eks/metrics-server/remote-state.tf index 6ef90fd26..ac55ba94c 100644 --- a/modules/eks/metrics-server/remote-state.tf +++ b/modules/eks/metrics-server/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 28768a7f5..9f4356de9 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -85,7 +85,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [redis\_operator](#module\_redis\_operator) | cloudposse/helm-release/aws | 0.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/redis-operator/remote-state.tf b/modules/eks/redis-operator/remote-state.tf index 6ef90fd26..ac55ba94c 100644 --- a/modules/eks/redis-operator/remote-state.tf +++ b/modules/eks/redis-operator/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index 7435b4d93..d43bab34b 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -91,7 +91,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [redis](#module\_redis) | cloudposse/helm-release/aws | 0.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/redis/remote-state.tf b/modules/eks/redis/remote-state.tf index 6ef90fd26..ac55ba94c 100644 --- a/modules/eks/redis/remote-state.tf +++ b/modules/eks/redis/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/eks/reloader/README.md b/modules/eks/reloader/README.md index f921e50af..37c1aeb4e 100644 --- a/modules/eks/reloader/README.md +++ b/modules/eks/reloader/README.md @@ -51,7 +51,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/reloader/remote-state.tf b/modules/eks/reloader/remote-state.tf index 90c6ab1a8..ac55ba94c 100644 --- a/modules/eks/reloader/remote-state.tf +++ b/modules/eks/reloader/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.eks_component_name diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md index d5578e4c8..f8587b200 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -77,13 +77,13 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [redis\_clusters](#module\_redis\_clusters) | ./modules/redis_cluster | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/elasticache-redis/remote-state.tf b/modules/elasticache-redis/remote-state.tf index 5b320c9a9..d9d2833f0 100644 --- a/modules/elasticache-redis/remote-state.tf +++ b/modules/elasticache-redis/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" for_each = local.eks_security_group_enabled ? var.eks_component_names : toset([]) @@ -20,7 +20,7 @@ module "eks" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" component = "dns-delegated" environment = "gbl" @@ -30,7 +30,7 @@ module "dns_delegated" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" for_each = toset(var.allow_ingress_from_vpc_stages) diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index b82625c22..5d5eef5c1 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -45,12 +45,12 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.17.0 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [elasticsearch](#module\_elasticsearch) | cloudposse/elasticsearch/aws | 0.33.0 | | [elasticsearch\_log\_cleanup](#module\_elasticsearch\_log\_cleanup) | cloudposse/lambda-elasticsearch-cleanup/aws | 0.12.3 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.24.1 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.17.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/elasticsearch/remote-state.tf b/modules/elasticsearch/remote-state.tf index bd8a75ff2..3c2873007 100644 --- a/modules/elasticsearch/remote-state.tf +++ b/modules/elasticsearch/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.17.0" + version = "1.4.1" stack_config_local_path = "../../../stacks" component = "vpc" @@ -11,7 +11,7 @@ module "vpc" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.17.0" + version = "1.4.1" stack_config_local_path = "../../../stacks" component = "dns-delegated" diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 8dbbc7599..498252ac2 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -87,13 +87,13 @@ chamber write github/runners/ registration-token ghp_secretstring | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.30.1 | | [graceful\_scale\_in](#module\_graceful\_scale\_in) | ./modules/graceful_scale_in | n/a | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [sg](#module\_sg) | cloudposse/security-group/aws | 1.0.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/github-runners/remote-state.tf b/modules/github-runners/remote-state.tf index 22923da60..48c302f75 100644 --- a/modules/github-runners/remote-state.tf +++ b/modules/github-runners/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.1" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.1" + version = "1.4.1" component = "account-map" environment = var.account_map_environment_name diff --git a/modules/global-accelerator-endpoint-group/README.md b/modules/global-accelerator-endpoint-group/README.md index 88bd603fd..90ef5f060 100644 --- a/modules/global-accelerator-endpoint-group/README.md +++ b/modules/global-accelerator-endpoint-group/README.md @@ -38,7 +38,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [endpoint\_group](#module\_endpoint\_group) | cloudposse/global-accelerator/aws//modules/endpoint-group | 0.5.0 | -| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/global-accelerator-endpoint-group/remote-state.tf b/modules/global-accelerator-endpoint-group/remote-state.tf index ca7d88eb2..78317b80d 100644 --- a/modules/global-accelerator-endpoint-group/remote-state.tf +++ b/modules/global-accelerator-endpoint-group/remote-state.tf @@ -1,6 +1,6 @@ module "global_accelerator" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "global-accelerator" environment = var.global_accelerator_environment_name diff --git a/modules/global-accelerator/README.md b/modules/global-accelerator/README.md index eadacb491..656924734 100644 --- a/modules/global-accelerator/README.md +++ b/modules/global-accelerator/README.md @@ -41,7 +41,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [flow\_logs\_bucket](#module\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [flow\_logs\_bucket](#module\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [global\_accelerator](#module\_global\_accelerator) | cloudposse/global-accelerator/aws | 0.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/global-accelerator/remote-state.tf b/modules/global-accelerator/remote-state.tf index 5db291bff..1e30a00b2 100644 --- a/modules/global-accelerator/remote-state.tf +++ b/modules/global-accelerator/remote-state.tf @@ -2,7 +2,7 @@ module "flow_logs_bucket" { count = var.flow_logs_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.flow_logs_s3_bucket_component tenant = var.flow_logs_s3_bucket_tenant diff --git a/modules/mq-broker/README.md b/modules/mq-broker/README.md index cc7b92a12..63c518902 100644 --- a/modules/mq-broker/README.md +++ b/modules/mq-broker/README.md @@ -46,11 +46,11 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.17.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [mq\_broker](#module\_mq\_broker) | cloudposse/mq-broker/aws | 0.14.0 | | [this](#module\_this) | cloudposse/label/null | 0.24.1 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.17.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/mq-broker/remote-state.tf b/modules/mq-broker/remote-state.tf index 82d7050f1..7227b9927 100644 --- a/modules/mq-broker/remote-state.tf +++ b/modules/mq-broker/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.17.0" + version = "1.4.1" stack_config_local_path = "../../../stacks" component = "vpc" @@ -10,7 +10,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.17.0" + version = "1.4.1" stack_config_local_path = "../../../stacks" component = "eks" diff --git a/modules/mwaa/README.md b/modules/mwaa/README.md index a6dc52da9..2213f4c7a 100644 --- a/modules/mwaa/README.md +++ b/modules/mwaa/README.md @@ -63,8 +63,8 @@ components: | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [mwaa\_environment](#module\_mwaa\_environment) | cloudposse/mwaa/aws | 0.4.8 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/mwaa/remote-state.tf b/modules/mwaa/remote-state.tf index a25614047..61064e9e1 100644 --- a/modules/mwaa/remote-state.tf +++ b/modules/mwaa/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" for_each = toset(var.allow_ingress_from_vpc_stages) diff --git a/modules/rds/README.md b/modules/rds/README.md index dd892a694..de0de21d5 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -78,15 +78,15 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | Name | Source | Version | |------|--------|---------| -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.10.0 | | [rds\_client\_sg](#module\_rds\_client\_sg) | cloudposse/security-group/aws | 0.3.1 | | [rds\_instance](#module\_rds\_instance) | cloudposse/rds/aws | 0.38.5 | | [rds\_monitoring\_role](#module\_rds\_monitoring\_role) | cloudposse/iam-role/aws | 0.16.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/rds/remote-state.tf b/modules/rds/remote-state.tf index ec3542d53..600d1fdcf 100644 --- a/modules/rds/remote-state.tf +++ b/modules/rds/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" count = var.use_eks_security_group ? 1 : 0 @@ -20,7 +20,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" component = "dns-delegated" environment = var.dns_gbl_delegated_environment_name diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index a684ed2ec..f3f3b9462 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -93,7 +93,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [bucket\_policy](#module\_bucket\_policy) | cloudposse/iam-policy/aws | 0.4.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [s3\_bucket](#module\_s3\_bucket) | cloudposse/s3-bucket/aws | 3.0.0 | diff --git a/modules/s3-bucket/remote-state.tf b/modules/s3-bucket/remote-state.tf index 89c2a7fc0..db6a74ff4 100644 --- a/modules/s3-bucket/remote-state.tf +++ b/modules/s3-bucket/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "account-map" environment = var.account_map_environment_name diff --git a/modules/ses/README.md b/modules/ses/README.md index ccfecf72f..9d72033be 100644 --- a/modules/ses/README.md +++ b/modules/ses/README.md @@ -45,7 +45,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.3 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_ses](#module\_kms\_key\_ses) | cloudposse/kms-key/aws | 0.12.1 | | [ses](#module\_ses) | cloudposse/ses/aws | 0.22.3 | diff --git a/modules/ses/remote-state.tf b/modules/ses/remote-state.tf index 5fcc2261f..3026a74ec 100644 --- a/modules/ses/remote-state.tf +++ b/modules/ses/remote-state.tf @@ -1,6 +1,6 @@ module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.3" + version = "1.4.1" component = "dns-delegated" environment = "gbl" diff --git a/modules/sftp/README.md b/modules/sftp/README.md index 9d5989ba7..07f239bd5 100644 --- a/modules/sftp/README.md +++ b/modules/sftp/README.md @@ -44,7 +44,7 @@ components: | [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 1.0.1 | | [sftp](#module\_sftp) | cloudposse/transfer-sftp/aws | 1.2.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/sftp/remote-state.tf b/modules/sftp/remote-state.tf index 1b1079219..3e0ccd51e 100644 --- a/modules/sftp/remote-state.tf +++ b/modules/sftp/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" diff --git a/modules/snowflake-account/README.md b/modules/snowflake-account/README.md index 1e6398999..8c2f23355 100644 --- a/modules/snowflake-account/README.md +++ b/modules/snowflake-account/README.md @@ -78,7 +78,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [account](#module\_account) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.1 | +| [account](#module\_account) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [introspection](#module\_introspection) | cloudposse/label/null | 0.25.0 | | [snowflake\_account](#module\_snowflake\_account) | cloudposse/label/null | 0.25.0 | diff --git a/modules/snowflake-account/remote-state.tf b/modules/snowflake-account/remote-state.tf index a4a24923d..5f0597fa0 100644 --- a/modules/snowflake-account/remote-state.tf +++ b/modules/snowflake-account/remote-state.tf @@ -1,6 +1,6 @@ module "account" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.1" + version = "1.4.1" component = "account" stage = var.root_account_stage_name diff --git a/modules/snowflake-database/README.md b/modules/snowflake-database/README.md index a468c0c0e..e55547e5e 100644 --- a/modules/snowflake-database/README.md +++ b/modules/snowflake-database/README.md @@ -61,7 +61,7 @@ components: |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [introspection](#module\_introspection) | cloudposse/label/null | 0.25.0 | -| [snowflake\_account](#module\_snowflake\_account) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.1 | +| [snowflake\_account](#module\_snowflake\_account) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [snowflake\_database](#module\_snowflake\_database) | cloudposse/label/null | 0.25.0 | | [snowflake\_label](#module\_snowflake\_label) | cloudposse/label/null | 0.25.0 | | [snowflake\_schema](#module\_snowflake\_schema) | cloudposse/label/null | 0.25.0 | diff --git a/modules/snowflake-database/remote-state.tf b/modules/snowflake-database/remote-state.tf index 13385a710..bd00a12f5 100644 --- a/modules/snowflake-database/remote-state.tf +++ b/modules/snowflake-database/remote-state.tf @@ -1,6 +1,6 @@ module "snowflake_account" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.1" + version = "1.4.1" component = "snowflake-account" diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 45d7d1b77..d914c5912 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -113,17 +113,17 @@ an extensive explanation for how these preview environments work. | Name | Source | Version | |------|--------|---------| | [acm\_request\_certificate](#module\_acm\_request\_certificate) | cloudposse/acm-request-certificate/aws | 0.17.0 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | | [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | -| [github\_runners](#module\_github\_runners) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [github\_runners](#module\_github\_runners) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [lambda\_edge\_preview](#module\_lambda\_edge\_preview) | ./modules/lambda-edge-preview | n/a | | [lambda\_edge\_redirect\_404](#module\_lambda\_edge\_redirect\_404) | ./modules/lambda_edge_redirect_404 | n/a | | [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.83.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 0.8.1 | -| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/spa-s3-cloudfront/remote-state.tf b/modules/spa-s3-cloudfront/remote-state.tf index 9685e2987..ef44ee892 100644 --- a/modules/spa-s3-cloudfront/remote-state.tf +++ b/modules/spa-s3-cloudfront/remote-state.tf @@ -1,6 +1,6 @@ module "waf" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" bypass = !local.aws_waf_enabled component = var.cloudfront_aws_waf_component_name @@ -18,7 +18,7 @@ module "waf" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "dns-delegated" environment = var.dns_delegated_environment_name @@ -28,7 +28,7 @@ module "dns_delegated" { module "github_runners" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" count = local.github_runners_enabled ? 1 : 0 diff --git a/modules/spacelift-worker-pool/README.md b/modules/spacelift-worker-pool/README.md index e72ff71bd..50da9d239 100644 --- a/modules/spacelift-worker-pool/README.md +++ b/modules/spacelift-worker-pool/README.md @@ -96,14 +96,14 @@ the output to the `trusted_role_arns` list for the `spacelift` role in `aws-team | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.0 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.30.1 | -| [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.0 | +| [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_label](#module\_iam\_label) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.0.0-rc1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.1.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/spacelift-worker-pool/remote-state.tf b/modules/spacelift-worker-pool/remote-state.tf index 1e56e3749..5d78adb0d 100644 --- a/modules/spacelift-worker-pool/remote-state.tf +++ b/modules/spacelift-worker-pool/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.0" + version = "1.4.1" component = "account-map" environment = coalesce(var.account_map_environment_name, module.this.environment) @@ -12,7 +12,7 @@ module "account_map" { module "ecr" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.0" + version = "1.4.1" component = "ecr" environment = coalesce(var.ecr_environment_name, module.this.environment) @@ -24,7 +24,7 @@ module "ecr" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.1.0" + version = "1.4.1" component = "vpc" diff --git a/modules/tgw/cross-region-hub-connector/README.md b/modules/tgw/cross-region-hub-connector/README.md index 9f8b7e84c..29be5ddb8 100644 --- a/modules/tgw/cross-region-hub-connector/README.md +++ b/modules/tgw/cross-region-hub-connector/README.md @@ -49,12 +49,12 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_role\_tgw\_home\_region](#module\_iam\_role\_tgw\_home\_region) | ../../account-map/modules/iam-roles | n/a | | [iam\_role\_tgw\_this\_region](#module\_iam\_role\_tgw\_this\_region) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [tgw\_home\_region](#module\_tgw\_home\_region) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [tgw\_this\_region](#module\_tgw\_this\_region) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [tgw\_home\_region](#module\_tgw\_home\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [tgw\_this\_region](#module\_tgw\_this\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/tgw/cross-region-hub-connector/remote-state.tf b/modules/tgw/cross-region-hub-connector/remote-state.tf index acfd2f8ab..9f3339c58 100644 --- a/modules/tgw/cross-region-hub-connector/remote-state.tf +++ b/modules/tgw/cross-region-hub-connector/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "account-map" stage = "root" @@ -11,7 +11,7 @@ module "account_map" { module "tgw_this_region" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "tgw/hub" stage = var.this_region["tgw_stage_name"] @@ -21,7 +21,7 @@ module "tgw_this_region" { module "tgw_home_region" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "tgw/hub" stage = var.home_region["tgw_stage_name"] diff --git a/modules/tgw/cross-region-spoke/README.md b/modules/tgw/cross-region-spoke/README.md index 7860bf2c1..d860bd20a 100644 --- a/modules/tgw/cross-region-spoke/README.md +++ b/modules/tgw/cross-region-spoke/README.md @@ -54,21 +54,21 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [az\_abbreviation](#module\_az\_abbreviation) | cloudposse/utils/aws | 1.0.0 | | [iam\_role\_tgw\_home\_region](#module\_iam\_role\_tgw\_home\_region) | ../../account-map/modules/iam-roles | n/a | | [iam\_role\_tgw\_this\_region](#module\_iam\_role\_tgw\_this\_region) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [tgw\_cross\_region\_connector](#module\_tgw\_cross\_region\_connector) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [tgw\_home\_region](#module\_tgw\_home\_region) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [tgw\_cross\_region\_connector](#module\_tgw\_cross\_region\_connector) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [tgw\_home\_region](#module\_tgw\_home\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [tgw\_routes\_home\_region](#module\_tgw\_routes\_home\_region) | ./modules/tgw_routes | n/a | | [tgw\_routes\_this\_region](#module\_tgw\_routes\_this\_region) | ./modules/tgw_routes | n/a | -| [tgw\_this\_region](#module\_tgw\_this\_region) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [tgw\_this\_region](#module\_tgw\_this\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc\_routes\_home](#module\_vpc\_routes\_home) | ./modules/vpc_routes | n/a | | [vpc\_routes\_this](#module\_vpc\_routes\_this) | ./modules/vpc_routes | n/a | -| [vpcs\_home\_region](#module\_vpcs\_home\_region) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [vpcs\_this\_region](#module\_vpcs\_this\_region) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpcs\_home\_region](#module\_vpcs\_home\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpcs\_this\_region](#module\_vpcs\_this\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/tgw/cross-region-spoke/remote-state.tf b/modules/tgw/cross-region-spoke/remote-state.tf index aaa246847..de7ab6a31 100644 --- a/modules/tgw/cross-region-spoke/remote-state.tf +++ b/modules/tgw/cross-region-spoke/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "account-map" stage = "root" @@ -11,7 +11,7 @@ module "account_map" { module "vpcs_this_region" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" for_each = toset(concat(tolist(var.this_region.connections), [var.tenant == null ? module.this.stage : format("%s-%s", module.this.tenant, module.this.stage)])) @@ -25,7 +25,7 @@ module "vpcs_this_region" { module "vpcs_home_region" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" for_each = toset(concat(tolist(var.home_region.connections), [var.tenant == null ? module.this.stage : format("%s-%s", module.this.tenant, module.this.stage)])) @@ -39,7 +39,7 @@ module "vpcs_home_region" { module "tgw_this_region" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "tgw/hub" stage = var.this_region["tgw_stage_name"] @@ -49,7 +49,7 @@ module "tgw_this_region" { module "tgw_cross_region_connector" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "tgw/cross-region-hub-connector" stage = var.this_region["tgw_stage_name"] @@ -59,7 +59,7 @@ module "tgw_cross_region_connector" { module "tgw_home_region" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "tgw/hub" stage = var.home_region["tgw_stage_name"] diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 16871d5ae..47599820b 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -58,12 +58,12 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [tgw\_hub](#module\_tgw\_hub) | cloudposse/transit-gateway/aws | 0.9.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/tgw/hub/remote-state.tf b/modules/tgw/hub/remote-state.tf index d072d3f8d..775fd7825 100644 --- a/modules/tgw/hub/remote-state.tf +++ b/modules/tgw/hub/remote-state.tf @@ -21,7 +21,7 @@ locals { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "account-map" environment = var.account_map_environment_name @@ -35,7 +35,7 @@ module "vpc" { for_each = local.accounts_with_vpc source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = "vpc" stage = each.value.stage @@ -46,7 +46,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" for_each = local.eks_remote_states diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index 02b28ecb0..f2002cec1 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -82,7 +82,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [tgw\_hub](#module\_tgw\_hub) | cloudposse/stack-config/yaml//modules/remote-state | 0.22.4 | +| [tgw\_hub](#module\_tgw\_hub) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [tgw\_hub\_role](#module\_tgw\_hub\_role) | ../../account-map/modules/iam-roles | n/a | | [tgw\_hub\_routes](#module\_tgw\_hub\_routes) | cloudposse/transit-gateway/aws | 0.9.1 | | [tgw\_spoke\_vpc\_attachment](#module\_tgw\_spoke\_vpc\_attachment) | ./modules/standard_vpc_attachment | n/a | diff --git a/modules/tgw/spoke/remote-state.tf b/modules/tgw/spoke/remote-state.tf index a66b0cd9f..d074fca2d 100644 --- a/modules/tgw/spoke/remote-state.tf +++ b/modules/tgw/spoke/remote-state.tf @@ -1,6 +1,6 @@ module "tgw_hub" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "0.22.4" + version = "1.4.1" component = var.tgw_hub_component_name stage = var.tgw_hub_stage_name diff --git a/modules/vpc-peering/README.md b/modules/vpc-peering/README.md index 41a69c6fa..d9d38e192 100644 --- a/modules/vpc-peering/README.md +++ b/modules/vpc-peering/README.md @@ -201,7 +201,7 @@ atmos terraform apply vpc-peering -s ue1-prod | Name | Source | Version | |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [requester\_vpc](#module\_requester\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [requester\_vpc](#module\_requester\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc\_peering](#module\_vpc\_peering) | cloudposse/vpc-peering-multi-account/aws | 0.19.1 | diff --git a/modules/vpc-peering/remote-state.tf b/modules/vpc-peering/remote-state.tf index e0eae6f3b..108ec8737 100644 --- a/modules/vpc-peering/remote-state.tf +++ b/modules/vpc-peering/remote-state.tf @@ -1,6 +1,6 @@ module "requester_vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = var.requester_vpc_component_name diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 5e889cef6..6a15a9bc2 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -77,7 +77,7 @@ components: | [utils](#module\_utils) | cloudposse/utils/aws | 1.1.0 | | [vpc](#module\_vpc) | cloudposse/vpc/aws | 2.0.0 | | [vpc\_endpoints](#module\_vpc\_endpoints) | cloudposse/vpc/aws//modules/vpc-endpoints | 2.0.0 | -| [vpc\_flow\_logs\_bucket](#module\_vpc\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.3.1 | +| [vpc\_flow\_logs\_bucket](#module\_vpc\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/vpc/remote-state.tf b/modules/vpc/remote-state.tf index ba5b81928..cc5bd0fb4 100644 --- a/modules/vpc/remote-state.tf +++ b/modules/vpc/remote-state.tf @@ -2,7 +2,7 @@ module "vpc_flow_logs_bucket" { count = var.vpc_flow_logs_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.3.1" + version = "1.4.1" component = "vpc-flow-logs-bucket" environment = var.vpc_flow_logs_bucket_environment_name From 64d9988777fe0fd82de2d473937e63b01662445a Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 6 Mar 2023 16:28:00 -0800 Subject: [PATCH 057/501] `eks/actions-runner-controller`: use coalesce (#586) --- modules/eks/actions-runner-controller/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index f3003a392..673300e2c 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -216,7 +216,7 @@ module "actions_runner" { min_replicas = each.value.min_replicas max_replicas = each.value.max_replicas webhook_driven_scaling_enabled = each.value.webhook_driven_scaling_enabled - webhook_startup_timeout = try(each.value.webhook_startup_timeout, "${each.value.scale_down_delay_seconds}s") # if webhook_startup_timeout isnt defined, use scale_down_delay_seconds + webhook_startup_timeout = coalesce(each.value.webhook_startup_timeout, "${each.value.scale_down_delay_seconds}s") # if webhook_startup_timeout isnt defined, use scale_down_delay_seconds pull_driven_scaling_enabled = each.value.pull_driven_scaling_enabled pvc_enabled = each.value.pvc_enabled }), From 164cbe072fc837916a03667a0b2269522bd62737 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Thu, 9 Mar 2023 02:12:50 +0300 Subject: [PATCH 058/501] Improve platform and external-dns for release engineering (#588) Co-authored-by: cloudpossebot --- modules/eks/external-dns/README.md | 2 ++ modules/eks/external-dns/main.tf | 3 ++- modules/eks/external-dns/remote-state.tf | 16 ++++++++++++++ modules/eks/external-dns/variables.tf | 6 ++++++ modules/eks/platform/README.md | 5 ++++- modules/eks/platform/main.tf | 27 +++++++++++++++++------- modules/eks/platform/providers.tf | 2 ++ modules/eks/platform/remote-state.tf | 6 +++++- modules/eks/platform/variables.tf | 8 +++++-- modules/eks/platform/versions.tf | 4 ++++ 10 files changed, 66 insertions(+), 13 deletions(-) diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index 67cb4abd7..d950a7bf4 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -66,6 +66,7 @@ components: | Name | Source | Version | |------|--------|---------| | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_gbl\_primary](#module\_dns\_gbl\_primary) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [external\_dns](#module\_external\_dns) | cloudposse/helm-release/aws | 0.7.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | @@ -97,6 +98,7 @@ components: | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_gbl\_delegated\_environment\_name](#input\_dns\_gbl\_delegated\_environment\_name) | The name of the environment where global `dns_delegated` is provisioned | `string` | `"gbl"` | no | +| [dns\_gbl\_primary\_environment\_name](#input\_dns\_gbl\_primary\_environment\_name) | The name of the environment where global `dns_primary` is provisioned | `string` | `"gbl"` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | diff --git a/modules/eks/external-dns/main.tf b/modules/eks/external-dns/main.tf index 9292bd032..7a0d67ec9 100644 --- a/modules/eks/external-dns/main.tf +++ b/modules/eks/external-dns/main.tf @@ -8,7 +8,8 @@ locals { txt_owner = var.txt_prefix != "" ? format(module.this.tenant != null ? "%[1]s-%[2]s-%[3]s-%[4]s" : "%[1]s-%[2]s-%[4]s", var.txt_prefix, module.this.environment, module.this.tenant, module.this.stage) : "" txt_prefix = var.txt_prefix != "" ? format("%s-", local.txt_owner) : "" zone_ids = compact(concat( - values(module.dns_gbl_delegated.outputs.zones)[*].zone_id + values(module.dns_gbl_delegated.outputs.zones)[*].zone_id, + values(module.dns_gbl_primary.outputs.zones)[*].zone_id )) } diff --git a/modules/eks/external-dns/remote-state.tf b/modules/eks/external-dns/remote-state.tf index 0b00224ea..3110659b3 100644 --- a/modules/eks/external-dns/remote-state.tf +++ b/modules/eks/external-dns/remote-state.tf @@ -20,3 +20,19 @@ module "dns_gbl_delegated" { zones = {} } } + +module "dns_gbl_primary" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "dns-primary" + environment = var.dns_gbl_primary_environment_name + + context = module.this.context + + ignore_errors = true + + defaults = { + zones = {} + } +} diff --git a/modules/eks/external-dns/variables.tf b/modules/eks/external-dns/variables.tf index 0f596ae83..68e9091a7 100644 --- a/modules/eks/external-dns/variables.tf +++ b/modules/eks/external-dns/variables.tf @@ -126,6 +126,12 @@ variable "dns_gbl_delegated_environment_name" { default = "gbl" } +variable "dns_gbl_primary_environment_name" { + type = string + description = "The name of the environment where global `dns_primary` is provisioned" + default = "gbl" +} + variable "publish_internal_services" { type = bool description = "Allow external-dns to publish DNS records for ClusterIP services" diff --git a/modules/eks/platform/README.md b/modules/eks/platform/README.md index e6926914f..dc35066bc 100644 --- a/modules/eks/platform/README.md +++ b/modules/eks/platform/README.md @@ -54,12 +54,14 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | +| [jq](#requirement\_jq) | >= 0.2.1 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | +| [jq](#provider\_jq) | >= 0.2.1 | ## Modules @@ -76,6 +78,7 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | Name | Type | |------|------| | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [jq_query.default](https://registry.terraform.io/providers/massdriver-cloud/jq/latest/docs/data-sources/query) | data source | ## Inputs @@ -99,7 +102,7 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [platform\_environment](#input\_platform\_environment) | Platform environment | `string` | `"default"` | no | -| [references](#input\_references) | Platform mapping from remote components outputs |
map(object({
component = string
output = string
}))
| `{}` | no | +| [references](#input\_references) | Platform mapping from remote components outputs |
map(object({
component = string
privileged = optional(bool)
tenant = optional(string)
environment = optional(string)
stage = optional(string)
output = string
}))
| `{}` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [ssm\_platform\_path](#input\_ssm\_platform\_path) | Format SSM path to store platform configs | `string` | `"/platform/%s/%s"` | no | diff --git a/modules/eks/platform/main.tf b/modules/eks/platform/main.tf index 7c795fcdf..d66f63e19 100644 --- a/modules/eks/platform/main.tf +++ b/modules/eks/platform/main.tf @@ -17,14 +17,15 @@ module "store_write" { source = "cloudposse/ssm-parameter-store/aws" version = "0.10.0" - parameter_write = concat([for k, v in var.references : - { - name = format("%s/%s", format(var.ssm_platform_path, module.eks.outputs.eks_cluster_id, var.platform_environment), k) - value = lookup(module.remote[k].outputs, v.output) - type = "SecureString" - overwrite = true - description = "Platform config for ${var.platform_environment} at ${module.eks.outputs.eks_cluster_id} cluster" - } + parameter_write = concat( + [for k, v in var.references : + { + name = format("%s/%s", format(var.ssm_platform_path, module.eks.outputs.eks_cluster_id, var.platform_environment), k) + value = local.result[k] + type = "SecureString" + overwrite = true + description = "Platform config for ${var.platform_environment} at ${module.eks.outputs.eks_cluster_id} cluster" + } ], [for k, v in local.metadata : { @@ -37,3 +38,13 @@ module "store_write" { ]) context = module.this.context } + +data "jq_query" "default" { + for_each = var.references + data = jsonencode(module.remote[each.key].outputs) + query = ".${each.value.output}" +} + +locals { + result = { for k, v in data.jq_query.default : k => jsondecode(v.result) } +} diff --git a/modules/eks/platform/providers.tf b/modules/eks/platform/providers.tf index c2419aabb..af67fa874 100644 --- a/modules/eks/platform/providers.tf +++ b/modules/eks/platform/providers.tf @@ -27,3 +27,5 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } + +provider "jq" {} diff --git a/modules/eks/platform/remote-state.tf b/modules/eks/platform/remote-state.tf index 1c2e7e8fd..6ee221982 100644 --- a/modules/eks/platform/remote-state.tf +++ b/modules/eks/platform/remote-state.tf @@ -13,7 +13,11 @@ module "remote" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" - component = each.value["component"] + component = each.value["component"] + privileged = coalesce(try(each.value["privileged"], null), false) + tenant = coalesce(try(each.value["tenant"], null), module.this.context["tenant"], null) + environment = coalesce(try(each.value["environment"], null), module.this.context["environment"], null) + stage = coalesce(try(each.value["stage"], null), module.this.context["stage"], null) context = module.this.context } diff --git a/modules/eks/platform/variables.tf b/modules/eks/platform/variables.tf index 3c3cbf9d3..66a2710f6 100644 --- a/modules/eks/platform/variables.tf +++ b/modules/eks/platform/variables.tf @@ -7,8 +7,12 @@ variable "references" { description = "Platform mapping from remote components outputs" default = {} type = map(object({ - component = string - output = string + component = string + privileged = optional(bool) + tenant = optional(string) + environment = optional(string) + stage = optional(string) + output = string })) } diff --git a/modules/eks/platform/versions.tf b/modules/eks/platform/versions.tf index 47f2ceea4..557b20ee3 100644 --- a/modules/eks/platform/versions.tf +++ b/modules/eks/platform/versions.tf @@ -10,5 +10,9 @@ terraform { source = "hashicorp/helm" version = ">= 2.0" } + jq = { + source = "massdriver-cloud/jq" + version = ">= 0.2.1" + } } } From ac83e4c233d5463dab2ad05f6065fa066c975ed2 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 10 Mar 2023 09:51:51 -0800 Subject: [PATCH 059/501] Upstream: `eks/echo-server` (#591) --- modules/eks/echo-server/README.md | 2 ++ modules/eks/echo-server/main.tf | 5 +++++ modules/eks/echo-server/remote-state.tf | 9 +++++++++ modules/eks/echo-server/variables.tf | 6 ++++++ 4 files changed, 22 insertions(+) diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index b6ad9be06..b10dbd9fb 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -85,6 +85,7 @@ components: | Name | Source | Version | |------|--------|---------| +| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.7.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | @@ -103,6 +104,7 @@ components: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_controller\_ingress\_group\_component\_name](#input\_alb\_controller\_ingress\_group\_component\_name) | The name of the alb\_controller\_ingress\_group component | `string` | `"eks/alb-controller-ingress-group"` | no | | [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [chart\_values](#input\_chart\_values) | Addition map values to yamlencode as `helm_release` values. | `any` | `{}` | no | diff --git a/modules/eks/echo-server/main.tf b/modules/eks/echo-server/main.tf index c11f21f9c..4f662eff7 100644 --- a/modules/eks/echo-server/main.tf +++ b/modules/eks/echo-server/main.tf @@ -38,6 +38,11 @@ module "echo_server" { value = local.ingress_nginx_enabled type = "auto" }, + { + name = "ingress.alb.group_name" + value = module.alb.outputs.group_name + type = "auto" + }, { name = "ingress.alb.enabled" value = local.ingress_alb_enabled diff --git a/modules/eks/echo-server/remote-state.tf b/modules/eks/echo-server/remote-state.tf index ac55ba94c..a712431cf 100644 --- a/modules/eks/echo-server/remote-state.tf +++ b/modules/eks/echo-server/remote-state.tf @@ -6,3 +6,12 @@ module "eks" { context = module.this.context } + +module "alb" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.alb_controller_ingress_group_component_name + + context = module.this.context +} diff --git a/modules/eks/echo-server/variables.tf b/modules/eks/echo-server/variables.tf index f28f8f89a..48da55209 100644 --- a/modules/eks/echo-server/variables.tf +++ b/modules/eks/echo-server/variables.tf @@ -9,6 +9,12 @@ variable "eks_component_name" { default = "eks/cluster" } +variable "alb_controller_ingress_group_component_name" { + type = string + description = "The name of the alb_controller_ingress_group component" + default = "eks/alb-controller-ingress-group" +} + variable "chart_values" { type = any description = "Addition map values to yamlencode as `helm_release` values." From 2f6f94d19055018cce4702ff1d7326ed6adadd3f Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Fri, 10 Mar 2023 21:07:10 +0300 Subject: [PATCH 060/501] ArgoCD SSO improvements (#590) Co-authored-by: cloudpossebot --- modules/eks/argocd/README.md | 4 +++- modules/eks/argocd/main.tf | 14 +++++++------- .../eks/argocd/resources/argocd-values.yaml.tpl | 1 + modules/eks/argocd/variables-argocd.tf | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 7753b7ac8..36357e484 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -83,13 +83,14 @@ components: | [aws_ssm_parameter.oidc_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.oidc_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameters_by_path.argocd_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | -| [kubernetes_resources.example](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/resources) | data source | +| [kubernetes_resources.crd](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/resources) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_enabled](#input\_admin\_enabled) | Toggles Admin user creation the deployed chart | `bool` | `false` | no | | [alb\_group\_name](#input\_alb\_group\_name) | A name used in annotations to reuse an ALB (e.g. `argocd`) or to generate a new one | `string` | `null` | no | | [alb\_logs\_bucket](#input\_alb\_logs\_bucket) | The name of the bucket for ALB access logs. The bucket must have policy allowing the ELB logging principal | `string` | `""` | no | | [alb\_logs\_prefix](#input\_alb\_logs\_prefix) | `alb_logs_bucket` s3 bucket prefix | `string` | `""` | no | @@ -102,6 +103,7 @@ components: | [argocd\_apps\_chart\_version](#input\_argocd\_apps\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"0.0.3"` | no | | [argocd\_apps\_enabled](#input\_argocd\_apps\_enabled) | Enable argocd apps | `bool` | `true` | no | | [argocd\_create\_namespaces](#input\_argocd\_create\_namespaces) | ArgoCD create namespaces policy | `bool` | `false` | no | +| [argocd\_rbac\_default\_policy](#input\_argocd\_rbac\_default\_policy) | Default ArgoCD RBAC default role.

See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#basic-built-in-roles for more information. | `string` | `"role:readonly"` | no | | [argocd\_rbac\_groups](#input\_argocd\_rbac\_groups) | List of ArgoCD Group Role Assignment strings to be added to the argocd-rbac configmap policy.csv item.
e.g.
[
{
group: idp-group-name,
role: argocd-role-name
},
]
becomes: `g, idp-group-name, role:argocd-role-name`
See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. |
list(object({
group = string,
role = string
}))
| `[]` | no | | [argocd\_rbac\_policies](#input\_argocd\_rbac\_policies) | List of ArgoCD RBAC Permission strings to be added to the argocd-rbac configmap policy.csv item.

See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. | `list(string)` | `[]` | no | | [argocd\_repositories](#input\_argocd\_repositories) | Map of objects defining an `argocd_repo` to configure. The key is the name of the ArgoCD repository. |
map(object({
environment = string # The environment where the `argocd_repo` component is deployed.
stage = string # The stage where the `argocd_repo` component is deployed.
tenant = string # The tenant where the `argocd_repo` component is deployed.
}))
| `{}` | no | diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 98a576705..76867ba1e 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -70,8 +70,9 @@ locals { caData = base64encode(format("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", module.saml_sso_providers[name].outputs.ca)) redirectURI = format("https://%s/api/dex/callback", local.host) entityIssuer = format("https://%s/api/dex/callback", local.host) - usernameAttr = "name" - emailAttr = "email" + usernameAttr = module.saml_sso_providers[name].outputs.usernameAttr + emailAttr = module.saml_sso_providers[name].outputs.emailAttr + groupsAttr = module.saml_sso_providers[name].outputs.groupsAttr ssoIssuer = module.saml_sso_providers[name].outputs.issuer } } @@ -156,8 +157,7 @@ module "argocd" { templatefile( "${path.module}/resources/argocd-values.yaml.tpl", { - # admin_enabled = !(local.oidc_enabled || local.saml_enabled) - admin_enabled = true + admin_enabled = var.admin_enabled alb_group_name = var.alb_group_name == null ? "" : var.alb_group_name alb_logs_bucket = var.alb_logs_bucket alb_logs_prefix = var.alb_logs_prefix @@ -173,6 +173,7 @@ module "argocd" { organization = var.github_organization saml_enabled = local.saml_enabled saml_rbac_scopes = var.saml_rbac_scopes + rbac_default_policy = var.argocd_rbac_default_policy rbac_policies = var.argocd_rbac_policies rbac_groups = var.argocd_rbac_groups enable_argo_workflows_auth = local.enable_argo_workflows_auth @@ -201,7 +202,6 @@ module "argocd" { { notifications = { triggers = { for key, value in var.notifications_triggers : - # replace(key, "_", ".") => merge(yamlencode(value), data.aws_ssm_parameters_by_path.argocd_notifications[0].values) replace(key, "_", ".") => yamlencode(value) } } @@ -225,7 +225,7 @@ module "argocd" { context = module.this.context } -data "kubernetes_resources" "example" { +data "kubernetes_resources" "crd" { api_version = "apiextensions.k8s.io/v1" kind = "CustomResourceDefinition" field_selector = "metadata.name==applications.argoproj.io" @@ -246,7 +246,7 @@ module "argocd_apps" { atomic = var.atomic cleanup_on_fail = var.cleanup_on_fail timeout = var.timeout - enabled = local.enabled && var.argocd_apps_enabled && length(data.kubernetes_resources.example.objects) > 0 + enabled = local.enabled && var.argocd_apps_enabled && length(data.kubernetes_resources.crd.objects) > 0 values = compact([ templatefile( "${path.module}/resources/argocd-apps-values.yaml.tpl", diff --git a/modules/eks/argocd/resources/argocd-values.yaml.tpl b/modules/eks/argocd/resources/argocd-values.yaml.tpl index 1347a2c42..16ae99c63 100644 --- a/modules/eks/argocd/resources/argocd-values.yaml.tpl +++ b/modules/eks/argocd/resources/argocd-values.yaml.tpl @@ -115,6 +115,7 @@ server: return hs rbacConfig: + policy.default: ${rbac_default_policy} policy.csv: | %{ for policy in rbac_policies ~} ${policy} diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index 1db151129..cf8429304 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -95,6 +95,12 @@ variable "forecastle_enabled" { default = false } +variable "admin_enabled" { + type = bool + description = "Toggles Admin user creation the deployed chart" + default = false +} + variable "oidc_enabled" { type = bool description = "Toggles OIDC integration in the deployed chart" @@ -165,6 +171,16 @@ variable "argocd_rbac_policies" { EOT } +variable "argocd_rbac_default_policy" { + type = string + default = "role:readonly" + description = <<-EOT + Default ArgoCD RBAC default role. + + See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#basic-built-in-roles for more information. + EOT +} + variable "argocd_rbac_groups" { type = list(object({ group = string, From bde1d2c7dc3d314463d8d3bfc3468154ebc972e8 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Mon, 13 Mar 2023 18:24:58 +0300 Subject: [PATCH 061/501] Fix SSO SAML provider fixes (#592) --- modules/sso-saml-provider/outputs.tf | 15 +++++++++++++++ modules/sso-saml-provider/variables.tf | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/modules/sso-saml-provider/outputs.tf b/modules/sso-saml-provider/outputs.tf index b95c47afe..971d6f05a 100644 --- a/modules/sso-saml-provider/outputs.tf +++ b/modules/sso-saml-provider/outputs.tf @@ -16,3 +16,18 @@ output "issuer" { sensitive = true } +output "usernameAttr" { + value = local.enabled ? var.usernameAttr : null + description = "User name attribute" +} + +output "emailAttr" { + value = local.enabled ? var.emailAttr : null + description = "Email attribute" +} + +output "groupsAttr" { + value = local.enabled ? var.groupsAttr : null + description = "Groups attribute" +} + diff --git a/modules/sso-saml-provider/variables.tf b/modules/sso-saml-provider/variables.tf index a2f76a6b9..bc5f6d3cd 100644 --- a/modules/sso-saml-provider/variables.tf +++ b/modules/sso-saml-provider/variables.tf @@ -7,3 +7,21 @@ variable "ssm_path_prefix" { type = string description = "Top level SSM path prefix (without leading or trailing slash)" } + +variable "usernameAttr" { + type = string + description = "User name attribute" + default = null +} + +variable "emailAttr" { + type = string + description = "Email attribute" + default = null +} + +variable "groupsAttr" { + type = string + description = "Group attribute" + default = null +} From 2c865f5654453d43b85d722a8f48a94b16e7f2a4 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 14 Mar 2023 16:20:17 -0400 Subject: [PATCH 062/501] chore(spacelift): update with dependency resource (#594) Co-authored-by: cloudpossebot --- modules/spacelift/README.md | 3 ++- modules/spacelift/main.tf | 9 +++++---- modules/spacelift/variables.tf | 6 ++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index bd797476c..3e3a4a485 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -346,7 +346,7 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | Name | Source | Version | |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.51.3 | +| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.55.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -408,6 +408,7 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | | [spacelift\_component\_path](#input\_spacelift\_component\_path) | The Spacelift Component Path | `string` | `"components/terraform"` | no | | [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | +| [spacelift\_stack\_dependency\_enabled](#input\_spacelift\_stack\_dependency\_enabled) | If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached | `bool` | `false` | no | | [stack\_config\_path\_template](#input\_stack\_config\_path\_template) | Stack config path template | `string` | `"stacks/%s.yaml"` | no | | [stack\_destructor\_enabled](#input\_stack\_destructor\_enabled) | Flag to enable/disable the stack destructor to destroy the resources of a stack before deleting the stack itself | `bool` | `false` | no | | [stacks\_space\_id](#input\_stacks\_space\_id) | Override the space ID for all stacks (unless the stack config has `dedicated_space` set to true). Otherwise, it will default to the admin stack's space. | `string` | `null` | no | diff --git a/modules/spacelift/main.tf b/modules/spacelift/main.tf index 2b0cd90b9..4320947a3 100644 --- a/modules/spacelift/main.tf +++ b/modules/spacelift/main.tf @@ -1,6 +1,6 @@ module "spacelift" { source = "cloudposse/cloud-infrastructure-automation/spacelift" - version = "0.51.3" + version = "0.55.0" context_filters = var.context_filters tag_filters = var.tag_filters @@ -23,9 +23,10 @@ module "spacelift" { terraform_version = var.terraform_version terraform_version_map = var.terraform_version_map - imports_processing_enabled = false - stack_deps_processing_enabled = false - component_deps_processing_enabled = true + imports_processing_enabled = false + stack_deps_processing_enabled = false + component_deps_processing_enabled = true + spacelift_stack_dependency_enabled = var.spacelift_stack_dependency_enabled policies_available = var.policies_available policies_enabled = var.policies_enabled diff --git a/modules/spacelift/variables.tf b/modules/spacelift/variables.tf index f90999116..4b878ac84 100644 --- a/modules/spacelift/variables.tf +++ b/modules/spacelift/variables.tf @@ -220,3 +220,9 @@ variable "stacks_space_id" { description = "Override the space ID for all stacks (unless the stack config has `dedicated_space` set to true). Otherwise, it will default to the admin stack's space." default = null } + +variable "spacelift_stack_dependency_enabled" { + type = bool + description = "If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached" + default = false +} From 307c558880d266e11be6fe8e84d2e0a8bf6986f8 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 15 Mar 2023 08:55:51 -0700 Subject: [PATCH 063/501] Upstream `eks/external-secrets-operator` (#595) --- .../eks/external-secrets-operator/README.md | 165 +++++++++++ .../charts/external-ssm-secrets/.helmignore | 23 ++ .../charts/external-ssm-secrets/Chart.yaml | 24 ++ .../templates/ssm-secret-store.yaml | 10 + .../eks/external-secrets-operator/context.tf | 279 ++++++++++++++++++ .../examples/app-secrets.yaml | 24 ++ .../examples/external-secrets.yaml | 18 ++ .../helm-variables.tf | 58 ++++ modules/eks/external-secrets-operator/main.tf | 120 ++++++++ .../eks/external-secrets-operator/outputs.tf | 4 + .../provider-helm.tf | 158 ++++++++++ .../external-secrets-operator/providers.tf | 40 +++ .../external-secrets-operator/remote-state.tf | 18 ++ .../external-secrets-operator/variables.tf | 36 +++ .../eks/external-secrets-operator/versions.tf | 18 ++ 15 files changed, 995 insertions(+) create mode 100644 modules/eks/external-secrets-operator/README.md create mode 100644 modules/eks/external-secrets-operator/charts/external-ssm-secrets/.helmignore create mode 100644 modules/eks/external-secrets-operator/charts/external-ssm-secrets/Chart.yaml create mode 100644 modules/eks/external-secrets-operator/charts/external-ssm-secrets/templates/ssm-secret-store.yaml create mode 100644 modules/eks/external-secrets-operator/context.tf create mode 100644 modules/eks/external-secrets-operator/examples/app-secrets.yaml create mode 100644 modules/eks/external-secrets-operator/examples/external-secrets.yaml create mode 100644 modules/eks/external-secrets-operator/helm-variables.tf create mode 100644 modules/eks/external-secrets-operator/main.tf create mode 100644 modules/eks/external-secrets-operator/outputs.tf create mode 100644 modules/eks/external-secrets-operator/provider-helm.tf create mode 100644 modules/eks/external-secrets-operator/providers.tf create mode 100644 modules/eks/external-secrets-operator/remote-state.tf create mode 100644 modules/eks/external-secrets-operator/variables.tf create mode 100644 modules/eks/external-secrets-operator/versions.tf diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md new file mode 100644 index 000000000..2f488bdaa --- /dev/null +++ b/modules/eks/external-secrets-operator/README.md @@ -0,0 +1,165 @@ +# Component: `external-secrets-operator` + +This component (ESO) is used to create an external `SecretStore` configured to synchronize secrets from AWS SSM Parameter store as Kubernetes Secrets within the cluster. Per the operator pattern, the `external-secret-operator` pods will watch for any `ExternalSecret` resources which reference the `SecretStore` to pull secrets from. + +In practice, this means apps will define an `ExternalSecret` that pulls all env into a single secret as part of a helm chart; e.g.: + +``` +# Part of the charts in `/releases + +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: app-secrets +spec: + refreshInterval: 0 + secretStoreRef: + name: "secret-store-parameter-store" # must match name of our store + kind: SecretStore + target: + creationPolicy: 'Owner' + name: app-secrets + dataFrom: + - find: + name: + regexp: "^/app" +``` + +This component assumes secrets are prefixed by "service" in parameter store (e.g. `/app/my_secret`). The `SecretStore`. The component is designed to pull secrets from a `path` prefix (defaulting to `"app"`). This should work nicely along `chamber` which uses this same path (called a "service" in Chamber). For example, developers should store keys like so. + + +```bash +assume-role acme-platform-gbl-sandbox-admin +chamber write app MY_KEY my-value +``` + + +See `docs/recipies.md` for more information on managing secrets. + +## Usage + +**Stack Level**: Regional + +Use this in the catalog or use these variables to overwrite the catalog values. + +```yaml +components: + terraform: + eks/external-secrets-operator: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: "external-secrets-operator" + kubernetes_namespace: "secrets" + create_namespace: true + timeout: 90 + wait: true + atomic: true + cleanup_on_fail: true + parameter_store_paths: + - app + - rds + +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.5.0 | +| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [parameter\_store\_paths](#input\_parameter\_store\_paths) | A list of path prefixes that the SecretStore is allowed to access via IAM. This should match the convention 'service' that Chamber uploads keys under. | `set(string)` |
[
"app"
]
| no | +| [rbac\_enabled](#input\_rbac\_enabled) | Service Account for pods. | `bool` | `true` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [repository](#input\_repository) | Repository URL where to locate the requested chart. | `string` | `null` | no | +| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | +| [verify](#input\_verify) | Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart | `bool` | `false` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [metadata](#output\_metadata) | Block status of the deployed release | + + +## References +* [ADR-0067](../../../../docs/adr/0067-secrets-manager-strategy.md) +* https://external-secrets.io/v0.5.9/ +* https://external-secrets.io/v0.5.9/provider-aws-parameter-store/ diff --git a/modules/eks/external-secrets-operator/charts/external-ssm-secrets/.helmignore b/modules/eks/external-secrets-operator/charts/external-ssm-secrets/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/modules/eks/external-secrets-operator/charts/external-ssm-secrets/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/modules/eks/external-secrets-operator/charts/external-ssm-secrets/Chart.yaml b/modules/eks/external-secrets-operator/charts/external-ssm-secrets/Chart.yaml new file mode 100644 index 000000000..3725b354f --- /dev/null +++ b/modules/eks/external-secrets-operator/charts/external-ssm-secrets/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: external-ssm-secrets +description: This Chart handles deploying custom resource definitions needed to access SSM via external-secrets-operator + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/modules/eks/external-secrets-operator/charts/external-ssm-secrets/templates/ssm-secret-store.yaml b/modules/eks/external-secrets-operator/charts/external-ssm-secrets/templates/ssm-secret-store.yaml new file mode 100644 index 000000000..a1482674b --- /dev/null +++ b/modules/eks/external-secrets-operator/charts/external-ssm-secrets/templates/ssm-secret-store.yaml @@ -0,0 +1,10 @@ +apiVersion: external-secrets.io/v1beta1 +kind: ClusterSecretStore +metadata: + name: "secret-store-parameter-store" +spec: + provider: + aws: + service: ParameterStore + region: {{ .Values.region }} + role: {{ .Values.role }} # role is created via helm-release; see `service_account_set_key_path` diff --git a/modules/eks/external-secrets-operator/context.tf b/modules/eks/external-secrets-operator/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/external-secrets-operator/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/external-secrets-operator/examples/app-secrets.yaml b/modules/eks/external-secrets-operator/examples/app-secrets.yaml new file mode 100644 index 000000000..48e296f26 --- /dev/null +++ b/modules/eks/external-secrets-operator/examples/app-secrets.yaml @@ -0,0 +1,24 @@ +# example to fetch all secrets underneath the `/app` prefix (service). +# Keys are rewritten within the K8S Secret to be predictable and omit the +# prefix. + +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: app-secrets +spec: + refreshInterval: "0" + secretStoreRef: + name: "secret-store-parameter-store" + kind: SecretStore + target: + creationPolicy: 'Owner' + name: app-secrets + dataFrom: + - find: + name: + regexp: "^/app" + rewrite: + - regexp: + source: "/app/(.*)" + target: "$1" \ No newline at end of file diff --git a/modules/eks/external-secrets-operator/examples/external-secrets.yaml b/modules/eks/external-secrets-operator/examples/external-secrets.yaml new file mode 100644 index 000000000..18a346538 --- /dev/null +++ b/modules/eks/external-secrets-operator/examples/external-secrets.yaml @@ -0,0 +1,18 @@ +# example to fetch a single secret from our Parameter Store `SecretStore` + +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: single-secret +spec: + refreshInterval: 0 + secretStoreRef: + name: "secret-store-parameter-store" + kind: SecretStore + target: + creationPolicy: 'Owner' + name: single-secret + data: + - secretKey: good_secret + remoteRef: + key: /app/good_secret \ No newline at end of file diff --git a/modules/eks/external-secrets-operator/helm-variables.tf b/modules/eks/external-secrets-operator/helm-variables.tf new file mode 100644 index 000000000..ca1c6c7b2 --- /dev/null +++ b/modules/eks/external-secrets-operator/helm-variables.tf @@ -0,0 +1,58 @@ +variable "kubernetes_namespace" { + type = string + description = "The namespace to install the release into." +} + +variable "description" { + type = string + description = "Set release description attribute (visible in the history)." + default = null +} + +variable "repository" { + type = string + description = "Repository URL where to locate the requested chart." + default = null +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." + default = null +} + +variable "create_namespace" { + type = bool + description = "Create the Kubernetes namespace if it does not yet exist" + default = true +} + +variable "verify" { + type = bool + description = "Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart" + default = false +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`." + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used." + default = true +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails." + default = true +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = null +} diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf new file mode 100644 index 000000000..6827768dd --- /dev/null +++ b/modules/eks/external-secrets-operator/main.tf @@ -0,0 +1,120 @@ +locals { + enabled = module.this.enabled + account_name = lookup(module.this.descriptors, "account_name", module.this.stage) + account = module.account_map.outputs.full_account_map[local.account_name] +} + +resource "kubernetes_namespace" "default" { + count = local.enabled && var.create_namespace ? 1 : 0 + + metadata { + name = var.kubernetes_namespace + + labels = module.this.tags + } +} + +# CRDs are automatically installed by "cloudposse/helm-release/aws" +# https://external-secrets.io/v0.5.9/guides-getting-started/ +module "external_secrets_operator" { + source = "cloudposse/helm-release/aws" + version = "0.5.0" + + name = "" # avoids hitting length restrictions on IAM Role names + description = "External Secrets Operator is a Kubernetes operator that integrates external secret management systems including AWS SSM, Parameter Store, Hasicorp Vault, 1Password Secrets Automation, etc. It reads values from external vaults and injects values as a Kubernetes Secret" + + repository = "https://charts.external-secrets.io" + chart = "external-secrets" + chart_version = "0.6.0-rc1" # using RC to address this bug https://github.com/external-secrets/external-secrets/issues/1511 + kubernetes_namespace = join("", kubernetes_namespace.default.*.id) + create_namespace = false + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") + + service_account_name = module.this.name + service_account_namespace = var.kubernetes_namespace + + iam_role_enabled = true + iam_policy_statements = { + ReadParameterStore = { + effect = "Allow" + actions = [ + "ssm:GetParameter*" + ] + resources = [for parameter_store_path in var.parameter_store_paths : ( + "arn:aws:ssm:${var.region}:${local.account}:parameter/${parameter_store_path}/*" + )] + } + DescribeParameters = { + effect = "Allow" + actions = [ + "ssm:DescribeParameter*" + ] + resources = [ + "arn:aws:ssm:${var.region}:${local.account}:*" + ] + } + } + + values = compact([ + yamlencode({ + serviceAccount = { + name = module.this.name + } + rbac = { + create = var.rbac_enabled + } + }) + ]) + + context = module.this.context +} + +module "external_ssm_secrets" { + source = "cloudposse/helm-release/aws" + version = "0.5.0" + + name = "ssm" # avoids hitting length restrictions on IAM Role names + description = "This Chart uses creates a SecretStore and ExternalSecret to pull variables (under a given path) from AWS SSM Parameter Store into a Kubernetes secret." + + chart = "${path.module}/charts/external-ssm-secrets" + kubernetes_namespace = join("", kubernetes_namespace.default.*.id) + create_namespace = false + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") + + service_account_name = module.this.name + service_account_namespace = var.kubernetes_namespace + service_account_role_arn_annotation_enabled = true + service_account_set_key_path = "role" + + values = compact([ + yamlencode({ + region = var.region, + parameter_store_paths = var.parameter_store_paths + resources = var.resources + serviceAccount = { + name = module.this.name + } + rbac = { + create = var.rbac_enabled + } + }) + ]) + + context = module.this.context + + depends_on = [ + # CRDs from external_secrets_operator need to be installed first + module.external_secrets_operator, + ] +} + diff --git a/modules/eks/external-secrets-operator/outputs.tf b/modules/eks/external-secrets-operator/outputs.tf new file mode 100644 index 000000000..273251dd7 --- /dev/null +++ b/modules/eks/external-secrets-operator/outputs.tf @@ -0,0 +1,4 @@ +output "metadata" { + value = try(one(module.external_secrets_operator.metadata), null) + description = "Block status of the deployed release" +} diff --git a/modules/eks/external-secrets-operator/provider-helm.tf b/modules/eks/external-secrets-operator/provider-helm.tf new file mode 100644 index 000000000..20e4d3837 --- /dev/null +++ b/modules/eks/external-secrets-operator/provider-helm.tf @@ -0,0 +1,158 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = true + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data + eks_cluster_id = module.eks.outputs.eks_cluster_id + eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/external-secrets-operator/providers.tf b/modules/eks/external-secrets-operator/providers.tf new file mode 100644 index 000000000..2775903d2 --- /dev/null +++ b/modules/eks/external-secrets-operator/providers.tf @@ -0,0 +1,40 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} + +data "aws_eks_cluster" "kubernetes" { + count = local.enabled ? 1 : 0 + + name = module.eks.outputs.eks_cluster_id +} + +data "aws_eks_cluster_auth" "kubernetes" { + count = local.enabled ? 1 : 0 + + name = module.eks.outputs.eks_cluster_id +} diff --git a/modules/eks/external-secrets-operator/remote-state.tf b/modules/eks/external-secrets-operator/remote-state.tf new file mode 100644 index 000000000..9503744ff --- /dev/null +++ b/modules/eks/external-secrets-operator/remote-state.tf @@ -0,0 +1,18 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.eks_component_name + + context = module.this.context +} + +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + component = "account-map" + tenant = module.iam_roles.global_tenant_name + environment = module.iam_roles.global_environment_name + stage = module.iam_roles.global_stage_name + context = module.this.context +} diff --git a/modules/eks/external-secrets-operator/variables.tf b/modules/eks/external-secrets-operator/variables.tf new file mode 100644 index 000000000..d95d9ca22 --- /dev/null +++ b/modules/eks/external-secrets-operator/variables.tf @@ -0,0 +1,36 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "rbac_enabled" { + type = bool + default = true + description = "Service Account for pods." +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "parameter_store_paths" { + type = set(string) + description = "A list of path prefixes that the SecretStore is allowed to access via IAM. This should match the convention 'service' that Chamber uploads keys under." + default = ["app"] +} + +variable "resources" { + type = object({ + limits = object({ + cpu = string + memory = string + }) + requests = object({ + cpu = string + memory = string + }) + }) + description = "The cpu and memory of the deployment's limits and requests." +} diff --git a/modules/eks/external-secrets-operator/versions.tf b/modules/eks/external-secrets-operator/versions.tf new file mode 100644 index 000000000..b7a1a1986 --- /dev/null +++ b/modules/eks/external-secrets-operator/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1" + } + } +} From c08921ec277064a5c9a1ed3ff23289c4034f8acc Mon Sep 17 00:00:00 2001 From: Veronika Gnilitska <30597968+gberenice@users.noreply.github.com> Date: Tue, 21 Mar 2023 17:42:24 +0200 Subject: [PATCH 064/501] update 'datadog-lambda-forwarder' v1.3.0 (#601) --- modules/datadog-lambda-forwarder/README.md | 3 +- modules/datadog-lambda-forwarder/main.tf | 7 ++-- modules/datadog-lambda-forwarder/variables.tf | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 50aee326e..6dd50b355 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -64,7 +64,7 @@ components: |------|--------|---------| | [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.2.0 | +| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.3.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [log\_group\_prefix](#module\_log\_group\_prefix) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -84,6 +84,7 @@ components: |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [cloudwatch\_forwarder\_event\_patterns](#input\_cloudwatch\_forwarder\_event\_patterns) | Map of title => CloudWatch Event patterns to forward to Datadog. Event structure from here:
Example:
hcl
cloudwatch_forwarder_event_rules = {
"guardduty" = {
source = ["aws.guardduty"]
detail-type = ["GuardDuty Finding"]
}
"ec2-terminated" = {
source = ["aws.ec2"]
detail-type = ["EC2 Instance State-change Notification"]
detail = {
state = ["terminated"]
}
}
}
|
map(object({
version = optional(list(string))
id = optional(list(string))
detail-type = optional(list(string))
source = optional(list(string))
account = optional(list(string))
time = optional(list(string))
region = optional(list(string))
resources = optional(list(string))
detail = optional(map(list(string)))
}))
| `{}` | no | | [cloudwatch\_forwarder\_log\_groups](#input\_cloudwatch\_forwarder\_log\_groups) | Map of CloudWatch Log Groups with a filter pattern that the Lambda forwarder will send logs from. For example: { mysql1 = { name = "/aws/rds/maincluster", filter\_pattern = "" } | `map(map(string))` | `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [context\_tags](#input\_context\_tags) | List of context tags to add to each monitor | `set(string)` |
[
"namespace",
"tenant",
"environment",
"stage"
]
| no | diff --git a/modules/datadog-lambda-forwarder/main.tf b/modules/datadog-lambda-forwarder/main.tf index 90249f919..afc5c8b32 100644 --- a/modules/datadog-lambda-forwarder/main.tf +++ b/modules/datadog-lambda-forwarder/main.tf @@ -40,10 +40,11 @@ module "log_group_prefix" { module "datadog_lambda_forwarder" { source = "cloudposse/datadog-lambda-forwarder/aws" - version = "1.2.0" + version = "1.3.0" - cloudwatch_forwarder_log_groups = local.cloudwatch_forwarder_log_groups - dd_api_key_kms_ciphertext_blob = var.dd_api_key_kms_ciphertext_blob + cloudwatch_forwarder_log_groups = local.cloudwatch_forwarder_log_groups + cloudwatch_forwarder_event_patterns = var.cloudwatch_forwarder_event_patterns + dd_api_key_kms_ciphertext_blob = var.dd_api_key_kms_ciphertext_blob dd_api_key_source = { resource = lower(module.datadog_configuration.datadog_secrets_store_type) identifier = module.datadog_configuration.datadog_api_key_location diff --git a/modules/datadog-lambda-forwarder/variables.tf b/modules/datadog-lambda-forwarder/variables.tf index f7be4190b..fbb83bd6c 100644 --- a/modules/datadog-lambda-forwarder/variables.tf +++ b/modules/datadog-lambda-forwarder/variables.tf @@ -114,6 +114,40 @@ variable "cloudwatch_forwarder_log_groups" { default = {} } +variable "cloudwatch_forwarder_event_patterns" { + type = map(object({ + version = optional(list(string)) + id = optional(list(string)) + detail-type = optional(list(string)) + source = optional(list(string)) + account = optional(list(string)) + time = optional(list(string)) + region = optional(list(string)) + resources = optional(list(string)) + detail = optional(map(list(string))) + })) + description = <<-EOF + Map of title => CloudWatch Event patterns to forward to Datadog. Event structure from here: + Example: + ```hcl + cloudwatch_forwarder_event_rules = { + "guardduty" = { + source = ["aws.guardduty"] + detail-type = ["GuardDuty Finding"] + } + "ec2-terminated" = { + source = ["aws.ec2"] + detail-type = ["EC2 Instance State-change Notification"] + detail = { + state = ["terminated"] + } + } + } + ``` + EOF + default = {} +} + variable "forwarder_lambda_debug_enabled" { type = bool description = "Whether to enable or disable debug for the Lambda forwarder" From f73a4a068279bfd36e9d1d388ef662a45c36d02e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 21 Mar 2023 12:21:40 -0700 Subject: [PATCH 065/501] Upstream AWS Teams components (#600) --- modules/aws-sso/README.md | 1 + modules/aws-sso/policy-ReadOnlyAccess.tf | 20 +++++++++- modules/aws-team-roles/README.md | 6 ++- modules/aws-team-roles/main.tf | 1 + modules/aws-team-roles/policy-eks-viewer.tf | 41 +++++++++++++++++++++ modules/aws-team-roles/versions.tf | 4 ++ 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 modules/aws-team-roles/policy-eks-viewer.tf diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index 202b6b2c5..c75d699d5 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -137,6 +137,7 @@ components: | [aws_iam_policy_document.TerraformUpdateAccess](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.assume_aws_team](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.dns_administrator_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.eks_read_only](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | ## Inputs diff --git a/modules/aws-sso/policy-ReadOnlyAccess.tf b/modules/aws-sso/policy-ReadOnlyAccess.tf index e9ce242a4..88660787d 100644 --- a/modules/aws-sso/policy-ReadOnlyAccess.tf +++ b/modules/aws-sso/policy-ReadOnlyAccess.tf @@ -5,11 +5,27 @@ locals { relay_state = "", session_duration = "", tags = {}, - inline_policy = "" + inline_policy = data.aws_iam_policy_document.eks_read_only.json, policy_attachments = [ "arn:${local.aws_partition}:iam::aws:policy/ReadOnlyAccess", - "arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess", + "arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess" ] customer_managed_policy_attachments = [] }] } + +data "aws_iam_policy_document" "eks_read_only" { + statement { + sid = "AllowEKSView" + effect = "Allow" + actions = [ + "eks:Get*", + "eks:Describe*", + "eks:List*", + "eks:Access*" + ] + resources = [ + "*" + ] + } +} diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index 420226cc6..8b12f21dd 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -112,13 +112,14 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | +| [local](#requirement\_local) | >= 1.3 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [local](#provider\_local) | n/a | +| [local](#provider\_local) | >= 1.3 | ## Modules @@ -135,6 +136,7 @@ components: |------|------| | [aws_iam_policy.billing_admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.billing_read_only](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.eks_viewer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.support](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | @@ -144,6 +146,8 @@ components: | [aws_iam_policy.aws_support_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | | [aws_iam_policy_document.assume_role_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.billing_admin_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.eks_view_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.eks_viewer_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.support_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.support_access_trusted_advisor](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | diff --git a/modules/aws-team-roles/main.tf b/modules/aws-team-roles/main.tf index b812de247..cc17c2556 100644 --- a/modules/aws-team-roles/main.tf +++ b/modules/aws-team-roles/main.tf @@ -14,6 +14,7 @@ locals { billing_read_only = try(aws_iam_policy.billing_read_only[0].arn, null) billing_admin = try(aws_iam_policy.billing_admin[0].arn, null) support = try(aws_iam_policy.support[0].arn, null) + eks_viewer = try(aws_iam_policy.eks_viewer[0].arn, null) } configured_policies = flatten([for k, v in local.roles_config : v.role_policy_arns]) diff --git a/modules/aws-team-roles/policy-eks-viewer.tf b/modules/aws-team-roles/policy-eks-viewer.tf new file mode 100644 index 000000000..2a758a72d --- /dev/null +++ b/modules/aws-team-roles/policy-eks-viewer.tf @@ -0,0 +1,41 @@ +locals { + eks_viewer_enabled = contains(local.configured_policies, "eks_viewer") + account_name = lookup(module.this.descriptors, "account_name", module.this.stage) + account_number = module.account_map.outputs.full_account_map[local.account_name] +} + +data "aws_iam_policy_document" "eks_view_access" { + count = local.eks_viewer_enabled ? 1 : 0 + + statement { + sid = "AllowEKSView" + effect = "Allow" + actions = [ + "eks:Get*", + "eks:Describe*", + "eks:List*", + "eks:Access*" + ] + resources = [ + "*" + ] + } + +} + +data "aws_iam_policy_document" "eks_viewer_access_aggregated" { + count = local.eks_viewer_enabled ? 1 : 0 + + source_policy_documents = [ + data.aws_iam_policy_document.eks_view_access[0].json, + ] +} + +resource "aws_iam_policy" "eks_viewer" { + count = local.eks_viewer_enabled ? 1 : 0 + + name = format("%s-eks_viewer", module.this.id) + policy = data.aws_iam_policy_document.eks_viewer_access_aggregated[0].json + + tags = module.this.tags +} diff --git a/modules/aws-team-roles/versions.tf b/modules/aws-team-roles/versions.tf index cc73ffd35..2fdade250 100644 --- a/modules/aws-team-roles/versions.tf +++ b/modules/aws-team-roles/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = ">= 4.9.0" } + local = { + source = "hashicorp/local" + version = ">= 1.3" + } } } From ed17ab62a67d431b7b7cfc8203ff112ca1bb08b2 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 22 Mar 2023 10:40:07 -0700 Subject: [PATCH 066/501] update `opsgenie-team` to be delete-able via `enabled: false` (#589) --- modules/opsgenie-team/README.md | 12 +-- modules/opsgenie-team/datadog-integration.tf | 76 +------------------ modules/opsgenie-team/main.tf | 2 +- .../opsgenie-team/modules/escalation/main.tf | 2 +- modules/opsgenie-team/provider-datadog.tf | 15 ++++ modules/opsgenie-team/providers.tf | 1 - 6 files changed, 20 insertions(+), 88 deletions(-) create mode 100644 modules/opsgenie-team/provider-datadog.tf diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index a3e1d196c..abf26988f 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -266,10 +266,11 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | Name | Source | Version | |------|--------|---------| +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [escalation](#module\_escalation) | ./modules/escalation | n/a | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [integration](#module\_integration) | ./modules/integration | n/a | -| [members\_merge](#module\_members\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.1 | +| [members\_merge](#module\_members\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | | [routing](#module\_routing) | ./modules/routing | n/a | | [schedule](#module\_schedule) | cloudposse/incident-management/opsgenie//modules/schedule | 0.16.0 | | [service](#module\_service) | cloudposse/incident-management/opsgenie//modules/service | 0.16.0 | @@ -281,12 +282,6 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | Name | Type | |------|------| | [datadog_integration_opsgenie_service_object.fake_service_name](https://registry.terraform.io/providers/datadog/datadog/latest/docs/resources/integration_opsgenie_service_object) | resource | -| [aws_secretsmanager_secret.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | -| [aws_secretsmanager_secret.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source | -| [aws_secretsmanager_secret_version.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | -| [aws_secretsmanager_secret_version.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source | -| [aws_ssm_parameter.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [aws_ssm_parameter.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.opsgenie_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.opsgenie_team_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [opsgenie_team.existing](https://registry.terraform.io/providers/opsgenie/opsgenie/latest/docs/data-sources/team) | data source | @@ -300,10 +295,7 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_only\_integrations\_enabled](#input\_create\_only\_integrations\_enabled) | Whether to reuse all existing resources and only create new integrations | `bool` | `false` | no | -| [datadog\_api\_secret\_key](#input\_datadog\_api\_secret\_key) | The key of the Datadog API secret | `string` | `"datadog/datadog_api_key"` | no | -| [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The key of the Datadog Application secret | `string` | `"datadog/datadog_app_key"` | no | | [datadog\_integration\_enabled](#input\_datadog\_integration\_enabled) | Whether to enable Datadog integration with opsgenie (datadog side) | `bool` | `true` | no | -| [datadog\_secrets\_store\_type](#input\_datadog\_secrets\_store\_type) | Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM` | `string` | `"SSM"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | diff --git a/modules/opsgenie-team/datadog-integration.tf b/modules/opsgenie-team/datadog-integration.tf index 0e61bafc0..132712d01 100644 --- a/modules/opsgenie-team/datadog-integration.tf +++ b/modules/opsgenie-team/datadog-integration.tf @@ -16,79 +16,5 @@ resource "datadog_integration_opsgenie_service_object" "fake_service_name" { name = local.team_name opsgenie_api_key = data.aws_ssm_parameter.opsgenie_team_api_key[0].value region = "us" - depends_on = [module.integration] -} - - - -// Provider Configuration - -provider "datadog" { - api_key = local.datadog_api_key - app_key = local.datadog_app_key - validate = local.enabled -} - -locals { - asm_enabled = local.enabled && var.datadog_secrets_store_type == "ASM" - ssm_enabled = local.enabled && var.datadog_secrets_store_type == "SSM" - - # https://docs.datadoghq.com/account_management/api-app-keys/ - datadog_api_key = local.enabled ? (local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string : data.aws_ssm_parameter.datadog_api_key[0].value) : null - datadog_app_key = local.enabled ? (local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string : data.aws_ssm_parameter.datadog_app_key[0].value) : null -} - -variable "datadog_secrets_store_type" { - type = string - description = "Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" - default = "SSM" -} - -variable "datadog_api_secret_key" { - type = string - description = "The key of the Datadog API secret" - default = "datadog/datadog_api_key" -} - -variable "datadog_app_secret_key" { - type = string - description = "The key of the Datadog Application secret" - default = "datadog/datadog_app_key" -} - -// ASM - -data "aws_secretsmanager_secret" "datadog_api_key" { - count = local.asm_enabled ? 1 : 0 - name = var.datadog_api_secret_key -} - -data "aws_secretsmanager_secret_version" "datadog_api_key" { - count = local.asm_enabled ? 1 : 0 - secret_id = data.aws_secretsmanager_secret.datadog_api_key[0].id -} - -data "aws_secretsmanager_secret" "datadog_app_key" { - count = local.asm_enabled ? 1 : 0 - name = var.datadog_app_secret_key -} - -data "aws_secretsmanager_secret_version" "datadog_app_key" { - count = local.asm_enabled ? 1 : 0 - secret_id = data.aws_secretsmanager_secret.datadog_app_key[0].id -} - - -// SSM - -data "aws_ssm_parameter" "datadog_api_key" { - count = local.ssm_enabled ? 1 : 0 - name = format("/%s", var.datadog_api_secret_key) - with_decryption = true -} - -data "aws_ssm_parameter" "datadog_app_key" { - count = local.ssm_enabled ? 1 : 0 - name = format("/%s", var.datadog_app_secret_key) - with_decryption = true + depends_on = [module.integration, module.datadog_configuration] } diff --git a/modules/opsgenie-team/main.tf b/modules/opsgenie-team/main.tf index 9b98fdfd8..1e5d4c250 100644 --- a/modules/opsgenie-team/main.tf +++ b/modules/opsgenie-team/main.tf @@ -33,7 +33,7 @@ data "opsgenie_user" "team_members" { module "members_merge" { source = "cloudposse/config/yaml//modules/deepmerge" - version = "1.0.1" + version = "1.0.2" # Cannot use context to disable # See issue: https://github.com/cloudposse/terraform-yaml-config/issues/18 diff --git a/modules/opsgenie-team/modules/escalation/main.tf b/modules/opsgenie-team/modules/escalation/main.tf index a62909395..f98dba087 100644 --- a/modules/opsgenie-team/modules/escalation/main.tf +++ b/modules/opsgenie-team/modules/escalation/main.tf @@ -12,7 +12,7 @@ locals { lookup_schedules = distinct(flatten([ for rule in var.escalation.rules : format(var.team_naming_format, var.team_name, rule.recipient.name) - if rule.recipient.type == "schedule" + if rule.recipient.type == "schedule" && module.this.enabled ])) } diff --git a/modules/opsgenie-team/provider-datadog.tf b/modules/opsgenie-team/provider-datadog.tf new file mode 100644 index 000000000..a3a1ab557 --- /dev/null +++ b/modules/opsgenie-team/provider-datadog.tf @@ -0,0 +1,15 @@ +// This is a custom provider-datadog.tf because it is always enabled, this is because we always need the datadog provider to be configured, even if the module is disabled. + +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + enabled = true + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = "true" +} diff --git a/modules/opsgenie-team/providers.tf b/modules/opsgenie-team/providers.tf index 393058066..643bee068 100644 --- a/modules/opsgenie-team/providers.tf +++ b/modules/opsgenie-team/providers.tf @@ -28,7 +28,6 @@ variable "import_role_arn" { } data "aws_ssm_parameter" "opsgenie_api_key" { - count = local.enabled ? 1 : 0 name = format(var.ssm_parameter_name_format, var.ssm_path, "opsgenie_api_key") with_decryption = true } From 7a4fd8307f91735bf5720aba4e18054cd75c2bea Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 22 Mar 2023 19:01:05 -0700 Subject: [PATCH 067/501] Add Privileged Option for GH OIDC (#603) --- .github/workflows/bats.yml | 2 +- .../github-actions-iam-role.mixin.tf | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bats.yml b/.github/workflows/bats.yml index 6aa05718c..dfb901a67 100644 --- a/.github/workflows/bats.yml +++ b/.github/workflows/bats.yml @@ -33,7 +33,7 @@ jobs: run: | # when running in test-harness, need to mark the directory safe for git operations make safe-directory - MODIFIED_MODULES=($(git diff --name-only origin/${BASE_REF} origin/${HEAD_REF} | xargs -n 1 dirname | sort | uniq | grep ^modules/)) + MODIFIED_MODULES=($(git diff --name-only origin/${BASE_REF} origin/${HEAD_REF} | xargs -n 1 dirname | sort | uniq | grep ^modules/ || true)) if [ -z "$MODIFIED_MODULES" ]; then echo "No modules changed in this PR. Skipping tests." exit 0 diff --git a/mixins/github-actions-iam-role/github-actions-iam-role.mixin.tf b/mixins/github-actions-iam-role/github-actions-iam-role.mixin.tf index 4294f1aac..732d9d68b 100644 --- a/mixins/github-actions-iam-role/github-actions-iam-role.mixin.tf +++ b/mixins/github-actions-iam-role/github-actions-iam-role.mixin.tf @@ -1,3 +1,9 @@ +# This mixin creates an IAM role that a GitHub Action Runner can assume, +# with appropriate controls. Usually this file is included in the component +# that needs to allow the GitHub Action (GHA) to operate with it. For example, +# the `ecr` component includes this to create a role that will +# allow the GHA to push images to the ECR it creates. + # This mixin requires that a local variable named `github_actions_iam_policy` be defined # and its value to be a JSON IAM Policy Document defining the permissions for the role. # It also requires that the `github-oidc-provider` has been previously installed and the @@ -27,6 +33,11 @@ variable "github_actions_iam_role_attributes" { default = [] } +variable "privileged" { + type = bool + description = "True if the default provider already has access to the backend" + default = false +} locals { github_actions_iam_role_enabled = module.this.enabled && var.github_actions_iam_role_enabled && length(var.github_actions_allowed_repos) > 0 @@ -46,6 +57,7 @@ module "gha_assume_role" { source = "../account-map/modules/team-assume-role-policy" trusted_github_repos = var.github_actions_allowed_repos + privileged = var.privileged context = module.gha_role_name.context } From eeb818cf6b01535edfee09cf12b9cdd567c8f6b0 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 23 Mar 2023 14:03:20 -0700 Subject: [PATCH 068/501] exposing variables from 2.0.0 of `VPC` module (#604) --- modules/vpc/README.md | 3 +++ modules/vpc/main.tf | 4 ++++ modules/vpc/variables.tf | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 6a15a9bc2..5e39e192c 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -107,8 +107,11 @@ components: | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [interface\_vpc\_endpoints](#input\_interface\_vpc\_endpoints) | A list of Interface VPC Endpoints to provision into the VPC. | `set(string)` | `[]` | no | +| [ipv4\_additional\_cidr\_block\_associations](#input\_ipv4\_additional\_cidr\_block\_associations) | IPv4 CIDR blocks to assign to the VPC.
`ipv4_cidr_block` can be set explicitly, or set to `null` with the CIDR block derived from `ipv4_ipam_pool_id` using `ipv4_netmask_length`.
Map keys must be known at `plan` time, and are only used to track changes. |
map(object({
ipv4_cidr_block = string
ipv4_ipam_pool_id = string
ipv4_netmask_length = number
}))
| `{}` | no | +| [ipv4\_cidr\_block\_association\_timeouts](#input\_ipv4\_cidr\_block\_association\_timeouts) | Timeouts (in `go` duration format) for creating and destroying IPv4 CIDR block associations |
object({
create = string
delete = string
})
| `null` | no | | [ipv4\_cidrs](#input\_ipv4\_cidrs) | Lists of CIDRs to assign to subnets. Order of CIDRs in the lists must not change over time.
Lists may contain more CIDRs than needed. |
list(object({
private = list(string)
public = list(string)
}))
| `[]` | no | | [ipv4\_primary\_cidr\_block](#input\_ipv4\_primary\_cidr\_block) | The primary IPv4 CIDR block for the VPC.
Either `ipv4_primary_cidr_block` or `ipv4_primary_cidr_block_association` must be set, but not both. | `string` | `null` | no | +| [ipv4\_primary\_cidr\_block\_association](#input\_ipv4\_primary\_cidr\_block\_association) | Configuration of the VPC's primary IPv4 CIDR block via IPAM. Conflicts with `ipv4_primary_cidr_block`.
One of `ipv4_primary_cidr_block` or `ipv4_primary_cidr_block_association` must be set.
Additional CIDR blocks can be set via `ipv4_additional_cidr_block_associations`. |
object({
ipv4_ipam_pool_id = string
ipv4_netmask_length = number
})
| `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index 1125b6063..7cc4a2c0a 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -78,6 +78,10 @@ module "vpc" { internet_gateway_enabled = var.public_subnets_enabled assign_generated_ipv6_cidr_block = var.assign_generated_ipv6_cidr_block + ipv4_primary_cidr_block_association = var.ipv4_primary_cidr_block_association + ipv4_additional_cidr_block_associations = var.ipv4_additional_cidr_block_associations + ipv4_cidr_block_association_timeouts = var.ipv4_cidr_block_association_timeouts + # Required for DNS resolution of VPC Endpoint interfaces, and generally harmless # See https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html#vpc-dns-support dns_hostnames_enabled = true diff --git a/modules/vpc/variables.tf b/modules/vpc/variables.tf index 99133e44e..dd94b519c 100644 --- a/modules/vpc/variables.tf +++ b/modules/vpc/variables.tf @@ -34,6 +34,42 @@ variable "ipv4_primary_cidr_block" { default = null } +variable "ipv4_primary_cidr_block_association" { + type = object({ + ipv4_ipam_pool_id = string + ipv4_netmask_length = number + }) + description = <<-EOT + Configuration of the VPC's primary IPv4 CIDR block via IPAM. Conflicts with `ipv4_primary_cidr_block`. + One of `ipv4_primary_cidr_block` or `ipv4_primary_cidr_block_association` must be set. + Additional CIDR blocks can be set via `ipv4_additional_cidr_block_associations`. + EOT + default = null +} + +variable "ipv4_additional_cidr_block_associations" { + type = map(object({ + ipv4_cidr_block = string + ipv4_ipam_pool_id = string + ipv4_netmask_length = number + })) + description = <<-EOT + IPv4 CIDR blocks to assign to the VPC. + `ipv4_cidr_block` can be set explicitly, or set to `null` with the CIDR block derived from `ipv4_ipam_pool_id` using `ipv4_netmask_length`. + Map keys must be known at `plan` time, and are only used to track changes. + EOT + default = {} +} + +variable "ipv4_cidr_block_association_timeouts" { + type = object({ + create = string + delete = string + }) + description = "Timeouts (in `go` duration format) for creating and destroying IPv4 CIDR block associations" + default = null +} + variable "ipv4_cidrs" { type = list(object({ private = list(string) From decab2cc440a26fdc8b1829d549d129ebb23c4b3 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 24 Mar 2023 12:11:51 -0700 Subject: [PATCH 069/501] Remove `root_account_tenant_name` (#605) --- modules/account-settings/README.md | 1 - modules/account-settings/providers.tf | 13 +++---------- modules/ecr/README.md | 2 +- modules/ecr/main.tf | 2 +- modules/eks/alb-controller-ingress-group/README.md | 3 --- modules/eks/karpenter/README.md | 1 - modules/iam-service-linked-roles/README.md | 1 - modules/sso/README.md | 1 - modules/sso/providers.tf | 12 +++--------- modules/tgw/hub/README.md | 2 +- modules/tgw/hub/versions.tf | 2 +- modules/tgw/spoke/README.md | 3 +-- modules/tgw/spoke/versions.tf | 2 +- 13 files changed, 12 insertions(+), 33 deletions(-) diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index 12320f033..17567e944 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -107,7 +107,6 @@ components: | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [root\_account\_tenant\_name](#input\_root\_account\_tenant\_name) | The tenant name for the root account | `string` | `null` | no | | [service\_quotas](#input\_service\_quotas) | A list of service quotas to manage or lookup.
To lookup the value of a service quota, set `value = null` and either `quota_code` or `quota_name`.
To manage a service quota, set `value` to a number. Service Quotas can only be managed via `quota_code`.
For a more specific example, see https://github.com/cloudposse/terraform-aws-service-quotas/blob/master/examples/complete/fixtures.us-east-2.tfvars. | `list(any)` | `[]` | no | | [service\_quotas\_enabled](#input\_service\_quotas\_enabled) | Whether or not this component should handle Service Quotas | `bool` | `false` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/account-settings/providers.tf b/modules/account-settings/providers.tf index 54f0d0f04..c4f45ca75 100644 --- a/modules/account-settings/providers.tf +++ b/modules/account-settings/providers.tf @@ -12,10 +12,9 @@ provider "aws" { } module "iam_roles" { - source = "../account-map/modules/iam-roles" - privileged = true - global_tenant_name = var.root_account_tenant_name - context = module.this.context + source = "../account-map/modules/iam-roles" + privileged = true + context = module.this.context } variable "import_profile_name" { @@ -29,9 +28,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - -variable "root_account_tenant_name" { - type = string - description = "The tenant name for the root account" - default = null -} diff --git a/modules/ecr/README.md b/modules/ecr/README.md index a5b6ae6ce..64b333f4e 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -63,7 +63,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [ecr](#module\_ecr) | cloudposse/ecr/aws | 0.34.0 | +| [ecr](#module\_ecr) | cloudposse/ecr/aws | 0.35.0 | | [full\_access](#module\_full\_access) | ../account-map/modules/roles-to-principals | n/a | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [readonly\_access](#module\_readonly\_access) | ../account-map/modules/roles-to-principals | n/a | diff --git a/modules/ecr/main.tf b/modules/ecr/main.tf index 038f98184..41f56fff7 100644 --- a/modules/ecr/main.tf +++ b/modules/ecr/main.tf @@ -20,7 +20,7 @@ locals { module "ecr" { source = "cloudposse/ecr/aws" - version = "0.34.0" + version = "0.35.0" protected_tags = var.protected_tags enable_lifecycle_policy = var.enable_lifecycle_policy diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index 1d5e41867..d5b4f004c 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -28,9 +28,6 @@ components: enabled: true # change the name of the Ingress Group name: alb-controller-ingress-group - # if this is not set, the expectation is that account-map - # is deployed within the same tenant - root_account_tenant_name: core ``` diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 1d2e32f9e..db88f6df2 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -24,7 +24,6 @@ components: workspace_enabled: true vars: enabled: true - root_account_tenant_name: core tags: Team: sre Service: karpenter diff --git a/modules/iam-service-linked-roles/README.md b/modules/iam-service-linked-roles/README.md index d51670da7..7ad3912e3 100644 --- a/modules/iam-service-linked-roles/README.md +++ b/modules/iam-service-linked-roles/README.md @@ -15,7 +15,6 @@ components: workspace_enabled: true vars: enabled: true - root_account_tenant_name: core service_linked_roles: spot_amazonaws_com: aws_service_name: "spot.amazonaws.com" diff --git a/modules/sso/README.md b/modules/sso/README.md index 6627e2045..86ac7e2c8 100644 --- a/modules/sso/README.md +++ b/modules/sso/README.md @@ -71,7 +71,6 @@ components: | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [root\_account\_tenant\_name](#input\_root\_account\_tenant\_name) | The tenant name for the root account | `string` | `null` | no | | [saml\_providers](#input\_saml\_providers) | Map of provider names to XML data filenames | `map(string)` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/modules/sso/providers.tf b/modules/sso/providers.tf index 54f0d0f04..0f428a1a7 100644 --- a/modules/sso/providers.tf +++ b/modules/sso/providers.tf @@ -12,10 +12,9 @@ provider "aws" { } module "iam_roles" { - source = "../account-map/modules/iam-roles" - privileged = true - global_tenant_name = var.root_account_tenant_name - context = module.this.context + source = "../account-map/modules/iam-roles" + privileged = true + context = module.this.context } variable "import_profile_name" { @@ -30,8 +29,3 @@ variable "import_role_arn" { description = "IAM Role ARN to use when importing a resource" } -variable "root_account_tenant_name" { - type = string - description = "The tenant name for the root account" - default = null -} diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 47599820b..9b88a8c2b 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -48,7 +48,7 @@ atmos terraform apply tgw/hub -s --network | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.1 | +| [aws](#requirement\_aws) | >= 4.1 | ## Providers diff --git a/modules/tgw/hub/versions.tf b/modules/tgw/hub/versions.tf index 99bf30a36..f0e7120a6 100644 --- a/modules/tgw/hub/versions.tf +++ b/modules/tgw/hub/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.1" + version = ">= 4.1" } } } diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index f2002cec1..605510858 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -26,7 +26,6 @@ components: tags: Team: sre Service: tgw-spoke - root_account_tenant_name: core tgw/spoke: metadata: @@ -71,7 +70,7 @@ atmos terraform apply tgw/spoke -s -- | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.1 | +| [aws](#requirement\_aws) | >= 4.1 | ## Providers diff --git a/modules/tgw/spoke/versions.tf b/modules/tgw/spoke/versions.tf index 99bf30a36..f0e7120a6 100644 --- a/modules/tgw/spoke/versions.tf +++ b/modules/tgw/spoke/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.1" + version = ">= 4.1" } } } From ef094187bbc30e0f3bb46e21355d989300ed2e0e Mon Sep 17 00:00:00 2001 From: nitrocode <7775707+nitrocode@users.noreply.github.com> Date: Sun, 26 Mar 2023 22:41:17 -0500 Subject: [PATCH 070/501] Upstream latest datadog-agent and datadog-configuration updates (#598) --- .gitignore | 2 + modules/datadog-configuration/README.md | 12 ++- .../modules/datadog_keys/README.md | 72 ++++++++++++++++- .../modules/datadog_keys/main.tf | 1 + .../modules/datadog_keys/variables.tf | 11 --- .../modules/datadog_keys/versions.tf | 10 +++ modules/datadog-configuration/ssm.tf | 8 +- modules/datadog-configuration/variables.tf | 6 -- modules/{ => eks}/datadog-agent/README.md | 19 +++-- .../cluster-checks/defaults/http_checks.yaml | 1 - .../cluster-checks/dev/http_checks.yaml | 0 modules/{ => eks}/datadog-agent/context.tf | 0 .../{ => eks}/datadog-agent/helm-variables.tf | 0 modules/{ => eks}/datadog-agent/main.tf | 79 +++++++++++++++---- modules/{ => eks}/datadog-agent/outputs.tf | 0 .../{ => eks}/datadog-agent/provider-helm.tf | 0 modules/{ => eks}/datadog-agent/providers.tf | 15 +--- .../{ => eks}/datadog-agent/remote-state.tf | 0 modules/{ => eks}/datadog-agent/values.yaml | 28 ++++--- modules/{ => eks}/datadog-agent/variables.tf | 30 +++---- modules/{ => eks}/datadog-agent/versions.tf | 2 +- 21 files changed, 195 insertions(+), 101 deletions(-) create mode 100644 modules/datadog-configuration/modules/datadog_keys/versions.tf rename modules/{ => eks}/datadog-agent/README.md (94%) rename modules/{ => eks}/datadog-agent/catalog/cluster-checks/defaults/http_checks.yaml (99%) rename modules/{ => eks}/datadog-agent/catalog/cluster-checks/dev/http_checks.yaml (100%) rename modules/{ => eks}/datadog-agent/context.tf (100%) rename modules/{ => eks}/datadog-agent/helm-variables.tf (100%) rename modules/{ => eks}/datadog-agent/main.tf (65%) rename modules/{ => eks}/datadog-agent/outputs.tf (100%) rename modules/{ => eks}/datadog-agent/provider-helm.tf (100%) rename modules/{ => eks}/datadog-agent/providers.tf (71%) rename modules/{ => eks}/datadog-agent/remote-state.tf (100%) rename modules/{ => eks}/datadog-agent/values.yaml (79%) rename modules/{ => eks}/datadog-agent/variables.tf (68%) rename modules/{ => eks}/datadog-agent/versions.tf (93%) diff --git a/.gitignore b/.gitignore index 9dae6cd4b..252dc829d 100644 --- a/.gitignore +++ b/.gitignore @@ -191,3 +191,5 @@ dmypy.json cython_debug/ *.backup + +default.auto.tfvars diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index d8aa79159..0c3f75405 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -1,14 +1,15 @@ # Component: `datadog-configuration` -This component is responsible for provisioning SSM or ASM entries for datadog api keys. +This component is responsible for provisioning SSM or ASM entries for Datadog API keys. It's required that the DataDog API and APP secret keys are available in the `var.datadog_secrets_source_store_account` account in AWS SSM Parameter Store at the `/datadog/%v/datadog_app_key` paths (where `%v` are the corresponding account names). This component copies keys from the source account (e.g. `auto`) to the destination account where this is being deployed. The purpose of using this formatted copying of keys handles a couple of problems. + 1. The keys are needed in each account where datadog resources will be deployed. -2. The keys might need to be different per account or tenant, or any subset of accounts. -3. If the keys need to be rotated they can be rotated from a single management account. +1. The keys might need to be different per account or tenant, or any subset of accounts. +1. If the keys need to be rotated they can be rotated from a single management account. This module also has a submodule which allows other resources to quickly use it to create a datadog provider. @@ -24,7 +25,6 @@ Here's an example snippet for how to use this component. It's suggested to apply In this example we use the key paths `/datadog/%v/datadog_api_key` and `/datadog/%v/datadog_app_key` where `%v` is `default`, this can be changed through `datadog_app_secret_key` & `datadog_api_secret_key` variables. The output Keys in the deployed account will be `/datadog/datadog_api_key` and `/datadog/datadog_app_key`. - ```yaml components: terraform: @@ -45,7 +45,6 @@ Here is a snippet of using the `datadog_keys` submodule: ```terraform module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region context = module.this.context } @@ -104,7 +103,6 @@ provider "datadog" { | [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The name of the Datadog APP secret | `string` | `"default"` | no | | [datadog\_app\_secret\_key\_source\_pattern](#input\_datadog\_app\_secret\_key\_source\_pattern) | The format string (%v will be replaced by the var.datadog\_app\_secret\_key) for the key of the Datadog APP secret in the source account | `string` | `"/datadog/%v/datadog_app_key"` | no | | [datadog\_app\_secret\_key\_target\_pattern](#input\_datadog\_app\_secret\_key\_target\_pattern) | The format string (%v will be replaced by the var.datadog\_api\_secret\_key) for the key of the Datadog APP secret in the target account | `string` | `"/datadog/datadog_app_key"` | no | -| [datadog\_aws\_account\_id](#input\_datadog\_aws\_account\_id) | The AWS account ID Datadog's integration servers use for all integrations | `string` | `"464622532012"` | no | | [datadog\_secrets\_source\_store\_account\_region](#input\_datadog\_secrets\_source\_store\_account\_region) | Region for holding Secret Store Datadog Keys, leave as null to use the same region as the stack | `string` | `null` | no | | [datadog\_secrets\_source\_store\_account\_stage](#input\_datadog\_secrets\_source\_store\_account\_stage) | Stage holding Secret Store for Datadog API and app keys. | `string` | `"auto"` | no | | [datadog\_secrets\_source\_store\_account\_tenant](#input\_datadog\_secrets\_source\_store\_account\_tenant) | Tenant holding Secret Store for Datadog API and app keys. | `string` | `"core"` | no | @@ -144,7 +142,7 @@ provider "datadog" { ## References * Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-integration) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-configuration) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index 7591a6f4a..cd9cb611f 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -7,7 +7,6 @@ Useful submodule for other modules to quickly configure the datadog provider ```hcl module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region context = module.this.context } @@ -19,3 +18,74 @@ provider "datadog" { } ``` + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [aws](#requirement\_aws) | >= 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws.dd\_api\_keys](#provider\_aws.dd\_api\_keys) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [always](#module\_always) | cloudposse/label/null | 0.25.0 | +| [datadog\_configuration](#module\_datadog\_configuration) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [utils\_example\_complete](#module\_utils\_example\_complete) | cloudposse/utils/aws | 1.1.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ssm_parameter.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.datadog_app_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global\_environment\_name](#input\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [api\_key\_ssm\_arn](#output\_api\_key\_ssm\_arn) | Datadog API Key SSM ARN | +| [datadog\_api\_key](#output\_datadog\_api\_key) | Datadog API Key | +| [datadog\_api\_key\_location](#output\_datadog\_api\_key\_location) | The Datadog API key in the secrets store | +| [datadog\_api\_url](#output\_datadog\_api\_url) | Datadog API URL | +| [datadog\_app\_key](#output\_datadog\_app\_key) | Datadog APP Key | +| [datadog\_app\_key\_location](#output\_datadog\_app\_key\_location) | The Datadog APP key location in the secrets store | +| [datadog\_secrets\_store\_type](#output\_datadog\_secrets\_store\_type) | The type of the secrets store to use for Datadog API and APP keys | +| [datadog\_site](#output\_datadog\_site) | Datadog Site | +| [datadog\_tags](#output\_datadog\_tags) | The Context Tags in datadog tag format (list of strings formated as 'key:value') | + diff --git a/modules/datadog-configuration/modules/datadog_keys/main.tf b/modules/datadog-configuration/modules/datadog_keys/main.tf index cc4c03d3b..edf24e52e 100644 --- a/modules/datadog-configuration/modules/datadog_keys/main.tf +++ b/modules/datadog-configuration/modules/datadog_keys/main.tf @@ -24,6 +24,7 @@ locals { v != null ? format("%s:%s", k, v) : k ] } + module "datadog_configuration" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" diff --git a/modules/datadog-configuration/modules/datadog_keys/variables.tf b/modules/datadog-configuration/modules/datadog_keys/variables.tf index f6865f7a8..baa0ba33c 100644 --- a/modules/datadog-configuration/modules/datadog_keys/variables.tf +++ b/modules/datadog-configuration/modules/datadog_keys/variables.tf @@ -1,16 +1,5 @@ -variable "region" { - type = string - description = "AWS Region" -} - variable "global_environment_name" { type = string description = "Global environment name" default = "gbl" } - -variable "region_abbreviation_type" { - type = string - description = "Region abbreviation type, must be `to_fixed`, `to_short`, or `identity`" - default = "to_short" -} diff --git a/modules/datadog-configuration/modules/datadog_keys/versions.tf b/modules/datadog-configuration/modules/datadog_keys/versions.tf new file mode 100644 index 000000000..fe97db94b --- /dev/null +++ b/modules/datadog-configuration/modules/datadog_keys/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} diff --git a/modules/datadog-configuration/ssm.tf b/modules/datadog-configuration/ssm.tf index edbf9360d..28e3ccc72 100644 --- a/modules/datadog-configuration/ssm.tf +++ b/modules/datadog-configuration/ssm.tf @@ -11,7 +11,7 @@ locals { } data "aws_ssm_parameter" "datadog_api_key" { - count = var.datadog_secrets_store_type == "SSM" ? 1 : 0 + count = local.ssm_enabled ? 1 : 0 name = format(var.datadog_api_secret_key_source_pattern, var.datadog_api_secret_key) with_decryption = true @@ -19,7 +19,7 @@ data "aws_ssm_parameter" "datadog_api_key" { } data "aws_ssm_parameter" "datadog_app_key" { - count = var.datadog_secrets_store_type == "SSM" ? 1 : 0 + count = local.ssm_enabled ? 1 : 0 name = format(var.datadog_app_secret_key_source_pattern, var.datadog_app_secret_key) with_decryption = true provider = aws.api_keys @@ -32,14 +32,14 @@ module "store_write" { parameter_write = [ { name = local.datadog_api_key_name - value = data.aws_ssm_parameter.datadog_api_key[0].value + value = local.datadog_api_key type = "SecureString" overwrite = "true" description = "Datadog API key" }, { name = local.datadog_app_key_name - value = data.aws_ssm_parameter.datadog_app_key[0].value + value = local.datadog_app_key type = "SecureString" overwrite = "true" description = "Datadog APP key" diff --git a/modules/datadog-configuration/variables.tf b/modules/datadog-configuration/variables.tf index c4ad89d85..c7664670f 100644 --- a/modules/datadog-configuration/variables.tf +++ b/modules/datadog-configuration/variables.tf @@ -3,12 +3,6 @@ variable "region" { description = "AWS Region" } -variable "datadog_aws_account_id" { - type = string - description = "The AWS account ID Datadog's integration servers use for all integrations" - default = "464622532012" -} - variable "datadog_site_url" { type = string description = "The Datadog Site URL, https://docs.datadoghq.com/getting_started/site/" diff --git a/modules/datadog-agent/README.md b/modules/eks/datadog-agent/README.md similarity index 94% rename from modules/datadog-agent/README.md rename to modules/eks/datadog-agent/README.md index 633374c62..c5c09d4f0 100644 --- a/modules/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -145,7 +145,7 @@ https-checks: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | -| [helm](#requirement\_helm) | >= 2.3.0 | +| [helm](#requirement\_helm) | >= 2.7 | | [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | | [utils](#requirement\_utils) | >= 0.3.0 | @@ -161,21 +161,21 @@ https-checks: | Name | Source | Version | |------|--------|---------| | [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.7.0 | -| [datadog\_cluster\_check\_yaml\_config](#module\_datadog\_cluster\_check\_yaml\_config) | cloudposse/config/yaml | 1.0.1 | -| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [datadog\_cluster\_check\_yaml\_config](#module\_datadog\_cluster\_check\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | +| [datadog\_configuration](#module\_datadog\_configuration) | ../../datadog-configuration/modules/datadog_keys | n/a | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [values\_merge](#module\_values\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.1 | +| [values\_merge](#module\_values\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | ## Resources | Name | Type | |------|------| | [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | -| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | ## Inputs @@ -190,8 +190,6 @@ https-checks: | [cluster\_checks\_enabled](#input\_cluster\_checks\_enabled) | Enable Cluster Checks for the Datadog Agent | `bool` | `false` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | -| [datadog\_api\_secret\_key](#input\_datadog\_api\_secret\_key) | The key of the Datadog API secret | `string` | `"datadog/datadog_api_key"` | no | -| [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The key of the Datadog Application secret | `string` | `"datadog/datadog_app_key"` | no | | [datadog\_cluster\_check\_auto\_added\_tags](#input\_datadog\_cluster\_check\_auto\_added\_tags) | List of tags to add to Datadog Cluster Check | `list(string)` |
[
"stage",
"environment"
]
| no | | [datadog\_cluster\_check\_config\_parameters](#input\_datadog\_cluster\_check\_config\_parameters) | Map of parameters to Datadog Cluster Check configurations | `map(any)` | `{}` | no | | [datadog\_cluster\_check\_config\_paths](#input\_datadog\_cluster\_check\_config\_paths) | List of paths to Datadog Cluster Check configurations | `list(string)` | `[]` | no | @@ -203,6 +201,8 @@ https-checks: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [iam\_policy\_statements](#input\_iam\_policy\_statements) | IAM policy for the service account. Required if `var.iam_role_enabled` is `true`. This will not do variable replacements. Please see `var.iam_policy_statements_template_path`. | `any` | `{}` | no | +| [iam\_role\_enabled](#input\_iam\_role\_enabled) | Whether to create an IAM role. Setting this to `true` will also replace any occurrences of `{service_account_role_arn}` in `var.values_template_path` with the ARN of the IAM role created by this module. | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | @@ -226,7 +226,6 @@ https-checks: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [repository](#input\_repository) | Repository URL where to locate the requested chart | `string` | `null` | no | -| [secrets\_store\_type](#input\_secrets\_store\_type) | Secret store type for Datadog API and app keys. Valid values: `SSM`, `ASM` | `string` | `"SSM"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/datadog-agent/catalog/cluster-checks/defaults/http_checks.yaml b/modules/eks/datadog-agent/catalog/cluster-checks/defaults/http_checks.yaml similarity index 99% rename from modules/datadog-agent/catalog/cluster-checks/defaults/http_checks.yaml rename to modules/eks/datadog-agent/catalog/cluster-checks/defaults/http_checks.yaml index 0f33229b3..cdde56c92 100644 --- a/modules/datadog-agent/catalog/cluster-checks/defaults/http_checks.yaml +++ b/modules/eks/datadog-agent/catalog/cluster-checks/defaults/http_checks.yaml @@ -4,4 +4,3 @@ http_check.yaml: instances: - name: "[${stage}] Echo Server" url: "https://echo.${stage}.acme.com" - diff --git a/modules/datadog-agent/catalog/cluster-checks/dev/http_checks.yaml b/modules/eks/datadog-agent/catalog/cluster-checks/dev/http_checks.yaml similarity index 100% rename from modules/datadog-agent/catalog/cluster-checks/dev/http_checks.yaml rename to modules/eks/datadog-agent/catalog/cluster-checks/dev/http_checks.yaml diff --git a/modules/datadog-agent/context.tf b/modules/eks/datadog-agent/context.tf similarity index 100% rename from modules/datadog-agent/context.tf rename to modules/eks/datadog-agent/context.tf diff --git a/modules/datadog-agent/helm-variables.tf b/modules/eks/datadog-agent/helm-variables.tf similarity index 100% rename from modules/datadog-agent/helm-variables.tf rename to modules/eks/datadog-agent/helm-variables.tf diff --git a/modules/datadog-agent/main.tf b/modules/eks/datadog-agent/main.tf similarity index 65% rename from modules/datadog-agent/main.tf rename to modules/eks/datadog-agent/main.tf index 08ad386f8..8f57f4250 100644 --- a/modules/datadog-agent/main.tf +++ b/modules/eks/datadog-agent/main.tf @@ -1,10 +1,3 @@ -module "datadog_configuration" { - source = "../datadog-configuration/modules/datadog_keys" - region = var.region - context = module.this.context -} - - locals { enabled = module.this.enabled @@ -28,10 +21,10 @@ locals { datadog_cluster_checks = { for k, v in local.deep_map_merge : k => merge(v, { - instances : [ + instances = [ for key, val in v.instances : merge(val, { - tags : [ + tags = [ for tag, tag_value in local.context_tags : format("%s:%s", tag, tag_value) if contains(var.datadog_cluster_check_auto_added_tags, tag) @@ -40,21 +33,59 @@ locals { ] }) } - set_datadog_cluster_checks = [for cluster_check_key, cluster_check_value in local.datadog_cluster_checks : - { + set_datadog_cluster_checks = [ + for cluster_check_key, cluster_check_value in local.datadog_cluster_checks : { # Since we are using json pathing to set deep yaml values, and the key we want to set is `something.yaml` # we need to escape the key of the cluster check. name = format("clusterAgent.confd.%s", replace(cluster_check_key, ".", "\\.")) type = "auto" value = yamlencode(cluster_check_value) - }] + } + ] + + # This will match both datadog and datadog-cluster-agent service account names + service_account_name = "datadog*" + + partition = join("", data.aws_partition.current[*].partition) + account_id = join("", data.aws_caller_identity.current[*].account_id) + + iam_role_arn = "arn:${local.partition}:iam::${local.account_id}:role/${module.this.id}-${trimsuffix(local.service_account_name, "*")}@${var.kubernetes_namespace}" + + set_datadog_irsa = var.iam_role_enabled ? [ + { + name = "clusterAgent.rbac.serviceAccountAnnotations.eks\\.amazonaws\\.com/role-arn" + type = "string" + value = local.iam_role_arn + }, + { + name = "agents.rbac.serviceAccountAnnotations.eks\\.amazonaws\\.com/role-arn" + type = "string" + value = local.iam_role_arn + }, + ] : [] +} + +module "datadog_configuration" { + source = "../../datadog-configuration/modules/datadog_keys" + + global_environment_name = null + + context = module.this.context +} + +data "aws_caller_identity" "current" { + count = local.enabled ? 1 : 0 +} + +data "aws_partition" "current" { + count = local.enabled ? 1 : 0 } module "datadog_cluster_check_yaml_config" { count = local.cluster_checks_enabled ? 1 : 0 source = "cloudposse/config/yaml" - version = "1.0.1" + version = "1.0.2" map_config_local_base_path = path.module map_config_paths = var.datadog_cluster_check_config_paths @@ -71,7 +102,7 @@ module "datadog_cluster_check_yaml_config" { module "values_merge" { source = "cloudposse/config/yaml//modules/deepmerge" - version = "1.0.1" + version = "1.0.2" # Merge in order: datadog values, var.values maps = [ @@ -101,7 +132,7 @@ module "datadog_agent" { description = var.description repository = var.repository chart_version = var.chart_version - kubernetes_namespace = join("", kubernetes_namespace.default.*.id) + kubernetes_namespace = var.kubernetes_namespace # join("", kubernetes_namespace.default[*].id) create_namespace = false verify = var.verify wait = var.wait @@ -138,8 +169,22 @@ module "datadog_agent" { name = "datadog.clusterName" type = "string" value = module.eks.outputs.eks_cluster_id - } - ], local.set_datadog_cluster_checks) + }, + ], local.set_datadog_cluster_checks, local.set_datadog_irsa) + + iam_role_enabled = var.iam_role_enabled + + # Add the IAM role using set() + service_account_role_arn_annotation_enabled = false + + # Dictates which ServiceAccounts are allowed to assume the IAM Role. + service_account_name = local.service_account_name + service_account_namespace = var.kubernetes_namespace + + # IAM policy statements to add to the IAM role + iam_policy_statements = var.iam_policy_statements depends_on = [kubernetes_namespace.default] + + context = module.this.context } diff --git a/modules/datadog-agent/outputs.tf b/modules/eks/datadog-agent/outputs.tf similarity index 100% rename from modules/datadog-agent/outputs.tf rename to modules/eks/datadog-agent/outputs.tf diff --git a/modules/datadog-agent/provider-helm.tf b/modules/eks/datadog-agent/provider-helm.tf similarity index 100% rename from modules/datadog-agent/provider-helm.tf rename to modules/eks/datadog-agent/provider-helm.tf diff --git a/modules/datadog-agent/providers.tf b/modules/eks/datadog-agent/providers.tf similarity index 71% rename from modules/datadog-agent/providers.tf rename to modules/eks/datadog-agent/providers.tf index 79558d342..4f6c5692d 100644 --- a/modules/datadog-agent/providers.tf +++ b/modules/eks/datadog-agent/providers.tf @@ -11,7 +11,7 @@ provider "aws" { } module "iam_roles" { - source = "../account-map/modules/iam-roles" + source = "../../account-map/modules/iam-roles" context = module.this.context } @@ -27,17 +27,4 @@ variable "import_role_arn" { description = "IAM Role ARN to use when importing a resource" } -data "aws_eks_cluster" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} - -data "aws_eks_cluster_auth" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} - provider "utils" {} - diff --git a/modules/datadog-agent/remote-state.tf b/modules/eks/datadog-agent/remote-state.tf similarity index 100% rename from modules/datadog-agent/remote-state.tf rename to modules/eks/datadog-agent/remote-state.tf diff --git a/modules/datadog-agent/values.yaml b/modules/eks/datadog-agent/values.yaml similarity index 79% rename from modules/datadog-agent/values.yaml rename to modules/eks/datadog-agent/values.yaml index 8c64186ec..757554c24 100644 --- a/modules/datadog-agent/values.yaml +++ b/modules/eks/datadog-agent/values.yaml @@ -37,8 +37,25 @@ datadog: helmCheck: enabled: true collectEvents: true +agents: + enabled: true + image: + repository: "public.ecr.aws/datadog/agent" + tag: 7 + tolerations: + - effect: NoSchedule + operator: Exists + - effect: NoExecute + operator: Exists + # Per https://github.com/DataDog/helm-charts/blob/main/charts/datadog/README.md#configuration-required-for-amazon-linux-2-based-nodes + podSecurity: + apparmor: + enabled: false clusterAgent: enabled: true + image: + repository: "public.ecr.aws/datadog/cluster-agent" + tag: "7.43.1" replicas: 1 metricsProvider: enabled: false @@ -49,14 +66,3 @@ clusterAgent: limits: cpu: 300m memory: 512Mi -agents: - priorityClassName: "system-node-critical" - tolerations: - - effect: NoSchedule - operator: Exists - - effect: NoExecute - operator: Exists -# Per https://github.com/DataDog/helm-charts/blob/main/charts/datadog/README.md#configuration-required-for-amazon-linux-2-based-nodes - podSecurity: - apparmor: - enabled: false diff --git a/modules/datadog-agent/variables.tf b/modules/eks/datadog-agent/variables.tf similarity index 68% rename from modules/datadog-agent/variables.tf rename to modules/eks/datadog-agent/variables.tf index 1c3adbc3d..f0a6720b3 100644 --- a/modules/datadog-agent/variables.tf +++ b/modules/eks/datadog-agent/variables.tf @@ -3,24 +3,6 @@ variable "region" { description = "AWS Region" } -variable "secrets_store_type" { - type = string - description = "Secret store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" - default = "SSM" -} - -variable "datadog_api_secret_key" { - type = string - description = "The key of the Datadog API secret" - default = "datadog/datadog_api_key" -} - -variable "datadog_app_secret_key" { - type = string - description = "The key of the Datadog Application secret" - default = "datadog/datadog_app_key" -} - variable "datadog_tags" { type = set(string) description = "List of static tags to attach to every metric, event and service check collected by the agent" @@ -62,3 +44,15 @@ variable "values" { description = "Additional values to yamlencode as `helm_release` values." default = {} } + +variable "iam_role_enabled" { + type = bool + description = "Whether to create an IAM role. Setting this to `true` will also replace any occurrences of `{service_account_role_arn}` in `var.values_template_path` with the ARN of the IAM role created by this module." + default = false +} + +variable "iam_policy_statements" { + type = any + description = "IAM policy for the service account. Required if `var.iam_role_enabled` is `true`. This will not do variable replacements. Please see `var.iam_policy_statements_template_path`." + default = {} +} diff --git a/modules/datadog-agent/versions.tf b/modules/eks/datadog-agent/versions.tf similarity index 93% rename from modules/datadog-agent/versions.tf rename to modules/eks/datadog-agent/versions.tf index fea35c3da..3990fdb16 100644 --- a/modules/datadog-agent/versions.tf +++ b/modules/eks/datadog-agent/versions.tf @@ -8,7 +8,7 @@ terraform { } helm = { source = "hashicorp/helm" - version = ">= 2.3.0" + version = ">= 2.7" } utils = { source = "cloudposse/utils" From 30d0da5c33ff4743f591b87816dea7fab3b490b1 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 27 Mar 2023 08:24:24 -0700 Subject: [PATCH 071/501] Update CODEOWNERS to remove contributors (#607) --- .github/CODEOWNERS | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6f64b5a33..d1dc5b195 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,22 +4,18 @@ # Order is important: the last matching pattern has the highest precedence # These owners will be the default owners for everything -* @cloudposse/engineering @cloudposse/contributors +* @cloudposse/engineering @cloudposse/admins -# Cloud Posse must review any changes to Makefiles -**/Makefile @cloudposse/engineering -**/Makefile.* @cloudposse/engineering +# Cloud Posse admins must review any changes to Makefiles +**/Makefile @cloudposse/admins +**/Makefile.* @cloudposse/admins -# Cloud Posse must review any changes to GitHub actions -.github/* @cloudposse/engineering +# Cloud Posse admins must review any changes to GitHub actions +.github/workflows/* @cloudposse/admins -# Cloud Posse must review any changes to standard context definition, -# but some changes can be rubber-stamped. -**/*.tf @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers -README.yaml @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers -README.md @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers -docs/*.md @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers -# Cloud Posse Admins must review all changes to CODEOWNERS or the mergify configuration +# Cloud Posse admins must review all changes to CODEOWNERS or the mergify or release configuration +.github/.github-update-disabled @cloudposse/admins +.github/auto-release.yml @cloudposse/admins .github/mergify.yml @cloudposse/admins .github/CODEOWNERS @cloudposse/admins From b2dfcc2f8f164b5312cf006eb641103df7613c09 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 27 Mar 2023 09:18:59 -0700 Subject: [PATCH 072/501] Update account-map to output account information for aws-config script (#608) --- modules/account-map/README.md | 1 + modules/account-map/account-info.tftmpl | 8 ++++- modules/account-map/outputs.tf | 1 + modules/account-map/variables.tf | 6 ++++ rootfs/usr/local/bin/aws-config | 39 +++++++++++++++++++------ 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index f810124fd..de960d7e6 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -82,6 +82,7 @@ components: | [artifacts\_account\_account\_name](#input\_artifacts\_account\_account\_name) | The stage name for the artifacts account | `string` | `"artifacts"` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [audit\_account\_account\_name](#input\_audit\_account\_account\_name) | The stage name for the audit account | `string` | `"audit"` | no | +| [aws\_config\_identity\_profile\_name](#input\_aws\_config\_identity\_profile\_name) | The AWS config profile name to use as `source_profile` for credentials. | `string` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/account-map/account-info.tftmpl b/modules/account-map/account-info.tftmpl index 3c13181a7..c0b8ce87b 100644 --- a/modules/account-map/account-info.tftmpl +++ b/modules/account-map/account-info.tftmpl @@ -2,7 +2,7 @@ # This script is automatically generated by `atmos terraform account-map`. # Do not modify this script directly. Instead, modify the template file. -# Path: modules/account-map/account-info.tftmpl +# Path: components/terraform/account-map/account-info.tftmpl # CAUTION: this script is appended to other scripts, # so it must not destroy variables like `functions`. @@ -16,6 +16,12 @@ function namespace() { echo ${namespace} } +functions+=("source-profile") +function source-profile() { + echo ${source_profile} +} + + declare -A accounts # root account included diff --git a/modules/account-map/outputs.tf b/modules/account-map/outputs.tf index 51f6855de..3cc8ad634 100644 --- a/modules/account-map/outputs.tf +++ b/modules/account-map/outputs.tf @@ -112,6 +112,7 @@ resource "local_file" "account_info" { account_profiles = local.account_profiles account_role_map = local.account_role_map namespace = module.this.namespace + source_profile = coalesce(var.aws_config_identity_profile_name, format("%s-identity", module.this.namespace)) }) filename = "${path.module}/account-info/${module.this.id}.sh" } diff --git a/modules/account-map/variables.tf b/modules/account-map/variables.tf index 4eb1cfd62..d325b3de2 100644 --- a/modules/account-map/variables.tf +++ b/modules/account-map/variables.tf @@ -77,3 +77,9 @@ variable "profiles_enabled" { default = false description = "Whether or not to enable profiles instead of roles for the backend. If true, profile must be set. If false, role_arn must be set." } + +variable "aws_config_identity_profile_name" { + type = string + default = null + description = "The AWS config profile name to use as `source_profile` for credentials." +} diff --git a/rootfs/usr/local/bin/aws-config b/rootfs/usr/local/bin/aws-config index dbf2579c2..453d07755 100755 --- a/rootfs/usr/local/bin/aws-config +++ b/rootfs/usr/local/bin/aws-config @@ -7,9 +7,15 @@ ## Generates full `aws` CLI configuration for use in Geodesic ## based on SAML authentication and SAML roles. ## -## aws-config saml admin > rootfs/etc/aws-config/aws-switch-roles -## Generates configuration for AWS Switch Role browser plugin, -## except it omits the source profile. TODO: generate source profile +## aws-config switch-roles > rootfs/etc/aws-config/aws-switch-roles +## aws-config switch-roles billing > rootfs/etc/aws-config/aws-switch-roles-billing +## aws-config switch-roles billing_admin > rootfs/etc/aws-config/aws-switch-roles-billing_admin +## Generates configuration for AWS Extend Switch Roles browser plugin +## https://github.com/tilfinltd/aws-extend-switch-roles +## +## aws-config spacelift > rootfs/etc/aws-config/aws-config-spacelift +## Generates `aws` CLI/SDK configuration for Spacelift workers to use +## ## TODO: maybe pull the source files from S3 rather than file system account_sources=("$ATMOS_BASE_PATH/"components/terraform/account-map/account-info/*sh) @@ -17,6 +23,13 @@ iam_sources=("$ATMOS_BASE_PATH/"components/terraform/aws-team-roles/iam-role-inf namespaces=($(for script in "${account_sources[@]}"; do $script namespace; done)) +declare -A source_profiles +for script in "${account_sources[@]}"; do + namespace=$($script namespace) + source_profiles[$namespace]=$($script source-profile) + [[ -n "${source_profiles[$namespace]}" ]] || source_profiles[$namespace]="${namespace}-identity" +done +unset namespace unset _auto_generated_warning function _auto-generated-warning() { @@ -45,7 +58,6 @@ function _extra-profiles() { # Usage: _saml [ ...] function _saml() { local namespace - local source_profile local selected_roles local region="${AWS_REGION:-${AWS_DEFAULT_REGION}}" @@ -57,7 +69,7 @@ function _saml() { [[ -n $selected_roles ]] && ! [[ $selected_roles =~ " $role " ]] && continue printf "[profile %s]\n" "$($source profile $role)" [[ -n ${region} ]] && printf "region = %s\n" "$region" - printf "source_profile = %s-identity\n" "$namespace" + printf "source_profile = %s\n" "${source_profiles[$namespace]}" printf "role_arn = %s\n\n" $($source role-arn $role) done done @@ -82,16 +94,18 @@ functions+=(switch-roles) function switch-roles() { local region="${AWS_REGION:-${AWS_DEFAULT_REGION}}" + printf ";; This configuration file is for the AWS Extend Switch Roles browser plugin.\n\n" + _auto-generated-warning for namespace in "${namespaces[@]}"; do - printf "[profile %s-identity]\n" "$namespace" + printf "[profile %s]\n" "${source_profiles[$namespace]}" [[ -n ${region} ]] && printf "region = %s\n" "$region" printf "aws_account_id = %s\n\n" $($0 -n $namespace account-profile $($0 -n $namespace account-for-role identity)) done echo _no_source_profile=skip - saml admin + saml "${@:-admin}" } functions+=(spacelift) @@ -105,7 +119,7 @@ function spacelift() { # TODO: lookup Spacelift target Role ARN rather than guess/hard code it. profile_base="$($0 -n $namespace account-profile $($0 -n $namespace account-for-role identity))" account_id="$($0 -n $namespace account-id $($0 -n $namespace account-for-role identity))" - printf "[profile %s-identity]\n" "$namespace" + printf "[profile %s]\n" "${source_profiles[$namespace]}" [[ -n ${region} ]] && printf "region = %s\n" "$region" printf "role_arn = arn:aws:iam::%s:role/%s-spacelift\n" "$account_id" "$profile_base" printf "credential_source = Ec2InstanceMetadata\n" @@ -117,6 +131,11 @@ function spacelift() { saml admin terraform } +functions+=(accounts) +function accounts() { + account-ids +} + case $1 in -a*) target_namespace=("${namespaces[@]}") @@ -156,6 +175,8 @@ for namespace in "${target_namespace[@]}"; do export CONFIG_NAMESPACE=$namespace fi main "${args[@]}" - [[ $? == 99 ]] && exit 0 + exit_code=$? + [[ $exit_code == 99 ]] && exit 0 done +exit $exit_code From 60abfec47e98bb119619872dd33c04b4f72273d4 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 28 Mar 2023 11:25:51 -0700 Subject: [PATCH 073/501] Upstream EKS/ARC amd64 Support (#609) --- .../eks/actions-runner-controller/README.md | 73 ++++++++++++------- .../charts/actions-runner/Chart.yaml | 2 +- .../templates/runnerdeployment.yaml | 8 ++ .../charts/actions-runner/values.yaml | 3 + modules/eks/actions-runner-controller/main.tf | 2 + .../resources/values.yaml | 4 + .../actions-runner-controller/variables.tf | 15 +++- 7 files changed, 77 insertions(+), 30 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 90e912762..c9ade2ddd 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -20,55 +20,78 @@ The default catalog values `e.g. stacks/catalog/eks/actions-runner-controller.ya components: terraform: eks/actions-runner-controller: - settings: - spacelift: - workspace_enabled: true vars: enabled: true name: "actions-runner" # avoids hitting name length limit on IAM role chart: "actions-runner-controller" chart_repository: "https://actions-runner-controller.github.io/actions-runner-controller" - chart_version: "0.21.0" + chart_version: "0.22.0" kubernetes_namespace: "actions-runner-system" create_namespace: true + kubeconfig_exec_auth_api_version: "client.authentication.k8s.io/v1beta1" + # helm_manifest_experiment_enabled feature causes inconsistent final plans with charts that have CRDs + # see https://github.com/hashicorp/terraform-provider-helm/issues/711#issuecomment-836192991 + helm_manifest_experiment_enabled: false + + ssm_github_secret_path: "/github_runners/controller_github_app_secret" + github_app_id: "REPLACE_ME_GH_APP_ID" + github_app_installation_id: "REPLACE_ME_GH_INSTALLATION_ID" + + # ssm_github_webhook_secret_token_path: "/github_runners/github_webhook_secret_token" + webhook: + enabled: false + hostname_template: "gha-webhook.%[3]v.%[2]v.%[1]v.acme.com" + + eks_component_name: "eks/cluster" resources: limits: - cpu: 100m - memory: 128Mi + cpu: 500m + memory: 256Mi requests: - cpu: 100m + cpu: 250m memory: 128Mi - ssm_github_token_path: "/github_runners/controller_github_app_secret" - ssm_github_webhook_secret_token_path: "/github_runners/controller_github_app_secret" - github_app_id: "123456" - github_app_installation_id: "234567890" - webhook: - enabled: true - # gha-webhook.use1.auto.core.acme.net - hostname_template: "gha-webhook.%[3]v.%[2]v.%[1]v.acme.net" - timeout: 120 runners: - infrastructure-runner: + infra-runner: + node_selector: + kubernetes.io/os: "linux" + kubernetes.io/arch: "arm64" + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "arm64" + effect: "NoSchedule" type: "repository" # can be either 'organization' or 'repository' dind_enabled: false # If `true`, a Docker sidecar container will be deployed - # To run Docker in Docker (dind), change image from summerwind/actions-runner to summerwind/actions-runner-dind - image: summerwind/actions-runner - scope: "acme/infrastructure" - scale_down_delay_seconds: 300 + # To run Docker in Docker (dind), change image to summerwind/actions-runner-dind + # If not running Docker, change image to summerwind/actions-runner use a smaller image + image: summerwind/actions-runner-dind + # `scope` is org name for Organization runners, repo name for Repository runners + scope: "org/infra" min_replicas: 1 - max_replicas: 5 + max_replicas: 20 + scale_down_delay_seconds: 100 resources: limits: cpu: 200m - memory: 256Mi + memory: 512Mi requests: cpu: 100m memory: 128Mi webhook_driven_scaling_enabled: true + webhook_startup_timeout: "2m" pull_driven_scaling_enabled: false labels: + - "Linux" + - "linux" - "Ubuntu" - - "self-hosted" + - "ubuntu" + - "X64" + - "x64" + - "x86_64" + - "amd64" + - "AMD64" + - "core-auto" + - "common" ``` ### Generating Required Secrets @@ -266,7 +289,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = string
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | | [ssm\_github\_webhook\_secret\_token\_path](#input\_ssm\_github\_webhook\_secret\_token\_path) | The path in SSM to the GitHub Webhook Secret token. | `string` | `""` | no | diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml index 8aea5901e..7d49f9e99 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.1.1 # This chart only deploys Resources for actions-runner-controller, so app version does not really apply. # We use Resource API version instead. diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index e7512d07c..a9204feb4 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -58,6 +58,14 @@ spec: {{- range .Values.labels }} - {{ . | quote }} {{- end }} + {{- if gt ( len (index .Values "node_selector") ) 0 }} + nodeSelector: + {{- toYaml .Values.node_selector | nindent 8 }} + {{- end }} + {{- if gt ( len (index .Values "tolerations") ) 0 }} + tolerations: + {{- toYaml .Values.tolerations | nindent 8 }} + {{- end }} # dockerdWithinRunnerContainer = false means access to a Docker daemon is provided by a sidecar container. dockerdWithinRunnerContainer: {{ .Values.dind_enabled }} image: {{ .Values.image | quote }} diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml index 7f22f82ff..543203cbf 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml @@ -2,6 +2,9 @@ type: "repository" # can be either 'organization' or 'repository' dind_enabled: true # If `true`, a Docker sidecar container will be deployed # To run Docker in Docker (dind), change image from summerwind/actions-runner to summerwind/actions-runner-dind image: summerwind/actions-runner-dind +node_selector: + kubernetes.io/os: "linux" + kubernetes.io/arch: "amd64" scope: "example/app" scale_down_delay_seconds: 300 min_replicas: 1 diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index 673300e2c..ee1357a6c 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -219,6 +219,8 @@ module "actions_runner" { webhook_startup_timeout = coalesce(each.value.webhook_startup_timeout, "${each.value.scale_down_delay_seconds}s") # if webhook_startup_timeout isnt defined, use scale_down_delay_seconds pull_driven_scaling_enabled = each.value.pull_driven_scaling_enabled pvc_enabled = each.value.pvc_enabled + node_selector = each.value.node_selector + tolerations = each.value.tolerations }), local.busy_metrics_filtered[each.key] == null ? "" : yamlencode(local.busy_metrics_filtered[each.key]), ]) diff --git a/modules/eks/actions-runner-controller/resources/values.yaml b/modules/eks/actions-runner-controller/resources/values.yaml index c48e9bbcb..6652ec9a2 100644 --- a/modules/eks/actions-runner-controller/resources/values.yaml +++ b/modules/eks/actions-runner-controller/resources/values.yaml @@ -33,3 +33,7 @@ githubWebhookServer: kubernetes.io/ingress.class: alb podDisruptionBudget: maxUnavailable: "60%" + +nodeSelector: + kubernetes.io/os: "linux" + kubernetes.io/arch: "amd64" diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index ee5bde6e3..dc440bbc6 100644 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -162,10 +162,17 @@ variable "runners" { EOT type = map(object({ - type = string - scope = string - image = optional(string, "") - dind_enabled = bool + type = string + scope = string + image = optional(string, "") + dind_enabled = bool + node_selector = optional(map(string), {}) + tolerations = optional(list(object({ + key = string + operator = string + value = string + effect = string + })), []) scale_down_delay_seconds = number min_replicas = number max_replicas = number From 649d8a67255f1b745b8a5e0d3526e6f3567982ca Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 28 Mar 2023 12:51:06 -0700 Subject: [PATCH 074/501] Quick fixes to EKS/ARC arm64 Support (#610) --- .../eks/actions-runner-controller/README.md | 64 ++++++++++++++++--- .../actions-runner-controller/variables.tf | 2 +- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index c9ade2ddd..72487f5ee 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -38,8 +38,9 @@ components: github_app_installation_id: "REPLACE_ME_GH_INSTALLATION_ID" # ssm_github_webhook_secret_token_path: "/github_runners/github_webhook_secret_token" + # The webhook based autoscaler is much more efficient than the polling based autoscaler webhook: - enabled: false + enabled: true hostname_template: "gha-webhook.%[3]v.%[2]v.%[1]v.acme.com" eks_component_name: "eks/cluster" @@ -54,12 +55,7 @@ components: infra-runner: node_selector: kubernetes.io/os: "linux" - kubernetes.io/arch: "arm64" - tolerations: - - key: "kubernetes.io/arch" - operator: "Equal" - value: "arm64" - effect: "NoSchedule" + kubernetes.io/arch: "amd64" type: "repository" # can be either 'organization' or 'repository' dind_enabled: false # If `true`, a Docker sidecar container will be deployed # To run Docker in Docker (dind), change image to summerwind/actions-runner-dind @@ -80,6 +76,9 @@ components: webhook_driven_scaling_enabled: true webhook_startup_timeout: "2m" pull_driven_scaling_enabled: false + # Labels are not case-sensitive to GitHub, but *are* case-sensitive + # to the webhook based autoscaler, which requires exact matches + # between the `runs-on:` label in the workflow and the runner labels. labels: - "Linux" - "linux" @@ -92,6 +91,55 @@ components: - "AMD64" - "core-auto" - "common" + # Uncomment this additional runner if you want to run a second + # runner pool for `arm64` architecture + #infra-runner-arm64: + # node_selector: + # kubernetes.io/os: "linux" + # kubernetes.io/arch: "arm64" + # # Add the corresponding taint to the Kubernetes nodes running `arm64` architecture + # # to prevent Kubernetes pods without node selectors from being scheduled on them. + # tolerations: + # - key: "kubernetes.io/arch" + # operator: "Equal" + # value: "arm64" + # effect: "NoSchedule" + # type: "repository" # can be either 'organization' or 'repository' + # dind_enabled: false # If `true`, a Docker sidecar container will be deployed + # # To run Docker in Docker (dind), change image to summerwind/actions-runner-dind + # # If not running Docker, change image to summerwind/actions-runner use a smaller image + # image: summerwind/actions-runner-dind + # # `scope` is org name for Organization runners, repo name for Repository runners + # scope: "org/infra" + # min_replicas: 1 + # max_replicas: 20 + # scale_down_delay_seconds: 100 + # resources: + # limits: + # cpu: 200m + # memory: 512Mi + # requests: + # cpu: 100m + # memory: 128Mi + # webhook_driven_scaling_enabled: true + # webhook_startup_timeout: "2m" + # pull_driven_scaling_enabled: false + # # Labels are not case-sensitive to GitHub, but *are* case-sensitive + # # to the webhook based autoscaler, which requires exact matches + # # between the `runs-on:` label in the workflow and the runner labels. + # # Leave "common" off the list so that "common" jobs are always + # # scheduled on the amd64 runners. This is because the webhook + # # based autoscaler will not scale a runner pool if the + # # `runs-on:` labels in the workflow match more than one pool. + # labels: + # - "Linux" + # - "linux" + # - "Ubuntu" + # - "ubuntu" + # - "amd64" + # - "AMD64" + # - "core-auto" + ``` ### Generating Required Secrets @@ -289,7 +337,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = string
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | | [ssm\_github\_webhook\_secret\_token\_path](#input\_ssm\_github\_webhook\_secret\_token\_path) | The path in SSM to the GitHub Webhook Secret token. | `string` | `""` | no | diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index dc440bbc6..ad718114a 100644 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -170,7 +170,7 @@ variable "runners" { tolerations = optional(list(object({ key = string operator = string - value = string + value = optional(string, null) effect = string })), []) scale_down_delay_seconds = number From f5f0607931840703acd2deadcfc28b62c4f50a49 Mon Sep 17 00:00:00 2001 From: Michael Pursifull Date: Wed, 29 Mar 2023 12:17:03 -0500 Subject: [PATCH 075/501] waf component, update dependency versions for aws provider and waf terraform module (#612) Co-authored-by: cloudpossebot --- modules/waf/README.md | 8 ++++---- modules/waf/main.tf | 2 +- modules/waf/versions.tf | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/waf/README.md b/modules/waf/README.md index ca37fa944..160dbd63b 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -12,7 +12,7 @@ Here's an example snippet for how to use this component. ```yaml components: terraform: - aws-waf-acl: + waf: vars: enabled: true name: waf @@ -41,19 +41,19 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 0.0.4 | +| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 0.2.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/waf/main.tf b/modules/waf/main.tf index 17ada3147..fdbc4ea5c 100644 --- a/modules/waf/main.tf +++ b/modules/waf/main.tf @@ -4,7 +4,7 @@ locals { module "aws_waf" { source = "cloudposse/waf/aws" - version = "0.0.4" + version = "0.2.0" association_resource_arns = var.association_resource_arns byte_match_statement_rules = var.byte_match_statement_rules diff --git a/modules/waf/versions.tf b/modules/waf/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/waf/versions.tf +++ b/modules/waf/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } From 17e747cd9dce543c37c7c890bbb31dfd4b9cca8e Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 29 Mar 2023 22:29:50 +0300 Subject: [PATCH 076/501] Added ArgoCD GitHub notification subscription (#615) --- modules/argocd-repo/templates/applicationset.yaml.tpl | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index 550bfe29f..203db543b 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -65,6 +65,7 @@ spec: app_repository: '{{app_repository}}' app_commit: '{{app_commit}}' app_hostname: 'https://{{app_hostname}}' + notifications.argoproj.io/subscribe.on-deployed.github: "" name: '{{name}}' spec: project: ${name} From edb2660479ef69c6e4db51a0488df4fffe22bd9e Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 29 Mar 2023 12:52:23 -0700 Subject: [PATCH 077/501] add providers to `mixins` folder (#613) --- mixins/provider-datadog.tf | 12 ++++++++++++ mixins/providers-aws-superadmin.tf | 3 +++ mixins/providers.tf | 29 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 mixins/provider-datadog.tf create mode 100644 mixins/providers-aws-superadmin.tf create mode 100644 mixins/providers.tf diff --git a/mixins/provider-datadog.tf b/mixins/provider-datadog.tf new file mode 100644 index 000000000..8db220f1f --- /dev/null +++ b/mixins/provider-datadog.tf @@ -0,0 +1,12 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + region = var.region + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} diff --git a/mixins/providers-aws-superadmin.tf b/mixins/providers-aws-superadmin.tf new file mode 100644 index 000000000..dc58d9a25 --- /dev/null +++ b/mixins/providers-aws-superadmin.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = var.region +} diff --git a/mixins/providers.tf b/mixins/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/mixins/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} From 3ad7da598cfd69acf497b09c5351f8ba3ba9a121 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 30 Mar 2023 09:25:02 -0700 Subject: [PATCH 078/501] Update several component Readmes (#611) --- modules/account-map/README.md | 4 + modules/account/README.md | 186 ++++++++++++++++++++++++++ modules/acm/README.md | 5 + modules/aws-backup/README.md | 4 + modules/aws-team-roles/README.md | 9 +- modules/aws-teams/README.md | 55 ++++++-- modules/datadog-integration/README.md | 45 ++++++- modules/datadog-monitor/README.md | 147 ++++++++++++++++++++ modules/dns-delegated/README.md | 36 +++++ modules/dns-primary/README.md | 5 + modules/ecr/README.md | 11 ++ modules/eks/cluster/README.md | 9 ++ modules/eks/datadog-agent/README.md | 29 +++- modules/github-runners/README.md | 166 +++++++++++++++++++++++ modules/opsgenie-team/README.md | 12 +- modules/tfstate-backend/README.md | 4 + 16 files changed, 705 insertions(+), 22 deletions(-) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index de960d7e6..a90f26e03 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -2,6 +2,10 @@ This component is responsible for provisioning information only: it simply populates Terraform state with data (account ids, groups, and roles) that other root modules need via outputs. +## Pre-requisites + +- [account](/reference-architecture/components/account) must be provisioned before [account-map](/reference-architecture/components/account-map) component + ## Usage **Stack Level**: Global diff --git a/modules/account/README.md b/modules/account/README.md index c1b7e8b7d..89aad5421 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -2,6 +2,11 @@ This component is responsible for provisioning the full account hierarchy along with Organizational Units (OUs). It includes the ability to associate Service Control Policies (SCPs) to the Organization, each Organizational Unit and account. +:::info +Part of a [cold start](/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) so it has to be initially run with `SuperAdmin` role. + +::: + In addition, it enables [AWS IAM Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html), which helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. Access Analyzer identifies resources that are shared with external principals by using logic-based reasoning to analyze the resource-based policies in your AWS environment. For each instance of a resource that is shared outside of your account, Access Analyzer generates a finding. Findings include information about the access and the external principal that it is granted to. You can review findings to determine whether the access is intended and safe, or the access is unintended and a security risk. ## Usage @@ -143,6 +148,187 @@ components: - "https://raw.githubusercontent.com/cloudposse/terraform-aws-service-control-policies/0.12.0/catalog/ec2-policies.yaml" ``` +## First Time Organization Setup + +Your AWS Organization is managed by the `account` component, along with accounts and organizational units. + +However, because the AWS defaults for an Organization and its accounts are not exactly what we want, and there is no way to change them via Terraform, we have to first provision the AWS Organization, then take some steps on the AWS console, and then we can provision the rest. + +### Use AWS Console to create and set up the Organization + +Unfortunately, there are some tasks that need to be done via the console. Log into the AWS Console with the root (not SuperAdmin) credentials you have saved in 1Password. + +#### Request an increase in the maximum number of accounts allowed + +:::caution +Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is necessary to expedite the quota increase requests, which could take several days on a basic support plan. Without it, AWS support will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the requests. AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to your AWS TAM. See [AWS](/reference-architecture/reference/aws). + +::: + +1. From the region list, select "US East (N. Virginia) us-east-1". + +2. From the account dropdown menu, select "My Service Quotas". + +3. From the Sidebar, select "AWS Services". + +4. Type "org" in the search field under "AWS services" + +5. Click on "AWS Organizations" in the "Service" list + +6. Click on "Default maximum number of accounts", which should take you to a new view + +7. Click on "Request quota increase" on the right side of the view, which should pop us a request form + +8. At the bottom of the form, under "Change quota value", enter the number you decided on in the previous step (probably "20") and click "Request" + +#### (Optional) Create templates to request other quota increases + +New accounts start with a low limit on the number of instances you can create. However, as you add accounts, and use more instances, the numbers automatically adjust up. So you may or may not want to create a template to generate automatic quota increase requests, depending on how many instances per account you expect to want to provision right away. + +Create a [Quota request template](https://docs.aws.amazon.com/servicequotas/latest/userguide/organization-templates.html) for the organization. From the Sidebar, click "Quota request template" + +Add each EC2 quota increase request you want to make: + +1. Click "Add Quota" on the right side of the view + +2. Under "Region", select your default region (repeat with the backup region if you are using one) + +3. Under "Service", type "EC2" and select "Amazon Elastic Compute Cloud (Amazon EC2)" + +4. Under "Quota", find the quota you want to increase. The likely candidates are: + +5. type "stand" and select "Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) Instances" + +6. type "stand" and select "All Standard (A, C, D, H, I, M, R, T, Z) Spot Instance Request" + +7. type "g i" and select "Running On-Demand G Instances" + +8. type "all g" and select "All G Spot Instance Requests" + +9. Under "Desired quota value" enter your desired default quota + +10. Click "Add" + +After you have added all the templates, click "Enable" on the Quota request template screen to enable the templates. + +#### Enable resource sharing with AWS Organization + +[AWS Resource Access Manager (RAM)](https://docs.aws.amazon.com/ram/latest/userguide/what-is.html) lets you share your resources with any AWS account or through AWS Organizations. + +
+ +If you have multiple AWS accounts, you can create resources centrally and use AWS RAM to share those resources with other accounts. + +Resource sharing through AWS Organization will be used to share the Transit Gateway deployed in the `network` account with other accounts to connect their VPCs to the shared Transit Gateway. + +This is a one-time manual step in the AWS Resource Access Manager console. When you share resources within your organization, AWS RAM does not send invitations to principals. Principals in your organization get access to shared resources without exchanging invitations. + +To enable resource sharing with AWS Organization via AWS Management Console + +- Open the Settings page of AWS Resource Access Manager console at [https://console.aws.amazon.com/ram/home#Settings](https://console.aws.amazon.com/ram/home#Settings) + +- Choose "Enable sharing with AWS Organizations" + +To enable resource sharing with AWS Organization via AWS CLI + +``` + √ . [xamp-SuperAdmin] (HOST) infra ⨠ aws ram enable-sharing-with-aws-organization +{ + "returnValue": true +} +``` + +For more information, see: + +- [https://docs.aws.amazon.com/ram/latest/userguide/what-is.html](https://docs.aws.amazon.com/ram/latest/userguide/what-is.html) + +- [https://docs.aws.amazon.com/ram/latest/userguide/getting-started-sharing.html](https://docs.aws.amazon.com/ram/latest/userguide/getting-started-sharing.html) + +- [https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-ram.html](https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-ram.html) + +### Import the organization into Terraform using the `account` component + +After we are done with the above ClickOps and the Service Quota Increase for maximum number of accounts has been granted, we can then do the rest via Terraform. + +In the Geodesic shell, as SuperAdmin, execute the following command to get the AWS Organization ID that will be used to import the organization: + +``` +aws organizations describe-organization +``` + +From the output, identify the _organization-id_: + +``` +{ + "Organization": { + "Id": "o-7qcakq6zxw", + "Arn": "arn:aws:organizations:: + ... +``` + +Using the example above, the _organization-id_ is o-7qcakq6zxw. + +In the Geodesic shell, as SuperAdmin, execute the following command to import the AWS Organization, changing the stack name `core-gbl-root` if needed, to reflect the stack where the organization management account is defined, and changing the last argument to reflect the _organization-id_ from the output of the previous command. + +``` +atmos terraform import account --stack core-gbl-root 'aws_organizations_organization.this[0]' 'o-7qcakq6zxw' +``` + +### Provision AWS OUs and Accounts using the `account` component + +AWS accounts and organizational units are generated dynamically by the `terraform/account` component using the configuration in the `gbl-root` stack. + +:::info +_**Special note:**_ **** In the rare case where you will need to be enabling non-default AWS Regions, temporarily comment out the `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore it later, after enabling the optional Regions. +See related: [Decide on Opting Into Non-default Regions](/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) + +::: + +:::caution +**You must wait until your quota increase request has been granted.** If you try to create the accounts before the quota increase is granted, you can expect to see failures like `ACCOUNT_NUMBER_LIMIT_EXCEEDED`. + +::: + +In the Geodesic shell, execute the following commands to provision AWS Organizational Units and AWS accounts: + +``` +atmos terraform apply account --stack gbl-root +``` + +Review the Terraform plan, _**ensure that no new organization will be created**_ (look for `aws_organizations_organization.this[0]`), type "yes" to approve and apply. This creates the AWS organizational units and AWS accounts. + +### Configure root account credentials for each account + +Note: unless you need to enable non-default AWS regions (see next step), this step can be done later or in parallel with other steps, for example while waiting for Terraform to create resources. + +**For** _**each**_ **new account:** + +1. Perform a password reset by attempting to [log in to the AWS console](https://signin.aws.amazon.com/signin) as a "root user", using that account's email address, and then clicking the "Forgot password?" link. You will receive a password reset link via email, which should be forwarded to the shared Slack channel for automated messages. Click the link and enter a new password. (Use 1Password or [Random.org](https://www.random.org/passwords) to create a password 26-38 characters long, including at least 3 of each class of character: lower case, uppercase, digit, and symbol. You may need to manually combine or add to the generated password to ensure 3 symbols and digits are present.) Save the email address and generated password as web login credentials in 1Password. While you are at it, save the account number in a separate field. + +2. Log in using the new password, choose "My Security Credentials" from the account dropdown menu and set up Multi-Factor Authentication (MFA) to use a Virutal MFA device. Save the MFA TOTP key in 1Password by using 1Password's TOTP field and built-in screen scanner. Also, save the Virutal MFA ARN (sometimes shown as "serial number"). + +3. While logged in, enable optional regions as described in the next step, if needed. + +4. (Optional, but highly recommended): [Unsubscribe](https://pages.awscloud.com/communication-preferences.html) the account's email address from all marketing emails. + +### (Optional) Enable regions + +Most AWS regions are enabled by default. If you are using a region that is not enabled by default (such as Middle East/Bahrain), you need to take extra steps. + +1. While logged in using root credentials (see the previous step), in the account dropdown menu, select "My Account" to get to the [Billing home page](https://console.aws.amazon.com/billing/home?#/account). + +2. In the "AWS Regions" section, enable the regions you want to enable. + +3. Go to the IAM [account settings page](https://console.aws.amazon.com/iam/home?#/account_settings) and edit the STS Global endpoint to create session tokens valid in all AWS regions. + +You will need to wait a few minutes for the regions to be enabled before you can proceed to the next step. Until they are enabled, you may get what look like AWS authentication or permissions errors. + +After enabling the regions in all accounts, re-enable the `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml` and rerun + +``` +atmos terraform apply account --stack gbl-root +``` + ## Requirements diff --git a/modules/acm/README.md b/modules/acm/README.md index ac6407386..f6a4a5e03 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -2,6 +2,11 @@ This component is responsible for requesting an ACM certificate for a domain and adding a CNAME record to the DNS zone to complete certificate validation. +The ACM component is to manage an unlimited number of certificates, predominantly for vanity domains. While the [dns-primary](/reference-architecture/components/dns-primary) component has the ability to generate ACM certificates, it is very opinionated and can only manage one zone. In reality, companies have many branded domains associated with a load balancer, so we need to be able to generate more complicated certificates. + +We have, as a convenience, the ability to create an ACM certificate as part of creating a DNS zone, whether primary or delegated. That convenience is limited to creating `example.com` and `*.example.com` when creating a zone for `example.com`. For example, Acme has delegated `acct.acme.com` and in addition to `*.acct.acme.com` needed an ACM certificate for `*.usw2.acct.acme.com`, so we use the ACM component to provision that, rather than extend the DNS primary or delegated components to take a list of additional certificates. Both are different views on the Single Responsibility Principle. + + ## Usage **Stack Level**: Global or Regional diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index fafb61fb9..b4f4161c6 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -265,3 +265,7 @@ No resources. [](https://cpco.io/component) + +## Related How-to Guides + +- [How to Enable Cross-Region Backups in AWS-Backup](/reference-architecture/how-to-guides/tutorials/how-to-enable-cross-region-backups-in-aws-backup) diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index 8b12f21dd..97577a59c 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -1,19 +1,20 @@ # Component: `aws-team-roles` -This component is responsible for provisioning user and system IAM roles outside the `identity` account. +This component is responsible for provisioning user and system IAM roles outside the `identity` account. It sets them up to be assumed from the "team" roles defined in the `identity` account by -[the `aws-teams` component](../aws-teams) and/or the AWS SSO permission sets +[the `aws-teams` component](../aws-teams) and/or the AWS SSO permission sets defined in [the `aws-sso` component](../aws-sso). ## Usage **Stack Level**: Global -**Deployment**: Must be deployed by SuperAdmin using `atmos` CLI + +**Deployment**: Must be deployed by _SuperAdmin_ using `atmos` CLI Here's an example snippet for how to use this component. This specific usage is an example only, and not intended for production use. You set the defaults in one YAML file, and import that file into each account's Global stack (except for the `identity` account itself). If desired, you can make account-specific changes by overriding settings, for example -- Disable entire roles in the account by setting `enabled: false` +- Disable entire roles in the account by setting `enabled: false` - Limit who can access the role by setting a different value for `trusted_teams` - Change the permissions available to that role by overriding the `role_policy_arns` (not recommended, limit access to the role or create a different role with the desired set of permissions instead). diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index fa4e184ca..3e6aca037 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -1,29 +1,29 @@ # Component: `aws-teams` -This component is responsible for provisioning all primary user and system roles into the centralized identity account. -This is expected to be use alongside [the `aws-team-roles` component](../aws-team-roles) to provide +This component is responsible for provisioning all primary user and system roles into the centralized identity account. +This is expected to be use alongside [the `aws-team-roles` component](../aws-team-roles) to provide fine grained role delegation across the account hierarchy. ### Teams Function Like Groups and are Implemented as Roles -The "teams" created in the `identity` account by this module can be thought of as access control "groups": -a user who is allowed access one of these teams gets access to a set of roles (and corresponding permissions) +The "teams" created in the `identity` account by this module can be thought of as access control "groups": +a user who is allowed access one of these teams gets access to a set of roles (and corresponding permissions) across a set of accounts. Generally, there is nothing else provisioned in the `identity` account, so the teams have limited access to resources in the `identity` account by design. Teams are implemented as IAM Roles in each account. Access to the "teams" in the `identity` -account is controlled by the `aws-saml` and `aws-sso` components. Access to the roles in all the +account is controlled by the `aws-saml` and `aws-sso` components. Access to the roles in all the other accounts is controlled by the "assume role" policies of those roles, which allow the "team" or AWS SSO Permission set to assume the role (or not). ### Privileges are Defined for Each Role in Each Account by `aws-team-roles` -Every account besides the `identity` account has a set of IAM roles created by the +Every account besides the `identity` account has a set of IAM roles created by the `aws-team-roles` component. In that component, the account's roles are assigned privileges, and those privileges ultimately determine what a user can do in that account. -Access to the roles can be granted in a number of ways. -One way is by listing "teams" created by this component as "trusted" (`trusted_teams`), -meaning that users who have access to the team role in the `identity` account are +Access to the roles can be granted in a number of ways. +One way is by listing "teams" created by this component as "trusted" (`trusted_teams`), +meaning that users who have access to the team role in the `identity` account are allowed (trusted) to assume the role configured in the target account. Another is by listing an AWS SSO Permission Set in the account (`trusted_permission_sets`). @@ -31,14 +31,14 @@ Another is by listing an AWS SSO Permission Set in the account (`trusted_permiss Users can again access to a role in the `identity` account through either (or both) of 2 mechanisms: #### SAML Access -- SAML access is globally configured via the `aws-saml` component, enabling an external -SAML Identity Provider (IdP) to control access to roles in the `identity` account. +- SAML access is globally configured via the `aws-saml` component, enabling an external +SAML Identity Provider (IdP) to control access to roles in the `identity` account. (SAML access can be separately configured for other accounts, see the `aws-saml` and `aws-team-roles` components for more on that.) - Individual roles are enabled for SAML access by setting `aws_saml_login_enabled: true` in the role configuration. - Individual users are granted access to these roles by configuration in the SAML IdP. #### AWS SSO Access -The `aws-sso` component can create AWS Permission Sets that allow users to assume specific roles +The `aws-sso` component can create AWS Permission Sets that allow users to assume specific roles in the `identity` account. See the `aws-sso` component for details. ## Usage @@ -46,7 +46,7 @@ in the `identity` account. See the `aws-sso` component for details. **Stack Level**: Global **Deployment**: Must be deployed by SuperAdmin using `atmos` CLI -Here's an example snippet for how to use this component. The component should only be applied once, +Here's an example snippet for how to use this component. The component should only be applied once, which is typically done via the identity stack (e.g. `gbl-identity.yaml`). ```yaml @@ -210,5 +210,34 @@ components: +## Known Problems + +### Error: `assume role policy: LimitExceeded: Cannot exceed quota for ACLSizePerRole: 2048` + +The `aws-teams` architecture, when enabling access to a role via lots of AWS SSO Profiles, can create large "assume role" policies, large enough to exceed the default quota of 2048 characters. If you run into this limitation, you will get an error like this: + +``` +Error: error updating IAM Role (acme-gbl-root-tfstate-backend-analytics-ro) assume role policy: LimitExceeded: Cannot exceed quota for ACLSizePerRole: 2048 +``` + +This can happen in either/both the `identity` and `root` accounts (for Terraform state access). So far, we have always been able to resolve this by requesting a quota increase, which is automatically granted a few minutes after making the request. To request the quota increase: + +- Log in to the AWS Web console as admin in the affected account + +- Set your region to N. Virginia `us-east-1` + +- Navigate to the Service Quotas page via the account dropdown menu + +- Click on AWS Services in the left sidebar + +- Search for "IAM" and select "AWS Identity and Access Management (IAM)". (If you don't find that option, make sure you have selected the `us-east-1` region. + +- Find and select "Role trust policy length" + +- Request an increase to 4096 characters + +- Wait for the request to be approved, usually less than a few minutes + + ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components)- Cloud Posse's upstream component diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index a8e26b809..eeb6f1d0c 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -1,6 +1,6 @@ # Component: `datadog-integration` -This component is responsible for provisioning Datadog AWS integrations. +This component is responsible for provisioning Datadog AWS integrations. See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. @@ -91,6 +91,49 @@ No resources. +## FAQ: + +### Stack Errors (Spacelift): + +``` +╷ +│ Error: error creating AWS integration from https://api.datadoghq.com/api/v1/integration/aws: 409 Conflict: {"errors": ["Could not update AWS Integration due to conflicting updates"]} +│ +│ with module.datadog_integration.datadog_integration_aws.integration[0], +│ on .terraform/modules/datadog_integration/main.tf line 18, in resource "datadog_integration_aws" "integration": +│ 18: resource "datadog_integration_aws" "integration" { +│ +╵ +``` + +This can happen when you apply multiple integrations at the same time. Fix is easy though, re-trigger the stack. + +## Enabling Security Audits + +To enable the Datadog compliance capabilities, AWS integration to must have the `SecurityAudit` policy attached to the Datadog IAM role. This is handled by our [https://github.com/cloudposse/terraform-aws-datadog-integration](https://github.com/cloudposse/terraform-aws-datadog-integration) module used + +the by the `datadog-integration` component. + +Attaching the `SecurityAudit` policy allows Datadog to collect information about how AWS resources are configured (used in Datadog Cloud Security Posture Management to read security configuration metadata) + +- Datadog Cloud Security Posture Management (CSPM) makes it easier to assess and visualize the current and historic security posture of cloud environments, automate audit evidence collection, and catch misconfigurations that leave your organization vulnerable to attacks + +- Cloud Security Posture Management (CSPM) can be accessed at [https://app.datadoghq.com/security/compliance/home](https://app.datadoghq.com/security/compliance/home) + +- The process to enable Datadog Cloud Security Posture Management (CSPM) consists of two steps (one automated, the other manual): + +- Enable `SecurityAudit` policy and provision it with terraform + +- In Datadog UI, perform the following manual steps: + +``` +Go to the Datadog AWS integration tile +Click on the AWS account where you wish to enable resource collection +Go to the Resource collection section for that account and check the box "Route resource data to the Cloud Security Posture Management product" +At the bottom left of the tile, click Update Configuration + +``` + ## References * Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-integration) - Cloud Posse's upstream component diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 785243127..44b2d902a 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -35,6 +35,143 @@ components: - "site-reliability-dev" ``` +## Conventions +- Treat datadog like a separate cloud provider with integrations ([datadog-integration](/reference-architecture/components/datadog-integration)) into your accounts. + +- Use the `catalog` convention to define a step of alerts. You can use ours or define your own. + [https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors](https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors) + +## Adjust Thresholds per Stack + +Since there are so many parameters that may be adjusted for a given monitor, we define all monitors through YAML. By convention, we define the **default monitors** that should apply to all environments, and then adjust the thresholds per environment. This is accomplished using the `datadog-monitor` components variable `datadog_monitors_config_paths` which defines the path to the YAML configuration files. By passing a path for `dev` and `prod`, we can define configurations that are different per environment. + +For example, you might have the following settings defined for `prod` and `dev` stacks that override the defaults. + +For the `dev` stack: + +``` +components: + terraform: + datadog-monitor: + vars: + # Located in the components/terraform/datadog-monitor directory + datadog_monitors_config_paths: + - catalog/monitors/*.yaml + - catalog/monitors/dev/*.yaml # note this line +``` +For `prod` stack: + +``` +components: + terraform: + datadog-monitor: + vars: + # Located in the components/terraform/datadog-monitor directory + datadog_monitors_config_paths: + - catalog/monitors/*.yaml + - catalog/monitors/prod/*.yaml # note this line +``` + +Behind the scenes (with `atmos`) we fetch all files from these glob patterns, template them, and merge them by key. If we peek into the `*.yaml` and `dev/*.yaml` files above you could see an example like this: + +**components/terraform/datadog-monitor/catalog/monitors/elb.yaml** + +``` +elb-lb-httpcode-5xx-notify: + name: "(ELB) {{ env }} HTTP 5XX client error detected" + type: query alert + query: | + avg(last_15m):max:aws.elb.httpcode_elb_5xx{${context_dd_tags}} by {env,host} > 20 + message: | + [${ dd_env }] [ {{ env }} ] lb:[ {{host}} ] + {{#is_warning}} + Number of HTTP 5XX client error codes generated by the load balancer > {{warn_threshold}}% + {{/is_warning}} + {{#is_alert}} + Number of HTTP 5XX client error codes generated by the load balancer > {{threshold}}% + {{/is_alert}} + Check LB + escalation_message: "" + tags: {} + priority: 3 + renotify_interval: 60 + notify_audit: false + require_full_window: true + enable_logs_sample: false + force_delete: true + include_tags: true + locked: false + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + new_group_delay: 0 + groupby_simple_monitor: false + renotify_occurrences: 0 + renotify_statuses: [] + validate: true + notify_no_data: false + no_data_timeframe: 5 + priority: 3 + threshold_windows: {} + thresholds: + critical: 50 + warning: 20 +``` +**components/terraform/datadog-monitor/catalog/monitors/dev/elb.yaml** + +``` +elb-lb-httpcode-5xx-notify: + query: | + avg(last_15m):max:aws.elb.httpcode_elb_5xx{${context_dd_tags}} by {env,host} > 30 + priority: 2 + thresholds: + critical: 30 + warning: 10 +``` + +## Key Notes + +### Inheritance +The important thing to note here is that the default yaml is applied to every stage that it's deployed to. For dev specifically however, we want to override the thresholds and priority for this monitor. This merging is done by key of the monitor, in this case `elb-lb-httpcode-5xx-notify`. + +### Templating +The second thing to note is `${ dd_env }`. This is **terraform** templating in action. While double braces (`{{ env }}`) refers to datadog templating, `${ dd_env }` is a template variable we pass into our monitors. in this example we use it to specify a grouping int he message. This value is passed in and can be overridden via stacks. + +We pass a value via: + +``` +components: + terraform: + datadog-monitor: + vars: + # Located in the components/terraform/datadog-monitor directory + datadog_monitors_config_paths: + - catalog/monitors/*.yaml + - catalog/monitors/dev/*.yaml + # templatefile() is used for all yaml config paths with these variables. + datadog_monitors_config_parameters: + dd_env: "dev" +``` +This allows us to further use inheritance from stack configuration to keep our monitors dry, but configurable. + +Another available option is to use our catalog as base monitors and then override them with your specific fine tuning. + +``` +components: + terraform: + datadog-monitor: + vars: + datadog_monitors_config_paths: + - https://raw.githubusercontent.com/cloudposse/terraform-datadog-platform/0.27.0/catalog/monitors/ec2.yaml + - catalog/monitors/ec2.yaml +``` + +## Other Gotchas + +Our integration action that checks for `'source_type_name' equals 'Monitor Alert'` will also be true for synthetics. Whereas if we check for `'event_type' equals 'query_alert_monitor'`, that's only true for monitors, because synthetics will only be picked up by an integration action when `event_type` is `synthetics_alert`. + +This is important if we need to distinguish between monitors and synthetics in OpsGenie, which is the case when we want to ensure clean messaging on OpsGenie incidents in Statuspage. + ## Requirements @@ -115,6 +252,16 @@ No resources. +## Related How-to Guides + +- [How to Onboard a New Service with Datadog and OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-onboard-a-new-service-with-datadog-and-opsgenie) +- [How to Sign Up for Datadog?](/reference-architecture/how-to-guides/tutorials/how-to-sign-up-for-datadog) +- [How to use Datadog Metrics for Horizontal Pod Autoscaling (HPA)](/reference-architecture/how-to-guides/tutorials/how-to-use-datadog-metrics-for-horizontal-pod-autoscaling-hpa) +- [How to Implement SRE with Datadog](/reference-architecture/how-to-guides/tutorials/how-to-implement-sre-with-datadog) + +## Component Dependencies +- [datadog-integration](/reference-architecture/components/datadog-integration) + ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-monitor) - Cloud Posse's upstream component diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index 9e0e1a383..914a2e878 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -84,6 +84,42 @@ NOTE: With each of these workarounds, you may have an issue connecting to the se 1. Deploy the new dns-delegated-private component 1. Move aurora-postgres, msk, external-dns, echo-server, etc to the new hosted zone by re-deploying + +## Caveats + +- Do not create a delegation for subdomain of a domain in a zone for which that zone is not authoritative for the subdomain (usually because you already delegated a parent subdomain). Though Amazon Route 53 will allow you to, you should not do it. For historic reasons, Route 53 Public DNS allows customers to create two NS delegations within a hosted zone which creates a conflict (and can return either set to resolvers depending on the query). + +For example, in a single hosted zone with the domain name `example.com`, it is possible to create two NS delegations which are parent and child of each other as follows: + +``` +a.example.com. 172800 IN NS ns-1084.awsdns-07.org. +a.example.com. 172800 IN NS ns-634.awsdns-15.net. +a.example.com. 172800 IN NS ns-1831.awsdns-36.co.uk. +a.example.com. 172800 IN NS ns-190.awsdns-23.com. + +b.a.example.com. 172800 IN NS ns-1178.awsdns-19.org. +b.a.example.com. 172800 IN NS ns-614.awsdns-12.net. +b.a.example.com. 172800 IN NS ns-1575.awsdns-04.co.uk. +b.a.example.com. 172800 IN NS ns-338.awsdns-42.com. +``` + +This configuration creates two discrete possible resolution paths. + +1. If a resolver directly queries the `example.com` nameservers for `c.b.a.example.com`, it will receive the second set of nameservers. + +2. If a resolver queries `example.com` for `a.example.com`, it will receive the first set of nameservers. + +If the resolver then proceeds to query the `a.example.com` nameservers for `c.b.a.example.com`, the response is driven by the contents of the `a.example.com` zone, which may be different than the results returned by the `b.a.example.com` nameservers. `c.b.a.example.com` may not have an entry in the `a.example.com` nameservers, resulting in an error (`NXDOMAIN`) being returned. + +From 15th May 2020, Route 53 Resolver has been enabling a modern DNS resolver standard called "QName Minimization"[*]. This change causes the resolver to more strictly use recursion path [2] described above where path [1] was common before. [*] [https://tools.ietf.org/html/rfc7816](https://tools.ietf.org/html/rfc7816) + +As of January 2022, you can observe the different query strategies in use by Google DNS at `8.8.8.8` (strategy 1) and Cloudflare DNS at `1.1.1.1` (strategy 2). You should verify that both DNS servers resolve your host records properly. + +Takeaway + +1. In order to ensure DNS resolution is consistent no matter the resolver, it is important to always create NS delegations only authoritative zones. + + ## Requirements diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index ddee9d352..a2649df88 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -33,6 +33,11 @@ components: - example.net ``` +:::info +Use the [acm](/reference-architecture/components/acm) component for more advanced certificate requirements. + +::: + ## Requirements diff --git a/modules/ecr/README.md b/modules/ecr/README.md index 64b333f4e..b7052a7ae 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -5,6 +5,12 @@ This utilizes [the roles-to-principals submodule](https://github.com/cloudposse/ to assign accounts to various roles. It is also compatible with the [GitHub Actions IAM Role mixin](https://github.com/cloudposse/terraform-aws-components/blob/master/mixins/github-actions-iam-role/README-github-action-iam-role.md). + +:::caution +Older versions of our reference architecture have an`eks-iam` component that needs to be updated to provide sufficient IAM roles to allow pods to pull from ECR repos + +::: + ## Usage **Stack Level**: Regional @@ -124,6 +130,11 @@ components: | [ecr\_user\_unique\_id](#output\_ecr\_user\_unique\_id) | ECR user unique ID assigned by AWS | +## Related + +- [Decide How to distribute Docker Images](/reference-architecture/design-decisions/foundational-platform/decide-how-to-distribute-docker-images) + +- [Decide on ECR Strategy](/reference-architecture/design-decisions/foundational-platform/decide-on-ecr-strategy) ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecr) - Cloud Posse's upstream component diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 9250822cb..89f57adda 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -189,6 +189,15 @@ components: | [karpenter\_iam\_role\_name](#output\_karpenter\_iam\_role\_name) | Karpenter IAM Role name | +## Related How-to Guides + +- [How to Load Test in AWS](/reference-architecture/how-to-guides/tutorials/how-to-load-test-in-aws) +- [How to Tune EKS with AWS Managed Node Groups](/reference-architecture/how-to-guides/tutorials/how-to-tune-eks-with-aws-managed-node-groups) +- [How to Keep Everything Up to Date](/reference-architecture/how-to-guides/upgrades/how-to-keep-everything-up-to-date) +- [How to Tune SpotInst Parameters for EKS](/reference-architecture/how-to-guides/tutorials/how-to-tune-spotinst-parameters-for-eks) +- [How to Upgrade EKS Cluster Addons](/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons) +- [How to Upgrade EKS](/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks) + ## References - [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/cluster) - Cloud Posse's upstream component diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index c5c09d4f0..513e29358 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -76,7 +76,7 @@ components: - "stage:dev" ``` -# Cluster Checks +## Cluster Checks Cluster Checks are configurations that allow us to setup external URLs to be monitored. They can be configured through the datadog agent or annotations on kubernetes services. @@ -84,7 +84,7 @@ Cluster Checks are similar to synthetics checks, they are not as indepth, but si Public addresses that test endpoints must use the agent configuration, whereas service addresses internal to the cluster can be tested by annotations. -## Adding Cluster Checks +### Adding Cluster Checks Cluster Checks can be enabled or disabled via the `cluster_checks_enabled` variable. We recommend this be set to true. @@ -94,7 +94,30 @@ Once they are added, and properly configured, the new checks show up in the netw **Please note:** the yaml file name doesn't matter, but the root key inside which is `something.yaml` does matter. this is following [datadogs docs](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) for `.yaml`. -## Monitoring Cluster Checks +#### Sample Yaml + +:::caution +The key of a filename must match datadog docs, which is `.yaml` +[Datadog Cluster Checks](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) + +::: +Cluster Checks **can** be used for external URL testing (loadbalancer endpoints), whereas annotations **must** be used for kubernetes services. + +``` +http_check.yaml: + cluster_check: true + init_config: + instances: + - name: "[${stage}] Echo Server" + url: "https://echo.${stage}.uw2.acme.com" + - name: "[${stage}] Portal" + url: "https://portal.${stage}.uw2.acme.com" + - name: "[${stage}] ArgoCD" + url: "https://argocd.${stage}.uw2.acme.com" + +``` + +### Monitoring Cluster Checks Using Cloudposse's `datadog-monitor` component. The following yaml snippet will monitor all HTTP Cluster Checks, this can be added to each stage (usually via a defaults folder). diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 498252ac2..ef0139887 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -2,6 +2,10 @@ This component is responsible for provisioning EC2 instances for GitHub runners. +:::info +We also have a similar component based on [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) for Kubernetes. + +::: ## Usage **Stack Level**: Regional @@ -67,6 +71,94 @@ assume-role chamber write github/runners/ registration-token ghp_secretstring ``` + +## Background + +### Registration +Github Actions Self-Hosted runners can be scoped to the Github Organization, a Single Repository, or a group of Repositories (Github Enterprise-Only). Upon startup, each runner uses a `REGISTRATION_TOKEN` to call the Github API to register itself with the Organization, Repository, or Runner Group (Github Enterprise). + +### Running Workflows +Once a Self-Hosted runner is registered, you will have to update your workflow with the `runs-on` attribute specify it should run on a self-hosted runner: + +``` +name: Test Self Hosted Runners +on: + push: + branches: [main] +jobs: + build: + runs-on: [self-hosted] +``` + +### Workflow Github Permissions (GITHUB_TOKEN) +Each run of the Github Actions Workflow is assigned a GITHUB_TOKEN, which allows your workflow to perform actions against Github itself such as cloning a repo, updating the checks API status, etc., and expires at the end of the workflow run. The GITHUB_TOKEN has two permission "modes" it can operate in `Read and write permissions` ("Permissive" or "Full Access") and `Read repository contents permission` ("Restricted" or "Read-Only"). By default, the GITHUB_TOKEN is granted Full Access permissions, but you can change this via the Organization or Repo settings. If you opt for the Read-Only permissions, you can optionally grant or revoke access to specific APIs via the workflow `yaml` file and a full list of APIs that can be accessed can be found in the [documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) and is shown below in the table. It should be noted that the downside to this permissions model is that any user with write access to the repository can escalate permissions for the workflow by updating the `yaml` file, however, the APIs available via this token are limited. Most notably the GITHUB_TOKEN does not have access to the `users`, `repos`, `apps`, `billing`, or `collaborators` APIs, so the tokens do not have access to modify sensitive settings or add/remove users from the Organization/Repository. + +
+ +> Example of using escalated permissions for the entire workflow +``` +name: Pull request labeler +on: [ pull_request_target ] +permissions: + contents: read + pull-requests: write +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} +``` + +> Example of using escalated permissions for a job +``` +name: Create issue on commit +on: [ push ] +jobs: + create_commit: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Create issue using REST API + run: | + curl --request POST \ + --url https://api.github.com/repos/${{ github.repository }}/issues \ + --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ + --header 'content-type: application/json' \ + --data '{ + "title": "Automated issue for commit: ${{ github.sha }}", + "body": "This issue was automatically created by the GitHub Action workflow **${{ github.workflow }}**. \n\n The commit hash was: _${{ github.sha }}_." + }' \ + --fail +``` + +### Pre-Requisites for Using This Component +In order to use this component, you will have to obtain the `REGISTRATION_TOKEN` mentioned above from your Github Organization or Repository and store it in SSM Parameter store. In addition, it is recommended that you set the permissions “mode” for Self-hosted runners to Read-Only. The instructions for doing both are below. + +#### Workflow Permissions +1. Browse to [https://github.com/organizations/{Org}/settings/actions](https://github.com/organizations/{Org}/settings/actions) (Organization) or [https://github.com/{Org}/{Repo}/settings/actions](https://github.com/{Org}/{Repo}/settings/actions) (Repository) + +2. Set the default permissions for the GITHUB_TOKEN to Read Only + +
+ +#### Obtain the Runner Registration Token +1. Browse to [https://github.com/organizations/{Org}/settings/actions/runners](https://github.com/organizations/{Org}/settings/actions/runners) (Organization) or [https://github.com/{Org}/{Repo}/settings/actions/runners](https://github.com/{Org}/{Repo}/settings/actions/runners) (Repository) + +2. Click the **New Runner** button (Organization) or **New Self Hosted Runner** button (Repository) + +3. Copy the Github Runner token from the next screen. Note that this is the only time you will see this token. Note that if you exit the `New {Self Hosted} Runner` screen and then later return by clicking the `New {Self Hosted} Runner` button again, the registration token will be invalidated and a new token will be generated. + +
+ +4. Add the `REGISTRATION_TOKEN` to the `/github/token` SSM parameter in the account where Github runners are hosted (usually `automation`), encrypted with KMS. + +``` +chamber write github token +``` + ## Requirements @@ -179,7 +271,81 @@ chamber write github/runners/ registration-token ghp_secretstring | [ssm\_document\_arn](#output\_ssm\_document\_arn) | The ARN of the SSM document. | + +## FAQ + +### Can we scope it to a github org with both private and public repos ? + +Yes but this requires Github Enterprise Cloud and the usage of runner groups to scope permissions of runners to specific repos. If you set the scope to the entire org without runner groups and if the org has both public and private repos, then the risk of using a self-hosted runner incorrectly is a vulnerability within public repos. + +[https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups) + +If you do not have github enterprise cloud and runner groups cannot be utilized, then it’s best to create new github runners per repo or use the summerwind action-runners-controller via a Github App to set the scope to specific repos. + +### How can we see the current spot pricing? + +Go to [ec2instances.info](http://ec2instances.info/) + +### If we don’t use mixed at all does that mean we can’t do spot? + +It’s possible to do spot without using mixed instances but you leave yourself open to zero instance availability with a single instance type. + +For example, if you wanted to use spot and use `t3.xlarge` in `us-east-2` and for some reason, AWS ran out of `t3.xlarge`, you wouldn't have the option to choose another instance type and so all the GitHub Action runs would stall until availability returned. If you use on-demand pricing, it’s more expensive, but you’re more likely to get scheduling priority. For guaranteed availability, reserved instances are required. + +### Do the overrides apply to both the on-demand and the spot instances, or only the spot instances? + +Since the overrides affect the launch template, I believe they will affect both spot instances and override since weighted capacity can be set for either or. The override terraform option is on the ASG’s `launch_template` + +> List of nested arguments provides the ability to specify multiple instance types. This will override the same parameter in the launch template. For on-demand instances, Auto Scaling considers the order of preference of instance types to launch based on the order specified in the overrides list. Defined below. +And in the terraform resource for `instances_distribution` + +> `spot_max_price` - (Optional) Maximum price per unit hour that the user is willing to pay for the Spot instances. Default: an empty string which means the on-demand price. +For a `mixed_instances_policy`, this will do purely on-demand + +``` + mixed_instances_policy: + instances_distribution: + on_demand_allocation_strategy: "prioritized" + on_demand_base_capacity: 1 + on_demand_percentage_above_base_capacity: 0 + spot_allocation_strategy: "capacity-optimized" + spot_instance_pools: null + spot_max_price: [] +``` + +This will always do spot unless instances are unavailable, then switch to on-demand. + +``` + mixed_instances_policy: + instances_distribution: + # ... + spot_max_price: 0.05 +``` + +If you want a single instance type, you could still use the mixed instances policy to define that like above, or you can use these other inputs and comment out the `mixed_instances_policy` + +``` + instance_type: "t3.xlarge" + # the below is optional in order to set the spot max price + instance_market_options: + market_type = "spot" + spot_options: + block_duration_minutes: 6000 + instance_interruption_behavior: terminate + max_price: 0.05 + spot_instance_type = persistent + valid_until: null +``` + +The `overrides` will override the `instance_type` above. + + + ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/github-runners) - Cloud Posse's upstream component +* [AWS: Auto Scaling groups with multiple instance types and purchase options](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-mixed-instances-groups.html) +* [InstancesDistribution](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_InstancesDistribution.html) +- [MixedInstancesPolicy](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_MixedInstancesPolicy.html) +- [Terraform ASG `Override` Attribute](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#override) [](https://cpco.io/component) diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index abf26988f..c21722b5d 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -176,7 +176,6 @@ AWS_PROFILE=foo chamber list opsgenie-team/ ``` ### ClickOps Work - - The initial Setup requires ClickOps to setup the datadog integration on the datadog side. This is a limitation because there isn’t a resource for datadog to create an opsgenie integration so this has to be done manually via ClickOps. (See Limitations Below) - After deploying the opsgenie-team component the created team will have a schedule named after the team. This is purposely left to be clickOps’d so the UI can be used to set who is on call, as that is the usual way (not through code). Additionally We do not want a re-apply of the terraform to delete or shuffle who is planned to be on call, thus we left who is on-call on a schedule out of the component. ## Known Issues @@ -340,6 +339,17 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [team\_name](#output\_team\_name) | Team Name | +## Related How-to Guides + +- [How to Add Users to a Team in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-add-users-to-a-team-in-opsgenie) +- [How to Pass Tags Along to Datadog](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-pass-tags-along-to-datadog) +- [How to Onboard a New Service with Datadog and OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-onboard-a-new-service-with-datadog-and-opsgenie) +- [How to Create Escalation Rules in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-escalation-rules-in-opsgenie) +- [How to Setup Rotations in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-setup-rotations-in-opsgenie) +- [How to Create New Teams in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-new-teams-in-opsgenie) +- [How to Sign Up for OpsGenie?](/reference-architecture/how-to-guides/tutorials/how-to-sign-up-for-opsgenie) +- [How to Implement Incident Management with OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie) + ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/opsgenie-team) - Cloud Posse's upstream component diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 090eea7ed..0d6b344e5 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -5,6 +5,10 @@ This component is responsible for provisioning an S3 Bucket and DynamoDB table t Once the initial S3 backend is configured, this component can create additional backends, allowing you to segregate them and control access to each backend separately. This may be desirable because any secret or sensitive information (such as generated passwords) that Terraform has access to gets stored in the Terraform state backend S3 bucket, so you may wish to restrict who can read the production Terraform state backend S3 bucket. However, perhaps counter-intuitively, all Terraform users require read access to the most sensitive accounts, such as `root` and `audit`, in order to read security configuration information, so careful planning is required when architecting backend splits. +:::info +Part of cold start so it has to initially be run with `SuperAdmin` +Follow the guide **[here](/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** to get started. +::: ### Access Control From 6da272530d01c449b1a886860ccaeac3573880ae Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Thu, 30 Mar 2023 15:02:49 -0500 Subject: [PATCH 079/501] spacelift: Update README.md example login policy (#597) --- modules/spacelift/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index 3e3a4a485..91b5bc9ce 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -213,6 +213,18 @@ components: not user_collaborators[username] not admin_collaborators[username] } + + # Grant spaces read only user access to all members + space_read[space.id] { + space := input.spaces[_] + GITHUBORG + } + + # Grant spaces write access to GITHUBORG org members in the Developers group + # space_write[space.id] { + # space := input.spaces[_] + # member_of[_] == "Developers" + # } ``` ## Spacelift Layout From b5846c0e1d1312369cf11bafcfcb2a86688de03b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 31 Mar 2023 15:11:00 -0700 Subject: [PATCH 080/501] Update `eks/cluster` README (#616) --- modules/eks/cluster/README.md | 127 ++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 6 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 89f57adda..96a043828 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -1,7 +1,12 @@ -# Component: `eks` +# Component: `eks/cluster` This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups. -NOTE: This component can only be deployed after logging in to AWS via Federated login with SAML (e.g. GSuite) or assuming an IAM role (e.g. from a CI/CD system). It cannot be deployed if you login to AWS via AWS SSO, the reason being is that on initial deployment, the EKS cluster will be owned by the assumed role that provisioned it. If this were to be the AWS SSO Role, then we risk losing access to the EKS cluster once the ARN of the AWS SSO Role eventually changes. + + +:::warning +This component should only be deployed after logging into AWS via Federated login with SAML (e.g. GSuite) or assuming an IAM role (e.g. from a CI/CD system). It should not be deployed if you log into AWS via AWS SSO, the reason being that on initial deployment, the EKS cluster will be owned by the assumed role that provisioned it. If this were to be the AWS SSO Role, then we risk losing access to the EKS cluster once the ARN of the AWS SSO Role eventually changes. + +::: ## Usage @@ -9,16 +14,126 @@ NOTE: This component can only be deployed after logging in to AWS via Federated Here's an example snippet for how to use this component. +This example expects the [Cloud Posse Reference Architecture](https://docs.cloudposse.com/reference-architecture/) +Identity and Network designs deployed for mapping users to EKS service roles and granting access in a private network. +In addition, this example has the GitHub OIDC integration added and makes use of Karpenter to dynamically scale cluster nodes. + +For more on these requirements, see +[Identity Reference Architecture](https://docs.cloudposse.com/reference-architecture/quickstart/iam-identity/), +[Network Reference Architecture](https://docs.cloudposse.com/reference-architecture/scaffolding/setup/network/), +the [Github OIDC component](https://docs.cloudposse.com/components/catalog/aws/github-oidc-provider/), +and the [Karpenter component](https://docs.cloudposse.com/components/catalog/aws/eks/karpenter/). + ```yaml components: terraform: - eks: + eks/cluster: vars: enabled: true - cluster_kubernetes_version: "1.21" - availability_zones: ["us-west-2a", "us-west-2b", "us-west-2c"] + name: eks + iam_primary_roles_tenant_name: core + cluster_kubernetes_version: "1.25" + availability_zones: ["us-east-1a", "us-east-1b", "us-east-1c"] + aws_ssm_agent_enabled: true + allow_ingress_from_vpc_accounts: + - tenant: core + stage: auto + - tenant: core + stage: corp + - tenant: core + stage: network + public_access_cidrs: [] + allowed_cidr_blocks: [] + allowed_security_groups: [] + enabled_cluster_log_types: + # Caution: enabling `api` log events may lead to a substantial increase in Cloudwatch Logs expenses. + - api + - audit + - authenticator + - controllerManager + - scheduler oidc_provider_enabled: true - public_access_cidrs: ["72.107.0.0/24"] + + # Allows GitHub OIDC role + github_actions_iam_role_enabled: true + github_actions_iam_role_attributes: [ "eks" ] + github_actions_allowed_repos: + - acme/infra + + # We use karpenter to provision nodes + # See below for using node_groups + managed_node_groups_enabled: false + node_groups: {} + + # EKS IAM Authentication settings + # By default, you can authenticate to EKS cluster only by assuming the role that created the cluster. + # After the Auth Config Map is applied, the other IAM roles in + # `primary_iam_roles`, `delegated_iam_roles`, and `sso_iam_roles` will be able to authenticate. + apply_config_map_aws_auth: true + availability_zone_abbreviation_type: fixed + cluster_private_subnets_only: true + cluster_encryption_config_enabled: true + cluster_endpoint_private_access: true + cluster_endpoint_public_access: false + cluster_log_retention_period: 90 + # Roles from the primary account to allow access to the cluster + # See `aws-teams` component. + primary_iam_roles: + - groups: + - system:masters + - idp:ops + role: devops + - groups: + - system:masters + role: spacelift + # Roles from the account owning the cluster to allow access to the cluster + # See `aws-team-roles` component. + delegated_iam_roles: + - groups: + - system:masters + - idp:ops + role: admin + - groups: + - idp:poweruser + role: poweruser + - groups: + - idp:observer + role: observer + - groups: + - system:masters + role: terraform + # Roles from AWS SSO allowing cluster access + # See `aws-sso` component. + sso_iam_roles: + - groups: + - idp:observer + - idp:observer-extra + role: ReadOnlyAccess + - groups: + - system:masters + - idp:ops + role: AdministratorAccess + fargate_profiles: + karpenter: + kubernetes_namespace: karpenter + kubernetes_labels: null + karpenter_iam_role_enabled: true +``` + +### Usage with Node Groups + +The `eks/cluster` component also supports node groups! In order to add a set list of nodes to +provision with the cluster, add values for `var.managed_node_groups_enabled` and `var.node_groups`. + +:::info +You can use manage node groups in conjunction with Karpenter node groups! Simply deploy node groups as demonstrated below, +and then deploy Karpenter with the necessary components. + +::: + +For example: + +```yaml managed_node_groups_enabled: true node_groups: # null means use default set in defaults.auto.tf.vars main: From 6d89090ebb865e28081767c5903c2194ff771006 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Mon, 3 Apr 2023 09:39:26 -0400 Subject: [PATCH 081/501] Add `datadog-synthetics` component (#619) Co-authored-by: Erik Osterman (CEO @ Cloud Posse) Co-authored-by: cloudpossebot --- modules/datadog-synthetics/README.md | 217 ++++++++++++++ .../catalog/synthetics/examples/api-test.yaml | 33 +++ .../synthetics/examples/browser-test.yaml | 31 ++ modules/datadog-synthetics/context.tf | 279 ++++++++++++++++++ modules/datadog-synthetics/main.tf | 66 +++++ modules/datadog-synthetics/outputs.tf | 19 ++ .../datadog-synthetics/provider-datadog.tf | 14 + modules/datadog-synthetics/providers.tf | 29 ++ modules/datadog-synthetics/remote-state.tf | 15 + modules/datadog-synthetics/variables.tf | 63 ++++ modules/datadog-synthetics/versions.tf | 14 + 11 files changed, 780 insertions(+) create mode 100644 modules/datadog-synthetics/README.md create mode 100644 modules/datadog-synthetics/catalog/synthetics/examples/api-test.yaml create mode 100644 modules/datadog-synthetics/catalog/synthetics/examples/browser-test.yaml create mode 100644 modules/datadog-synthetics/context.tf create mode 100644 modules/datadog-synthetics/main.tf create mode 100644 modules/datadog-synthetics/outputs.tf create mode 100644 modules/datadog-synthetics/provider-datadog.tf create mode 100755 modules/datadog-synthetics/providers.tf create mode 100644 modules/datadog-synthetics/remote-state.tf create mode 100644 modules/datadog-synthetics/variables.tf create mode 100755 modules/datadog-synthetics/versions.tf diff --git a/modules/datadog-synthetics/README.md b/modules/datadog-synthetics/README.md new file mode 100644 index 000000000..116e85a50 --- /dev/null +++ b/modules/datadog-synthetics/README.md @@ -0,0 +1,217 @@ +# Component: `datadog-synthetics` + +This component provides the ability to implement [Datadog synthetic tests](https://docs.datadoghq.com/synthetics/guide/). + +Synthetic tests allow you to observe how your systems and applications are performing using simulated requests and actions +from the AWS managed locations around the globe, and to monitor internal endpoints +from [Private Locations](https://docs.datadoghq.com/synthetics/private_locations). + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component: + +### Stack Configuration + +```yaml +components: + terraform: + datadog-synthetics: + metadata: + component: "datadog-synthetics" + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: "datadog-synthetics" + locations: + - "all" + # List of paths to Datadog synthetic test configurations + synthetics_paths: + - "catalog/synthetics/examples/*.yaml" + synthetics_private_location_component_name: "datadog-synthetics-private-location" + private_location_test_enabled: true +``` + +### Synthetics Configuration Examples + +Below are examples of Datadog browser and API synthetic tests. + +The synthetic tests are defined in YAML using the [Datadog Terraform provider](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) schema. + +```yaml +my-browser-test: + name: "Browser Test" + message: "Browser Test Failed" + type: browser + subtype: http + device_ids: + - "laptop_large" + tags: + - "managed-by:Terraform" + status: "live" + request_definition: + url: "CHANGEME" + method: GET + request_headers: + Accept-Charset: "utf-8, iso-8859-1;q=0.5" + Accept: "text/html" + options_list: + tick_every: 1800 + no_screenshot: false + follow_redirects: false + retry: + count: 2 + interval: 10 + monitor_options: + renotify_interval: 300 + browser_step: + - name: "Check current URL" + type: assertCurrentUrl + params: + check: contains + value: "CHANGEME" + +my-api-test: + name: "API Test" + message: "API Test Failed" + type: api + subtype: http + tags: + - "managed-by:Terraform" + status: "live" + request_definition: + url: "CHANGEME" + method: GET + request_headers: + Accept-Charset: "utf-8, iso-8859-1;q=0.5" + Accept: "text/json" + options_list: + tick_every: 1800 + no_screenshot: false + follow_redirects: true + retry: + count: 2 + interval: 10 + monitor_options: + renotify_interval: 300 + assertion: + - type: statusCode + operator: is + target: "200" + - type: body + operator: validatesJSONPath + targetjsonpath: + operator: is + targetvalue: true + jsonpath: foo.bar +``` + +These configuration examples are defined in the YAML files in the [catalog/synthetics/examples](catalog/synthetics/examples) folder. + +You can use different subfolders for your use-case. +For example, you can have `dev` and `prod` subfolders to define different synthetic tests for the `dev` and `prod` environments. + +Then use the `synthetic_paths` variable to point the component to the synthetic test configuration files. + +The configuration files are processed and transformed in the following order: + +- The `datadog-synthetics` component loads the YAML configuration files from the filesystem paths specified by the `synthetics_paths` variable + +- Then, in the [synthetics](https://github.com/cloudposse/terraform-datadog-platform/blob/master/modules/synthetics/main.tf) module, + the YAML configuration files are merged and transformed from YAML into + the [Datadog Terraform provider](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) schema + +- And finally, the Datadog Terraform provider uses the + [Datadog Synthetics API](https://docs.datadoghq.com/api/latest/synthetics) specifications to call the Datadog API and provision the synthetic tests + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [datadog](#requirement\_datadog) | >= 3.3.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [datadog\_synthetics](#module\_datadog\_synthetics) | cloudposse/platform/datadog//modules/synthetics | 1.0.1 | +| [datadog\_synthetics\_merge](#module\_datadog\_synthetics\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | +| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [datadog\_synthetics\_yaml\_config](#module\_datadog\_synthetics\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alert\_tags](#input\_alert\_tags) | List of alert tags to add to all alert messages, e.g. `["@opsgenie"]` or `["@devops", "@opsgenie"]` | `list(string)` | `null` | no | +| [alert\_tags\_separator](#input\_alert\_tags\_separator) | Separator for the alert tags. All strings from the `alert_tags` variable will be joined into one string using the separator and then added to the alert message | `string` | `"\n"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [config\_parameters](#input\_config\_parameters) | Map of parameters to Datadog Synthetic configurations | `map(any)` | `{}` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [context\_tags](#input\_context\_tags) | List of context tags to add to each synthetic check | `set(string)` |
[
"namespace",
"tenant",
"environment",
"stage"
]
| no | +| [context\_tags\_enabled](#input\_context\_tags\_enabled) | Whether to add context tags to add to each synthetic check | `bool` | `true` | no | +| [datadog\_synthetics\_globals](#input\_datadog\_synthetics\_globals) | Map of keys to add to every monitor | `any` | `{}` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [locations](#input\_locations) | Array of locations used to run synthetic tests | `list(string)` |
[
"all"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [private\_location\_test\_enabled](#input\_private\_location\_test\_enabled) | Use private locations or the public locations provided by datadog | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [synthetics\_paths](#input\_synthetics\_paths) | List of paths to Datadog synthetic test configurations | `list(string)` | n/a | yes | +| [synthetics\_private\_location\_component\_name](#input\_synthetics\_private\_location\_component\_name) | The name of the Datadog synthetics private location component | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [datadog\_synthetics\_test\_ids](#output\_datadog\_synthetics\_test\_ids) | IDs of the created Datadog synthetic tests | +| [datadog\_synthetics\_test\_monitor\_ids](#output\_datadog\_synthetics\_test\_monitor\_ids) | IDs of the monitors associated with the Datadog synthetics tests | +| [datadog\_synthetics\_test\_names](#output\_datadog\_synthetics\_test\_names) | Names of the created Datadog synthetic tests | +| [datadog\_synthetics\_tests](#output\_datadog\_synthetics\_tests) | The synthetic tests created in Datadog | + + +## References + +- [Datadog Synthetics](https://docs.datadoghq.com/synthetics) +- [Getting Started with Synthetic Monitoring](https://docs.datadoghq.com/getting_started/synthetics) +- [Synthetic Monitoring Guides](https://docs.datadoghq.com/synthetics/guide) +- [Using Synthetic Test Monitors](https://docs.datadoghq.com/synthetics/guide/synthetic-test-monitors) +- [Create An API Test With The API](https://docs.datadoghq.com/synthetics/guide/create-api-test-with-the-api) +- [Manage Your Browser Tests Programmatically](https://docs.datadoghq.com/synthetics/guide/manage-browser-tests-through-the-api) +- [Browser Tests](https://docs.datadoghq.com/synthetics/browser_tests) +- [Synthetics API](https://docs.datadoghq.com/api/latest/synthetics) +- [Terraform resource `datadog_synthetics_test`](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) + +[](https://cpco.io/component) diff --git a/modules/datadog-synthetics/catalog/synthetics/examples/api-test.yaml b/modules/datadog-synthetics/catalog/synthetics/examples/api-test.yaml new file mode 100644 index 000000000..1a8b8b240 --- /dev/null +++ b/modules/datadog-synthetics/catalog/synthetics/examples/api-test.yaml @@ -0,0 +1,33 @@ +my-api-test: + name: "API Test" + message: "API Test Failed" + type: api + subtype: http + tags: + - "managed-by:Terraform" + status: "live" + request_definition: + url: "CHANGEME" + method: GET + request_headers: + Accept-Charset: "utf-8, iso-8859-1;q=0.5" + Accept: "text/json" + options_list: + tick_every: 1800 + no_screenshot: false + follow_redirects: true + retry: + count: 2 + interval: 10 + monitor_options: + renotify_interval: 300 + assertion: + - type: statusCode + operator: is + target: "200" + - type: body + operator: validatesJSONPath + targetjsonpath: + operator: is + targetvalue: true + jsonpath: foo.bar diff --git a/modules/datadog-synthetics/catalog/synthetics/examples/browser-test.yaml b/modules/datadog-synthetics/catalog/synthetics/examples/browser-test.yaml new file mode 100644 index 000000000..eced215a6 --- /dev/null +++ b/modules/datadog-synthetics/catalog/synthetics/examples/browser-test.yaml @@ -0,0 +1,31 @@ +my-browser-test: + name: "Browser Test" + message: "Browser Test Failed" + type: browser + subtype: http + device_ids: + - "laptop_large" + tags: + - "managed-by:Terraform" + status: "live" + request_definition: + url: "CHANGEME" + method: GET + request_headers: + Accept-Charset: "utf-8, iso-8859-1;q=0.5" + Accept: "text/html" + options_list: + tick_every: 1800 + no_screenshot: false + follow_redirects: false + retry: + count: 2 + interval: 10 + monitor_options: + renotify_interval: 300 + browser_step: + - name: "Check current URL" + type: assertCurrentUrl + params: + check: contains + value: "CHANGEME" diff --git a/modules/datadog-synthetics/context.tf b/modules/datadog-synthetics/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/datadog-synthetics/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/datadog-synthetics/main.tf b/modules/datadog-synthetics/main.tf new file mode 100644 index 000000000..f6d16aff7 --- /dev/null +++ b/modules/datadog-synthetics/main.tf @@ -0,0 +1,66 @@ +locals { + enabled = module.this.enabled + + datadog_synthetics_private_location_id = module.datadog_synthetics_private_location.outputs.synthetics_private_location_id + + # Only return context tags that are specified + context_tags = var.context_tags_enabled ? { + for k, v in module.this.tags : + lower(k) => v + if contains(var.context_tags, lower(k)) + } : {} + + # For deep merge + context_tags_in_tags = var.context_tags_enabled ? { + tags = local.context_tags + } : {} + + synthetics_merged = { + for k, v in module.datadog_synthetics_merge : + k => v.merged + } +} + +# Convert all Datadog synthetics from YAML config to Terraform map +module "datadog_synthetics_yaml_config" { + source = "cloudposse/config/yaml" + version = "1.0.2" + + map_config_local_base_path = path.module + map_config_paths = var.synthetics_paths + + parameters = merge(var.config_parameters, local.context_tags) + + context = module.this.context +} + +module "datadog_synthetics_merge" { + source = "cloudposse/config/yaml//modules/deepmerge" + version = "1.0.2" + + for_each = local.enabled ? module.datadog_synthetics_yaml_config.map_configs : {} + + # Merge in order: 1) datadog synthetics, datadog synthetics globals, context tags + maps = [ + each.value, + var.datadog_synthetics_globals, + local.context_tags_in_tags + ] +} + +module "datadog_synthetics" { + source = "cloudposse/platform/datadog//modules/synthetics" + version = "1.0.1" + + datadog_synthetics = local.synthetics_merged + + locations = distinct(compact(concat( + var.locations, + [local.datadog_synthetics_private_location_id] + ))) + + alert_tags = var.alert_tags + alert_tags_separator = var.alert_tags_separator + + context = module.this.context +} diff --git a/modules/datadog-synthetics/outputs.tf b/modules/datadog-synthetics/outputs.tf new file mode 100644 index 000000000..ab3daaea4 --- /dev/null +++ b/modules/datadog-synthetics/outputs.tf @@ -0,0 +1,19 @@ +output "datadog_synthetics_tests" { + value = module.datadog_synthetics.datadog_synthetic_tests + description = "The synthetic tests created in Datadog" +} + +output "datadog_synthetics_test_names" { + value = module.datadog_synthetics.datadog_synthetics_test_names + description = "Names of the created Datadog synthetic tests" +} + +output "datadog_synthetics_test_ids" { + value = module.datadog_synthetics.datadog_synthetics_test_ids + description = "IDs of the created Datadog synthetic tests" +} + +output "datadog_synthetics_test_monitor_ids" { + value = module.datadog_synthetics.datadog_synthetics_test_monitor_ids + description = "IDs of the monitors associated with the Datadog synthetics tests" +} diff --git a/modules/datadog-synthetics/provider-datadog.tf b/modules/datadog-synthetics/provider-datadog.tf new file mode 100644 index 000000000..07eff92ef --- /dev/null +++ b/modules/datadog-synthetics/provider-datadog.tf @@ -0,0 +1,14 @@ +module "datadog_configuration" { + source = "../datadog-configuration/modules/datadog_keys" + + region = var.region + + context = module.this.context +} + +provider "datadog" { + api_key = module.datadog_configuration.datadog_api_key + app_key = module.datadog_configuration.datadog_app_key + api_url = module.datadog_configuration.datadog_api_url + validate = local.enabled +} diff --git a/modules/datadog-synthetics/providers.tf b/modules/datadog-synthetics/providers.tf new file mode 100755 index 000000000..08ee01b2a --- /dev/null +++ b/modules/datadog-synthetics/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/datadog-synthetics/remote-state.tf b/modules/datadog-synthetics/remote-state.tf new file mode 100644 index 000000000..872949fc8 --- /dev/null +++ b/modules/datadog-synthetics/remote-state.tf @@ -0,0 +1,15 @@ +module "datadog_synthetics_private_location" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.synthetics_private_location_component_name + + bypass = !local.enabled || !var.private_location_test_enabled + ignore_errors = !var.private_location_test_enabled + + defaults = { + synthetics_private_location_id = "" + } + + context = module.this.context +} diff --git a/modules/datadog-synthetics/variables.tf b/modules/datadog-synthetics/variables.tf new file mode 100644 index 000000000..c3ff9f236 --- /dev/null +++ b/modules/datadog-synthetics/variables.tf @@ -0,0 +1,63 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "synthetics_paths" { + type = list(string) + description = "List of paths to Datadog synthetic test configurations" +} + +variable "alert_tags" { + type = list(string) + description = "List of alert tags to add to all alert messages, e.g. `[\"@opsgenie\"]` or `[\"@devops\", \"@opsgenie\"]`" + default = null +} + +variable "alert_tags_separator" { + type = string + description = "Separator for the alert tags. All strings from the `alert_tags` variable will be joined into one string using the separator and then added to the alert message" + default = "\n" +} + +variable "context_tags_enabled" { + type = bool + description = "Whether to add context tags to add to each synthetic check" + default = true +} + +variable "context_tags" { + type = set(string) + description = "List of context tags to add to each synthetic check" + default = ["namespace", "tenant", "environment", "stage"] +} + +variable "config_parameters" { + type = map(any) + description = "Map of parameters to Datadog Synthetic configurations" + default = {} +} + +variable "datadog_synthetics_globals" { + type = any + description = "Map of keys to add to every monitor" + default = {} +} + +variable "locations" { + type = list(string) + description = "Array of locations used to run synthetic tests" + default = ["all"] +} + +variable "private_location_test_enabled" { + type = bool + description = "Use private locations or the public locations provided by datadog" + default = false +} + +variable "synthetics_private_location_component_name" { + type = string + description = "The name of the Datadog synthetics private location component" + default = null +} diff --git a/modules/datadog-synthetics/versions.tf b/modules/datadog-synthetics/versions.tf new file mode 100755 index 000000000..20f566652 --- /dev/null +++ b/modules/datadog-synthetics/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + datadog = { + source = "datadog/datadog" + version = ">= 3.3.0" + } + } +} From 4ca1dc3d90ccd5eca8f5b630085b62ac16126ec7 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 4 Apr 2023 14:14:19 -0400 Subject: [PATCH 082/501] chore: aws-sso modules updated to 1.0.0 (#623) --- modules/aws-sso/README.md | 6 +++--- modules/aws-sso/main.tf | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index c75d699d5..cc20caff7 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -123,10 +123,10 @@ components: |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 0.7.1 | +| [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 1.0.0 | | [role\_prefix](#module\_role\_prefix) | cloudposse/label/null | 0.25.0 | -| [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 0.7.1 | -| [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 0.7.1 | +| [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 1.0.0 | +| [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 1.0.0 | | [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aws-sso/main.tf b/modules/aws-sso/main.tf index cb6377bde..4c46b9e42 100644 --- a/modules/aws-sso/main.tf +++ b/modules/aws-sso/main.tf @@ -1,6 +1,6 @@ module "permission_sets" { source = "cloudposse/sso/aws//modules/permission-sets" - version = "0.7.1" + version = "1.0.0" permission_sets = concat( local.administrator_access_permission_set, @@ -18,7 +18,7 @@ module "permission_sets" { module "sso_account_assignments" { source = "cloudposse/sso/aws//modules/account-assignments" - version = "0.7.1" + version = "1.0.0" account_assignments = local.account_assignments context = module.this.context @@ -26,7 +26,7 @@ module "sso_account_assignments" { module "sso_account_assignments_root" { source = "cloudposse/sso/aws//modules/account-assignments" - version = "0.7.1" + version = "1.0.0" providers = { aws = aws.root From efabbf87131d047da7f8fdd57b5c3fef79b08d51 Mon Sep 17 00:00:00 2001 From: Kevin Mahoney Date: Tue, 4 Apr 2023 17:48:33 -0400 Subject: [PATCH 083/501] s3-bucket: use cloudposse template provider for arm64 (#618) Co-authored-by: cloudpossebot Co-authored-by: Erik Osterman (CEO @ Cloud Posse) --- modules/s3-bucket/README.md | 5 +++-- modules/s3-bucket/versions.tf | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index f3f3b9462..d1cf6b67b 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -81,13 +81,14 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | ~> 4.0 | +| [template](#requirement\_template) | 2.2.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | ~> 4.0 | -| [template](#provider\_template) | n/a | +| [template](#provider\_template) | 2.2.0 | ## Modules @@ -105,7 +106,7 @@ components: |------|------| | [aws_iam_policy_document.custom_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | -| [template_file.bucket_policy](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file) | data source | +| [template_file.bucket_policy](https://registry.terraform.io/providers/cloudposse/template/2.2.0/docs/data-sources/file) | data source | ## Inputs diff --git a/modules/s3-bucket/versions.tf b/modules/s3-bucket/versions.tf index e89eb16ed..6af2cf91e 100644 --- a/modules/s3-bucket/versions.tf +++ b/modules/s3-bucket/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = "~> 4.0" } + template = { + source = "cloudposse/template" + version = "2.2.0" + } } } From 79103c8c8c791f4b41f1e440be61a585264bac28 Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 5 Apr 2023 13:28:16 -0700 Subject: [PATCH 084/501] [eks/actions-runner-controller]: support Runner Group, webhook queue size (#621) --- .../eks/actions-runner-controller/README.md | 90 +++++++++++++++++-- .../charts/actions-runner/Chart.yaml | 2 +- .../templates/runnerdeployment.yaml | 18 +++- modules/eks/actions-runner-controller/main.tf | 10 ++- .../resources/values.yaml | 1 - .../actions-runner-controller/variables.tf | 19 ++-- 6 files changed, 118 insertions(+), 22 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 72487f5ee..9464c2af1 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -74,7 +74,7 @@ components: cpu: 100m memory: 128Mi webhook_driven_scaling_enabled: true - webhook_startup_timeout: "2m" + webhook_startup_timeout: "30m" pull_driven_scaling_enabled: false # Labels are not case-sensitive to GitHub, but *are* case-sensitive # to the webhook based autoscaler, which requires exact matches @@ -111,6 +111,7 @@ components: # image: summerwind/actions-runner-dind # # `scope` is org name for Organization runners, repo name for Repository runners # scope: "org/infra" + # group: "ArmRunners" # min_replicas: 1 # max_replicas: 20 # scale_down_delay_seconds: 100 @@ -122,7 +123,7 @@ components: # cpu: 100m # memory: 128Mi # webhook_driven_scaling_enabled: true - # webhook_startup_timeout: "2m" + # webhook_startup_timeout: "30m" # pull_driven_scaling_enabled: false # # Labels are not case-sensitive to GitHub, but *are* case-sensitive # # to the webhook based autoscaler, which requires exact matches @@ -196,7 +197,7 @@ github_app_installation_id: "12345" OR (obsolete) - A PAT with the scope outlined in [this document](https://github.com/actions-runner-controller/actions-runner-controller#deploying-using-pat-authentication). Save this to the value specified by `ssm_github_token_path` using the following command, adjusting the - AWS_PROFILE to refer to the `admin` role in the account to which you are deploying the runner controller: + AWS\_PROFILE to refer to the `admin` role in the account to which you are deploying the runner controller: ``` AWS_PROFILE=acme-mgmt-use2-auto-admin chamber write github_runners controller_github_app_secret -- "" @@ -214,10 +215,21 @@ Store this key in AWS SSM under the same path specified by `ssm_github_webhook_s ssm_github_webhook_secret_token_path: "/github_runners/github_webhook_secret" ``` -### Using Webhook Driven Autoscaling +### Using Runner Groups -To use the Webhook Driven autoscaling, you must also install the GitHub organization-level webhook after deploying the component -(specifically, the webhook server). The URL for the webhook is determined by the `webhook.hostname_template` and where +GitHub supports grouping runners into distinct [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups), which allow you to have different access controls +for different runners. Read the linked documentation about creating and configuring Runner Groups, which you must do +through the GitHub Web UI. If you choose to create Runner Groups, you can assign one or more Runner pools (from the +`runners` map) to groups (only one group per runner pool) by including `group: ` in the runner +configuration. We recommend including it immediately after `scope`. + +### Using Webhook Driven Autoscaling (recommended) + +We recommend using Webhook Driven Autoscaling until GitHub releases their own autoscaling solution (said to be "in the works" as of April 2023). + +To use the Webhook Driven Autoscaling, in addition to setting `webhook_driven_scaling_enabled` to `true`, you must +also install the GitHub organization-level webhook after deploying the component (specifically, the webhook server). +The URL for the webhook is determined by the `webhook.hostname_template` and where it is deployed. Recommended URL is `https://gha-webhook.[environment].[stage].[tenant].[service-discovery-domain]`. As a GitHub organization admin, go to `https://github.com/organizations/[organization]/settings/hooks`, and then: @@ -236,6 +248,68 @@ After the webhook is created, select "edit" for the webhook and go to the "Recen (of a "ping" event) with a green check mark. If not, verify all the settings and consult the logs of the `actions-runner-controller-github-webhook-server` pod. +### Configuring Webhook Driven Autoscaling + +The `HorizontalRunnerAutoscaler scaleUpTriggers.duration` (see [Webhook Driven Scaling documentation](https://github. com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#webhook-driven-scaling)) is +controlled by the `webhook_startup_timeout` setting for each Runner. The purpose of this timeout is to ensure, in +case a job cancellation or termination event gets missed, that the resulting idle runner eventually gets terminated. + +#### How the Autoscaler Determines the Desired Runner Pool Size + +When a job is queued, a `capacityReservation` is created for it. The HRA (Horizontal Runner Autoscaler) sums up all +the capacity reservations to calculate the desired size of the runner pool, subject to the limits of `minReplicas` +and `maxReplicas`. The idea is that a `capacityReservation` is deleted when a job is completed or canceled, and the +pool size will be equal to `jobsStarted - jobsFinished`. However, it can happen that a job will finish without the +HRA being successfully notified about it, so as a safety measure, the `capacityReservation` will expire after a +configurable amount of time, at which point it will be deleted without regard to the job being finished. This +ensures that eventually an idle runner pool will scale down to `minReplicas`. + +However, there are some problems with this scheme. In theory, `webhook_startup_timeout` should only need to be long +enough to cover the delay between the time the HRA starts a scale up request and the time the runner actually starts, +is allocated to the runner pool, and picks up a job to run. But there are edge cases that seem not to be covered +properly (see [actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)). As a result, we recommend setting `webhook_startup_timeout` to +a period long enough to cover the full time a job may have to wait between the time it is queued and the time it +actually starts. Consider this scenario: +- You set `maxReplicas = 5` +- Some trigger starts 20 jobs, each of which take 5 minutes to run +- The replica pool scales up to 5, and the first 5 jobs run +- 5 minutes later, the next 5 jobs run, and so on +- The last set of 5 jobs will have to wait 15 minutes to start because of the previous jobs + +The HRA is designed to handle this situation by updating the expiration time of the `capacityReservation` of any +job stuck waiting because the pool has scaled up to `maxReplicas`, but as discussed in issue #2466 linked above, +that does not seem to be working correctly as of version 0.27.2. + +For now, our recommendation is to set `webhook_startup_timeout` to a duration long enough to cover the time the job +may have to wait in the queue for a runner to become available due to there being more jobs than `maxReplicas`. +Alternatively, you could set `maxReplicas` to a big enough number that there will always be a runner for every +queued job, in which case the duration only needs to be long enough to allow for all the scale-up activities (such +as launching new EKS nodes as well as starting new pods) to finish. Remember, when everything works properly, the +HRA will scale down the pool as jobs finish, so there is little cost to setting a long duration. + +### Recommended `webhook_startup_timeout` Duration + +#### Consequences of Too Short of a `webhook_startup_timeout` Duration + +If you set `webhook_startup_timeout` to too short a duration, the Horizontal Runner Autoscaler will cancel capacity +reservations for jobs that have not yet run, and the pool will be too small. This will be most serious if you have +set `minReplicas = 0` because in this case, jobs will be left in the queue indefinitely. With a higher value of +`minReplicas`, the pool will eventually make it through all the queued jobs, but not as quickly as intended due to +the incorrectly reduced capacity. + +#### Consequences of Too Long of a `webhook_startup_timeout` Duration + +If the Horizontal Runner Autoscaler misses a scale-down event (which can happen because events do not have delivery +guarantees), a runner may be left running idly for as long as the `webhook_startup_timeout` duration. The only +problem with this is the added expense of leaving the idle runner running. + +#### Recommendation + +Therefore we recommend that for lightly used runner pools, set `webhook_startup_timeout` to `"30m"`. For heavily +used pools, find the typical or maximum length of a job, multiply by the number of jobs likely to be queued in an +hour, and divide by `maxReplicas`, then round up. As a rule of thumb, we recommend setting `maxReplicas` high enough +that jobs never wait on the queue more than an hour and setting `webhook_startup_timeout` to `"2h30m"`. Monitor your +usage and adjust accordingly. ### Updating CRDs @@ -337,7 +411,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
node_selector = {} # optional Kubernetes node selector map for the runner pods
tolerations = [] # optional Kubernetes tolerations list for the runner pods
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
webhook_driven_scaling_enabled = bool # Recommended to be true to enable event-based scaling of runner pool
webhook_startup_timeout = optional(string, null) # Duration after which capacity for a queued job will be discarded

labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | | [ssm\_github\_webhook\_secret\_token\_path](#input\_ssm\_github\_webhook\_secret\_token\_path) | The path in SSM to the GitHub Webhook Secret token. | `string` | `""` | no | @@ -346,7 +420,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | | [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `null` | no | -| [webhook](#input\_webhook) | Configuration for the GitHub Webhook Server.
`hostname_template` is the `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`"
Typically something like `"echo.%[3]v.%[2]v.example.com"`. |
object({
enabled = bool
hostname_template = string
})
|
{
"enabled": false,
"hostname_template": null
}
| no | +| [webhook](#input\_webhook) | Configuration for the GitHub Webhook Server.
`hostname_template` is the `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`"
Typically something like `"echo.%[3]v.%[2]v.example.com"`.
`queue_limit` is the maximum number of webhook events that can be queued up processing by the autoscaler.
When the queue gets full, webhook events will be dropped (status 500). |
object({
enabled = bool
hostname_template = string
queue_limit = optional(number, 100)
})
|
{
"enabled": false,
"hostname_template": null,
"queue_limit": 100
}
| no | ## Outputs diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml index 7d49f9e99..b5c10525b 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.1 +version: 0.1.2 # This chart only deploys Resources for actions-runner-controller, so app version does not really apply. # We use Resource API version instead. diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index a9204feb4..69e994046 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -29,6 +29,17 @@ spec: # replicas: 1 template: spec: + # As of 2023-03-31 + # Recommended by https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md + terminationGracePeriodSeconds: 100 + env: + # RUNNER_GRACEFUL_STOP_TIMEOUT is the time the runner will give itself to try to finish + # a job before it gracefully cancels itself in response to a pod termination signal. + # It should be less than the terminationGracePeriodSeconds above so that it has time + # to report its status and deregister itself from the runner pool. + - name: RUNNER_GRACEFUL_STOP_TIMEOUT + value: "90" + # You could reserve nodes for runners by labeling and tainting nodes with # node-role.kubernetes.io/actions-runner # and then adding the following to this RunnerDeployment @@ -43,10 +54,13 @@ spec: {{ if eq .Values.type "organization" }} organization: {{ .Values.scope }} - {{ end }} + {{- end }} {{ if eq .Values.type "repository" }} repository: {{ .Values.scope }} - {{ end }} + {{- end }} + {{ if index .Values "group" }} + group: {{ .Values.group }} + {{- end }} # You can use labels to create subsets of runners. # See https://github.com/summerwind/actions-runner-controller#runner-labels # and https://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners/using-self-hosted-runners-in-a-workflow diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index ee1357a6c..db38e54e7 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -1,8 +1,9 @@ locals { enabled = module.this.enabled - webhook_enabled = local.enabled ? try(var.webhook.enabled, false) : false - webhook_host = local.webhook_enabled ? format(var.webhook.hostname_template, var.tenant, var.stage, var.environment) : "example.com" + webhook_enabled = local.enabled ? try(var.webhook.enabled, false) : false + webhook_host = local.webhook_enabled ? format(var.webhook.hostname_template, var.tenant, var.stage, var.environment) : "example.com" + runner_groups_enabled = length(compact(values(var.runners)[*].group)) > 0 github_app_enabled = length(var.github_app_id) > 0 && length(var.github_app_installation_id) > 0 create_secret = local.enabled && length(var.existing_kubernetes_secret_name) == 0 @@ -139,7 +140,9 @@ module "actions_runner_controller" { create = var.rbac_enabled } githubWebhookServer = { - enabled = var.webhook.enabled + enabled = var.webhook.enabled + queueLimit = var.webhook.queue_limit + useRunnerGroupsVisibility = local.runner_groups_enabled ingress = { enabled = var.webhook.enabled hosts = [ @@ -222,6 +225,7 @@ module "actions_runner" { node_selector = each.value.node_selector tolerations = each.value.tolerations }), + each.value.group == null ? "" : yamlencode({ group = each.value.group }), local.busy_metrics_filtered[each.key] == null ? "" : yamlencode(local.busy_metrics_filtered[each.key]), ]) diff --git a/modules/eks/actions-runner-controller/resources/values.yaml b/modules/eks/actions-runner-controller/resources/values.yaml index 6652ec9a2..fe4f6cc45 100644 --- a/modules/eks/actions-runner-controller/resources/values.yaml +++ b/modules/eks/actions-runner-controller/resources/values.yaml @@ -12,7 +12,6 @@ syncPeriod: 120s githubWebhookServer: enabled: false - syncPeriod: 120s secret: # Webhook secret, used to authenticate incoming webhook events from GitHub # When using Sops, stored in same SopsSecret as authSecret under key `github_webhook_secret_token` diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index ad718114a..0189658a0 100644 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -142,17 +142,17 @@ variable "runners" { organization_runner = { type = "organization" # can be either 'organization' or 'repository' dind_enabled: false # A Docker sidecar container will be deployed - image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind' scope = "ACME" # org name for Organization runners, repo name for Repository runners + group = "core-automation" # Optional. Assigns the runners to a runner group, for access control. + image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind' + node_selector = {} # optional Kubernetes node selector map for the runner pods + tolerations = [] # optional Kubernetes tolerations list for the runner pods scale_down_delay_seconds = 300 min_replicas = 1 max_replicas = 5 - busy_metrics = { - scale_up_threshold = 0.75 - scale_down_threshold = 0.25 - scale_up_factor = 2 - scale_down_factor = 0.5 - } + webhook_driven_scaling_enabled = bool # Recommended to be true to enable event-based scaling of runner pool + webhook_startup_timeout = optional(string, null) # Duration after which capacity for a queued job will be discarded + labels = [ "Ubuntu", "core-automation", @@ -164,6 +164,7 @@ variable "runners" { type = map(object({ type = string scope = string + group = optional(string, null) image = optional(string, "") dind_enabled = bool node_selector = optional(map(string), {}) @@ -208,15 +209,19 @@ variable "webhook" { type = object({ enabled = bool hostname_template = string + queue_limit = optional(number, 100) }) description = <<-EOT Configuration for the GitHub Webhook Server. `hostname_template` is the `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`" Typically something like `"echo.%[3]v.%[2]v.example.com"`. + `queue_limit` is the maximum number of webhook events that can be queued up processing by the autoscaler. + When the queue gets full, webhook events will be dropped (status 500). EOT default = { enabled = false hostname_template = null + queue_limit = 100 } } From 312da6741834e9d42d25e17da2ad380eef0f0ac8 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 6 Apr 2023 10:38:04 -0700 Subject: [PATCH 085/501] Bats Version Pinning (#627) --- modules/alb/README.md | 2 +- modules/alb/versions.tf | 2 +- modules/argocd-repo/README.md | 4 +-- modules/argocd-repo/versions.tf | 2 +- modules/athena/README.md | 2 +- modules/athena/versions.tf | 2 +- modules/aws-waf-acl/README.md | 12 ++++----- modules/aws-waf-acl/versions.tf | 10 +++---- .../datadog-private-location-ecs/README.md | 2 +- .../datadog-private-location-ecs/versions.tf | 2 +- modules/ecs-service/README.md | 27 ++++++++++--------- modules/ecs-service/versions.tf | 6 ++++- modules/ecs/README.md | 4 +-- modules/ecs/versions.tf | 2 +- modules/elasticache-redis/README.md | 2 +- modules/elasticache-redis/versions.tf | 2 +- modules/github-action-token-rotator/README.md | 2 +- .../github-action-token-rotator/versions.tf | 2 +- .../README.md | 2 +- .../versions.tf | 2 +- modules/global-accelerator/README.md | 2 +- modules/global-accelerator/versions.tf | 2 +- modules/kinesis-stream/README.md | 2 +- modules/kinesis-stream/versions.tf | 2 +- modules/kms/versions.tf | 2 +- modules/mwaa/README.md | 4 +-- modules/mwaa/versions.tf | 2 +- modules/rds/README.md | 4 +-- modules/rds/versions.tf | 2 +- modules/s3-bucket/README.md | 10 +++---- modules/s3-bucket/versions.tf | 4 +-- modules/ses/README.md | 4 +-- modules/ses/versions.tf | 2 +- modules/sftp/README.md | 4 +-- modules/sftp/versions.tf | 2 +- modules/snowflake-account/README.md | 15 ++++++----- modules/snowflake-account/versions.tf | 10 ++++--- modules/snowflake-database/README.md | 8 +++--- modules/snowflake-database/versions.tf | 4 +-- modules/sns-topic/README.md | 2 +- modules/sns-topic/versions.tf | 2 +- modules/sqs-queue/README.md | 2 +- modules/sqs-queue/versions.tf | 2 +- 43 files changed, 97 insertions(+), 87 deletions(-) diff --git a/modules/alb/README.md b/modules/alb/README.md index 734726c3e..1333bc650 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -23,7 +23,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [local](#requirement\_local) | >= 2.1 | ## Providers diff --git a/modules/alb/versions.tf b/modules/alb/versions.tf index 56a1e6c82..757d32d5a 100644 --- a/modules/alb/versions.tf +++ b/modules/alb/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } local = { source = "hashicorp/local" diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index a7b9e4784..45b7cba7d 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -82,7 +82,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [github](#requirement\_github) | >= 4.0 | | [tls](#requirement\_tls) | >= 3.0 | @@ -90,7 +90,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | | [github](#provider\_github) | >= 4.0 | | [tls](#provider\_tls) | >= 3.0 | diff --git a/modules/argocd-repo/versions.tf b/modules/argocd-repo/versions.tf index 6e86f8282..a1ac788f4 100644 --- a/modules/argocd-repo/versions.tf +++ b/modules/argocd-repo/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } github = { source = "integrations/github" diff --git a/modules/athena/README.md b/modules/athena/README.md index c7b643499..be10de410 100644 --- a/modules/athena/README.md +++ b/modules/athena/README.md @@ -63,7 +63,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers diff --git a/modules/athena/versions.tf b/modules/athena/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/athena/versions.tf +++ b/modules/athena/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/aws-waf-acl/README.md b/modules/aws-waf-acl/README.md index 781d4730f..32cf4f1da 100644 --- a/modules/aws-waf-acl/README.md +++ b/modules/aws-waf-acl/README.md @@ -39,18 +39,18 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | ~> 0.14.9 | -| [aws](#requirement\_aws) | ~> 3.36 | -| [external](#requirement\_external) | ~> 2.1 | -| [local](#requirement\_local) | ~> 2.1 | +| [terraform](#requirement\_terraform) | >= 0.14.9 | +| [aws](#requirement\_aws) | >= 3.36 | +| [external](#requirement\_external) | >= 2.1 | +| [local](#requirement\_local) | >= 2.1 | | [template](#requirement\_template) | >= 2.2 | -| [utils](#requirement\_utils) | ~> 0.3 | +| [utils](#requirement\_utils) | >= 0.3 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 3.36 | +| [aws](#provider\_aws) | >= 3.36 | ## Modules diff --git a/modules/aws-waf-acl/versions.tf b/modules/aws-waf-acl/versions.tf index fe0bd210b..4da0d958d 100644 --- a/modules/aws-waf-acl/versions.tf +++ b/modules/aws-waf-acl/versions.tf @@ -1,14 +1,14 @@ terraform { - required_version = "~> 0.14.9" + required_version = ">= 0.14.9" required_providers { aws = { source = "hashicorp/aws" - version = "~> 3.36" + version = ">= 3.36" } external = { source = "hashicorp/external" - version = "~> 2.1" + version = ">= 2.1" } template = { source = "cloudposse/template" @@ -16,11 +16,11 @@ terraform { } local = { source = "hashicorp/local" - version = "~> 2.1" + version = ">= 2.1" } utils = { source = "cloudposse/utils" - version = "~> 0.3" + version = ">= 0.3" } } } diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index a52f2ab7f..c5948db9d 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -64,7 +64,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | = 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [datadog](#requirement\_datadog) | >= 3.3.0 | ## Providers diff --git a/modules/datadog-private-location-ecs/versions.tf b/modules/datadog-private-location-ecs/versions.tf index a36b08e52..f636a1364 100644 --- a/modules/datadog-private-location-ecs/versions.tf +++ b/modules/datadog-private-location-ecs/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "= 4.0" + version = ">= 4.0" } datadog = { source = "datadog/datadog" diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index b804f45f8..6e069dd88 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -150,14 +150,15 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | = 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [template](#requirement\_template) | >= 2.2 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | = 4.0 | -| [template](#provider\_template) | n/a | +| [aws](#provider\_aws) | >= 4.0 | +| [template](#provider\_template) | >= 2.2 | ## Modules @@ -188,16 +189,16 @@ components: | Name | Type | |------|------| -| [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/resources/iam_policy) | resource | -| [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/resources/iam_role) | resource | -| [aws_kinesis_stream.default](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/resources/kinesis_stream) | resource | -| [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/iam_policy_document) | data source | -| [aws_kms_alias.selected](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/kms_alias) | data source | -| [aws_route53_zone.selected](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/route53_zone) | data source | -| [aws_route53_zone.selected_vanity](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/route53_zone) | data source | -| [aws_ssm_parameters_by_path.default](https://registry.terraform.io/providers/hashicorp/aws/4.0/docs/data-sources/ssm_parameters_by_path) | data source | -| [template_file.envs](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file) | data source | +| [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_kinesis_stream.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_stream) | resource | +| [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_kms_alias.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | +| [aws_route53_zone.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | +| [aws_route53_zone.selected_vanity](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | +| [aws_ssm_parameters_by_path.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | +| [template_file.envs](https://registry.terraform.io/providers/cloudposse/template/latest/docs/data-sources/file) | data source | ## Inputs diff --git a/modules/ecs-service/versions.tf b/modules/ecs-service/versions.tf index 5a6c84926..7a90cef78 100644 --- a/modules/ecs-service/versions.tf +++ b/modules/ecs-service/versions.tf @@ -4,7 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "= 4.0" + version = ">= 4.0" + } + template = { + source = "cloudposse/template" + version = ">= 2.2" } } } diff --git a/modules/ecs/README.md b/modules/ecs/README.md index ff758e68b..adfe47cd4 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -43,13 +43,13 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | ## Modules diff --git a/modules/ecs/versions.tf b/modules/ecs/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/ecs/versions.tf +++ b/modules/ecs/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md index f8587b200..ba3df5929 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -67,7 +67,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers diff --git a/modules/elasticache-redis/versions.tf b/modules/elasticache-redis/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/elasticache-redis/versions.tf +++ b/modules/elasticache-redis/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/github-action-token-rotator/README.md b/modules/github-action-token-rotator/README.md index 8a4091e64..96e2725aa 100644 --- a/modules/github-action-token-rotator/README.md +++ b/modules/github-action-token-rotator/README.md @@ -33,7 +33,7 @@ Follow the manual steps using the [guide in the upstream module](https://github. | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers diff --git a/modules/github-action-token-rotator/versions.tf b/modules/github-action-token-rotator/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/github-action-token-rotator/versions.tf +++ b/modules/github-action-token-rotator/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/global-accelerator-endpoint-group/README.md b/modules/global-accelerator-endpoint-group/README.md index 90ef5f060..debecb5e4 100644 --- a/modules/global-accelerator-endpoint-group/README.md +++ b/modules/global-accelerator-endpoint-group/README.md @@ -27,7 +27,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers diff --git a/modules/global-accelerator-endpoint-group/versions.tf b/modules/global-accelerator-endpoint-group/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/global-accelerator-endpoint-group/versions.tf +++ b/modules/global-accelerator-endpoint-group/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/global-accelerator/README.md b/modules/global-accelerator/README.md index 656924734..47bc35e42 100644 --- a/modules/global-accelerator/README.md +++ b/modules/global-accelerator/README.md @@ -31,7 +31,7 @@ global-accelerator: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers diff --git a/modules/global-accelerator/versions.tf b/modules/global-accelerator/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/global-accelerator/versions.tf +++ b/modules/global-accelerator/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/kinesis-stream/README.md b/modules/kinesis-stream/README.md index 0c8303c84..9cd47fef6 100644 --- a/modules/kinesis-stream/README.md +++ b/modules/kinesis-stream/README.md @@ -51,7 +51,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers diff --git a/modules/kinesis-stream/versions.tf b/modules/kinesis-stream/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/kinesis-stream/versions.tf +++ b/modules/kinesis-stream/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/kms/versions.tf b/modules/kms/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/kms/versions.tf +++ b/modules/kms/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/mwaa/README.md b/modules/mwaa/README.md index 2213f4c7a..6122cbf90 100644 --- a/modules/mwaa/README.md +++ b/modules/mwaa/README.md @@ -47,13 +47,13 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | ## Modules diff --git a/modules/mwaa/versions.tf b/modules/mwaa/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/mwaa/versions.tf +++ b/modules/mwaa/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/rds/README.md b/modules/rds/README.md index de0de21d5..8a07708df 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -64,14 +64,14 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [random](#requirement\_random) | >= 2.3 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | | [random](#provider\_random) | >= 2.3 | ## Modules diff --git a/modules/rds/versions.tf b/modules/rds/versions.tf index c1f3754a0..02c321b67 100644 --- a/modules/rds/versions.tf +++ b/modules/rds/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } random = { source = "hashicorp/random" diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index d1cf6b67b..1eacd9043 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -80,15 +80,15 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | -| [template](#requirement\_template) | 2.2.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [template](#requirement\_template) | >= 2.2.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [template](#provider\_template) | 2.2.0 | +| [aws](#provider\_aws) | >= 4.0 | +| [template](#provider\_template) | >= 2.2.0 | ## Modules @@ -106,7 +106,7 @@ components: |------|------| | [aws_iam_policy_document.custom_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | -| [template_file.bucket_policy](https://registry.terraform.io/providers/cloudposse/template/2.2.0/docs/data-sources/file) | data source | +| [template_file.bucket_policy](https://registry.terraform.io/providers/cloudposse/template/latest/docs/data-sources/file) | data source | ## Inputs diff --git a/modules/s3-bucket/versions.tf b/modules/s3-bucket/versions.tf index 6af2cf91e..da561d739 100644 --- a/modules/s3-bucket/versions.tf +++ b/modules/s3-bucket/versions.tf @@ -4,11 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } template = { source = "cloudposse/template" - version = "2.2.0" + version = ">= 2.2.0" } } } diff --git a/modules/ses/README.md b/modules/ses/README.md index 9d72033be..05583a331 100644 --- a/modules/ses/README.md +++ b/modules/ses/README.md @@ -32,14 +32,14 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [awsutils](#requirement\_awsutils) | >= 0.11.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | ## Modules diff --git a/modules/ses/versions.tf b/modules/ses/versions.tf index 7c5de18a2..c32df7c10 100644 --- a/modules/ses/versions.tf +++ b/modules/ses/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } awsutils = { source = "cloudposse/awsutils" diff --git a/modules/sftp/README.md b/modules/sftp/README.md index 07f239bd5..9748c903f 100644 --- a/modules/sftp/README.md +++ b/modules/sftp/README.md @@ -25,7 +25,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [awsutils](#requirement\_awsutils) | >= 0.11.0 | | [local](#requirement\_local) | >= 2.0 | @@ -33,7 +33,7 @@ components: | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | ## Modules diff --git a/modules/sftp/versions.tf b/modules/sftp/versions.tf index 32d228e73..fb4436329 100644 --- a/modules/sftp/versions.tf +++ b/modules/sftp/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } awsutils = { source = "cloudposse/awsutils" diff --git a/modules/snowflake-account/README.md b/modules/snowflake-account/README.md index 8c2f23355..742fc96f2 100644 --- a/modules/snowflake-account/README.md +++ b/modules/snowflake-account/README.md @@ -61,18 +61,19 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 3.0 | -| [snowflake](#requirement\_snowflake) | ~> 0.25 | -| [tls](#requirement\_tls) | ~> 3.0 | +| [aws](#requirement\_aws) | >= 3.0 | +| [random](#requirement\_random) | >= 2.3 | +| [snowflake](#requirement\_snowflake) | >= 0.25 | +| [tls](#requirement\_tls) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 3.0 | -| [random](#provider\_random) | n/a | -| [snowflake](#provider\_snowflake) | ~> 0.25 | -| [tls](#provider\_tls) | ~> 3.0 | +| [aws](#provider\_aws) | >= 3.0 | +| [random](#provider\_random) | >= 2.3 | +| [snowflake](#provider\_snowflake) | >= 0.25 | +| [tls](#provider\_tls) | >= 3.0 | ## Modules diff --git a/modules/snowflake-account/versions.tf b/modules/snowflake-account/versions.tf index b4d6d2ffb..9d71862ba 100644 --- a/modules/snowflake-account/versions.tf +++ b/modules/snowflake-account/versions.tf @@ -4,15 +4,19 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 3.0" + version = ">= 3.0" } snowflake = { source = "chanzuckerberg/snowflake" - version = "~> 0.25" + version = ">= 0.25" } tls = { source = "hashicorp/tls" - version = "~> 3.0" + version = ">= 3.0" + } + random = { + source = "hashicorp/random" + version = ">= 2.3" } } } diff --git a/modules/snowflake-database/README.md b/modules/snowflake-database/README.md index e55547e5e..5ad269eee 100644 --- a/modules/snowflake-database/README.md +++ b/modules/snowflake-database/README.md @@ -45,15 +45,15 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 3.0 | -| [snowflake](#requirement\_snowflake) | ~> 0.25 | +| [aws](#requirement\_aws) | >= 3.0 | +| [snowflake](#requirement\_snowflake) | >= 0.25 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 3.0 | -| [snowflake](#provider\_snowflake) | ~> 0.25 | +| [aws](#provider\_aws) | >= 3.0 | +| [snowflake](#provider\_snowflake) | >= 0.25 | ## Modules diff --git a/modules/snowflake-database/versions.tf b/modules/snowflake-database/versions.tf index 0bdef00a2..480cd311b 100644 --- a/modules/snowflake-database/versions.tf +++ b/modules/snowflake-database/versions.tf @@ -4,11 +4,11 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 3.0" + version = ">= 3.0" } snowflake = { source = "chanzuckerberg/snowflake" - version = "~> 0.25" + version = ">= 0.25" } } } diff --git a/modules/sns-topic/README.md b/modules/sns-topic/README.md index 25b4c9744..e4f00c0b6 100644 --- a/modules/sns-topic/README.md +++ b/modules/sns-topic/README.md @@ -71,7 +71,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers diff --git a/modules/sns-topic/versions.tf b/modules/sns-topic/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/sns-topic/versions.tf +++ b/modules/sns-topic/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index 60db805c8..eade031f1 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -25,7 +25,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers diff --git a/modules/sqs-queue/versions.tf b/modules/sqs-queue/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/sqs-queue/versions.tf +++ b/modules/sqs-queue/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } From 6f4321a91b35f3ef13e9159c287ea82d2a5ba3a7 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 6 Apr 2023 11:11:19 -0700 Subject: [PATCH 086/501] Version Pinning Requirements (#628) --- modules/aws-teams/README.md | 3 ++- modules/aws-teams/versions.tf | 4 ++++ modules/eks/aws-node-termination-handler/README.md | 7 ++++--- modules/eks/aws-node-termination-handler/versions.tf | 6 +++++- modules/eks/efs-controller/README.md | 7 ++++--- modules/eks/efs-controller/versions.tf | 6 +++++- modules/eks/efs/README.md | 4 ++-- modules/eks/efs/versions.tf | 2 +- modules/eks/eks-without-spotinst/README.md | 2 +- modules/eks/eks-without-spotinst/versions.tf | 2 +- modules/eks/redis-operator/README.md | 7 ++++--- modules/eks/redis-operator/versions.tf | 6 +++++- modules/eks/redis/README.md | 7 ++++--- modules/eks/redis/versions.tf | 6 +++++- .../modules/redis_cluster/versions.tf | 2 +- .../modules/terraform-aws-sqs-queue/versions.tf | 2 +- modules/tgw/cross-region-hub-connector/README.md | 6 +++--- modules/tgw/cross-region-hub-connector/versions.tf | 2 +- modules/tgw/cross-region-spoke/README.md | 10 +++++----- .../cross-region-spoke/modules/tgw_routes/outputs.tf | 3 ++- .../modules/tgw_routes/versions.tf | 2 +- .../cross-region-spoke/modules/vpc_routes/outputs.tf | 3 ++- .../modules/vpc_routes/versions.tf | 2 +- modules/tgw/cross-region-spoke/outputs.tf | 12 ++++++++---- modules/tgw/cross-region-spoke/versions.tf | 2 +- .../spoke/modules/standard_vpc_attachment/outputs.tf | 1 + .../modules/standard_vpc_attachment/versions.tf | 2 +- 27 files changed, 75 insertions(+), 43 deletions(-) diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index 3e6aca037..bfb93f925 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -135,13 +135,14 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | +| [local](#requirement\_local) | >= 1.3 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [local](#provider\_local) | n/a | +| [local](#provider\_local) | >= 1.3 | ## Modules diff --git a/modules/aws-teams/versions.tf b/modules/aws-teams/versions.tf index cc73ffd35..2fdade250 100644 --- a/modules/aws-teams/versions.tf +++ b/modules/aws-teams/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = ">= 4.9.0" } + local = { + source = "hashicorp/local" + version = ">= 1.3" + } } } diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 82a9b174d..3d04c1adf 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -44,15 +44,16 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [kubernetes](#provider\_kubernetes) | n/a | +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0 | ## Modules diff --git a/modules/eks/aws-node-termination-handler/versions.tf b/modules/eks/aws-node-termination-handler/versions.tf index 58318d20e..71f1c0e3e 100644 --- a/modules/eks/aws-node-termination-handler/versions.tf +++ b/modules/eks/aws-node-termination-handler/versions.tf @@ -4,11 +4,15 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } helm = { source = "hashicorp/helm" version = ">= 2.0" } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.0" + } } } diff --git a/modules/eks/efs-controller/README.md b/modules/eks/efs-controller/README.md index 65cb93046..d8604dc72 100644 --- a/modules/eks/efs-controller/README.md +++ b/modules/eks/efs-controller/README.md @@ -44,15 +44,16 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [kubernetes](#provider\_kubernetes) | n/a | +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0 | ## Modules diff --git a/modules/eks/efs-controller/versions.tf b/modules/eks/efs-controller/versions.tf index 58318d20e..71f1c0e3e 100644 --- a/modules/eks/efs-controller/versions.tf +++ b/modules/eks/efs-controller/versions.tf @@ -4,11 +4,15 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } helm = { source = "hashicorp/helm" version = ">= 2.0" } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.0" + } } } diff --git a/modules/eks/efs/README.md b/modules/eks/efs/README.md index 2264c217e..523b7f45e 100644 --- a/modules/eks/efs/README.md +++ b/modules/eks/efs/README.md @@ -25,13 +25,13 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | +| [aws](#provider\_aws) | >= 4.0 | ## Modules diff --git a/modules/eks/efs/versions.tf b/modules/eks/efs/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/eks/efs/versions.tf +++ b/modules/eks/efs/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/eks/eks-without-spotinst/README.md b/modules/eks/eks-without-spotinst/README.md index 38e444248..7a67f1e2a 100644 --- a/modules/eks/eks-without-spotinst/README.md +++ b/modules/eks/eks-without-spotinst/README.md @@ -64,7 +64,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 3.0 | +| [aws](#requirement\_aws) | >= 3.0 | ## Providers diff --git a/modules/eks/eks-without-spotinst/versions.tf b/modules/eks/eks-without-spotinst/versions.tf index 9f0fb337c..8da21ddd5 100644 --- a/modules/eks/eks-without-spotinst/versions.tf +++ b/modules/eks/eks-without-spotinst/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 3.0" + version = ">= 3.0" } # spotinst = { # source = "spotinst/spotinst" diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 9f4356de9..87fc05ae9 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -71,15 +71,16 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [kubernetes](#provider\_kubernetes) | n/a | +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0 | ## Modules diff --git a/modules/eks/redis-operator/versions.tf b/modules/eks/redis-operator/versions.tf index 58318d20e..71f1c0e3e 100644 --- a/modules/eks/redis-operator/versions.tf +++ b/modules/eks/redis-operator/versions.tf @@ -4,11 +4,15 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } helm = { source = "hashicorp/helm" version = ">= 2.0" } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.0" + } } } diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index d43bab34b..062927b6f 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -77,15 +77,16 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 4.0 | -| [kubernetes](#provider\_kubernetes) | n/a | +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0 | ## Modules diff --git a/modules/eks/redis/versions.tf b/modules/eks/redis/versions.tf index 58318d20e..71f1c0e3e 100644 --- a/modules/eks/redis/versions.tf +++ b/modules/eks/redis/versions.tf @@ -4,11 +4,15 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } helm = { source = "hashicorp/helm" version = ">= 2.0" } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.0" + } } } diff --git a/modules/elasticache-redis/modules/redis_cluster/versions.tf b/modules/elasticache-redis/modules/redis_cluster/versions.tf index b3730a19e..5b9bb0612 100644 --- a/modules/elasticache-redis/modules/redis_cluster/versions.tf +++ b/modules/elasticache-redis/modules/redis_cluster/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } random = { source = "hashicorp/random" diff --git a/modules/sqs-queue/modules/terraform-aws-sqs-queue/versions.tf b/modules/sqs-queue/modules/terraform-aws-sqs-queue/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/sqs-queue/modules/terraform-aws-sqs-queue/versions.tf +++ b/modules/sqs-queue/modules/terraform-aws-sqs-queue/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/tgw/cross-region-hub-connector/README.md b/modules/tgw/cross-region-hub-connector/README.md index 29be5ddb8..e80e18c14 100644 --- a/modules/tgw/cross-region-hub-connector/README.md +++ b/modules/tgw/cross-region-hub-connector/README.md @@ -36,14 +36,14 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers | Name | Version | |------|---------| -| [aws.tgw\_home\_region](#provider\_aws.tgw\_home\_region) | ~> 4.0 | -| [aws.tgw\_this\_region](#provider\_aws.tgw\_this\_region) | ~> 4.0 | +| [aws.tgw\_home\_region](#provider\_aws.tgw\_home\_region) | >= 4.0 | +| [aws.tgw\_this\_region](#provider\_aws.tgw\_this\_region) | >= 4.0 | ## Modules diff --git a/modules/tgw/cross-region-hub-connector/versions.tf b/modules/tgw/cross-region-hub-connector/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/tgw/cross-region-hub-connector/versions.tf +++ b/modules/tgw/cross-region-hub-connector/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/tgw/cross-region-spoke/README.md b/modules/tgw/cross-region-spoke/README.md index d860bd20a..6c7803eed 100644 --- a/modules/tgw/cross-region-spoke/README.md +++ b/modules/tgw/cross-region-spoke/README.md @@ -44,7 +44,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | ~> 4.0 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers @@ -108,10 +108,10 @@ No resources. | Name | Description | |------|-------------| -| [tgw\_routes\_home\_region](#output\_tgw\_routes\_home\_region) | n/a | -| [tgw\_routes\_in\_region](#output\_tgw\_routes\_in\_region) | n/a | -| [vpc\_routes\_home](#output\_vpc\_routes\_home) | n/a | -| [vpc\_routes\_this](#output\_vpc\_routes\_this) | n/a | +| [tgw\_routes\_home\_region](#output\_tgw\_routes\_home\_region) | TGW Routes to the primary region | +| [tgw\_routes\_in\_region](#output\_tgw\_routes\_in\_region) | TGW reoutes in this region | +| [vpc\_routes\_home](#output\_vpc\_routes\_home) | VPC routes to the primary VPC | +| [vpc\_routes\_this](#output\_vpc\_routes\_this) | This modules VPC routes | ## References diff --git a/modules/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf b/modules/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf index 0832bbb24..ba6b38b14 100644 --- a/modules/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf +++ b/modules/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf @@ -1,3 +1,4 @@ output "aws_ec2_transit_gateway_routes" { - value = aws_ec2_transit_gateway_route.default + value = aws_ec2_transit_gateway_route.default + description = "AWS EC2 Transit Gateway routes" } diff --git a/modules/tgw/cross-region-spoke/modules/tgw_routes/versions.tf b/modules/tgw/cross-region-spoke/modules/tgw_routes/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/tgw/cross-region-spoke/modules/tgw_routes/versions.tf +++ b/modules/tgw/cross-region-spoke/modules/tgw_routes/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf b/modules/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf index 2f97ed8fc..e706a95fe 100644 --- a/modules/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf +++ b/modules/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf @@ -1,3 +1,4 @@ output "aws_routes" { - value = aws_route.route + value = aws_route.route + description = "AWS Routes" } diff --git a/modules/tgw/cross-region-spoke/modules/vpc_routes/versions.tf b/modules/tgw/cross-region-spoke/modules/vpc_routes/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/tgw/cross-region-spoke/modules/vpc_routes/versions.tf +++ b/modules/tgw/cross-region-spoke/modules/vpc_routes/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/tgw/cross-region-spoke/outputs.tf b/modules/tgw/cross-region-spoke/outputs.tf index 605da986c..1bcf17612 100644 --- a/modules/tgw/cross-region-spoke/outputs.tf +++ b/modules/tgw/cross-region-spoke/outputs.tf @@ -1,17 +1,21 @@ output "vpc_routes_this" { - value = module.vpc_routes_this + value = module.vpc_routes_this + description = "This modules VPC routes" } output "tgw_routes_in_region" { - value = module.tgw_routes_this_region + value = module.tgw_routes_this_region + description = "TGW reoutes in this region" } output "vpc_routes_home" { - value = module.vpc_routes_home + value = module.vpc_routes_home + description = "VPC routes to the primary VPC" } output "tgw_routes_home_region" { - value = module.tgw_routes_home_region + value = module.tgw_routes_home_region + description = "TGW Routes to the primary region" } # #output "tgw_this_region" { diff --git a/modules/tgw/cross-region-spoke/versions.tf b/modules/tgw/cross-region-spoke/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/tgw/cross-region-spoke/versions.tf +++ b/modules/tgw/cross-region-spoke/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/outputs.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/outputs.tf index dca466b3c..538a37725 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/outputs.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/outputs.tf @@ -10,4 +10,5 @@ output "tg_config" { static_routes = null transit_gateway_vpc_attachment_id = module.standard_vpc_attachment.transit_gateway_vpc_attachment_ids[var.owning_account] } + description = "Transit Gateway configuration formatted for handling" } diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/versions.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/versions.tf index e89eb16ed..f33ede77f 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/versions.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = ">= 4.0" } } } From 1beb592adbe6afcca917dd3bfe0f6d9a36d52f1d Mon Sep 17 00:00:00 2001 From: Kevin Mahoney Date: Thu, 6 Apr 2023 14:31:40 -0400 Subject: [PATCH 087/501] update datadog_lambda_forwarder ref for darwin_arm64 (#626) Co-authored-by: cloudpossebot Co-authored-by: Matt Gowie Co-authored-by: Dan Miller --- modules/datadog-lambda-forwarder/README.md | 2 +- modules/datadog-lambda-forwarder/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 6dd50b355..cee21da66 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -64,7 +64,7 @@ components: |------|--------|---------| | [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.3.0 | +| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.3.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [log\_group\_prefix](#module\_log\_group\_prefix) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/datadog-lambda-forwarder/main.tf b/modules/datadog-lambda-forwarder/main.tf index afc5c8b32..8080204c8 100644 --- a/modules/datadog-lambda-forwarder/main.tf +++ b/modules/datadog-lambda-forwarder/main.tf @@ -40,7 +40,7 @@ module "log_group_prefix" { module "datadog_lambda_forwarder" { source = "cloudposse/datadog-lambda-forwarder/aws" - version = "1.3.0" + version = "1.3.1" cloudwatch_forwarder_log_groups = local.cloudwatch_forwarder_log_groups cloudwatch_forwarder_event_patterns = var.cloudwatch_forwarder_event_patterns From 842739171ea697d6cb0588ffe1233033db6fc7a0 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 6 Apr 2023 12:20:52 -0700 Subject: [PATCH 088/501] Missing Version Pins for Bats (#629) --- modules/eks/idp-roles/README.md | 1 + modules/eks/idp-roles/versions.tf | 4 ++++ modules/elasticsearch/README.md | 3 ++- modules/elasticsearch/versions.tf | 4 ++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index 8fd9d9bd9..88989b7d6 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -29,6 +29,7 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | ## Providers diff --git a/modules/eks/idp-roles/versions.tf b/modules/eks/idp-roles/versions.tf index cbf605948..5a442caea 100644 --- a/modules/eks/idp-roles/versions.tf +++ b/modules/eks/idp-roles/versions.tf @@ -10,5 +10,9 @@ terraform { source = "hashicorp/helm" version = ">= 2.0" } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.14.0" + } } } diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index 5d5eef5c1..7eccffd95 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -33,13 +33,14 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 0.13.0 | | [aws](#requirement\_aws) | >= 3.8 | +| [random](#requirement\_random) | >= 3.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 3.8 | -| [random](#provider\_random) | n/a | +| [random](#provider\_random) | >= 3.0 | ## Modules diff --git a/modules/elasticsearch/versions.tf b/modules/elasticsearch/versions.tf index 207f9f727..f386dc252 100644 --- a/modules/elasticsearch/versions.tf +++ b/modules/elasticsearch/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = ">= 3.8" } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } } } From deeff64051c0c8898f53f35e0da40f608fbbe8a2 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Tue, 11 Apr 2023 11:52:38 +0300 Subject: [PATCH 089/501] [argocd] Added github commit status notifications (#631) Co-authored-by: cloudpossebot --- modules/eks/argocd/README.md | 4 +- modules/eks/argocd/main.tf | 44 ++++++++++++++----- .../argocd/variables-argocd-notifications.tf | 28 +++++++++++- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 36357e484..ecb7ee961 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -150,8 +150,8 @@ components: | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [notifications\_default\_triggers](#input\_notifications\_default\_triggers) | Default notification Triggers to configure.

See: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/triggers/#default-triggers
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/790438efebf423c2d56cb4b93471f4adb3fcd448/charts/argo-cd/values.yaml#L2841) | `map(list(string))` | `{}` | no | -| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = optional(number)
installationID = optional(number)
privateKey = optional(string)
}))
})
| `{}` | no | -| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
}))
| `{}` | no | +| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = number
installationID = number
privateKey = optional(string)
}))
# service.webhook.:
service_webhook = optional(map(
object({
url = string
headers = optional(list(
object({
name = string
value = string
})
), [])
basicAuth = optional(object({
username = string
password = string
}))
insecureSkipVerify = optional(bool, false)
})
))
})
| `{}` | no | +| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
webhook = optional(map(
object({
method = optional(string)
path = optional(string)
body = optional(string)
})
))
}))
| `{}` | no | | [notifications\_triggers](#input\_notifications\_triggers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
map(list(
object({
oncePer = optional(string)
send = list(string)
when = string
})
))
| `{}` | no | | [oidc\_enabled](#input\_oidc\_enabled) | Toggles OIDC integration in the deployed chart | `bool` | `false` | no | | [oidc\_issuer](#input\_oidc\_issuer) | OIDC issuer URL | `string` | `""` | no | diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 76867ba1e..0b2018be4 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -11,7 +11,7 @@ locals { github_deploy_key = data.aws_ssm_parameter.github_deploy_key[k].value } } : {} - credential_templates = flatten([ + credential_templates = flatten(concat([ for k, v in local.argocd_repositories : [ { name = "configs.credentialTemplates.${k}.url" @@ -20,11 +20,22 @@ locals { }, { name = "configs.credentialTemplates.${k}.sshPrivateKey" - value = v.github_deploy_key + value = nonsensitive(v.github_deploy_key) type = "string" }, ] - ]) + ], + [ + for s, v in local.notifications_notifiers_ssm_configs : [ + for k, i in v : [ + { + name = "notifications.secret.items.${s}_${k}" + value = i + type = "string" + } + ] + ] + ])) regional_service_discovery_domain = "${module.this.environment}.${module.dns_gbl_delegated.outputs.default_domain_name}" host = var.host != "" ? var.host : format("%s.%s", coalesce(var.alb_name, var.name), local.regional_service_discovery_domain) enable_argo_workflows_auth = local.saml_enabled && var.argo_enable_workflows_auth @@ -94,26 +105,35 @@ data "aws_ssm_parameters_by_path" "argocd_notifications" { } locals { - notifications_notifiers_ssm_path = { for key, value in var.notifications_notifiers : + notifications_notifiers_ssm_path = { for key, value in local.notifications_notifiers_variables : key => format("%s/%s/", var.notifications_notifiers.ssm_path_prefix, key) } notifications_notifiers_ssm_configs = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : - key => nonsensitive(zipmap( + key => zipmap( [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], value.values - )) + ) } - notifications_notifiers_variables = { - for key, value in var.notifications_notifiers : - key => { for param_name, param_value in value : param_name => param_value if param_value != null } - if key != "ssm_path_prefix" + notifications_notifiers_ssm_configs_keys = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : + key => zipmap( + [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], + [for name in value.names : format("$%s_%s", key, trimprefix(name, local.notifications_notifiers_ssm_path[key]))] + ) } + notifications_notifiers_variables = merge({ for key, value in var.notifications_notifiers : + key => { for param_name, param_value in value : param_name => param_value if param_value != null } + if key != "ssm_path_prefix" && key != "service_webhook" + }, + { for key, value in coalesce(var.notifications_notifiers.service_webhook, {}) : + format("service_webhook_%s", key) => { for param_name, param_value in value : param_name => param_value if param_value != null } + }) + notifications_notifiers = { for key, value in local.notifications_notifiers_variables : - replace(key, "_", ".") => yamlencode(merge(local.notifications_notifiers_ssm_configs[key], value)) + replace(key, "_", ".") => yamlencode(merge(local.notifications_notifiers_ssm_configs_keys[key], value)) } } @@ -139,7 +159,7 @@ module "argocd" { service_account_name = module.this.name service_account_namespace = var.kubernetes_namespace - set_sensitive = local.credential_templates + set_sensitive = nonsensitive(local.credential_templates) values = compact([ # standard k8s object settings diff --git a/modules/eks/argocd/variables-argocd-notifications.tf b/modules/eks/argocd/variables-argocd-notifications.tf index 748ee606e..4105c84e4 100644 --- a/modules/eks/argocd/variables-argocd-notifications.tf +++ b/modules/eks/argocd/variables-argocd-notifications.tf @@ -13,6 +13,13 @@ variable "notifications_templates" { targetURL = string }) })) + webhook = optional(map( + object({ + method = optional(string) + path = optional(string) + body = optional(string) + }) + )) })) default = {} description = <<-EOT @@ -44,10 +51,27 @@ variable "notifications_notifiers" { type = object({ ssm_path_prefix = optional(string, "/argocd/notifications/notifiers") service_github = optional(object({ - appID = optional(number) - installationID = optional(number) + appID = number + installationID = number privateKey = optional(string) })) + # service.webhook.: + service_webhook = optional(map( + object({ + url = string + headers = optional(list( + object({ + name = string + value = string + }) + ), []) + basicAuth = optional(object({ + username = string + password = string + })) + insecureSkipVerify = optional(bool, false) + }) + )) }) default = {} description = <<-EOT From d0b86017f2656cb9f34dbea8b09dd9318b6a9519 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Tue, 11 Apr 2023 15:11:20 +0300 Subject: [PATCH 090/501] [argocd-repo] Added ArgoCD git commit notifications (#633) --- modules/argocd-repo/templates/applicationset.yaml.tpl | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index 203db543b..62efbeff6 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -66,6 +66,7 @@ spec: app_commit: '{{app_commit}}' app_hostname: 'https://{{app_hostname}}' notifications.argoproj.io/subscribe.on-deployed.github: "" + notifications.argoproj.io/subscribe.on-deployed.github-commit-status: "" name: '{{name}}' spec: project: ${name} From aa82aea3820060046d939b5412522d4c6f01e5d4 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 18 Apr 2023 14:46:14 -0400 Subject: [PATCH 091/501] feat: cloudtrail-bucket can have acl configured (#643) --- modules/cloudtrail-bucket/README.md | 1 + modules/cloudtrail-bucket/main.tf | 1 + modules/cloudtrail-bucket/variables.tf | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/modules/cloudtrail-bucket/README.md b/modules/cloudtrail-bucket/README.md index 150c0c069..72a826ba6 100644 --- a/modules/cloudtrail-bucket/README.md +++ b/modules/cloudtrail-bucket/README.md @@ -51,6 +51,7 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [access\_log\_bucket\_name](#input\_access\_log\_bucket\_name) | If var.create\_access\_log\_bucket is false, this is the name of the S3 bucket where s3 access logs will be sent to. | `string` | `""` | no | +| [acl](#input\_acl) | The canned ACL to apply. We recommend log-delivery-write for
compatibility with AWS services. Valid values are private, public-read,
public-read-write, aws-exec-read, authenticated-read, bucket-owner-read,
bucket-owner-full-control, log-delivery-write.

Due to https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-faq.html, this
will need to be set to 'private' during creation, but you can update normally after. | `string` | `"log-delivery-write"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | diff --git a/modules/cloudtrail-bucket/main.tf b/modules/cloudtrail-bucket/main.tf index 805d13c6e..c004facf2 100644 --- a/modules/cloudtrail-bucket/main.tf +++ b/modules/cloudtrail-bucket/main.tf @@ -2,6 +2,7 @@ module "cloudtrail_s3_bucket" { source = "cloudposse/cloudtrail-s3-bucket/aws" version = "0.23.1" + acl = var.acl expiration_days = var.expiration_days force_destroy = false glacier_transition_days = var.glacier_transition_days diff --git a/modules/cloudtrail-bucket/variables.tf b/modules/cloudtrail-bucket/variables.tf index cff8e964f..cd51dc857 100644 --- a/modules/cloudtrail-bucket/variables.tf +++ b/modules/cloudtrail-bucket/variables.tf @@ -45,3 +45,17 @@ variable "access_log_bucket_name" { default = "" description = "If var.create_access_log_bucket is false, this is the name of the S3 bucket where s3 access logs will be sent to." } + +variable "acl" { + type = string + description = <<-EOT + The canned ACL to apply. We recommend log-delivery-write for + compatibility with AWS services. Valid values are private, public-read, + public-read-write, aws-exec-read, authenticated-read, bucket-owner-read, + bucket-owner-full-control, log-delivery-write. + + Due to https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-faq.html, this + will need to be set to 'private' during creation, but you can update normally after. + EOT + default = "log-delivery-write" +} \ No newline at end of file From 1a63bef931c72d489f22a78780a2187efe345315 Mon Sep 17 00:00:00 2001 From: Maksym Vlasov Date: Wed, 19 Apr 2023 17:20:03 +0300 Subject: [PATCH 092/501] fix: Use `vpc` without tenant (#644) --- modules/vpc/remote-state.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/vpc/remote-state.tf b/modules/vpc/remote-state.tf index cc5bd0fb4..a956d0c8a 100644 --- a/modules/vpc/remote-state.tf +++ b/modules/vpc/remote-state.tf @@ -7,7 +7,7 @@ module "vpc_flow_logs_bucket" { component = "vpc-flow-logs-bucket" environment = var.vpc_flow_logs_bucket_environment_name stage = var.vpc_flow_logs_bucket_stage_name - tenant = coalesce(var.vpc_flow_logs_bucket_tenant_name, module.this.tenant) + tenant = try(coalesce(var.vpc_flow_logs_bucket_tenant_name, module.this.tenant), null) context = module.this.context } From 5dd60e368476deae01a3607c2e576c592f885829 Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 19 Apr 2023 22:13:31 -0700 Subject: [PATCH 093/501] Convert eks/cluster to aws-teams and aws-sso (#645) --- .../account-map/modules/iam-roles/outputs.tf | 8 ++ .../modules/roles-to-principals/main.tf | 34 ++--- .../modules/roles-to-principals/outputs.tf | 7 +- .../eks/actions-runner-controller/README.md | 58 +++++---- modules/eks/cluster/README.md | 123 ++++++++++-------- modules/eks/cluster/aws_sso.tf | 30 +++++ modules/eks/cluster/main.tf | 38 +++--- modules/eks/cluster/remote-state.tf | 31 ++--- modules/eks/cluster/variables.tf | 54 ++++---- 9 files changed, 215 insertions(+), 168 deletions(-) create mode 100644 modules/eks/cluster/aws_sso.tf diff --git a/modules/account-map/modules/iam-roles/outputs.tf b/modules/account-map/modules/iam-roles/outputs.tf index ff281d756..04b3bb220 100644 --- a/modules/account-map/modules/iam-roles/outputs.tf +++ b/modules/account-map/modules/iam-roles/outputs.tf @@ -41,6 +41,14 @@ output "global_stage_name" { description = "The `null-label` `stage` value for the organization management account (where the `account-map` state is stored)" } +output "current_account_account_name" { + value = local.account_name + description = <<-EOT + The account name (usually `-`) for the account configured by this module's inputs. + Roughly analogous to `data "aws_caller_identity"`, but returning the name of the caller account as used in our configuration. + EOT +} + output "dns_terraform_role_arn" { value = module.account_map.outputs.terraform_roles[module.account_map.outputs.dns_account_account_name] description = "The AWS Role ARN for Terraform to use to provision DNS Zone delegations, when Role ARNs are in use" diff --git a/modules/account-map/modules/roles-to-principals/main.tf b/modules/account-map/modules/roles-to-principals/main.tf index 42fe98e0c..9addd704a 100644 --- a/modules/account-map/modules/roles-to-principals/main.tf +++ b/modules/account-map/modules/roles-to-principals/main.tf @@ -24,24 +24,24 @@ module "account_map" { locals { aws_partition = module.account_map.outputs.aws_partition - principals = distinct(compact(flatten([for acct, v in var.role_map : ( - contains(v, "*") ? [format("arn:%s:iam::%s:root", local.aws_partition, module.account_map.outputs.full_account_map[acct])] : - [ - for role in v : format(module.account_map.outputs.iam_role_arn_templates[acct], role) - ] - )]))) + principals_map = { for acct, v in var.role_map : acct => ( + contains(v, "*") ? { + "*" = format("arn:%s:iam::%s:root", local.aws_partition, module.account_map.outputs.full_account_map[acct]) + } : + { + for role in v : role => format(module.account_map.outputs.iam_role_arn_templates[acct], role) + } + ) } + + # This expression could be simplified, but then the order of principals would be different than in earlier versions, causing unnecessary plan changes. + principals = distinct(compact(flatten([for acct, v in var.role_map : values(local.principals_map[acct])]))) # Support for AWS SSO Permission Sets - permission_set_arn_like = distinct(compact(flatten([for acct, v in var.permission_set_map : concat(formatlist( - # arn:aws:iam::12345:role/aws-reserved/sso.amazonaws.com/ap-southeast-1/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc - # AWS SSO Sometimes includes `/region/`, but not always. + permission_set_arn_like = distinct(compact(flatten([for acct, v in var.permission_set_map : formatlist( + # Usually like: + # arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc + # But sometimes AWS SSO ARN includes `/region/`, like: + # arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-southeast-1/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc format("arn:%s:iam::%s:role/aws-reserved/sso.amazonaws.com*/AWSReservedSSO_%%s_*", local.aws_partition, module.account_map.outputs.full_account_map[acct]), - v), - formatlist( - # Support assume role from the allowed identity account SSO users - # arn:aws:iam::12345:role/aws-reserved/sso.amazonaws.com/ap-southeast-1/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc - # AWS SSO Sometimes includes `/region/`, but not always. - format("arn:%s:iam::%s:role/aws-reserved/sso.amazonaws.com*/AWSReservedSSO_%%s_*", local.aws_partition, module.account_map.outputs.full_account_map[module.account_map.outputs.identity_account_account_name]), - v), - )]))) + v)]))) } diff --git a/modules/account-map/modules/roles-to-principals/outputs.tf b/modules/account-map/modules/roles-to-principals/outputs.tf index 2ff84a8cf..434b4dd0b 100644 --- a/modules/account-map/modules/roles-to-principals/outputs.tf +++ b/modules/account-map/modules/roles-to-principals/outputs.tf @@ -1,6 +1,11 @@ output "principals" { value = local.principals - description = "List of AWS principals corresponding to given input `role_map`" + description = "Consolidated list of AWS principals corresponding to given input `role_map`" +} + +output "principals_map" { + value = local.principals_map + description = "Map of AWS principals corresponding to given input `role_map`" } output "permission_set_arn_like" { diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 9464c2af1..5336e49c1 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -264,35 +264,29 @@ HRA being successfully notified about it, so as a safety measure, the `capacityR configurable amount of time, at which point it will be deleted without regard to the job being finished. This ensures that eventually an idle runner pool will scale down to `minReplicas`. -However, there are some problems with this scheme. In theory, `webhook_startup_timeout` should only need to be long -enough to cover the delay between the time the HRA starts a scale up request and the time the runner actually starts, -is allocated to the runner pool, and picks up a job to run. But there are edge cases that seem not to be covered -properly (see [actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)). As a result, we recommend setting `webhook_startup_timeout` to -a period long enough to cover the full time a job may have to wait between the time it is queued and the time it -actually starts. Consider this scenario: -- You set `maxReplicas = 5` -- Some trigger starts 20 jobs, each of which take 5 minutes to run -- The replica pool scales up to 5, and the first 5 jobs run -- 5 minutes later, the next 5 jobs run, and so on -- The last set of 5 jobs will have to wait 15 minutes to start because of the previous jobs - -The HRA is designed to handle this situation by updating the expiration time of the `capacityReservation` of any -job stuck waiting because the pool has scaled up to `maxReplicas`, but as discussed in issue #2466 linked above, -that does not seem to be working correctly as of version 0.27.2. - -For now, our recommendation is to set `webhook_startup_timeout` to a duration long enough to cover the time the job -may have to wait in the queue for a runner to become available due to there being more jobs than `maxReplicas`. -Alternatively, you could set `maxReplicas` to a big enough number that there will always be a runner for every -queued job, in which case the duration only needs to be long enough to allow for all the scale-up activities (such -as launching new EKS nodes as well as starting new pods) to finish. Remember, when everything works properly, the -HRA will scale down the pool as jobs finish, so there is little cost to setting a long duration. +If it happens that the capacity reservation expires before the job is finished, the Horizontal Runner Autoscaler (HRA) will scale down the pool +by 2 instead of 1: once because the capacity reservation expired, and once because the job finished. This will +also cause starvation of waiting jobs, because the next in line will have its timeout timer started but will not +actually start running because no runner is available. And if `minReplicas` is set to zero, the pool will scale down +to zero before finishing all the jobs, leaving some waiting indefinitely. This is why it is important to set the +`webhook_startup_timeout` to a time long enough to cover the full time a job may have to wait between the time it is +queued and the time it finishes, assuming that the HRA scales up the pool by 1 and runs the job on the new runner. + +:::info +If there are more jobs queued than there are runners allowed by `maxReplicas`, the timeout timer does not start on the +capacity reservation until enough reservations ahead of it are removed for it to be considered as representing +and active job. Although there are some edge cases regarding `webhook_startup_timeout` that seem not to be covered +properly (see [actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)), +they only merit adding a few extra minutes to the timeout. +::: + ### Recommended `webhook_startup_timeout` Duration #### Consequences of Too Short of a `webhook_startup_timeout` Duration If you set `webhook_startup_timeout` to too short a duration, the Horizontal Runner Autoscaler will cancel capacity -reservations for jobs that have not yet run, and the pool will be too small. This will be most serious if you have +reservations for jobs that have not yet finished, and the pool will become too small. This will be most serious if you have set `minReplicas = 0` because in this case, jobs will be left in the queue indefinitely. With a higher value of `minReplicas`, the pool will eventually make it through all the queued jobs, but not as quickly as intended due to the incorrectly reduced capacity. @@ -305,11 +299,19 @@ problem with this is the added expense of leaving the idle runner running. #### Recommendation -Therefore we recommend that for lightly used runner pools, set `webhook_startup_timeout` to `"30m"`. For heavily -used pools, find the typical or maximum length of a job, multiply by the number of jobs likely to be queued in an -hour, and divide by `maxReplicas`, then round up. As a rule of thumb, we recommend setting `maxReplicas` high enough -that jobs never wait on the queue more than an hour and setting `webhook_startup_timeout` to `"2h30m"`. Monitor your -usage and adjust accordingly. +As a result, we recommend setting `webhook_startup_timeout` to a period long enough to cover: +- The time it takes for the HRA to scale up the pool and make a new runner available +- The time it takes for the runner to pick up the job from GitHub +- The time it takes for the job to start running on the new runner +- The maximum time a job might take + +Because the consequences of expiring a capacity reservation before the job is finished are so severe, we recommend +setting `webhook_startup_timeout` to a period at least 30 minutes longer than you expect the longest job to take. +Remember, when everything works properly, the HRA will scale down the pool as jobs finish, so there is little cost +to setting a long duration, and the cost looks even smaller by comparison to the cost of having too short a duration. + +For lightly used runner pools expecting only short jobs, you can set `webhook_startup_timeout` to `"30m"`. +As a rule of thumb, we recommend setting `maxReplicas` high enough that jobs never wait on the queue more than an hour. ### Updating CRDs diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 96a043828..a893e6ed7 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -4,7 +4,13 @@ This component is responsible for provisioning an end-to-end EKS Cluster, includ :::warning -This component should only be deployed after logging into AWS via Federated login with SAML (e.g. GSuite) or assuming an IAM role (e.g. from a CI/CD system). It should not be deployed if you log into AWS via AWS SSO, the reason being that on initial deployment, the EKS cluster will be owned by the assumed role that provisioned it. If this were to be the AWS SSO Role, then we risk losing access to the EKS cluster once the ARN of the AWS SSO Role eventually changes. + + +This component should only be deployed after logging into AWS via Federated login with SAML (e.g. GSuite) or +assuming an IAM role (e.g. from a CI/CD system). It should not be deployed if you log into AWS via AWS SSO, the +reason being that on initial deployment, the EKS cluster will be owned by the assumed role that provisioned it, +and AWS SSO roles are ephemeral (replaced on every configuration change). If this were to be the AWS SSO Role, then +we risk losing access to the EKS cluster once the ARN of the AWS SSO Role eventually changes. ::: @@ -15,10 +21,10 @@ This component should only be deployed after logging into AWS via Federated logi Here's an example snippet for how to use this component. This example expects the [Cloud Posse Reference Architecture](https://docs.cloudposse.com/reference-architecture/) -Identity and Network designs deployed for mapping users to EKS service roles and granting access in a private network. +Identity and Network designs deployed for mapping users to EKS service roles and granting access in a private network. In addition, this example has the GitHub OIDC integration added and makes use of Karpenter to dynamically scale cluster nodes. -For more on these requirements, see +For more on these requirements, see [Identity Reference Architecture](https://docs.cloudposse.com/reference-architecture/quickstart/iam-identity/), [Network Reference Architecture](https://docs.cloudposse.com/reference-architecture/scaffolding/setup/network/), the [Github OIDC component](https://docs.cloudposse.com/components/catalog/aws/github-oidc-provider/), @@ -33,7 +39,9 @@ components: name: eks iam_primary_roles_tenant_name: core cluster_kubernetes_version: "1.25" - availability_zones: ["us-east-1a", "us-east-1b", "us-east-1c"] + # Your choice of availability zones or availability zone ids + # availability_zones: ["us-east-1a", "us-east-1b", "us-east-1c"] + availability_zone_ids: ["use1-az4", "use1-az5", "use1-az6"] aws_ssm_agent_enabled: true allow_ingress_from_vpc_accounts: - tenant: core @@ -47,7 +55,7 @@ components: allowed_security_groups: [] enabled_cluster_log_types: # Caution: enabling `api` log events may lead to a substantial increase in Cloudwatch Logs expenses. - - api + - api - audit - authenticator - controllerManager @@ -63,7 +71,7 @@ components: # We use karpenter to provision nodes # See below for using node_groups managed_node_groups_enabled: false - node_groups: {} + node_groups: {} # EKS IAM Authentication settings # By default, you can authenticate to EKS cluster only by assuming the role that created the cluster. @@ -76,43 +84,47 @@ components: cluster_endpoint_private_access: true cluster_endpoint_public_access: false cluster_log_retention_period: 90 - # Roles from the primary account to allow access to the cluster - # See `aws-teams` component. - primary_iam_roles: - - groups: - - system:masters - - idp:ops - role: devops - - groups: - - system:masters - role: spacelift - # Roles from the account owning the cluster to allow access to the cluster - # See `aws-team-roles` component. - delegated_iam_roles: - - groups: - - system:masters - - idp:ops - role: admin - - groups: - - idp:poweruser - role: poweruser - - groups: - - idp:observer - role: observer - - groups: - - system:masters - role: terraform - # Roles from AWS SSO allowing cluster access + # List of `aws-teams` to map to Kubernetes RBAC groups. + # This gives teams direct access to Kubernetes without having to assume a team-role. + # RBAC groups must be created elsewhere. The "system:" groups are predefined by Kubernetes. + aws_teams_rbac: + - groups: + - system:masters + aws_team: admin + - groups: + - idp:poweruser + - system:authenticated + aws_team: poweruser + - groups: + - idp:observer + - system:authenticated + aws_team: observer + # List of `aws-teams-roles` (in the account where the EKS cluster is deployed) to map to Kubernetes RBAC groups + aws_team_roles_rbac: + - groups: + - system:masters + aws_team_role: admin + - groups: + - idp:poweruser + - system:authenticated + aws_team_role: poweruser + - groups: + - idp:observer + - system:authenticated + aws_team_role: observer + - groups: + - system:masters + aws_team_role: terraform + - groups: + - system:masters + aws_team_role: helm + # Permission sets from AWS SSO allowing cluster access # See `aws-sso` component. - sso_iam_roles: - - groups: - - idp:observer - - idp:observer-extra - role: ReadOnlyAccess - - groups: - - system:masters - - idp:ops - role: AdministratorAccess + aws_sso_permission_sets_rbac: + - aws_sso_permission_set: PowerUserAccess + groups: + - idp:poweruser + - system:authenticated fargate_profiles: karpenter: kubernetes_namespace: karpenter @@ -122,12 +134,12 @@ components: ### Usage with Node Groups -The `eks/cluster` component also supports node groups! In order to add a set list of nodes to +The `eks/cluster` component also supports managed node groups. In order to add a set list of nodes to provision with the cluster, add values for `var.managed_node_groups_enabled` and `var.node_groups`. :::info -You can use manage node groups in conjunction with Karpenter node groups! Simply deploy node groups as demonstrated below, -and then deploy Karpenter with the necessary components. +You can use managed node groups in conjunction with Karpenter node groups, though in most cases, +Karpenter is all you need. ::: @@ -135,11 +147,10 @@ For example: ```yaml managed_node_groups_enabled: true - node_groups: # null means use default set in defaults.auto.tf.vars + node_groups: # for most attributes, setting null here means use setting from node_group_defaults main: - # values of `null` will be replaced with default values - # availability_zones = null will create 1 auto scaling group in - # each availability zone in region_availability_zones + # availability_zones = null will create one autoscaling group + # in every private subnet in the VPC availability_zones: null desired_group_size: 3 # number of instances to start with, must be >= number of AZs @@ -184,14 +195,13 @@ For example: | Name | Source | Version | |------|--------|---------| -| [delegated\_roles](#module\_delegated\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.5.0 | +| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.6.0 | | [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.1.0 | +| [iam\_arns](#module\_iam\_arns) | ../../account-map/modules/roles-to-principals | n/a | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [karpenter\_label](#module\_karpenter\_label) | cloudposse/label/null | 0.25.0 | | [region\_node\_group](#module\_region\_node\_group) | ./modules/node_group_by_region | n/a | -| [team\_roles](#module\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | @@ -208,6 +218,7 @@ For example: | [aws_iam_role_policy_attachment.ipv6_eks_cni_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ipv6_eks_cni_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_roles.sso_roles](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_roles) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | ## Inputs @@ -225,6 +236,9 @@ For example: | [availability\_zones](#input\_availability\_zones) | AWS Availability Zones in which to deploy multi-AZ resources.
If not provided, resources will be provisioned in every private subnet in the VPC. | `list(string)` | `[]` | no | | [aws\_auth\_yaml\_strip\_quotes](#input\_aws\_auth\_yaml\_strip\_quotes) | If true, remove double quotes from the generated aws-auth ConfigMap YAML to reduce spurious diffs in plans | `bool` | `true` | no | | [aws\_ssm\_agent\_enabled](#input\_aws\_ssm\_agent\_enabled) | Set true to attach the required IAM policy for AWS SSM agent to each EC2 instance's IAM Role | `bool` | `false` | no | +| [aws\_sso\_permission\_sets\_rbac](#input\_aws\_sso\_permission\_sets\_rbac) | (Not Recommended): AWS SSO (IAM Identity Center) permission sets in the EKS deployment account to add to `aws-auth` ConfigMap.
Unfortunately, `aws-auth` ConfigMap does not support SSO permission sets, so we map the generated
IAM Role ARN corresponding to the permission set at the time Terraform runs. This is subject to change
when any changes are made to the AWS SSO configuration, invalidating the mapping, and requiring a
`terraform apply` in this project to update the `aws-auth` ConfigMap and restore access. |
list(object({
aws_sso_permission_set = string
groups = list(string)
}))
| `[]` | no | +| [aws\_team\_roles\_rbac](#input\_aws\_team\_roles\_rbac) | List of `aws-team-roles` (in the target AWS account) to map to Kubernetes RBAC groups. |
list(object({
aws_team_role = string
groups = list(string)
}))
| `[]` | no | +| [aws\_teams\_rbac](#input\_aws\_teams\_rbac) | List of `aws-teams` to map to Kubernetes RBAC groups.
This gives teams direct access to Kubernetes without having to assume a team-role. |
list(object({
aws_team = string
groups = list(string)
}))
| `[]` | no | | [cluster\_encryption\_config\_enabled](#input\_cluster\_encryption\_config\_enabled) | Set to `true` to enable Cluster Encryption Configuration | `bool` | `true` | no | | [cluster\_encryption\_config\_kms\_key\_deletion\_window\_in\_days](#input\_cluster\_encryption\_config\_kms\_key\_deletion\_window\_in\_days) | Cluster Encryption Config KMS Key Resource argument - key deletion windows in days post destruction | `number` | `10` | no | | [cluster\_encryption\_config\_kms\_key\_enable\_key\_rotation](#input\_cluster\_encryption\_config\_kms\_key\_enable\_key\_rotation) | Cluster Encryption Config KMS Key Resource argument - enable kms key rotation | `bool` | `true` | no | @@ -238,7 +252,6 @@ For example: | [cluster\_private\_subnets\_only](#input\_cluster\_private\_subnets\_only) | Whether or not to enable private subnets or both public and private subnets | `bool` | `false` | no | | [color](#input\_color) | The cluster stage represented by a color; e.g. blue, green | `string` | `""` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [delegated\_iam\_roles](#input\_delegated\_iam\_roles) | Delegated IAM roles to add to `aws-auth` ConfigMap |
list(object({
role = string
groups = list(string)
}))
| `[]` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | @@ -248,9 +261,6 @@ For example: | [fargate\_profile\_iam\_role\_kubernetes\_namespace\_delimiter](#input\_fargate\_profile\_iam\_role\_kubernetes\_namespace\_delimiter) | Delimiter for the Kubernetes namespace in the IAM Role name for Fargate Profiles | `string` | `"-"` | no | | [fargate\_profile\_iam\_role\_permissions\_boundary](#input\_fargate\_profile\_iam\_role\_permissions\_boundary) | If provided, all Fargate Profiles IAM roles will be created with this permissions boundary attached | `string` | `null` | no | | [fargate\_profiles](#input\_fargate\_profiles) | Fargate Profiles config |
map(object({
kubernetes_namespace = string
kubernetes_labels = map(string)
}))
| `{}` | no | -| [iam\_primary\_roles\_stage\_name](#input\_iam\_primary\_roles\_stage\_name) | The name of the stage where the IAM primary roles are provisioned | `string` | `"identity"` | no | -| [iam\_primary\_roles\_tenant\_name](#input\_iam\_primary\_roles\_tenant\_name) | The name of the tenant where the IAM primary roles are provisioned | `string` | `null` | no | -| [iam\_roles\_environment\_name](#input\_iam\_roles\_environment\_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [karpenter\_iam\_role\_enabled](#input\_karpenter\_iam\_role\_enabled) | Flag to enable/disable creation of IAM role for EC2 Instance Profile that is attached to the nodes launched by Karpenter | `bool` | `false` | no | @@ -270,7 +280,6 @@ For example: | [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = string
ami_type = string
attributes = list(string)
availability_zones = list(string) # set to null to use var.availability_zones
cluster_autoscaler_enabled = bool
create_before_destroy = bool
desired_group_size = number
disk_encryption_enabled = bool
disk_size = number
instance_types = list(string)
kubernetes_labels = map(string)
kubernetes_taints = list(object({
key = string
value = string
effect = string
}))
kubernetes_version = string # set to null to use cluster_kubernetes_version
max_group_size = number
min_group_size = number
resources_to_tag = list(string)
tags = map(string)
})
|
{
"ami_release_version": null,
"ami_type": null,
"attributes": null,
"availability_zones": null,
"cluster_autoscaler_enabled": true,
"create_before_destroy": true,
"desired_group_size": 1,
"disk_encryption_enabled": true,
"disk_size": 20,
"instance_types": [
"t3.medium"
],
"kubernetes_labels": null,
"kubernetes_taints": null,
"kubernetes_version": null,
"max_group_size": 100,
"min_group_size": null,
"resources_to_tag": null,
"tags": null
}
| no | | [node\_groups](#input\_node\_groups) | List of objects defining a node group for the cluster |
map(object({
# EKS AMI version to use, e.g. "1.16.13-20200821" (no "v").
ami_release_version = string
# Type of Amazon Machine Image (AMI) associated with the EKS Node Group
ami_type = string
# Additional attributes (e.g. `1`) for the node group
attributes = list(string)
# will create 1 auto scaling group in each specified availability zone
availability_zones = list(string)
# Whether to enable Node Group to scale its AutoScaling Group
cluster_autoscaler_enabled = bool
# True to create new node_groups before deleting old ones, avoiding a temporary outage
create_before_destroy = bool
# Desired number of worker nodes when initially provisioned
desired_group_size = number
# Enable disk encryption for the created launch template (if we aren't provided with an existing launch template)
disk_encryption_enabled = bool
# Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided.
disk_size = number
# Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided.
instance_types = list(string)
# Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed
kubernetes_labels = map(string)
# List of objects describing Kubernetes taints.
kubernetes_taints = list(object({
key = string
value = string
effect = string
}))
# Desired Kubernetes master version. If you do not specify a value, the latest available version is used
kubernetes_version = string
# The maximum size of the AutoScaling Group
max_group_size = number
# The minimum size of the AutoScaling Group
min_group_size = number
# List of auto-launched resource types to tag
resources_to_tag = list(string)
tags = map(string)
}))
| `{}` | no | | [oidc\_provider\_enabled](#input\_oidc\_provider\_enabled) | Create an IAM OIDC identity provider for the cluster, then you can create IAM roles to associate with a service account in the cluster, instead of using kiam or kube2iam. For more information, see https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html | `bool` | n/a | yes | -| [primary\_iam\_roles](#input\_primary\_iam\_roles) | Primary IAM roles to add to `aws-auth` ConfigMap |
list(object({
role = string
groups = list(string)
}))
| `[]` | no | | [public\_access\_cidrs](#input\_public\_access\_cidrs) | Indicates which CIDR blocks can access the Amazon EKS public API server endpoint when enabled. EKS defaults this to a list with 0.0.0.0/0. | `list(string)` |
[
"0.0.0.0/0"
]
| no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | diff --git a/modules/eks/cluster/aws_sso.tf b/modules/eks/cluster/aws_sso.tf new file mode 100644 index 000000000..992b755a0 --- /dev/null +++ b/modules/eks/cluster/aws_sso.tf @@ -0,0 +1,30 @@ +# This is split off into a separate file in the hopes we can drop it altogether in the future, +# or else move it into `roles-to-principals`. + + +locals { + + # EKS does not accept the actual role ARN of the permission set, + # but instead requires the ARN of the role with the path prefix removed. + # Unfortunately, the path prefix is not always the same. + # Usually it is only "/aws-reserved/sso.amazonaws.com/" + # but sometimes it includes a region, like "/aws-reserved/sso.amazonaws.com/ap-southeast-1/" + # Adapted from https://registry.terraform.io/providers/hashicorp/aws/3.75.1/docs/data-sources/iam_roles#role-arns-with-paths-removed + aws_sso_permission_set_to_eks_role_arn_map = { for k, v in data.aws_iam_roles.sso_roles : k => [ + for parts in [split("/", one(v.arns[*]))] : + format("%s/%s", parts[0], element(parts, length(parts) - 1)) + ][0] } + + aws_sso_iam_roles_auth = [for role in var.aws_sso_permission_sets_rbac : { + rolearn = local.aws_sso_permission_set_to_eks_role_arn_map[role.aws_sso_permission_set] + username = format("%s-%s", local.this_account_name, role.aws_sso_permission_set) + groups = role.groups + }] + +} + +data "aws_iam_roles" "sso_roles" { + for_each = toset(var.aws_sso_permission_sets_rbac[*].aws_sso_permission_set) + name_regex = format("AWSReservedSSO_%s_.*", each.value) + path_prefix = "/aws-reserved/sso.amazonaws.com/" +} diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index c92eeab1e..4db5873b7 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -1,26 +1,32 @@ locals { - enabled = module.this.enabled - primary_role_map = module.team_roles.outputs.team_name_role_arn_map - delegated_role_map = module.delegated_roles.outputs.role_name_role_arn_map - eks_outputs = module.eks.outputs - vpc_outputs = module.vpc.outputs + enabled = module.this.enabled + eks_outputs = module.eks.outputs + vpc_outputs = module.vpc.outputs attributes = flatten(concat(module.this.attributes, [var.color])) public_subnet_ids = local.vpc_outputs.public_subnet_ids private_subnet_ids = local.vpc_outputs.private_subnet_ids vpc_id = local.vpc_outputs.vpc_id - iam_primary_roles_tenant_name = coalesce(var.iam_primary_roles_tenant_name, module.this.tenant) + this_account_name = module.iam_roles.current_account_account_name + identity_account_name = module.iam_roles.identity_account_account_name - primary_iam_roles = [for role in var.primary_iam_roles : { - rolearn = local.primary_role_map[role.role] - username = module.this.context.tenant != null ? format("%s-identity-%s", local.iam_primary_roles_tenant_name, role.role) : format("identity-%s", role.role) + role_map = merge({ + (local.identity_account_name) = var.aws_teams_rbac[*].aws_team + }, { + (local.this_account_name) = var.aws_team_roles_rbac[*].aws_team_role + root = ["*"] + }) + + aws_teams_auth = [for role in var.aws_teams_rbac : { + rolearn = module.iam_arns.principals_map[local.identity_account_name][role.aws_team] + username = format("%s-%s", local.identity_account_name, role.aws_team) groups = role.groups }] - delegated_iam_roles = [for role in var.delegated_iam_roles : { - rolearn = local.delegated_role_map[role.role] - username = module.this.context.tenant != null ? format("%s-%s-%s", module.this.tenant, module.this.stage, role.role) : format("%s-%s", module.this.stage, role.role) + aws_team_roles_auth = [for role in var.aws_team_roles_rbac : { + rolearn = module.iam_arns.principals_map[local.this_account_name][role.aws_team_role] + username = format("%s-%s", local.this_account_name, role.aws_team_role) groups = role.groups }] @@ -43,8 +49,9 @@ locals { ] map_additional_iam_roles = concat( - local.primary_iam_roles, - local.delegated_iam_roles, + local.aws_teams_auth, + local.aws_team_roles_auth, + local.aws_sso_iam_roles_auth, var.map_additional_iam_roles, local.map_fargate_profile_roles, ) @@ -74,7 +81,7 @@ locals { module "eks_cluster" { source = "cloudposse/eks-cluster/aws" - version = "2.5.0" + version = "2.6.0" region = var.region attributes = local.attributes @@ -153,4 +160,3 @@ module "eks_cluster" { context = module.this.context } - diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf index 33afd7f60..ad18d7487 100644 --- a/modules/eks/cluster/remote-state.tf +++ b/modules/eks/cluster/remote-state.tf @@ -2,6 +2,14 @@ locals { accounts_with_vpc = { for i, account in var.allow_ingress_from_vpc_accounts : try(account.tenant, module.this.tenant) != null ? format("%s-%s", account.tenant, account.stage) : account.stage => account } } +module "iam_arns" { + source = "../../account-map/modules/roles-to-principals" + + role_map = local.role_map + + context = module.this.context +} + module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" @@ -25,29 +33,6 @@ module "vpc_ingress" { context = module.this.context } -module "team_roles" { - source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" - - component = "aws-teams" - - tenant = local.iam_primary_roles_tenant_name - environment = var.iam_roles_environment_name - stage = var.iam_primary_roles_stage_name - - context = module.this.context -} - -module "delegated_roles" { - source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" - - component = "aws-team-roles" - - environment = var.iam_roles_environment_name - - context = module.this.context -} # Yes, this is self-referential. # It obtains the previous state of the cluster so that we can add diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index c129f2bac..23a3d55f9 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -87,23 +87,43 @@ variable "map_additional_worker_roles" { default = [] } -variable "primary_iam_roles" { - description = "Primary IAM roles to add to `aws-auth` ConfigMap" +variable "aws_teams_rbac" { + description = <<-EOT + List of `aws-teams` to map to Kubernetes RBAC groups. + This gives teams direct access to Kubernetes without having to assume a team-role. + EOT type = list(object({ - role = string - groups = list(string) + aws_team = string + groups = list(string) })) default = [] } -variable "delegated_iam_roles" { - description = "Delegated IAM roles to add to `aws-auth` ConfigMap" +variable "aws_team_roles_rbac" { + description = "List of `aws-team-roles` (in the target AWS account) to map to Kubernetes RBAC groups." + + type = list(object({ + aws_team_role = string + groups = list(string) + })) + + default = [] +} + +variable "aws_sso_permission_sets_rbac" { + description = <<-EOT + (Not Recommended): AWS SSO (IAM Identity Center) permission sets in the EKS deployment account to add to `aws-auth` ConfigMap. + Unfortunately, `aws-auth` ConfigMap does not support SSO permission sets, so we map the generated + IAM Role ARN corresponding to the permission set at the time Terraform runs. This is subject to change + when any changes are made to the AWS SSO configuration, invalidating the mapping, and requiring a + `terraform apply` in this project to update the `aws-auth` ConfigMap and restore access. + EOT type = list(object({ - role = string - groups = list(string) + aws_sso_permission_set = string + groups = list(string) })) default = [] @@ -250,24 +270,6 @@ variable "node_group_defaults" { } } -variable "iam_roles_environment_name" { - type = string - description = "The name of the environment where the IAM roles are provisioned" - default = "gbl" -} - -variable "iam_primary_roles_stage_name" { - type = string - description = "The name of the stage where the IAM primary roles are provisioned" - default = "identity" -} - -variable "iam_primary_roles_tenant_name" { - type = string - description = "The name of the tenant where the IAM primary roles are provisioned" - default = null -} - variable "cluster_encryption_config_enabled" { type = bool default = true From 3c73a286b2f679742abde4176383f28c8d369065 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Thu, 20 Apr 2023 16:22:47 -0400 Subject: [PATCH 094/501] fix:aws-team-roles have stray locals (#642) Co-authored-by: Dan Miller --- modules/aws-team-roles/policy-eks-viewer.tf | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/aws-team-roles/policy-eks-viewer.tf b/modules/aws-team-roles/policy-eks-viewer.tf index 2a758a72d..9bbeceb80 100644 --- a/modules/aws-team-roles/policy-eks-viewer.tf +++ b/modules/aws-team-roles/policy-eks-viewer.tf @@ -1,7 +1,5 @@ locals { eks_viewer_enabled = contains(local.configured_policies, "eks_viewer") - account_name = lookup(module.this.descriptors, "account_name", module.this.stage) - account_number = module.account_map.outputs.full_account_map[local.account_name] } data "aws_iam_policy_document" "eks_view_access" { From 48447b8dbd28399c325a3d4340e83245ceaf376e Mon Sep 17 00:00:00 2001 From: Kevin Mahoney Date: Thu, 20 Apr 2023 16:25:58 -0400 Subject: [PATCH 095/501] ecs-service: fix lint issues (#636) Co-authored-by: cloudpossebot Co-authored-by: Dan Miller --- modules/ecs-service/README.md | 7 ++++--- modules/ecs-service/main.tf | 4 ++-- modules/ecs-service/remote-state.tf | 1 + modules/ecs-service/variables.tf | 10 +++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 6e069dd88..f8c393afc 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -268,9 +268,9 @@ components: | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [retention\_period](#input\_retention\_period) | Length of time data records are accessible after they are added to the stream | `string` | `"48"` | no | -| [shard\_count](#input\_shard\_count) | Number of shards that the stream will use | `string` | `"1"` | no | -| [shard\_level\_metrics](#input\_shard\_level\_metrics) | List of shard-level CloudWatch metrics which can be enabled for the stream | `list` |
[
"IncomingBytes",
"IncomingRecords",
"IteratorAgeMilliseconds",
"OutgoingBytes",
"OutgoingRecords",
"ReadProvisionedThroughputExceeded",
"WriteProvisionedThroughputExceeded"
]
| no | +| [retention\_period](#input\_retention\_period) | Length of time data records are accessible after they are added to the stream | `number` | `48` | no | +| [shard\_count](#input\_shard\_count) | Number of shards that the stream will use | `number` | `1` | no | +| [shard\_level\_metrics](#input\_shard\_level\_metrics) | List of shard-level CloudWatch metrics which can be enabled for the stream | `list(string)` |
[
"IncomingBytes",
"IncomingRecords",
"IteratorAgeMilliseconds",
"OutgoingBytes",
"OutgoingRecords",
"ReadProvisionedThroughputExceeded",
"WriteProvisionedThroughputExceeded"
]
| no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [stickiness\_cookie\_duration](#input\_stickiness\_cookie\_duration) | The time period, in seconds, during which requests from a client should be routed to the same target. After this time period expires, the load balancer-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds). The default value is 1 day (86400 seconds) | `number` | `86400` | no | | [stickiness\_enabled](#input\_stickiness\_enabled) | Boolean to enable / disable `stickiness`. Default is `true` | `bool` | `true` | no | @@ -306,6 +306,7 @@ components: ## References + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecs-service) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 2367dd8f4..d5cb5c11d 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -168,7 +168,7 @@ module "container_definition" { } locals { - awslogs_group = var.datadog_log_method_is_firelens ? "" : join("", module.logs.*.log_group_name) + awslogs_group = var.datadog_log_method_is_firelens ? "" : join("", module.logs[*].log_group_name) } module "ecs_alb_service_task" { @@ -315,7 +315,7 @@ data "aws_iam_policy_document" "this" { resource "aws_iam_policy" "default" { count = local.enabled && var.iam_policy_enabled ? 1 : 0 - policy = join("", data.aws_iam_policy_document.this.*.json) + policy = join("", data.aws_iam_policy_document.this[*].json) tags_all = module.this.tags } diff --git a/modules/ecs-service/remote-state.tf b/modules/ecs-service/remote-state.tf index 56a58b272..47f69a80f 100644 --- a/modules/ecs-service/remote-state.tf +++ b/modules/ecs-service/remote-state.tf @@ -72,6 +72,7 @@ module "ecs_cluster" { } # This is purely a check to ensure this zone exists +# tflint-ignore: terraform_unused_declarations data "aws_route53_zone" "selected" { count = local.enabled ? 1 : 0 diff --git a/modules/ecs-service/variables.tf b/modules/ecs-service/variables.tf index 0c204305c..874016bd8 100644 --- a/modules/ecs-service/variables.tf +++ b/modules/ecs-service/variables.tf @@ -86,17 +86,19 @@ variable "kinesis_enabled" { variable "shard_count" { description = "Number of shards that the stream will use" - default = "1" + type = number + default = 1 } variable "retention_period" { description = "Length of time data records are accessible after they are added to the stream" - default = "48" + type = number + default = 48 } variable "shard_level_metrics" { description = "List of shard-level CloudWatch metrics which can be enabled for the stream" - + type = list(string) default = [ "IncomingBytes", "IncomingRecords", @@ -110,6 +112,7 @@ variable "shard_level_metrics" { variable "kms_key_alias" { description = "ID of KMS key" + type = string default = "default" } @@ -133,6 +136,7 @@ variable "use_rds_client_sg" { variable "chamber_service" { default = "ecs-service" + type = string description = "SSM parameter service name for use with chamber. This is used in chamber_format where /$chamber_service/$name/$container_name/$parameter would be the default." } From e935e68f6258ac9f043c0e72c4a3ed5437234cb7 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 20 Apr 2023 14:12:06 -0700 Subject: [PATCH 096/501] Fix `s3-bucket` `var.bucket_name` (#637) --- modules/s3-bucket/README.md | 2 +- modules/s3-bucket/variables.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index 1eacd9043..1bd37e0b9 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -124,7 +124,7 @@ components: | [block\_public\_acls](#input\_block\_public\_acls) | Set to `false` to disable the blocking of new public access lists on the bucket | `bool` | `true` | no | | [block\_public\_policy](#input\_block\_public\_policy) | Set to `false` to disable the blocking of new public policies on the bucket | `bool` | `true` | no | | [bucket\_key\_enabled](#input\_bucket\_key\_enabled) | Set this to true to use Amazon S3 Bucket Keys for SSE-KMS, which reduce the cost of AWS KMS requests.
For more information, see: https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-key.html | `bool` | `false` | no | -| [bucket\_name](#input\_bucket\_name) | Bucket name. If provided, the bucket will be created with this name instead of generating the name from the context | `string` | `null` | no | +| [bucket\_name](#input\_bucket\_name) | Bucket name. If provided, the bucket will be created with this name instead of generating the name from the context | `string` | `""` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [cors\_configuration](#input\_cors\_configuration) | Specifies the allowed headers, methods, origins and exposed headers when using CORS on this bucket |
list(object({
allowed_headers = list(string)
allowed_methods = list(string)
allowed_origins = list(string)
expose_headers = list(string)
max_age_seconds = number
}))
| `null` | no | | [custom\_policy\_account\_names](#input\_custom\_policy\_account\_names) | List of accounts names to assign as principals for the s3 bucket custom policy | `list(string)` | `[]` | no | diff --git a/modules/s3-bucket/variables.tf b/modules/s3-bucket/variables.tf index d1fee56c8..9f51d70e9 100644 --- a/modules/s3-bucket/variables.tf +++ b/modules/s3-bucket/variables.tf @@ -296,7 +296,7 @@ variable "s3_replication_source_roles" { variable "bucket_name" { type = string - default = null + default = "" description = "Bucket name. If provided, the bucket will be created with this name instead of generating the name from the context" } From 8e4791dd97d4ae230d39639d0b9ecd68b703f00e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 20 Apr 2023 15:00:03 -0700 Subject: [PATCH 097/501] Format Identity Team Access Permission Set Name (#646) Co-authored-by: Nuru Co-authored-by: Andriy Knysh --- modules/aws-sso/README.md | 4 ++-- modules/aws-sso/policy-Identity-role-TeamAccess.tf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index cc20caff7..8cefb16e9 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -44,10 +44,10 @@ The `account_assignments` setting configures access to permission sets for users - The permission sets are defined (by convention) in files names `policy-.tf` in the `aws-sso` component. The definition includes the name of the permission set. See `components/terraform/aws-sso/policy-AdminstratorAccess.tf` for an example. #### `identity_roles_accessible` -The `identity_roles_accessible` element provides a list of role names corresponding to roles created in the `iam-primary-roles` component. For each names role, a corresponding permission set will be created which allows the user to assume that role. The permission set name is generated in Terraform from the role name using this statement: +The `identity_roles_accessible` element provides a list of role names corresponding to roles created in the `iam-primary-roles` component. For each named role, a corresponding permission set will be created which allows the user to assume that role. The permission set name is generated in Terraform from the role name using this statement: ``` -format("Identity%sTeamAccess", title(role)) +format("Identity%sTeamAccess", replace(title(role), "-", "")) ``` #### Example diff --git a/modules/aws-sso/policy-Identity-role-TeamAccess.tf b/modules/aws-sso/policy-Identity-role-TeamAccess.tf index 6028088bf..c1df61856 100644 --- a/modules/aws-sso/policy-Identity-role-TeamAccess.tf +++ b/modules/aws-sso/policy-Identity-role-TeamAccess.tf @@ -54,8 +54,8 @@ data "aws_iam_policy_document" "assume_aws_team" { locals { identity_access_permission_sets = [for role in var.aws_teams_accessible : { - name = format("Identity%sTeamAccess", title(role)), - description = format("Allow user to assume the %s Team role in the Identity account, which allows access to other accounts", title(role)) + name = format("Identity%sTeamAccess", replace(title(role), "-", "")), + description = format("Allow user to assume the %s Team role in the Identity account, which allows access to other accounts", replace(title(role), "-", "")) relay_state = "", session_duration = "", tags = {}, From c1a062261d1b0025c7d052273a153610ea128fef Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 21 Apr 2023 10:19:46 -0700 Subject: [PATCH 098/501] Athena CloudTrail Queries (#638) Co-authored-by: Andriy Knysh --- modules/athena/README.md | 72 ++++++++++++++- modules/athena/cloudtrail.tf | 111 +++++++++++++++++++++++ modules/athena/default.auto.tfvars | 2 - modules/athena/main.tf | 3 +- modules/athena/remote-state.tf | 22 +++++ modules/athena/variables.tf | 13 +++ modules/cloudtrail/README.md | 3 + modules/cloudtrail/cloudtrail-kms-key.tf | 25 +++++ modules/cloudtrail/remote-state.tf | 2 +- modules/cloudtrail/variables.tf | 18 ++++ 10 files changed, 262 insertions(+), 9 deletions(-) create mode 100644 modules/athena/cloudtrail.tf delete mode 100644 modules/athena/default.auto.tfvars create mode 100644 modules/athena/remote-state.tf diff --git a/modules/athena/README.md b/modules/athena/README.md index be10de410..092c54949 100644 --- a/modules/athena/README.md +++ b/modules/athena/README.md @@ -43,7 +43,7 @@ import: components: terraform: - athena-example: + athena/example: metadata: component: athena inherits: @@ -57,6 +57,60 @@ components: - example_db_2 ``` +### CloudTrail Integration + +Using Athena with CloudTrail logs is a powerful way to enhance your analysis of AWS service activity. This component supports creating +a CloudTrail table for each account and setting up queries to read CloudTrail logs from a centralized location. + +To set up the CloudTrail Integration, first create the `create` and `alter` queries in Athena with this component. When `var.cloudtrail_database` +is defined, this component will create these queries. + +```yaml +import: +- catalog/athena/defaults + +components: + terraform: + athena/audit: + metadata: + component: athena + inherits: + - athena/defaults + vars: + enabled: true + name: athena-audit + workgroup_description: "Athena Workgroup for Auditing" + cloudtrail_database : audit + databases: + audit: + comment: "Auditor database for Athena" + properties: {} + named_queries: + platform_dev: + database: audit + description: "example query against CloudTrail logs" + query: | + SELECT + useridentity.arn, + eventname, + sourceipaddress, + eventtime + FROM %s.platform_dev_cloudtrail_logs + LIMIT 100; + +``` + +Once those are created, run the `create` and then the `alter` queries in the AWS Console to create and then fill the tables in Athena. + +:::info + +Athena runs queries with the permissions of the user executing the query. In order to be able to query CloudTrail logs, +the `audit` account must have access to the KMS key used to encrypt CloudTrails logs. Set `var.audit_access_enabled` to `true` in the `cloudtrail` +component + +::: + + ## Requirements @@ -67,19 +121,26 @@ components: ## Providers -No providers. +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [athena](#module\_athena) | cloudposse/athena/aws | 0.1.0 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [athena](#module\_athena) | cloudposse/athena/aws | 0.1.1 | +| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_athena_named_query.cloudtrail_query_alter_tables](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/athena_named_query) | resource | +| [aws_athena_named_query.cloudtrail_query_create_tables](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/athena_named_query) | resource | ## Inputs @@ -91,6 +152,8 @@ No resources. | [athena\_s3\_bucket\_id](#input\_athena\_s3\_bucket\_id) | Use an existing S3 bucket for Athena query results if `create_s3_bucket` is `false`. | `string` | `null` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [bytes\_scanned\_cutoff\_per\_query](#input\_bytes\_scanned\_cutoff\_per\_query) | Integer for the upper data usage limit (cutoff) for the amount of bytes a single query in a workgroup is allowed to scan. Must be at least 10485760. | `number` | `null` | no | +| [cloudtrail\_bucket\_component\_name](#input\_cloudtrail\_bucket\_component\_name) | The name of the CloudTrail bucket component | `string` | `"cloudtrail-bucket"` | no | +| [cloudtrail\_database](#input\_cloudtrail\_database) | The name of the Athena Database to use for CloudTrail logs. If set, an Athena table will be created for the CloudTrail trail. | `string` | `""` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_kms\_key](#input\_create\_kms\_key) | Enable the creation of a KMS key used by Athena workgroup. | `bool` | `true` | no | | [create\_s3\_bucket](#input\_create\_s3\_bucket) | Enable the creation of an S3 bucket to use for Athena query results | `bool` | `true` | no | @@ -137,5 +200,6 @@ No resources. ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/athena) - Cloud Posse's upstream component +* [Querying AWS CloudTrail logs with AWS Athena](https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html) [](https://cpco.io/component) diff --git a/modules/athena/cloudtrail.tf b/modules/athena/cloudtrail.tf new file mode 100644 index 000000000..fc15b111a --- /dev/null +++ b/modules/athena/cloudtrail.tf @@ -0,0 +1,111 @@ + +# This file creates a table for Athena to query centralized Cloudtrail logs in S3. +# https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html#create-cloudtrail-table-ct + +locals { + cloudtrail_enabled = module.this.enabled && length(var.cloudtrail_database) > 0 + cloudtrail_table_name = "%s_cloudtrail_logs" + + # s3://cloudtrail_bucket_name/AWSLogs/organization_id/Account_ID/CloudTrail/ + organization_id = module.account_map.outputs.org.id + cloudtrail_s3_bucket_id = module.cloudtrail_bucket[0].outputs.cloudtrail_bucket_id + cloudtrail_s3_location = "s3://${local.cloudtrail_s3_bucket_id}/AWSLogs/${local.organization_id}/%s/CloudTrail/" + + cloudtrail_query_create_table = <, + sessionissuer:STRUCT< + type:STRING, + principalId:STRING, + arn:STRING, + accountId:STRING, + userName:STRING>, + ec2RoleDelivery:string, + webIdFederationData:map + > +>, +eventtime STRING, +eventsource STRING, +eventname STRING, +awsregion STRING, +sourceipaddress STRING, +useragent STRING, +errorcode STRING, +errormessage STRING, +requestparameters STRING, +responseelements STRING, +additionaleventdata STRING, +requestid STRING, +eventid STRING, +resources ARRAY>, +eventtype STRING, +apiversion STRING, +readonly STRING, +recipientaccountid STRING, +serviceeventdetails STRING, +sharedeventid STRING, +vpcendpointid STRING, +tlsDetails struct< + tlsVersion:string, + cipherSuite:string, + clientProvidedHostHeader:string> +) +PARTITIONED BY (account string, region string, year string, month string, day string) +ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe' +STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat' +OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' +LOCATION '${local.cloudtrail_s3_location}' +EOT + + + account_name = lookup(module.this.descriptors, "account_name", module.this.stage) + account_id = module.account_map.outputs.full_account_map[local.account_name] + timestamp = timestamp() + current_year = formatdate("YYYY", local.timestamp) + current_month = formatdate("MM", local.timestamp) + current_day = formatdate("DD", local.timestamp) + + cloudtrail_query_alter_table = < [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [audit\_access\_enabled](#input\_audit\_access\_enabled) | If `true`, allows the Audit account access to read Cloudtrail logs directly from S3. This is a requirement for running Athena queries in the Audit account. | `bool` | `false` | no | +| [audit\_account\_name](#input\_audit\_account\_name) | The key used in Account Map to find the Audit account | `string` | `"core-audit"` | no | +| [cloudtrail\_bucket\_component\_name](#input\_cloudtrail\_bucket\_component\_name) | The name of the CloudTrail bucket component | `string` | `"cloudtrail-bucket"` | no | | [cloudtrail\_bucket\_environment\_name](#input\_cloudtrail\_bucket\_environment\_name) | The name of the environment where the CloudTrail bucket is provisioned | `string` | n/a | yes | | [cloudtrail\_bucket\_stage\_name](#input\_cloudtrail\_bucket\_stage\_name) | The stage name where the CloudTrail bucket is provisioned | `string` | n/a | yes | | [cloudtrail\_cloudwatch\_logs\_role\_max\_session\_duration](#input\_cloudtrail\_cloudwatch\_logs\_role\_max\_session\_duration) | The maximum session duration (in seconds) for the CloudTrail CloudWatch Logs role. Can have a value from 1 hour to 12 hours | `number` | `43200` | no | diff --git a/modules/cloudtrail/cloudtrail-kms-key.tf b/modules/cloudtrail/cloudtrail-kms-key.tf index bebc60e60..ce62d1fec 100644 --- a/modules/cloudtrail/cloudtrail-kms-key.tf +++ b/modules/cloudtrail/cloudtrail-kms-key.tf @@ -1,3 +1,8 @@ +locals { + audit_access_enabled = module.this.enabled && var.audit_access_enabled + audit_account_id = module.account_map.outputs.full_account_map[var.audit_account_name] +} + module "kms_key_cloudtrail" { source = "cloudposse/kms-key/aws" version = "0.12.1" @@ -72,4 +77,24 @@ data "aws_iam_policy_document" "kms_key_cloudtrail" { ] } } + + dynamic "statement" { + for_each = local.audit_access_enabled ? [1] : [] + content { + sid = "Allow Audit to decrypt with the KMS key" + effect = "Allow" + actions = [ + "kms:Decrypt*", + ] + resources = [ + "*" + ] + principals { + type = "AWS" + identifiers = [ + format("arn:${join("", data.aws_partition.current[*].partition)}:iam::%s:root", local.audit_account_id) + ] + } + } + } } diff --git a/modules/cloudtrail/remote-state.tf b/modules/cloudtrail/remote-state.tf index ba51272a3..584b0a1ee 100644 --- a/modules/cloudtrail/remote-state.tf +++ b/modules/cloudtrail/remote-state.tf @@ -2,7 +2,7 @@ module "cloudtrail_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" - component = "cloudtrail-bucket" + component = var.cloudtrail_bucket_component_name environment = var.cloudtrail_bucket_environment_name stage = var.cloudtrail_bucket_stage_name diff --git a/modules/cloudtrail/variables.tf b/modules/cloudtrail/variables.tf index b718d2cb4..2ffc3216b 100644 --- a/modules/cloudtrail/variables.tf +++ b/modules/cloudtrail/variables.tf @@ -39,6 +39,12 @@ variable "cloudtrail_cloudwatch_logs_role_max_session_duration" { description = "The maximum session duration (in seconds) for the CloudTrail CloudWatch Logs role. Can have a value from 1 hour to 12 hours" } +variable "cloudtrail_bucket_component_name" { + type = string + description = "The name of the CloudTrail bucket component" + default = "cloudtrail-bucket" +} + variable "cloudtrail_bucket_environment_name" { type = string description = "The name of the environment where the CloudTrail bucket is provisioned" @@ -59,3 +65,15 @@ variable "is_organization_trail" { for an organization in AWS Organizations. EOT } + +variable "audit_access_enabled" { + type = bool + default = false + description = "If `true`, allows the Audit account access to read Cloudtrail logs directly from S3. This is a requirement for running Athena queries in the Audit account." +} + +variable "audit_account_name" { + type = string + default = "core-audit" + description = "The key used in Account Map to find the Audit account" +} From 228e55edbf9d8f431dfcd9d6933332bf6126214f Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 24 Apr 2023 12:37:10 -0700 Subject: [PATCH 099/501] [aws-config] Update usage info, add "help" and "teams" commands (#647) --- rootfs/usr/local/bin/aws-config | 50 +++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/rootfs/usr/local/bin/aws-config b/rootfs/usr/local/bin/aws-config index 453d07755..115034a4c 100755 --- a/rootfs/usr/local/bin/aws-config +++ b/rootfs/usr/local/bin/aws-config @@ -1,15 +1,25 @@ #!/bin/bash -## WORK IN PROGRESS +## Production ready, but still being developed and subject to frequent breaking changes. -## Usage: -## aws-config saml > rootfs/etc/aws-config/aws-config-saml +functions+=(help) +function help() { + fns=($(printf '%s\n' "${functions[@]}" | sort | uniq)) + # usage=${fns//$'\n'/ | } + printf "Usage: %s \n Where is one of:\n\n" "$(basename $0)" + printf ' %s\n' "${fns[@]}" + echo + + cat <<'EOF' + +## Examples: +## aws-config teams > rootfs/etc/aws-config/aws-config-teams ## Generates full `aws` CLI configuration for use in Geodesic -## based on SAML authentication and SAML roles. +## to access aws-teams and aws-team-roles. ## -## aws-config switch-roles > rootfs/etc/aws-config/aws-switch-roles -## aws-config switch-roles billing > rootfs/etc/aws-config/aws-switch-roles-billing -## aws-config switch-roles billing_admin > rootfs/etc/aws-config/aws-switch-roles-billing_admin +## aws-config switch-roles > rootfs/etc/aws-config/aws-extend-switch-roles +## aws-config switch-roles billing > rootfs/etc/aws-config/aws-extend-switch-roles-billing +## aws-config switch-roles billing_admin > rootfs/etc/aws-config/aws-extend-switch-roles-billing_admin ## Generates configuration for AWS Extend Switch Roles browser plugin ## https://github.com/tilfinltd/aws-extend-switch-roles ## @@ -17,6 +27,22 @@ ## Generates `aws` CLI/SDK configuration for Spacelift workers to use ## +EOF + +} + +# main needs to be defined before sourcing other files + +function main() { + if printf '%s\0' "${functions[@]}" | grep -Fxqz -- "$1"; then + "$@" + else + help + exit 99 + fi +} + + ## TODO: maybe pull the source files from S3 rather than file system account_sources=("$ATMOS_BASE_PATH/"components/terraform/account-map/account-info/*sh) iam_sources=("$ATMOS_BASE_PATH/"components/terraform/aws-team-roles/iam-role-info/*sh) @@ -90,6 +116,15 @@ function saml() { _saml "$@" } +# Generate AWS config file for assuming `aws-teams` and `aws-team-roles` roles. +# Will generate a profile for every role in every account, unless a role is specified, +# in which case it will only generate a profile for that role in every account. +# Usage: teams [] +functions+=(teams) +function teams() { + saml "$@" +} + functions+=(switch-roles) function switch-roles() { local region="${AWS_REGION:-${AWS_DEFAULT_REGION}}" @@ -167,7 +202,6 @@ case $1 in ;; esac -# Use main() from account-info script args=("$@") for namespace in "${target_namespace[@]}"; do source "$ATMOS_BASE_PATH/components/terraform/account-map/account-info/$namespace"*.sh From c5f8a0e05869fee2de2558cce3c58b5190142285 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 24 Apr 2023 16:20:54 -0700 Subject: [PATCH 100/501] GitHub OIDC FAQ (#648) --- modules/github-oidc-provider/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index 734fbbcfe..523f8e007 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -33,6 +33,25 @@ the provider's thumbprint may change, at which point you can use [scripts/get_github_oidc_thumbprint.sh](./scripts/get_github_oidc_thumbprint.sh) to get the new thumbprint and add it to the list in `var.thumbprint_list`. +## FAQ + +### I cannot assume the role from GitHub Actions after deploying + +The following error is very common if the GitHub workflow is missing proper permission. + +```bash +Error: User: arn:aws:sts::***:assumed-role/acme-core-use1-auto-actions-runner@actions-runner-system/token-file-web-identity is not authorized to perform: sts:TagSession on resource: arn:aws:iam::999999999999:role/acme-plat-use1-dev-gha +``` + +In order to use a web identity, GitHub Action pipelines must have the following permission. +See [GitHub Action documentation for more](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-permissions-settings). + +```yaml +permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout +``` + ## Requirements From dfe53fe067a77bd2b673b4a17f48debab4371a82 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 25 Apr 2023 07:28:53 -0700 Subject: [PATCH 101/501] Upstream: `eks/ebs-controller` (#640) Co-authored-by: Andriy Knysh --- modules/eks/ebs-controller/README.md | 126 +++++++++ modules/eks/ebs-controller/context.tf | 279 ++++++++++++++++++++ modules/eks/ebs-controller/main.tf | 56 ++++ modules/eks/ebs-controller/outputs.tf | 24 ++ modules/eks/ebs-controller/provider-helm.tf | 158 +++++++++++ modules/eks/ebs-controller/providers.tf | 40 +++ modules/eks/ebs-controller/remote-state.tf | 8 + modules/eks/ebs-controller/variables.tf | 22 ++ modules/eks/ebs-controller/versions.tf | 18 ++ 9 files changed, 731 insertions(+) create mode 100644 modules/eks/ebs-controller/README.md create mode 100644 modules/eks/ebs-controller/context.tf create mode 100644 modules/eks/ebs-controller/main.tf create mode 100644 modules/eks/ebs-controller/outputs.tf create mode 100644 modules/eks/ebs-controller/provider-helm.tf create mode 100644 modules/eks/ebs-controller/providers.tf create mode 100644 modules/eks/ebs-controller/remote-state.tf create mode 100644 modules/eks/ebs-controller/variables.tf create mode 100644 modules/eks/ebs-controller/versions.tf diff --git a/modules/eks/ebs-controller/README.md b/modules/eks/ebs-controller/README.md new file mode 100644 index 000000000..812b5ddac --- /dev/null +++ b/modules/eks/ebs-controller/README.md @@ -0,0 +1,126 @@ +# Component: `ebs-controller` + +This component creates a Helm release for `ebs-controller` on a Kubernetes cluster. + +## Usage + +**Stack Level**: Regional + +Once the catalog file is created, the file can be imported as follows. + +```yaml +import: + - catalog/eks/ebs-controller + ... +``` + +The default catalog values + +```yaml +components: + terraform: + eks/ebs-controller: + vars: + enabled: true + + # You can use `chart_values` to set any other chart options. Treat `chart_values` as the root of the doc. + # + # # For example + # --- + # chart_values: + # enableShield: false + chart_values: {} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [ebs\_csi\_driver\_controller](#module\_ebs\_csi\_driver\_controller) | DrFaust92/ebs-csi-driver/kubernetes | 3.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_annotations.default_storage_class](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/annotations) | resource | +| [kubernetes_storage_class.gp3_enc](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/storage_class) | resource | +| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [ebs\_csi\_controller\_image](#input\_ebs\_csi\_controller\_image) | The image to use for the EBS CSI controller | `string` | `"k8s.gcr.io/provider-aws/aws-ebs-csi-driver"` | no | +| [ebs\_csi\_driver\_version](#input\_ebs\_csi\_driver\_version) | The version of the EBS CSI driver | `string` | `"v1.6.2"` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [ebs\_csi\_driver\_controller\_role\_arn](#output\_ebs\_csi\_driver\_controller\_role\_arn) | The Name of the EBS CSI driver controller IAM role ARN | +| [ebs\_csi\_driver\_controller\_role\_name](#output\_ebs\_csi\_driver\_controller\_role\_name) | The Name of the EBS CSI driver controller IAM role name | +| [ebs\_csi\_driver\_controller\_role\_policy\_arn](#output\_ebs\_csi\_driver\_controller\_role\_policy\_arn) | The Name of the EBS CSI driver controller IAM role policy ARN | +| [ebs\_csi\_driver\_controller\_role\_policy\_name](#output\_ebs\_csi\_driver\_controller\_role\_policy\_name) | The Name of the EBS CSI driver controller IAM role policy name | +| [ebs\_csi\_driver\_name](#output\_ebs\_csi\_driver\_name) | The Name of the EBS CSI driver | + + +## References + +- [aws-ebs-csi-driver](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/releases) + +[](https://cpco.io/component) diff --git a/modules/eks/ebs-controller/context.tf b/modules/eks/ebs-controller/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/ebs-controller/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/ebs-controller/main.tf b/modules/eks/ebs-controller/main.tf new file mode 100644 index 000000000..ac54cab12 --- /dev/null +++ b/modules/eks/ebs-controller/main.tf @@ -0,0 +1,56 @@ +locals { + enabled = module.this.enabled +} + +module "ebs_csi_driver_controller" { + count = local.enabled ? 1 : 0 + + # https://github.com/DrFaust92/terraform-kubernetes-ebs-csi-driver + source = "DrFaust92/ebs-csi-driver/kubernetes" + version = "3.5.0" + + ebs_csi_driver_version = var.ebs_csi_driver_version + ebs_csi_controller_image = var.ebs_csi_controller_image + ebs_csi_controller_role_name = "ebs-csi-${module.eks.outputs.cluster_shortname}" + ebs_csi_controller_role_policy_name_prefix = "ebs-csi-${module.eks.outputs.cluster_shortname}" + oidc_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") + enable_volume_resizing = true +} + +# Remove non encrypted default storage class +resource "kubernetes_annotations" "default_storage_class" { + count = local.enabled ? 1 : 0 + depends_on = [module.ebs_csi_driver_controller] + + api_version = "storage.k8s.io/v1" + kind = "StorageClass" + force = "true" + + metadata { + name = "gp2" + } + + annotations = { + "storageclass.kubernetes.io/is-default-class" = "false" + } +} + +# Create the new StorageClass and make it default +resource "kubernetes_storage_class" "gp3_enc" { + count = local.enabled ? 1 : 0 + depends_on = [module.ebs_csi_driver_controller] + metadata { + name = "gp3-enc" + annotations = { + "storageclass.kubernetes.io/is-default-class" = "true" + } + } + storage_provisioner = "ebs.csi.aws.com" + volume_binding_mode = "WaitForFirstConsumer" + allow_volume_expansion = true + parameters = { + "encrypted" = "true" + "fsType" = "ext4" + "type" = "gp3" + } +} diff --git a/modules/eks/ebs-controller/outputs.tf b/modules/eks/ebs-controller/outputs.tf new file mode 100644 index 000000000..a30f9910f --- /dev/null +++ b/modules/eks/ebs-controller/outputs.tf @@ -0,0 +1,24 @@ +output "ebs_csi_driver_name" { + description = "The Name of the EBS CSI driver" + value = module.ebs_csi_driver_controller.ebs_csi_driver_name +} + +output "ebs_csi_driver_controller_role_arn" { + description = "The Name of the EBS CSI driver controller IAM role ARN" + value = module.ebs_csi_driver_controller.ebs_csi_driver_controller_role_arn +} + +output "ebs_csi_driver_controller_role_name" { + description = "The Name of the EBS CSI driver controller IAM role name" + value = module.ebs_csi_driver_controller.ebs_csi_driver_controller_role_name +} + +output "ebs_csi_driver_controller_role_policy_arn" { + description = "The Name of the EBS CSI driver controller IAM role policy ARN" + value = module.ebs_csi_driver_controller.ebs_csi_driver_controller_role_policy_arn +} + +output "ebs_csi_driver_controller_role_policy_name" { + description = "The Name of the EBS CSI driver controller IAM role policy name" + value = module.ebs_csi_driver_controller.ebs_csi_driver_controller_role_policy_name +} diff --git a/modules/eks/ebs-controller/provider-helm.tf b/modules/eks/ebs-controller/provider-helm.tf new file mode 100644 index 000000000..20e4d3837 --- /dev/null +++ b/modules/eks/ebs-controller/provider-helm.tf @@ -0,0 +1,158 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = true + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data + eks_cluster_id = module.eks.outputs.eks_cluster_id + eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/ebs-controller/providers.tf b/modules/eks/ebs-controller/providers.tf new file mode 100644 index 000000000..2775903d2 --- /dev/null +++ b/modules/eks/ebs-controller/providers.tf @@ -0,0 +1,40 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} + +data "aws_eks_cluster" "kubernetes" { + count = local.enabled ? 1 : 0 + + name = module.eks.outputs.eks_cluster_id +} + +data "aws_eks_cluster_auth" "kubernetes" { + count = local.enabled ? 1 : 0 + + name = module.eks.outputs.eks_cluster_id +} diff --git a/modules/eks/ebs-controller/remote-state.tf b/modules/eks/ebs-controller/remote-state.tf new file mode 100644 index 000000000..ac55ba94c --- /dev/null +++ b/modules/eks/ebs-controller/remote-state.tf @@ -0,0 +1,8 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.eks_component_name + + context = module.this.context +} diff --git a/modules/eks/ebs-controller/variables.tf b/modules/eks/ebs-controller/variables.tf new file mode 100644 index 000000000..e2e9313f0 --- /dev/null +++ b/modules/eks/ebs-controller/variables.tf @@ -0,0 +1,22 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "ebs_csi_driver_version" { + type = string + description = "The version of the EBS CSI driver" + default = "v1.6.2" +} + +variable "ebs_csi_controller_image" { + type = string + description = "The image to use for the EBS CSI controller" + default = "k8s.gcr.io/provider-aws/aws-ebs-csi-driver" +} diff --git a/modules/eks/ebs-controller/versions.tf b/modules/eks/ebs-controller/versions.tf new file mode 100644 index 000000000..b7a1a1986 --- /dev/null +++ b/modules/eks/ebs-controller/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1" + } + } +} From c9907fed24320a39523b49985dd072d79303c718 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Wed, 26 Apr 2023 17:30:03 -0400 Subject: [PATCH 102/501] Add `amplify` component (#650) --- modules/amplify/README.md | 231 ++++++++++++++++++++++++++ modules/amplify/context.tf | 279 ++++++++++++++++++++++++++++++++ modules/amplify/dns.tf | 41 +++++ modules/amplify/main.tf | 46 ++++++ modules/amplify/outputs.tf | 44 +++++ modules/amplify/providers.tf | 29 ++++ modules/amplify/remote-state.tf | 9 ++ modules/amplify/variables.tf | 216 +++++++++++++++++++++++++ modules/amplify/versions.tf | 10 ++ 9 files changed, 905 insertions(+) create mode 100644 modules/amplify/README.md create mode 100644 modules/amplify/context.tf create mode 100644 modules/amplify/dns.tf create mode 100644 modules/amplify/main.tf create mode 100644 modules/amplify/outputs.tf create mode 100644 modules/amplify/providers.tf create mode 100644 modules/amplify/remote-state.tf create mode 100644 modules/amplify/variables.tf create mode 100644 modules/amplify/versions.tf diff --git a/modules/amplify/README.md b/modules/amplify/README.md new file mode 100644 index 000000000..6c4bd91ac --- /dev/null +++ b/modules/amplify/README.md @@ -0,0 +1,231 @@ +# Component: `amplify` + +This component is responsible for provisioning +AWS Amplify apps, backend environments, branches, domain associations, and webhooks. + +## Usage + +**Stack Level**: Regional + +Here's an example for how to use this component: + +```yaml +# stacks/catalog/amplify/defaults.yaml +components: + terraform: + amplify/defaults: + metadata: + type: abstract + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + # https://docs.aws.amazon.com/amplify/latest/userguide/setting-up-GitHub-access.html + github_personal_access_token_secret_path: "/amplify/github_personal_access_token" + platform: "WEB" + enable_auto_branch_creation: false + enable_basic_auth: false + enable_branch_auto_build: true + enable_branch_auto_deletion: false + iam_service_role_enabled: false + environment_variables: {} + dns_delegated_component_name: "dns-delegated" + dns_delegated_environment_name: "gbl" +``` + +```yaml +# stacks/catalog/amplify/example.yaml +import: + - catalog/amplify/defaults + +components: + terraform: + amplify/example: + metadata: + # Point to the Terraform component + component: amplify + inherits: + # Inherit the default settings + - amplify/defaults + vars: + name: "example" + description: "example Amplify App" + repository: "https://github.com/cloudposse/amplify-test2" + platform: "WEB_COMPUTE" + enable_auto_branch_creation: false + enable_basic_auth: false + enable_branch_auto_build: true + enable_branch_auto_deletion: false + iam_service_role_enabled: true + # https://docs.aws.amazon.com/amplify/latest/userguide/ssr-CloudWatch-logs.html + iam_service_role_actions: + - "logs:CreateLogStream" + - "logs:CreateLogGroup" + - "logs:DescribeLogGroups" + - "logs:PutLogEvents" + custom_rules: [] + auto_branch_creation_patterns: [] + environment_variables: + NEXT_PRIVATE_STANDALONE: false + NEXT_PUBLIC_TEST: test + _LIVE_UPDATES: '[{"pkg":"node","type":"nvm","version":"16"},{"pkg":"next-version","type":"internal","version":"13.1.1"}]' + environments: + main: + branch_name: "main" + enable_auto_build: true + backend_enabled: false + enable_performance_mode: false + enable_pull_request_preview: false + framework: "Next.js - SSR" + stage: "PRODUCTION" + environment_variables: {} + develop: + branch_name: "develop" + enable_auto_build: true + backend_enabled: false + enable_performance_mode: false + enable_pull_request_preview: false + framework: "Next.js - SSR" + stage: "DEVELOPMENT" + environment_variables: {} + domain_config: + enable_auto_sub_domain: false + wait_for_verification: false + sub_domain: + - branch_name: "main" + prefix: "example-prod" + - branch_name: "develop" + prefix: "example-dev" + subdomains_dns_records_enabled: true + certificate_verification_dns_record_enabled: false +``` + +The `amplify/example` YAML configuration defines an Amplify app in AWS. +The app is set up to use the `Next.js` framework with SSR (server-side rendering) and is linked to the +GitHub repository "https://github.com/cloudposse/amplify-test2". + +The app is set up to have two environments: `main` and `develop`. +Each environment has different configuration settings, such as the branch name, framework, and stage. +The `main` environment is set up for production, while the `develop` environments is set up for development. + +The app is also configured to have custom subdomains for each environment, with prefixes such as `example-prod` and `example-dev`. +The subdomains are configured to use DNS records, which are enabled through the `subdomains_dns_records_enabled` variable. + +The app also has an IAM service role configured with specific IAM actions, and environment variables set up for each environment. +Additionally, the app is configured to use the Atmos Spacelift workspace, as indicated by the `workspace_enabled: true` setting. + +The `amplify/example` Atmos component extends the `amplify/defaults` component. + +The `amplify/example` configuration is imported into the `stacks/mixins/stage/dev.yaml` stack config file to be provisioned +in the `dev` account. + +```yaml +# stacks/mixins/stage/dev.yaml +import: + - catalog/amplify/example +``` + +You can execute the following command to provision the Amplify app using Atmos: + +```shell +atmos terraform apply amplify/example -s +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [amplify\_app](#module\_amplify\_app) | cloudposse/amplify-app/aws | 0.2.1 | +| [certificate\_verification\_dns\_record](#module\_certificate\_verification\_dns\_record) | cloudposse/route53-cluster-hostname/aws | 0.12.3 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [subdomains\_dns\_record](#module\_subdomains\_dns\_record) | cloudposse/route53-cluster-hostname/aws | 0.12.3 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ssm_parameter.github_pat](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [auto\_branch\_creation\_config](#input\_auto\_branch\_creation\_config) | The automated branch creation configuration for the Amplify app |
object({
basic_auth_credentials = optional(string)
build_spec = optional(string)
enable_auto_build = optional(bool)
enable_basic_auth = optional(bool)
enable_performance_mode = optional(bool)
enable_pull_request_preview = optional(bool)
environment_variables = optional(map(string))
framework = optional(string)
pull_request_environment_name = optional(string)
stage = optional(string)
})
| `null` | no | +| [auto\_branch\_creation\_patterns](#input\_auto\_branch\_creation\_patterns) | The automated branch creation glob patterns for the Amplify app | `list(string)` | `[]` | no | +| [basic\_auth\_credentials](#input\_basic\_auth\_credentials) | The credentials for basic authorization for the Amplify app | `string` | `null` | no | +| [build\_spec](#input\_build\_spec) | The [build specification](https://docs.aws.amazon.com/amplify/latest/userguide/build-settings.html) (build spec) for the Amplify app.
If not provided then it will use the `amplify.yml` at the root of your project / branch. | `string` | `null` | no | +| [certificate\_verification\_dns\_record\_enabled](#input\_certificate\_verification\_dns\_record\_enabled) | Whether or not to create DNS records for SSL certificate validation.
If using the DNS zone from `dns-delegated`, the SSL certificate is already validated, and this variable must be set to `false`. | `bool` | `false` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [custom\_rules](#input\_custom\_rules) | The custom rules to apply to the Amplify App |
list(object({
condition = optional(string)
source = string
status = optional(string)
target = string
}))
| `[]` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input\_description) | The description for the Amplify app | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | The component name of `dns-delegated` | `string` | `"dns-delegated"` | no | +| [dns\_delegated\_environment\_name](#input\_dns\_delegated\_environment\_name) | The environment name of `dns-delegated` | `string` | `"gbl"` | no | +| [domain\_config](#input\_domain\_config) | Amplify custom domain configuration |
object({
domain_name = optional(string)
enable_auto_sub_domain = optional(bool, false)
wait_for_verification = optional(bool, false)
sub_domain = list(object({
branch_name = string
prefix = string
}))
})
| `null` | no | +| [enable\_auto\_branch\_creation](#input\_enable\_auto\_branch\_creation) | Enables automated branch creation for the Amplify app | `bool` | `false` | no | +| [enable\_basic\_auth](#input\_enable\_basic\_auth) | Enables basic authorization for the Amplify app.
This will apply to all branches that are part of this app. | `bool` | `false` | no | +| [enable\_branch\_auto\_build](#input\_enable\_branch\_auto\_build) | Enables auto-building of branches for the Amplify App | `bool` | `true` | no | +| [enable\_branch\_auto\_deletion](#input\_enable\_branch\_auto\_deletion) | Automatically disconnects a branch in the Amplify Console when you delete a branch from your Git repository | `bool` | `false` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [environment\_variables](#input\_environment\_variables) | The environment variables for the Amplify app | `map(string)` | `{}` | no | +| [environments](#input\_environments) | The configuration of the environments for the Amplify App |
map(object({
branch_name = optional(string)
backend_enabled = optional(bool, false)
environment_name = optional(string)
deployment_artifacts = optional(string)
stack_name = optional(string)
display_name = optional(string)
description = optional(string)
enable_auto_build = optional(bool)
enable_basic_auth = optional(bool)
enable_notification = optional(bool)
enable_performance_mode = optional(bool)
enable_pull_request_preview = optional(bool)
environment_variables = optional(map(string))
framework = optional(string)
pull_request_environment_name = optional(string)
stage = optional(string)
ttl = optional(number)
webhook_enabled = optional(bool, false)
}))
| `{}` | no | +| [github\_personal\_access\_token\_secret\_path](#input\_github\_personal\_access\_token\_secret\_path) | Path to the GitHub personal access token in AWS Parameter Store | `string` | `"/amplify/github_personal_access_token"` | no | +| [iam\_service\_role\_actions](#input\_iam\_service\_role\_actions) | List of IAM policy actions for the AWS Identity and Access Management (IAM) service role for the Amplify app.
If not provided, the default set of actions will be used for the role if the variable `iam_service_role_enabled` is set to `true`. | `list(string)` | `[]` | no | +| [iam\_service\_role\_arn](#input\_iam\_service\_role\_arn) | The AWS Identity and Access Management (IAM) service role for the Amplify app.
If not provided, a new role will be created if the variable `iam_service_role_enabled` is set to `true`. | `list(string)` | `[]` | no | +| [iam\_service\_role\_enabled](#input\_iam\_service\_role\_enabled) | Flag to create the IAM service role for the Amplify app | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [oauth\_token](#input\_oauth\_token) | The OAuth token for a third-party source control system for the Amplify app.
The OAuth token is used to create a webhook and a read-only deploy key.
The OAuth token is not stored. | `string` | `null` | no | +| [platform](#input\_platform) | The platform or framework for the Amplify app | `string` | `"WEB"` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS region | `string` | n/a | yes | +| [repository](#input\_repository) | The repository for the Amplify app | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [subdomains\_dns\_records\_enabled](#input\_subdomains\_dns\_records\_enabled) | Whether or not to create DNS records for the Amplify app custom subdomains | `bool` | `false` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | Amplify App ARN | +| [backend\_environments](#output\_backend\_environments) | Created backend environments | +| [branch\_names](#output\_branch\_names) | The names of the created Amplify branches | +| [default\_domain](#output\_default\_domain) | Amplify App domain (non-custom) | +| [domain\_association\_arn](#output\_domain\_association\_arn) | ARN of the domain association | +| [domain\_association\_certificate\_verification\_dns\_record](#output\_domain\_association\_certificate\_verification\_dns\_record) | The DNS record for certificate verification | +| [name](#output\_name) | Amplify App name | +| [sub\_domains](#output\_sub\_domains) | DNS records and the verified status for the subdomains | +| [webhooks](#output\_webhooks) | Created webhooks | + + +[](https://cpco.io/component) diff --git a/modules/amplify/context.tf b/modules/amplify/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/amplify/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/amplify/dns.tf b/modules/amplify/dns.tf new file mode 100644 index 000000000..63e0d8f09 --- /dev/null +++ b/modules/amplify/dns.tf @@ -0,0 +1,41 @@ +locals { + certificate_verification_dns_record_parts = split(" ", module.amplify_app.domain_association_certificate_verification_dns_record) +} + +# Create the SSL certificate validation record +module "certificate_verification_dns_record" { + source = "cloudposse/route53-cluster-hostname/aws" + version = "0.12.3" + + count = var.certificate_verification_dns_record_enabled ? 1 : 0 + + zone_id = module.dns_delegated.outputs.default_dns_zone_id + + dns_name = trimspace(local.certificate_verification_dns_record_parts[0]) + type = trimspace(local.certificate_verification_dns_record_parts[1]) + + records = [ + trimspace(local.certificate_verification_dns_record_parts[2]) + ] + + context = module.this.context +} + +# Create DNS records for the subdomains +module "subdomains_dns_record" { + source = "cloudposse/route53-cluster-hostname/aws" + version = "0.12.3" + + count = var.subdomains_dns_records_enabled && local.domain_config != null ? length(local.domain_config.sub_domain) : 0 + + zone_id = module.dns_delegated.outputs.default_dns_zone_id + + dns_name = trimspace(split(" ", tolist(module.amplify_app.sub_domains)[count.index].dns_record)[0]) + type = trimspace(split(" ", tolist(module.amplify_app.sub_domains)[count.index].dns_record)[1]) + + records = [ + trimspace(split(" ", tolist(module.amplify_app.sub_domains)[count.index].dns_record)[2]) + ] + + context = module.this.context +} diff --git a/modules/amplify/main.tf b/modules/amplify/main.tf new file mode 100644 index 000000000..eb4672744 --- /dev/null +++ b/modules/amplify/main.tf @@ -0,0 +1,46 @@ +locals { + enabled = module.this.enabled + + domain_config = var.domain_config != null ? { + domain_name = coalesce(lookup(var.domain_config, "domain_name", null), module.dns_delegated.outputs.default_domain_name) + enable_auto_sub_domain = lookup(var.domain_config, "enable_auto_sub_domain", false) + wait_for_verification = lookup(var.domain_config, "wait_for_verification", false) + sub_domain = lookup(var.domain_config, "sub_domain") + } : null +} + +# Read the GitHub PAT from SSM +data "aws_ssm_parameter" "github_pat" { + count = local.enabled ? 1 : 0 + + name = var.github_personal_access_token_secret_path + with_decryption = true +} + +module "amplify_app" { + source = "cloudposse/amplify-app/aws" + version = "0.2.1" + + description = var.description + repository = var.repository + platform = var.platform + access_token = one(data.aws_ssm_parameter.github_pat[*].value) + oauth_token = var.oauth_token + auto_branch_creation_config = var.auto_branch_creation_config + auto_branch_creation_patterns = var.auto_branch_creation_patterns + basic_auth_credentials = var.basic_auth_credentials + build_spec = var.build_spec + enable_auto_branch_creation = var.enable_auto_branch_creation + enable_basic_auth = var.enable_basic_auth + enable_branch_auto_build = var.enable_branch_auto_build + enable_branch_auto_deletion = var.enable_branch_auto_deletion + environment_variables = var.environment_variables + custom_rules = var.custom_rules + iam_service_role_enabled = var.iam_service_role_enabled + iam_service_role_arn = var.iam_service_role_arn + iam_service_role_actions = var.iam_service_role_actions + environments = var.environments + domain_config = local.domain_config + + context = module.this.context +} diff --git a/modules/amplify/outputs.tf b/modules/amplify/outputs.tf new file mode 100644 index 000000000..f38d27372 --- /dev/null +++ b/modules/amplify/outputs.tf @@ -0,0 +1,44 @@ +output "name" { + description = "Amplify App name" + value = module.amplify_app.name +} + +output "arn" { + description = "Amplify App ARN " + value = module.amplify_app.arn +} + +output "default_domain" { + description = "Amplify App domain (non-custom)" + value = module.amplify_app.default_domain +} + +output "backend_environments" { + description = "Created backend environments" + value = module.amplify_app.backend_environments +} + +output "branch_names" { + description = "The names of the created Amplify branches" + value = module.amplify_app.branch_names +} + +output "webhooks" { + description = "Created webhooks" + value = module.amplify_app.webhooks +} + +output "domain_association_arn" { + description = "ARN of the domain association" + value = module.amplify_app.domain_association_arn +} + +output "domain_association_certificate_verification_dns_record" { + description = "The DNS record for certificate verification" + value = module.amplify_app.domain_association_certificate_verification_dns_record +} + +output "sub_domains" { + description = "DNS records and the verified status for the subdomains" + value = module.amplify_app.sub_domains +} diff --git a/modules/amplify/providers.tf b/modules/amplify/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/amplify/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/amplify/remote-state.tf b/modules/amplify/remote-state.tf new file mode 100644 index 000000000..83eb0c6e8 --- /dev/null +++ b/modules/amplify/remote-state.tf @@ -0,0 +1,9 @@ +module "dns_delegated" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.dns_delegated_component_name + environment = var.dns_delegated_environment_name + + context = module.this.context +} diff --git a/modules/amplify/variables.tf b/modules/amplify/variables.tf new file mode 100644 index 000000000..eb64cdbcb --- /dev/null +++ b/modules/amplify/variables.tf @@ -0,0 +1,216 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "github_personal_access_token_secret_path" { + description = "Path to the GitHub personal access token in AWS Parameter Store" + default = "/amplify/github_personal_access_token" + type = string +} + +variable "description" { + type = string + description = "The description for the Amplify app" + default = null +} + +variable "repository" { + type = string + description = "The repository for the Amplify app" + default = null +} + +variable "platform" { + type = string + description = "The platform or framework for the Amplify app" + default = "WEB" +} + +variable "oauth_token" { + type = string + description = <<-EOT + The OAuth token for a third-party source control system for the Amplify app. + The OAuth token is used to create a webhook and a read-only deploy key. + The OAuth token is not stored. + EOT + default = null + sensitive = true +} + +variable "auto_branch_creation_config" { + type = object({ + basic_auth_credentials = optional(string) + build_spec = optional(string) + enable_auto_build = optional(bool) + enable_basic_auth = optional(bool) + enable_performance_mode = optional(bool) + enable_pull_request_preview = optional(bool) + environment_variables = optional(map(string)) + framework = optional(string) + pull_request_environment_name = optional(string) + stage = optional(string) + }) + description = "The automated branch creation configuration for the Amplify app" + default = null +} + +variable "auto_branch_creation_patterns" { + type = list(string) + description = "The automated branch creation glob patterns for the Amplify app" + default = [] +} + +variable "basic_auth_credentials" { + type = string + description = "The credentials for basic authorization for the Amplify app" + default = null +} + +variable "build_spec" { + type = string + description = <<-EOT + The [build specification](https://docs.aws.amazon.com/amplify/latest/userguide/build-settings.html) (build spec) for the Amplify app. + If not provided then it will use the `amplify.yml` at the root of your project / branch. + EOT + default = null +} + +variable "enable_auto_branch_creation" { + type = bool + description = "Enables automated branch creation for the Amplify app" + default = false +} + +variable "enable_basic_auth" { + type = bool + description = <<-EOT + Enables basic authorization for the Amplify app. + This will apply to all branches that are part of this app. + EOT + default = false +} + +variable "enable_branch_auto_build" { + type = bool + description = "Enables auto-building of branches for the Amplify App" + default = true +} + +variable "enable_branch_auto_deletion" { + type = bool + description = "Automatically disconnects a branch in the Amplify Console when you delete a branch from your Git repository" + default = false +} + +variable "environment_variables" { + type = map(string) + description = "The environment variables for the Amplify app" + default = {} +} + +variable "iam_service_role_arn" { + type = list(string) + description = <<-EOT + The AWS Identity and Access Management (IAM) service role for the Amplify app. + If not provided, a new role will be created if the variable `iam_service_role_enabled` is set to `true`. + EOT + default = [] + nullable = false +} + +variable "iam_service_role_enabled" { + type = bool + description = "Flag to create the IAM service role for the Amplify app" + default = false + nullable = false +} + +variable "iam_service_role_actions" { + type = list(string) + description = <<-EOT + List of IAM policy actions for the AWS Identity and Access Management (IAM) service role for the Amplify app. + If not provided, the default set of actions will be used for the role if the variable `iam_service_role_enabled` is set to `true`. + EOT + default = [] + nullable = false +} + +variable "custom_rules" { + type = list(object({ + condition = optional(string) + source = string + status = optional(string) + target = string + })) + description = "The custom rules to apply to the Amplify App" + default = [] + nullable = false +} + +variable "environments" { + type = map(object({ + branch_name = optional(string) + backend_enabled = optional(bool, false) + environment_name = optional(string) + deployment_artifacts = optional(string) + stack_name = optional(string) + display_name = optional(string) + description = optional(string) + enable_auto_build = optional(bool) + enable_basic_auth = optional(bool) + enable_notification = optional(bool) + enable_performance_mode = optional(bool) + enable_pull_request_preview = optional(bool) + environment_variables = optional(map(string)) + framework = optional(string) + pull_request_environment_name = optional(string) + stage = optional(string) + ttl = optional(number) + webhook_enabled = optional(bool, false) + })) + description = "The configuration of the environments for the Amplify App" + default = {} + nullable = false +} + +variable "domain_config" { + type = object({ + domain_name = optional(string) + enable_auto_sub_domain = optional(bool, false) + wait_for_verification = optional(bool, false) + sub_domain = list(object({ + branch_name = string + prefix = string + })) + }) + description = "Amplify custom domain configuration" + default = null +} + +variable "certificate_verification_dns_record_enabled" { + type = bool + description = <<-EOT + Whether or not to create DNS records for SSL certificate validation. + If using the DNS zone from `dns-delegated`, the SSL certificate is already validated, and this variable must be set to `false`. + EOT + default = false +} + +variable "subdomains_dns_records_enabled" { + type = bool + description = "Whether or not to create DNS records for the Amplify app custom subdomains" + default = false +} + +variable "dns_delegated_component_name" { + type = string + description = "The component name of `dns-delegated`" + default = "dns-delegated" +} + +variable "dns_delegated_environment_name" { + type = string + description = "The environment name of `dns-delegated`" + default = "gbl" +} diff --git a/modules/amplify/versions.tf b/modules/amplify/versions.tf new file mode 100644 index 000000000..b5920b7b1 --- /dev/null +++ b/modules/amplify/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From 71b74c56e8956959d47e7b32a0391bafa21f60c3 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 4 May 2023 11:15:06 -0700 Subject: [PATCH 103/501] Update `RDS` (#657) --- modules/rds/README.md | 8 +++++--- modules/rds/default.auto.tfvars | 1 - modules/rds/kms.tf | 2 +- modules/rds/main.tf | 14 ++++++++------ modules/rds/remote-state.tf | 2 ++ modules/rds/systems-manager.tf | 2 +- modules/rds/variables.tf | 12 ++++++++++++ 7 files changed, 29 insertions(+), 12 deletions(-) delete mode 100644 modules/rds/default.auto.tfvars diff --git a/modules/rds/README.md b/modules/rds/README.md index 8a07708df..21ab638df 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -81,10 +81,10 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.10.0 | -| [rds\_client\_sg](#module\_rds\_client\_sg) | cloudposse/security-group/aws | 0.3.1 | +| [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | +| [rds\_client\_sg](#module\_rds\_client\_sg) | cloudposse/security-group/aws | 2.0.1 | | [rds\_instance](#module\_rds\_instance) | cloudposse/rds/aws | 0.38.5 | -| [rds\_monitoring\_role](#module\_rds\_monitoring\_role) | cloudposse/iam-role/aws | 0.16.2 | +| [rds\_monitoring\_role](#module\_rds\_monitoring\_role) | cloudposse/iam-role/aws | 0.17.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | @@ -187,7 +187,9 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [timezone](#input\_timezone) | Time zone of the DB instance. timezone is currently only supported by Microsoft SQL Server. The timezone can only be set on creation. See [MSSQL User Guide](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_SQLServer.html#SQLServer.Concepts.General.TimeZone) for more information. | `string` | `null` | no | +| [use\_dns\_delegated](#input\_use\_dns\_delegated) | Use the dns-delegated dns\_zone\_id | `bool` | `false` | no | | [use\_eks\_security\_group](#input\_use\_eks\_security\_group) | Use the eks default security group | `bool` | `false` | no | +| [use\_private\_subnets](#input\_use\_private\_subnets) | Use private subnets | `bool` | `true` | no | ## Outputs diff --git a/modules/rds/default.auto.tfvars b/modules/rds/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/rds/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/rds/kms.tf b/modules/rds/kms.tf index 4aef680ca..9fdc89244 100644 --- a/modules/rds/kms.tf +++ b/modules/rds/kms.tf @@ -1,6 +1,6 @@ module "kms_key_rds" { source = "cloudposse/kms-key/aws" - version = "0.10.0" + version = "0.12.1" description = "KMS key for RDS" deletion_window_in_days = 10 diff --git a/modules/rds/main.tf b/modules/rds/main.tf index 31bceff7d..941dc4077 100644 --- a/modules/rds/main.tf +++ b/modules/rds/main.tf @@ -2,10 +2,10 @@ locals { enabled = module.this.enabled vpc_id = module.vpc.outputs.vpc_id - subnet_ids = module.vpc.outputs.private_subnet_ids + subnet_ids = var.use_private_subnets ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids eks_security_groups = var.use_eks_security_group ? [module.eks[0].outputs.eks_cluster_managed_security_group_id] : [] - dns_zone_id = module.dns_gbl_delegated.outputs.default_dns_zone_id + dns_zone_id = one(module.dns_gbl_delegated[*].outputs.default_dns_zone_id) create_user = local.enabled && length(var.database_user) == 0 create_password = local.enabled && length(var.database_password) == 0 @@ -22,7 +22,7 @@ locals { module "rds_client_sg" { source = "cloudposse/security-group/aws" - version = "0.3.1" + version = "2.0.1" name = "${module.this.name}-client" enabled = module.this.enabled && var.client_security_group_enabled @@ -58,7 +58,7 @@ module "rds_instance" { db_parameter_group = var.db_parameter_group db_subnet_group_name = var.db_subnet_group_name deletion_protection = var.deletion_protection - dns_zone_id = local.dns_zone_id + dns_zone_id = local.dns_zone_id != null ? local.dns_zone_id : "" enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports engine = var.engine engine_version = var.engine_version @@ -73,7 +73,7 @@ module "rds_instance" { major_engine_version = var.major_engine_version max_allocated_storage = var.max_allocated_storage monitoring_interval = var.monitoring_interval - monitoring_role_arn = var.monitoring_interval != "0" ? module.rds_monitoring_role.arn : var.monitoring_role_arn + monitoring_role_arn = var.monitoring_interval != "0" ? module.rds_monitoring_role[0].arn : var.monitoring_role_arn multi_az = var.multi_az option_group_name = var.option_group_name parameter_group_name = var.parameter_group_name @@ -125,7 +125,9 @@ resource "random_password" "database_password" { module "rds_monitoring_role" { source = "cloudposse/iam-role/aws" - version = "0.16.2" + version = "0.17.0" + + count = var.monitoring_interval != "0" ? 1 : 0 name = "${module.this.name}-rds-enhanced-monitoring-role" enabled = module.this.enabled && var.monitoring_interval != 0 diff --git a/modules/rds/remote-state.tf b/modules/rds/remote-state.tf index 600d1fdcf..3cea5bb29 100644 --- a/modules/rds/remote-state.tf +++ b/modules/rds/remote-state.tf @@ -22,6 +22,8 @@ module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" + count = var.use_dns_delegated ? 1 : 0 + component = "dns-delegated" environment = var.dns_gbl_delegated_environment_name diff --git a/modules/rds/systems-manager.tf b/modules/rds/systems-manager.tf index 4daf32b46..5650017ee 100644 --- a/modules/rds/systems-manager.tf +++ b/modules/rds/systems-manager.tf @@ -76,7 +76,7 @@ resource "aws_ssm_parameter" "rds_database_hostname" { count = local.ssm_enabled ? 1 : 0 name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_hostname) - value = module.rds_instance.hostname + value = module.rds_instance.hostname == "" ? module.rds_instance.instance_address : module.rds_instance.hostname description = "RDS DB hostname" type = "String" overwrite = true diff --git a/modules/rds/variables.tf b/modules/rds/variables.tf index 6c60d3bb1..ace6633ca 100644 --- a/modules/rds/variables.tf +++ b/modules/rds/variables.tf @@ -3,6 +3,12 @@ variable "region" { description = "AWS Region" } +variable "use_dns_delegated" { + type = bool + description = "Use the dns-delegated dns_zone_id" + default = false +} + variable "dns_gbl_delegated_environment_name" { type = string description = "The name of the environment where global `dns_delegated` is provisioned" @@ -20,3 +26,9 @@ variable "client_security_group_enabled" { description = "create a client security group and include in attached default security group" default = true } + +variable "use_private_subnets" { + type = bool + description = "Use private subnets" + default = true +} From 2a0dca8d3db35baa84fa700f69529cdf96d324be Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 4 May 2023 16:04:06 -0700 Subject: [PATCH 104/501] ARC enhancement, aws-config bugfix, DNS documentation (#655) --- modules/dns-primary/README.md | 22 ++++++++ .../eks/actions-runner-controller/README.md | 52 ++++++++++++++++++- .../templates/horizontalrunnerautoscaler.yaml | 2 + .../templates/runnerdeployment.yaml | 10 +++- .../charts/actions-runner/values.yaml | 24 +++++---- modules/eks/actions-runner-controller/main.tf | 3 +- .../actions-runner-controller/variables.tf | 26 +++++----- rootfs/usr/local/bin/aws-config | 4 +- 8 files changed, 114 insertions(+), 29 deletions(-) mode change 100644 => 100755 modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml mode change 100644 => 100755 modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml mode change 100644 => 100755 modules/eks/actions-runner-controller/charts/actions-runner/values.yaml mode change 100644 => 100755 modules/eks/actions-runner-controller/main.tf mode change 100644 => 100755 modules/eks/actions-runner-controller/variables.tf diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index a2649df88..7c94804e0 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -4,6 +4,28 @@ This component is responsible for provisioning the primary DNS zones into an AWS The zones from the primary DNS zone are then expected to be delegated to other accounts via [the `dns-delegated` component](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dns-delegated). Additionally, external records can be created on the primary DNS zones via the `record_config` variable. +## Architecture + +### Summary + +The `dns` account gets a single `dns-primary` component deployed. Every other account that needs DNS entries gets a single `dns-delegated` component, chaining off the domains in the `dns` account. Optionally, accounts can have a single `dns-primary` component of their own, to have apex domains (which Cloud Posse calls "vanity domains"). Typically, these domains are configured with CNAME (or apex alias) records to point to service domain entries. + +### Details + +The purpose of the `dns` account is to host root domains shared by several accounts (with each account being delegated its own subdomain) and to be the owner of domain registrations purchased from Amazon. + +The purpose of the `dns-primary` component is to provision AWS Route53 zones for the root domains. These zones, once provisioned, must be manually configured into the Domain Name Registrar's records as name servers. A single component can provision multiple domains and, optionally, associated ACM (SSL) certificates in a single account. + +Cloud Posse's architecture expects root domains shared by several accounts to be provisioned in the `dns` account with `dns-primary` and delegated to other accounts using the `dns-delegated` component, with each account getting its own subdomain corresponding to a Route 53 zone in the delegated account. Cloud Posse's architecture requires at least one such domain, called "the service domain", be provisioned. The service domain is not customer facing, and is provisioned to allow fully automated construction of host names without any concerns about how they look. Although they are not secret, the public will never see them. + +Root domains used by a single account are provisioned with the `dns-primary` component directly in that account. Cloud Posse calls these "vanity domains". These can be whatever the marketing or PR or other stakeholders want to be. + +After a domain is provisioned in the `dns` account, the `dns-delegated` component can provision one or more subdomains for each account, and, optionally, associated ACM certificates. For the service domain, Cloud Posse recommends using the account name as the delegated subdomain (either directly, e.g. "plat-dev", or as multiple subdomains, e.g. "dev.plat") because that allows `dns-delegated` to automatically provision any required host name in that zone. + +There is no automated support for `dns-primary` to provision root domains outside of the `dns` account that are to be shared by multiple accounts, and such usage is not recommended. If you must, `dns-primary` can provision a subdomain of a root domain that is provisioned in another account (not `dns`). In this case, the delegation of the subdomain must be done manually by entering the name servers into the parent domain's records (instead of in the Registrar's records). + +The architecture does not support other configurations, or non-standard component names. + ## Usage diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 5336e49c1..0ea24740b 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -63,6 +63,12 @@ components: image: summerwind/actions-runner-dind # `scope` is org name for Organization runners, repo name for Repository runners scope: "org/infra" + # We can trade the fast-start behavior of min_replicas > 0 for the better guarantee + # that Karpenter will not terminate the runner while it is running a job. + # # Tell Karpenter not to evict this pod. This is only safe when min_replicas is 0. + # # If we do not set this, Karpenter will feel free to terminate the runner while it is running a job. + # pod_annotations: + # karpenter.sh/do-not-evict: "true" min_replicas: 1 max_replicas: 20 scale_down_delay_seconds: 100 @@ -112,7 +118,11 @@ components: # # `scope` is org name for Organization runners, repo name for Repository runners # scope: "org/infra" # group: "ArmRunners" - # min_replicas: 1 + # # Tell Karpenter not to evict this pod. This is only safe when min_replicas is 0. + # # If we do not set this, Karpenter will feel free to terminate the runner while it is running a job. + # pod_annotations: + # karpenter.sh/do-not-evict: "true" + # min_replicas: 0 # max_replicas: 20 # scale_down_delay_seconds: 100 # resources: @@ -313,6 +323,44 @@ to setting a long duration, and the cost looks even smaller by comparison to the For lightly used runner pools expecting only short jobs, you can set `webhook_startup_timeout` to `"30m"`. As a rule of thumb, we recommend setting `maxReplicas` high enough that jobs never wait on the queue more than an hour. +### Interaction with Karpenter or other EKS autoscaling solutions + +Kubernetes cluster autoscaling solutions generally expect that a Pod runs a service that can be terminated on one +Node and restarted on another with only a short duration needed to finish processing any in-flight requests. When +the cluster is resized, the cluster autoscaler will do just that. However, GitHub Action Runner Jobs do not fit this +model. If a Pod is terminated in the middle of a job, the job is lost. The likelihood of this happening is increased +by the fact that the Action Runner Controller Autoscaler is expanding and contracting the size of the Runner Pool on +a regular basis, causing the cluster autoscaler to more frequently want to scale up or scale down the EKS cluster, +and, consequently, to move Pods around. + +To handle these kinds of situations, Karpenter respects an annotation on the Pod: + +```yaml +spec: + template: + metadata: + annotations: + karpenter.sh/do-not-evict: "true" +``` + +When you set this annotation on the Pod, Karpenter will not evict it. This means that the Pod will stay on the Node +it is on, and the Node it is on will not be considered for eviction. This is good because it means that the Pod +will not be terminated in the middle of a job. However, it also means that the Node the Pod is on will not be considered +for termination, which means that the Node will not be removed from the cluster, which means that the cluster will +not shrink in size when you would like it to. + +Since the Runner Pods terminate at the end of the job, this is not a problem for the Pods actually running jobs. +However, if you have set `minReplicas > 0`, then you have some Pods that are just idling, waiting for jobs to be +assigned to them. These Pods are exactly the kind of Pods you want terminated and moved when the cluster is underutilized. +Therefore, when you set `minReplicas > 0`, you should **NOT** set `karpenter.sh/do-not-evict: "true"` on the Pod. + +We have [requested a feature](https://github.com/actions/actions-runner-controller/issues/2562) +that will allow you to set `karpenter.sh/do-not-evict: "true"` and `minReplicas > 0` at the same time by only +annotating Pods running jobs. Meanwhile, another option is to set `minReplicas = 0` on a schedule using an ARC +Autoscaler [scheduled override](https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides). +At present, this component does not support that option, but it could be added in the future if our preferred +solution is not implemented. + ### Updating CRDs When updating the chart or application version of `actions-runner-controller`, it is possible you will need to install @@ -413,7 +461,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
node_selector = {} # optional Kubernetes node selector map for the runner pods
tolerations = [] # optional Kubernetes tolerations list for the runner pods
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
webhook_driven_scaling_enabled = bool # Recommended to be true to enable event-based scaling of runner pool
webhook_startup_timeout = optional(string, null) # Duration after which capacity for a queued job will be discarded

labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | | [ssm\_github\_webhook\_secret\_token\_path](#input\_ssm\_github\_webhook\_secret\_token\_path) | The path in SSM to the GitHub Webhook Secret token. | `string` | `""` | no | diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml old mode 100644 new mode 100755 index 8e7979566..fa5c96452 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml @@ -31,5 +31,7 @@ spec: - githubEvent: workflowJob: {} amount: 1 + {{- if .Values.webhook_startup_timeout }} duration: "{{ .Values.webhook_startup_timeout }}" + {{- end }} {{- end }} diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml old mode 100644 new mode 100755 index 69e994046..b96f9da41 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -28,6 +28,11 @@ spec: # See https://github.com/actions-runner-controller/actions-runner-controller/issues/206#issuecomment-748601907 # replicas: 1 template: + {{- with index .Values "pod_annotations" }} + metadata: + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} spec: # As of 2023-03-31 # Recommended by https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md @@ -89,12 +94,15 @@ spec: limits: cpu: {{ .Values.resources.limits.cpu }} memory: {{ .Values.resources.limits.memory }} - {{- if and .Values.dind_enabled .Values.resources.limits.ephemeral_storage }} + {{- if index .Values.resources.limits "ephemeral_storage" }} ephemeral-storage: {{ .Values.resources.limits.ephemeral_storage }} {{- end }} requests: cpu: {{ .Values.resources.requests.cpu }} memory: {{ .Values.resources.requests.memory }} + {{- if index .Values.resources.requests "ephemeral_storage" }} + ephemeral-storage: {{ .Values.resources.requests.ephemeral_storage }} + {{- end }} {{- if and .Values.dind_enabled .Values.storage }} dockerVolumeMounts: - mountPath: /var/lib/docker diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml old mode 100644 new mode 100755 index 543203cbf..c5a34270b --- a/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml @@ -5,28 +5,30 @@ image: summerwind/actions-runner-dind node_selector: kubernetes.io/os: "linux" kubernetes.io/arch: "amd64" -scope: "example/app" +#scope: "example/app" scale_down_delay_seconds: 300 min_replicas: 1 max_replicas: 2 -busy_metrics: - scale_up_threshold: 0.75 - scale_down_threshold: 0.25 - scale_up_factor: 2 - scale_down_factor: 0.5 +#busy_metrics: +# scale_up_threshold: 0.75 +# scale_down_threshold: 0.25 +# scale_up_factor: 2 +# scale_down_factor: 0.5 resources: limits: cpu: 1.5 memory: 4Gi - ephemeral_storage: "10Gi" + # ephemeral_storage: "10Gi" requests: cpu: 0.5 memory: 1Gi + # ephemeral_storage: "10Gi" + storage: "10Gi" pvc_enabled: false -webhook_driven_scaling_enabled: false +webhook_driven_scaling_enabled: true webhook_startup_timeout: "30m" pull_driven_scaling_enabled: false -labels: - - "Ubuntu" - - "core-example" +#labels: +# - "Ubuntu" +# - "core-example" diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf old mode 100644 new mode 100755 index db38e54e7..e62e9298e --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -206,6 +206,7 @@ module "actions_runner" { values = compact([ yamlencode({ release_name = each.key + pod_annotations = lookup(each.value, "pod_annotations", "") service_account_name = module.actions_runner_controller.service_account_name type = each.value.type scope = each.value.scope @@ -219,7 +220,7 @@ module "actions_runner" { min_replicas = each.value.min_replicas max_replicas = each.value.max_replicas webhook_driven_scaling_enabled = each.value.webhook_driven_scaling_enabled - webhook_startup_timeout = coalesce(each.value.webhook_startup_timeout, "${each.value.scale_down_delay_seconds}s") # if webhook_startup_timeout isnt defined, use scale_down_delay_seconds + webhook_startup_timeout = lookup(each.value, "webhook_startup_timeout", "") pull_driven_scaling_enabled = each.value.pull_driven_scaling_enabled pvc_enabled = each.value.pvc_enabled node_selector = each.value.node_selector diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf old mode 100644 new mode 100755 index 0189658a0..0f02798ee --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -142,17 +142,18 @@ variable "runners" { organization_runner = { type = "organization" # can be either 'organization' or 'repository' dind_enabled: false # A Docker sidecar container will be deployed + image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind' scope = "ACME" # org name for Organization runners, repo name for Repository runners group = "core-automation" # Optional. Assigns the runners to a runner group, for access control. - image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind' - node_selector = {} # optional Kubernetes node selector map for the runner pods - tolerations = [] # optional Kubernetes tolerations list for the runner pods scale_down_delay_seconds = 300 min_replicas = 1 max_replicas = 5 - webhook_driven_scaling_enabled = bool # Recommended to be true to enable event-based scaling of runner pool - webhook_startup_timeout = optional(string, null) # Duration after which capacity for a queued job will be discarded - + busy_metrics = { + scale_up_threshold = 0.75 + scale_down_threshold = 0.25 + scale_up_factor = 2 + scale_down_factor = 0.5 + } labels = [ "Ubuntu", "core-automation", @@ -162,12 +163,13 @@ variable "runners" { EOT type = map(object({ - type = string - scope = string - group = optional(string, null) - image = optional(string, "") - dind_enabled = bool - node_selector = optional(map(string), {}) + type = string + scope = string + group = optional(string, null) + image = optional(string, "") + dind_enabled = bool + node_selector = optional(map(string), {}) + pod_annotations = optional(map(string), {}) tolerations = optional(list(object({ key = string operator = string diff --git a/rootfs/usr/local/bin/aws-config b/rootfs/usr/local/bin/aws-config index 115034a4c..6d081843e 100755 --- a/rootfs/usr/local/bin/aws-config +++ b/rootfs/usr/local/bin/aws-config @@ -177,13 +177,13 @@ case $1 in shift ;; -n*) - if [[ $x == ${x#*=} ]]; then + if [[ $1 == ${1#*=} ]]; then # -n namespace target_namespace=($2) shift 2 else # -n=namespace - target_namespace=(${x#*=}) + target_namespace=(${1#*=}) shift fi ;; From f74b69bdeb0775dbfa908cb6035655ba71e9684d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 5 May 2023 10:05:00 -0700 Subject: [PATCH 105/501] Move `eks/efs` to `efs` (#653) --- modules/{eks => }/efs/README.md | 2 +- modules/{eks => }/efs/context.tf | 0 modules/{eks => }/efs/main.tf | 0 modules/{eks => }/efs/outputs.tf | 0 modules/{eks => }/efs/providers.tf | 2 +- modules/{eks => }/efs/remote-state.tf | 0 modules/{eks => }/efs/variables.tf | 0 modules/{eks => }/efs/versions.tf | 0 modules/eks/efs/default.auto.tfvars | 3 --- 9 files changed, 2 insertions(+), 5 deletions(-) rename modules/{eks => }/efs/README.md (99%) rename modules/{eks => }/efs/context.tf (100%) rename modules/{eks => }/efs/main.tf (100%) rename modules/{eks => }/efs/outputs.tf (100%) rename modules/{eks => }/efs/providers.tf (93%) rename modules/{eks => }/efs/remote-state.tf (100%) rename modules/{eks => }/efs/variables.tf (100%) rename modules/{eks => }/efs/versions.tf (100%) delete mode 100644 modules/eks/efs/default.auto.tfvars diff --git a/modules/eks/efs/README.md b/modules/efs/README.md similarity index 99% rename from modules/eks/efs/README.md rename to modules/efs/README.md index 523b7f45e..43118870a 100644 --- a/modules/eks/efs/README.md +++ b/modules/efs/README.md @@ -40,7 +40,7 @@ components: | [efs](#module\_efs) | cloudposse/efs/aws | 0.32.7 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [gbl\_dns\_delegated](#module\_gbl\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_efs](#module\_kms\_key\_efs) | cloudposse/kms-key/aws | 0.12.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | diff --git a/modules/eks/efs/context.tf b/modules/efs/context.tf similarity index 100% rename from modules/eks/efs/context.tf rename to modules/efs/context.tf diff --git a/modules/eks/efs/main.tf b/modules/efs/main.tf similarity index 100% rename from modules/eks/efs/main.tf rename to modules/efs/main.tf diff --git a/modules/eks/efs/outputs.tf b/modules/efs/outputs.tf similarity index 100% rename from modules/eks/efs/outputs.tf rename to modules/efs/outputs.tf diff --git a/modules/eks/efs/providers.tf b/modules/efs/providers.tf similarity index 93% rename from modules/eks/efs/providers.tf rename to modules/efs/providers.tf index c2419aabb..08ee01b2a 100644 --- a/modules/eks/efs/providers.tf +++ b/modules/efs/providers.tf @@ -12,7 +12,7 @@ provider "aws" { } module "iam_roles" { - source = "../../account-map/modules/iam-roles" + source = "../account-map/modules/iam-roles" context = module.this.context } diff --git a/modules/eks/efs/remote-state.tf b/modules/efs/remote-state.tf similarity index 100% rename from modules/eks/efs/remote-state.tf rename to modules/efs/remote-state.tf diff --git a/modules/eks/efs/variables.tf b/modules/efs/variables.tf similarity index 100% rename from modules/eks/efs/variables.tf rename to modules/efs/variables.tf diff --git a/modules/eks/efs/versions.tf b/modules/efs/versions.tf similarity index 100% rename from modules/eks/efs/versions.tf rename to modules/efs/versions.tf diff --git a/modules/eks/efs/default.auto.tfvars b/modules/eks/efs/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/eks/efs/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false From 0b0d04c2bb872f9ec3758ee3159564695d473f8a Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 5 May 2023 14:21:40 -0400 Subject: [PATCH 106/501] fix: eks/efs-controller iam policy updates (#660) --- .../efs-controller/resources/iam_policy_statements.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/eks/efs-controller/resources/iam_policy_statements.yaml b/modules/eks/efs-controller/resources/iam_policy_statements.yaml index d56fbba86..6cd70e71e 100644 --- a/modules/eks/efs-controller/resources/iam_policy_statements.yaml +++ b/modules/eks/efs-controller/resources/iam_policy_statements.yaml @@ -5,14 +5,17 @@ AllowEFSDescribeOnAllResources: actions: - elasticfilesystem:DescribeAccessPoints - elasticfilesystem:DescribeFileSystems + - elasticfilesystem:DescribeMountTargets + - ec2:DescribeAvailabilityZones resources: ["*"] conditions: [] -AllowConditionalEFSCreateAccessPoint: - sid: "AllowConditionalEFSCreateAccessPoint" +AllowConditionalEFSAccess: + sid: "AllowConditionalEFSAccess" effect: Allow actions: - elasticfilesystem:CreateAccessPoint + - elasticfilesystem:TagResource resources: ["*"] conditions: - test: "StringLike" From 2b2f4eeb647e6400de5a0a5b5aaef967077389e8 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 5 May 2023 14:46:06 -0400 Subject: [PATCH 107/501] fix: remove stray component.yaml in lambda (#661) --- modules/lambda/component.yaml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 modules/lambda/component.yaml diff --git a/modules/lambda/component.yaml b/modules/lambda/component.yaml deleted file mode 100644 index 6d8efc69f..000000000 --- a/modules/lambda/component.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# 'lambda' component vendoring config - -# 'component.yaml' in the component folder is processed by the 'atmos' commands -# 'atmos vendor pull -c lambda' or 'atmos vendor pull --component lambda' - -apiVersion: atmos/v1 -kind: ComponentVendorConfig -spec: - source: - uri: github.com/cloudposse/terraform-aws-components.git//modules/lambda?ref={{ .Version }} - version: 1.122.0 - included_paths: - - "**/**" - excluded_paths: [] From 4fe7c8e412b2bd5a34372d8504b6483cc69a2e8d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 5 May 2023 15:16:23 -0700 Subject: [PATCH 108/501] `elasticsearch` Corrections (#662) --- modules/elasticsearch/README.md | 51 +++++----- modules/elasticsearch/context.tf | 111 ++++++++++++++++++---- modules/elasticsearch/default.auto.tfvars | 41 -------- modules/elasticsearch/main.tf | 4 +- modules/elasticsearch/providers.tf | 18 +++- modules/elasticsearch/remote-state.tf | 8 +- modules/elasticsearch/versions.tf | 4 +- 7 files changed, 143 insertions(+), 94 deletions(-) delete mode 100644 modules/elasticsearch/default.auto.tfvars diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index 7eccffd95..25b995986 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -11,12 +11,13 @@ Here's an example snippet for how to use this component. ```yaml components: terraform: - elasticache-redis: + elasticsearch: vars: enabled: true + name: foobar instance_type: "t3.medium.elasticsearch" elasticsearch_version: "7.9" - encrypt_at_rest_enabled: false + encrypt_at_rest_enabled: true dedicated_master_enabled: false elasticsearch_subdomain_name: "es" kibana_subdomain_name: "kibana" @@ -31,15 +32,15 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 3.8 | +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | | [random](#requirement\_random) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 3.8 | +| [aws](#provider\_aws) | >= 4.9.0 | | [random](#provider\_random) | >= 3.0 | ## Modules @@ -47,10 +48,10 @@ components: | Name | Source | Version | |------|--------|---------| | [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [elasticsearch](#module\_elasticsearch) | cloudposse/elasticsearch/aws | 0.33.0 | -| [elasticsearch\_log\_cleanup](#module\_elasticsearch\_log\_cleanup) | cloudposse/lambda-elasticsearch-cleanup/aws | 0.12.3 | +| [elasticsearch](#module\_elasticsearch) | cloudposse/elasticsearch/aws | 0.42.0 | +| [elasticsearch\_log\_cleanup](#module\_elasticsearch\_log\_cleanup) | cloudposse/lambda-elasticsearch-cleanup/aws | 0.13.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [this](#module\_this) | cloudposse/label/null | 0.24.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources @@ -66,12 +67,13 @@ components: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | -| [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_iam\_service\_linked\_role](#input\_create\_iam\_service\_linked\_role) | Whether to create `AWSServiceRoleForAmazonElasticsearchService` service-linked role.
Set this to `false` if you already have an ElasticSearch cluster created in the AWS account and `AWSServiceRoleForAmazonElasticsearchService` already exists.
See https://github.com/terraform-providers/terraform-provider-aws/issues/5218 for more information. | `bool` | n/a | yes | | [dedicated\_master\_enabled](#input\_dedicated\_master\_enabled) | Indicates whether dedicated master nodes are enabled for the cluster | `bool` | n/a | yes | -| [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [domain\_hostname\_enabled](#input\_domain\_hostname\_enabled) | Explicit flag to enable creating a DNS hostname for ES. If `true`, then `var.dns_zone_id` is required. | `bool` | n/a | yes | | [ebs\_volume\_size](#input\_ebs\_volume\_size) | EBS volumes for data storage in GB | `number` | n/a | yes | | [elasticsearch\_iam\_actions](#input\_elasticsearch\_iam\_actions) | List of actions to allow for the IAM roles, _e.g._ `es:ESHttpGet`, `es:ESHttpPut`, `es:ESHttpPost` | `list(string)` |
[
"es:ESHttpGet",
"es:ESHttpPut",
"es:ESHttpPost",
"es:ESHttpHead",
"es:Describe*",
"es:List*"
]
| no | @@ -81,21 +83,24 @@ components: | [elasticsearch\_version](#input\_elasticsearch\_version) | Version of Elasticsearch to deploy (\_e.g.\_ `7.1`, `6.8`, `6.7`, `6.5`, `6.4`, `6.3`, `6.2`, `6.0`, `5.6`, `5.5`, `5.3`, `5.1`, `2.3`, `1.5` | `string` | n/a | yes | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [encrypt\_at\_rest\_enabled](#input\_encrypt\_at\_rest\_enabled) | Whether to enable encryption at rest | `bool` | n/a | yes | -| [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | IAM Profile to use when importing a resource | `string` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_type](#input\_instance\_type) | The type of the instance | `string` | n/a | yes | | [kibana\_hostname\_enabled](#input\_kibana\_hostname\_enabled) | Explicit flag to enable creating a DNS hostname for Kibana. If `true`, then `var.dns_zone_id` is required. | `bool` | n/a | yes | | [kibana\_subdomain\_name](#input\_kibana\_subdomain\_name) | The name of the subdomain for Kibana in the DNS zone (\_e.g.\_ `kibana`, `ui`, `ui-es`, `search-ui`, `kibana.elasticsearch`) | `string` | n/a | yes | -| [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | -| [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | -| [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS region | `string` | n/a | yes | -| [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | ## Outputs diff --git a/modules/elasticsearch/context.tf b/modules/elasticsearch/context.tf index d4bf134dd..5e0ef8856 100644 --- a/modules/elasticsearch/context.tf +++ b/modules/elasticsearch/context.tf @@ -8,6 +8,8 @@ # Cloud Posse's standard configuration inputs suitable for passing # to Cloud Posse modules. # +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# # Modules should access the whole context as `module.this.context` # to get the input variables with nulls for defaults, # for example `context = module.this.context`, @@ -20,10 +22,11 @@ module "this" { source = "cloudposse/label/null" - version = "0.24.1" # requires Terraform >= 0.13.0 + version = "0.25.0" # requires Terraform >= 0.13.0 enabled = var.enabled namespace = var.namespace + tenant = var.tenant environment = var.environment stage = var.stage name = var.name @@ -36,6 +39,8 @@ module "this" { id_length_limit = var.id_length_limit label_key_case = var.label_key_case label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags context = var.context } @@ -47,6 +52,7 @@ variable "context" { default = { enabled = true namespace = null + tenant = null environment = null stage = null name = null @@ -59,6 +65,15 @@ variable "context" { id_length_limit = null label_key_case = null label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] } description = <<-EOT Single object for setting entire context at once. @@ -88,32 +103,42 @@ variable "enabled" { variable "namespace" { type = string default = null - description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" } variable "environment" { type = string default = null - description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" } variable "stage" { type = string default = null - description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" } variable "name" { type = string default = null - description = "Solution name, e.g. 'app' or 'jenkins'" + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT } variable "delimiter" { type = string default = null description = <<-EOT - Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. + Delimiter to be used between ID elements. Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. EOT } @@ -121,36 +146,64 @@ variable "delimiter" { variable "attributes" { type = list(string) default = [] - description = "Additional attributes (e.g. `1`)" + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT } variable "tags" { type = map(string) default = {} - description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT } variable "additional_tag_map" { type = map(string) default = {} - description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT } variable "label_order" { type = list(string) default = null description = <<-EOT - The naming order of the id output and Name tag. + The order in which the labels (ID elements) appear in the `id`. Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 5 elements, but at least one must be present. - EOT + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT } variable "regex_replace_chars" { type = string default = null description = <<-EOT - Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. EOT } @@ -161,7 +214,7 @@ variable "id_length_limit" { description = <<-EOT Limit `id` to this many characters (minimum 6). Set to `0` for unlimited length. - Set to `null` for default, which is `0`. + Set to `null` for keep the existing setting, which defaults to `0`. Does not affect `id_full`. EOT validation { @@ -174,7 +227,8 @@ variable "label_key_case" { type = string default = null description = <<-EOT - The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. Possible values: `lower`, `title`, `upper`. Default value: `title`. EOT @@ -189,8 +243,11 @@ variable "label_value_case" { type = string default = null description = <<-EOT - The letter case of output label values (also used in `tags` and `id`). + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. Default value: `lower`. EOT @@ -199,4 +256,24 @@ variable "label_value_case" { error_message = "Allowed values: `lower`, `title`, `upper`, `none`." } } -#### End of copy of cloudposse/terraform-null-label/variables.tf \ No newline at end of file + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/elasticsearch/default.auto.tfvars b/modules/elasticsearch/default.auto.tfvars deleted file mode 100644 index 9fd27e55a..000000000 --- a/modules/elasticsearch/default.auto.tfvars +++ /dev/null @@ -1,41 +0,0 @@ -enabled = false - -name = "es" - -instance_type = "t3.medium.elasticsearch" - -elasticsearch_version = "7.9" - -# calculated: length(local.vpc_private_subnet_ids) -# instance_count = 2 - -# calculated: length(local.vpc_private_subnet_ids) > 1 ? true : false -# zone_awareness_enabled = true - -encrypt_at_rest_enabled = false - -dedicated_master_enabled = false - -elasticsearch_subdomain_name = "es" - -kibana_subdomain_name = "kibana" - -ebs_volume_size = 40 - -create_iam_service_linked_role = true - -kibana_hostname_enabled = true - -domain_hostname_enabled = true - -# Allow anonymous access without request signing, relying on network access controls -# https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-ac.html#es-ac-types-ip -# https://aws.amazon.com/premiumsupport/knowledge-center/anonymous-not-authorized-elasticsearch/ -elasticsearch_iam_role_arns = [ - "*", -] -elasticsearch_iam_actions = [ - "es:ESHttpGet", "es:ESHttpPut", "es:ESHttpPost", "es:ESHttpHead", "es:Describe*", "es:List*", - // delete and patch are destructive and could be left out - "es:ESHttpDelete", "es:ESHttpPatch" -] diff --git a/modules/elasticsearch/main.tf b/modules/elasticsearch/main.tf index 2e4c38eed..28577a6bd 100644 --- a/modules/elasticsearch/main.tf +++ b/modules/elasticsearch/main.tf @@ -18,7 +18,7 @@ locals { module "elasticsearch" { source = "cloudposse/elasticsearch/aws" - version = "0.33.0" + version = "0.42.0" security_groups = [local.vpc_default_security_group] vpc_id = local.vpc_id @@ -99,7 +99,7 @@ resource "aws_ssm_parameter" "elasticsearch_kibana_endpoint" { module "elasticsearch_log_cleanup" { source = "cloudposse/lambda-elasticsearch-cleanup/aws" - version = "0.12.3" + version = "0.13.0" es_endpoint = module.elasticsearch.domain_endpoint es_domain_arn = module.elasticsearch.domain_arn diff --git a/modules/elasticsearch/providers.tf b/modules/elasticsearch/providers.tf index 908fbd595..08ee01b2a 100644 --- a/modules/elasticsearch/providers.tf +++ b/modules/elasticsearch/providers.tf @@ -1,8 +1,14 @@ provider "aws" { region = var.region - # `terraform import` will not use data from a data source, so on import we have to explicitly specify the profile - profile = coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } } module "iam_roles" { @@ -13,5 +19,11 @@ module "iam_roles" { variable "import_profile_name" { type = string default = null - description = "IAM Profile to use when importing a resource" + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" } diff --git a/modules/elasticsearch/remote-state.tf b/modules/elasticsearch/remote-state.tf index 3c2873007..1d2ce7056 100644 --- a/modules/elasticsearch/remote-state.tf +++ b/modules/elasticsearch/remote-state.tf @@ -2,20 +2,16 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" - stack_config_local_path = "../../../stacks" - component = "vpc" + component = "vpc" context = module.this.context - enabled = true } module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" - stack_config_local_path = "../../../stacks" - component = "dns-delegated" + component = "dns-delegated" context = module.this.context - enabled = true } diff --git a/modules/elasticsearch/versions.tf b/modules/elasticsearch/versions.tf index f386dc252..4a6389362 100644 --- a/modules/elasticsearch/versions.tf +++ b/modules/elasticsearch/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 0.13.0" + required_version = ">= 1.0.0" required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.8" + version = ">= 4.9.0" } random = { source = "hashicorp/random" From 45fee37879148d3ec52d8ff9f8a24c8d17a07419 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Tue, 9 May 2023 18:40:18 +0300 Subject: [PATCH 109/501] [ecs-service] Added IAM policies for ecspresso deployments (#659) Co-authored-by: cloudpossebot Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- modules/ecs-service/README.md | 7 +- .../ecs-service/github-actions-iam-policy.tf | 83 +++++++++++++++++-- modules/ecs-service/versions.tf | 12 ++- 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index f8c393afc..051e2ad24 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -150,14 +150,14 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 4.0 | +| [aws](#requirement\_aws) | >= 4.66.1 | | [template](#requirement\_template) | >= 2.2 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.0 | +| [aws](#provider\_aws) | >= 4.66.1 | | [template](#provider\_template) | >= 2.2 | ## Modules @@ -192,6 +192,8 @@ components: | [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_kinesis_stream.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_stream) | resource | +| [aws_iam_policy_document.github_actions_iam_ecspresso_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.github_actions_iam_platform_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_kms_alias.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | @@ -236,6 +238,7 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [github\_actions\_allowed\_repos](#input\_github\_actions\_allowed\_repos) | A list of the GitHub repositories that are allowed to assume this role from GitHub Actions. For example,
["cloudposse/infra-live"]. Can contain "*" as wildcard.
If org part of repo name is omitted, "cloudposse" will be assumed. | `list(string)` | `[]` | no | +| [github\_actions\_ecspresso\_enabled](#input\_github\_actions\_ecspresso\_enabled) | Create IAM policies required for deployments with Ecspresso | `bool` | `false` | no | | [github\_actions\_iam\_role\_attributes](#input\_github\_actions\_iam\_role\_attributes) | Additional attributes to add to the role name | `list(string)` | `[]` | no | | [github\_actions\_iam\_role\_enabled](#input\_github\_actions\_iam\_role\_enabled) | Flag to toggle creation of an IAM Role that GitHub Actions can assume to access AWS resources | `bool` | `false` | no | | [github\_oidc\_trusted\_role\_arns](#input\_github\_oidc\_trusted\_role\_arns) | A list of IAM Role ARNs allowed to assume this cluster's GitHub OIDC role | `list(string)` | `[]` | no | diff --git a/modules/ecs-service/github-actions-iam-policy.tf b/modules/ecs-service/github-actions-iam-policy.tf index e2ea48b9d..d2158b0db 100644 --- a/modules/ecs-service/github-actions-iam-policy.tf +++ b/modules/ecs-service/github-actions-iam-policy.tf @@ -4,23 +4,38 @@ variable "github_oidc_trusted_role_arns" { default = [] } +variable "github_actions_ecspresso_enabled" { + type = bool + description = "Create IAM policies required for deployments with Ecspresso" + default = false +} + locals { github_actions_iam_policy = data.aws_iam_policy_document.github_actions_iam_policy.json } data "aws_iam_policy_document" "github_actions_iam_policy" { + source_policy_documents = compact([ + data.aws_iam_policy_document.github_actions_iam_platform_policy.json, + join("", data.aws_iam_policy_document.github_actions_iam_ecspresso_policy.*.json) + ]) +} + +data "aws_iam_policy_document" "github_actions_iam_platform_policy" { # Allows trusted roles to assume this role - statement { - sid = "TrustedRoleAccess" - effect = "Allow" - actions = [ - "sts:AssumeRole", - "sts:TagSession" - ] - resources = var.github_oidc_trusted_role_arns + dynamic "statement" { + for_each = length(var.github_oidc_trusted_role_arns) == 0 ? [] : ["enabled"] + content { + sid = "TrustedRoleAccess" + effect = "Allow" + actions = [ + "sts:AssumeRole", + "sts:TagSession" + ] + resources = var.github_oidc_trusted_role_arns + } } - # Allow chamber to read secrets statement { sid = "AllowKMSAccess" effect = "Allow" @@ -28,6 +43,7 @@ data "aws_iam_policy_document" "github_actions_iam_policy" { "kms:Decrypt", "kms:DescribeKey" ] + #bridgecrew:skip=BC_AWS_IAM_57:OK Allow to Decrypt with any key. resources = [ "*" ] @@ -51,8 +67,57 @@ data "aws_iam_policy_document" "github_actions_iam_policy" { "ssm:DescribeParameters", "ssm:GetParametersByPath" ] + #bridgecrew:skip=BC_AWS_IAM_57:OK Allow to read from any ssm parameter store for chamber. resources = [ "*" ] } } + +data "aws_iam_policy_document" "github_actions_iam_ecspresso_policy" { + count = var.github_actions_ecspresso_enabled ? 1 : 0 + + statement { + effect = "Allow" + actions = [ + "ecs:DescribeServices", + "ecs:UpdateService" + ] + resources = [ + join("", module.ecs_alb_service_task.*.service_arn) + ] + } + + statement { + effect = "Allow" + actions = [ + "ecs:RegisterTaskDefinition" + ] + resources = [ + "*" + ] + } + + statement { + effect = "Allow" + actions = [ + "iam:PassRole" + ] + resources = [ + join("", module.ecs_alb_service_task.*.task_exec_role_arn), + join("", module.ecs_alb_service_task.*.task_role_arn), + ] + } + + statement { + effect = "Allow" + actions = [ + "application-autoscaling:DescribeScalableTargets" + ] + resources = [ + "*", + ] + } + + +} diff --git a/modules/ecs-service/versions.tf b/modules/ecs-service/versions.tf index 7a90cef78..697261ff1 100644 --- a/modules/ecs-service/versions.tf +++ b/modules/ecs-service/versions.tf @@ -3,8 +3,16 @@ terraform { required_providers { aws = { - source = "hashicorp/aws" - version = ">= 4.0" + source = "hashicorp/aws" + ## 4.66.0 version cause error + ## │ Error: listing tags for Application Auto Scaling Target (): InvalidParameter: 1 validation error(s) found. + ## │ - minimum field size of 1, ListTagsForResourceInput.ResourceARN. + ## │ + ## │ + ## │ with module.ecs_cloudwatch_autoscaling[0].aws_appautoscaling_target.default[0], + ## │ on .terraform/modules/ecs_cloudwatch_autoscaling/main.tf line 15, in resource "aws_appautoscaling_target" "default": + ## │ 15: resource "aws_appautoscaling_target" "default" { + version = ">= 4.66.1" } template = { source = "cloudposse/template" From b7bf880bc4d0c8507fd3e982baf6efc3fe6bf02f Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Tue, 9 May 2023 11:59:43 -0400 Subject: [PATCH 110/501] Add `route53-resolver-dns-firewall` and `network-firewall` components (#651) --- modules/network-firewall/README.md | 330 ++++++++++++++++++ modules/network-firewall/context.tf | 279 +++++++++++++++ modules/network-firewall/main.tf | 55 +++ modules/network-firewall/outputs.tf | 29 ++ modules/network-firewall/providers.tf | 29 ++ modules/network-firewall/remote-state.tf | 40 +++ modules/network-firewall/variables.tf | 113 ++++++ modules/network-firewall/versions.tf | 10 + .../route53-resolver-dns-firewall/README.md | 163 +++++++++ .../config/allowed_domains.txt | 1 + .../config/blocked_domains.txt | 48 +++ .../route53-resolver-dns-firewall/context.tf | 279 +++++++++++++++ modules/route53-resolver-dns-firewall/main.tf | 25 ++ .../route53-resolver-dns-firewall/outputs.tf | 24 ++ .../providers.tf | 29 ++ .../remote-state.tf | 24 ++ .../variables.tf | 68 ++++ .../route53-resolver-dns-firewall/versions.tf | 10 + 18 files changed, 1556 insertions(+) create mode 100644 modules/network-firewall/README.md create mode 100644 modules/network-firewall/context.tf create mode 100644 modules/network-firewall/main.tf create mode 100644 modules/network-firewall/outputs.tf create mode 100644 modules/network-firewall/providers.tf create mode 100644 modules/network-firewall/remote-state.tf create mode 100644 modules/network-firewall/variables.tf create mode 100644 modules/network-firewall/versions.tf create mode 100644 modules/route53-resolver-dns-firewall/README.md create mode 100644 modules/route53-resolver-dns-firewall/config/allowed_domains.txt create mode 100644 modules/route53-resolver-dns-firewall/config/blocked_domains.txt create mode 100644 modules/route53-resolver-dns-firewall/context.tf create mode 100644 modules/route53-resolver-dns-firewall/main.tf create mode 100644 modules/route53-resolver-dns-firewall/outputs.tf create mode 100644 modules/route53-resolver-dns-firewall/providers.tf create mode 100644 modules/route53-resolver-dns-firewall/remote-state.tf create mode 100644 modules/route53-resolver-dns-firewall/variables.tf create mode 100644 modules/route53-resolver-dns-firewall/versions.tf diff --git a/modules/network-firewall/README.md b/modules/network-firewall/README.md new file mode 100644 index 000000000..c7229393f --- /dev/null +++ b/modules/network-firewall/README.md @@ -0,0 +1,330 @@ +# Component: `network-firewall` + +This component is responsible for provisioning [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, +including Network Firewall, firewall policy, rule groups, and logging configuration. + +## Usage + +**Stack Level**: Regional + +Example of a Network Firewall with stateful 5-tuple rules: + +:::info + +The "5-tuple" means the five items (columns) that each rule (row, or tuple) in a firewall policy uses to define whether to block or allow traffic: +source and destination IP, source and destination port, and protocol. + +Refer to [Standard stateful rule groups in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-basic.html) +for more details. + +::: + +```yaml +components: + terraform: + network-firewall: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: network-firewall + # The name of a VPC component where the Network Firewall is provisioned + vpc_component_name: vpc + firewall_subnet_name: "firewall" + stateful_default_actions: + - "aws:alert_strict" + stateless_default_actions: + - "aws:forward_to_sfe" + stateless_fragment_default_actions: + - "aws:forward_to_sfe" + stateless_custom_actions: [] + delete_protection: false + firewall_policy_change_protection: false + subnet_change_protection: false + logging_config: [] + rule_group_config: + stateful-packet-inspection: + capacity: 50 + name: stateful-packet-inspection + description: "Stateful inspection of packets" + type: "STATEFUL" + rule_group: + stateful_rule_options: + rule_order: "STRICT_ORDER" + rules_source: + stateful_rule: + - action: "DROP" + header: + destination: "124.1.1.24/32" + destination_port: 53 + direction: "ANY" + protocol: "TCP" + source: "1.2.3.4/32" + source_port: 53 + rule_option: + keyword: "sid:1" + - action: "PASS" + header: + destination: "ANY" + destination_port: "ANY" + direction: "ANY" + protocol: "TCP" + source: "10.10.192.0/19" + source_port: "ANY" + rule_option: + keyword: "sid:2" + - action: "PASS" + header: + destination: "ANY" + destination_port: "ANY" + direction: "ANY" + protocol: "TCP" + source: "10.10.224.0/19" + source_port: "ANY" + rule_option: + keyword: "sid:3" +``` + +Example of a Network Firewall with [Suricata](https://suricata.readthedocs.io/en/suricata-6.0.0/rules/) rules: + +:::info + +For [Suricata](https://suricata.io/) rule group type, you provide match and action settings in a string, in a Suricata compatible specification. +The specification fully defines what the stateful rules engine looks for in a traffic flow and the action to take on the packets in a flow that matches the inspection criteria. + +Refer to [Suricata compatible rule strings in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-suricata.html) +for more details. + +::: + +```yaml +components: + terraform: + network-firewall: + metadata: + component: "network-firewall" + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: "network-firewall" + + # The name of a VPC component where the Network Firewall is provisioned + vpc_component_name: "vpc" + firewall_subnet_name: "firewall" + + delete_protection: false + firewall_policy_change_protection: false + subnet_change_protection: false + + # Logging config + logging_enabled: true + flow_logs_bucket_component_name: "network-firewall-logs-bucket-flow" + alert_logs_bucket_component_name: "network-firewall-logs-bucket-alert" + + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateless-default-actions.html + # https://docs.aws.amazon.com/network-firewall/latest/APIReference/API_FirewallPolicy.html + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-action.html#rule-action-stateless + stateless_default_actions: + - "aws:forward_to_sfe" + stateless_fragment_default_actions: + - "aws:forward_to_sfe" + stateless_custom_actions: [] + + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-strict-rule-evaluation-order.html + # https://github.com/aws-samples/aws-network-firewall-strict-rule-ordering-terraform + policy_stateful_engine_options_rule_order: "STRICT_ORDER" + + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-default-actions.html + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-default-rule-evaluation-order + # https://docs.aws.amazon.com/network-firewall/latest/APIReference/API_FirewallPolicy.html + stateful_default_actions: + - "aws:alert_established" + # - "aws:alert_strict" + # - "aws:drop_established" + # - "aws:drop_strict" + + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-groups.html + rule_group_config: + stateful-inspection: + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-group-managing.html#nwfw-rule-group-capacity + # For stateful rules, `capacity` means the max number of rules in the rule group + capacity: 1000 + name: "stateful-inspection" + description: "Stateful inspection of packets" + type: "STATEFUL" + + rule_group: + rule_variables: + port_sets: [] + ip_sets: + - key: "CIDR_1" + definition: + - "10.10.0.0/11" + - key: "CIDR_2" + definition: + - "10.11.0.0/11" + - key: "SCANNER" + definition: + - "10.12.48.186/32" + # bad actors + - key: "BLOCKED_LIST" + definition: + - "193.142.146.35/32" + - "69.40.195.236/32" + - "125.17.153.207/32" + - "185.220.101.4/32" + - "195.219.212.151/32" + - "162.247.72.199/32" + - "147.185.254.17/32" + - "179.60.147.101/32" + - "157.230.244.66/32" + - "192.99.4.116/32" + - "62.102.148.69/32" + - "185.129.62.62/32" + + stateful_rule_options: + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html#suricata-strict-rule-evaluation-order.html + # All the stateful rule groups are provided to the rule engine as Suricata compatible strings + # Suricata can evaluate stateful rule groups by using the default rule group ordering method, + # or you can set an exact order using the strict ordering method. + # The settings for your rule groups must match the settings for the firewall policy that they belong to. + # With strict ordering, the rule groups are evaluated by order of priority, starting from the lowest number, + # and the rules in each rule group are processed in the order in which they're defined. + rule_order: "STRICT_ORDER" + + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-how-to-provide-rules.html + rules_source: + + # Suricata rules for the rule group + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html + # https://github.com/aws-samples/aws-network-firewall-terraform/blob/main/firewall.tf#L66 + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-suricata.html + # https://coralogix.com/blog/writing-effective-suricata-rules-for-the-sta/ + # https://suricata.readthedocs.io/en/suricata-6.0.10/rules/intro.html + # https://suricata.readthedocs.io/en/suricata-6.0.0/rules/header-keywords.html + # https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-action.html + # + # With Strict evaluation order, the rules in each rule group are processed in the order in which they're defined + # + # Pass – Discontinue inspection of the matching packet and permit it to go to its intended destination + # + # Drop or Alert – Evaluate the packet against all rules with drop or alert action settings. + # If the firewall has alert logging configured, send a message to the firewall's alert logs for each matching rule. + # The first log entry for the packet will be for the first rule that matched the packet. + # After all rules have been evaluated, handle the packet according to the action setting in the first rule that matched the packet. + # If the first rule has a drop action, block the packet. If it has an alert action, continue evaluation. + # + # Reject – Drop traffic that matches the conditions of the stateful rule and send a TCP reset packet back to sender of the packet. + # A TCP reset packet is a packet with no payload and a RST bit contained in the TCP header flags. + # Reject is available only for TCP traffic. This option doesn't support FTP and IMAP protocols. + rules_string: | + alert ip $BLOCKED_LIST any <> any any ( msg:"Alert on blocked traffic"; sid:100; rev:1; ) + drop ip $BLOCKED_LIST any <> any any ( msg:"Blocked blocked traffic"; sid:200; rev:1; ) + + pass ip $SCANNER any -> any any ( msg: "Allow scanner"; sid:300; rev:1; ) + + alert ip $CIDR_1 any -> $CIDR_2 any ( msg:"Alert on CIDR_1 to CIDR_2 traffic"; sid:400; rev:1; ) + drop ip $CIDR_1 any -> $CIDR_2 any ( msg:"Blocked CIDR_1 to CIDR_2 traffic"; sid:410; rev:1; ) + + pass ip any any <> any any ( msg: "Allow general traffic"; sid:10000; rev:1; ) +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [alert\_logs\_bucket](#module\_alert\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [flow\_logs\_bucket](#module\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [network\_firewall](#module\_network\_firewall) | cloudposse/network-firewall/aws | 0.3.2 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alert\_logs\_bucket\_component\_name](#input\_alert\_logs\_bucket\_component\_name) | Alert logs bucket component name | `string` | `null` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delete\_protection](#input\_delete\_protection) | A boolean flag indicating whether it is possible to delete the firewall | `bool` | `false` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [firewall\_policy\_change\_protection](#input\_firewall\_policy\_change\_protection) | A boolean flag indicating whether it is possible to change the associated firewall policy | `bool` | `false` | no | +| [firewall\_subnet\_name](#input\_firewall\_subnet\_name) | Firewall subnet name | `string` | `"firewall"` | no | +| [flow\_logs\_bucket\_component\_name](#input\_flow\_logs\_bucket\_component\_name) | Flow logs bucket component name | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [logging\_enabled](#input\_logging\_enabled) | Flag to enable/disable Network Firewall Flow and Alert Logs | `bool` | `false` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [network\_firewall\_description](#input\_network\_firewall\_description) | AWS Network Firewall description. If not provided, the Network Firewall name will be used | `string` | `null` | no | +| [network\_firewall\_name](#input\_network\_firewall\_name) | Friendly name to give the Network Firewall. If not provided, the name will be derived from the context.
Changing the name will cause the Firewall to be deleted and recreated. | `string` | `null` | no | +| [network\_firewall\_policy\_name](#input\_network\_firewall\_policy\_name) | Friendly name to give the Network Firewall policy. If not provided, the name will be derived from the context.
Changing the name will cause the policy to be deleted and recreated. | `string` | `null` | no | +| [policy\_stateful\_engine\_options\_rule\_order](#input\_policy\_stateful\_engine\_options\_rule\_order) | Indicates how to manage the order of stateful rule evaluation for the policy. Valid values: DEFAULT\_ACTION\_ORDER, STRICT\_ORDER | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [rule\_group\_config](#input\_rule\_group\_config) | Rule group configuration. Refer to [networkfirewall\_rule\_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_rule_group) for configuration details | `any` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [stateful\_default\_actions](#input\_stateful\_default\_actions) | Default stateful actions | `list(string)` |
[
"aws:alert_strict"
]
| no | +| [stateless\_custom\_actions](#input\_stateless\_custom\_actions) | Set of configuration blocks describing the custom action definitions that are available for use in the firewall policy's `stateless_default_actions` |
list(object({
action_name = string
dimensions = list(string)
}))
| `[]` | no | +| [stateless\_default\_actions](#input\_stateless\_default\_actions) | Default stateless actions | `list(string)` |
[
"aws:forward_to_sfe"
]
| no | +| [stateless\_fragment\_default\_actions](#input\_stateless\_fragment\_default\_actions) | Default stateless actions for fragmented packets | `list(string)` |
[
"aws:forward_to_sfe"
]
| no | +| [subnet\_change\_protection](#input\_subnet\_change\_protection) | A boolean flag indicating whether it is possible to change the associated subnet(s) | `bool` | `false` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | The name of a VPC component where the Network Firewall is provisioned | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [az\_subnet\_endpoint\_stats](#output\_az\_subnet\_endpoint\_stats) | List of objects with each object having three items: AZ, subnet ID, VPC endpoint ID | +| [network\_firewall\_arn](#output\_network\_firewall\_arn) | Network Firewall ARN | +| [network\_firewall\_name](#output\_network\_firewall\_name) | Network Firewall name | +| [network\_firewall\_policy\_arn](#output\_network\_firewall\_policy\_arn) | Network Firewall policy ARN | +| [network\_firewall\_policy\_name](#output\_network\_firewall\_policy\_name) | Network Firewall policy name | +| [network\_firewall\_status](#output\_network\_firewall\_status) | Nested list of information about the current status of the Network Firewall | + + +## References + +- [Deploy centralized traffic filtering using AWS Network Firewall](https://aws.amazon.com/blogs/networking-and-content-delivery/deploy-centralized-traffic-filtering-using-aws-network-firewall) +- [AWS Network Firewall – New Managed Firewall Service in VPC](https://aws.amazon.com/blogs/aws/aws-network-firewall-new-managed-firewall-service-in-vpc) +- [Deployment models for AWS Network Firewall](https://aws.amazon.com/blogs/networking-and-content-delivery/deployment-models-for-aws-network-firewall) +- [Deployment models for AWS Network Firewall with VPC routing enhancements](https://aws.amazon.com/blogs/networking-and-content-delivery/deployment-models-for-aws-network-firewall-with-vpc-routing-enhancements) +- [Inspection Deployment Models with AWS Network Firewall](https://d1.awsstatic.com/architecture-diagrams/ArchitectureDiagrams/inspection-deployment-models-with-AWS-network-firewall-ra.pdf) +- [How to deploy AWS Network Firewall by using AWS Firewall Manager](https://aws.amazon.com/blogs/security/how-to-deploy-aws-network-firewall-by-using-aws-firewall-manager) +- [A Deep Dive into AWS Transit Gateway](https://www.youtube.com/watch?v=a55Iud-66q0) +- [Appliance in a shared services VPC](https://docs.aws.amazon.com/vpc/latest/tgw/transit-gateway-appliance-scenario.html) +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/network-firewall/context.tf b/modules/network-firewall/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/network-firewall/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/network-firewall/main.tf b/modules/network-firewall/main.tf new file mode 100644 index 000000000..5379a76d4 --- /dev/null +++ b/modules/network-firewall/main.tf @@ -0,0 +1,55 @@ +locals { + enabled = module.this.enabled + logging_enabled = local.enabled && var.logging_enabled + + # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_logging_configuration + logging_config = local.logging_enabled ? { + flow = { + log_destination_type = "S3" + log_type = "FLOW" + log_destination = { + bucketName = try(module.flow_logs_bucket.outputs.bucket_id, "") + prefix = null + } + }, + alert = { + log_destination_type = "S3" + log_type = "ALERT" + log_destination = { + bucketName = try(module.alert_logs_bucket.outputs.bucket_id, "") + prefix = null + } + } + } : {} + + vpc_outputs = module.vpc.outputs + firewall_subnet_ids = local.vpc_outputs.named_private_subnets_map[var.firewall_subnet_name] +} + +module "network_firewall" { + source = "cloudposse/network-firewall/aws" + version = "0.3.2" + + vpc_id = local.vpc_outputs.vpc_id + subnet_ids = local.firewall_subnet_ids + + network_firewall_name = var.network_firewall_name + network_firewall_description = var.network_firewall_description + network_firewall_policy_name = var.network_firewall_policy_name + policy_stateful_engine_options_rule_order = var.policy_stateful_engine_options_rule_order + stateful_default_actions = var.stateful_default_actions + stateless_default_actions = var.stateless_default_actions + stateless_fragment_default_actions = var.stateless_fragment_default_actions + stateless_custom_actions = var.stateless_custom_actions + delete_protection = var.delete_protection + firewall_policy_change_protection = var.firewall_policy_change_protection + subnet_change_protection = var.subnet_change_protection + + # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_logging_configuration + logging_config = local.logging_config + + # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_rule_group + rule_group_config = var.rule_group_config + + context = module.this.context +} diff --git a/modules/network-firewall/outputs.tf b/modules/network-firewall/outputs.tf new file mode 100644 index 000000000..e9d0c1e94 --- /dev/null +++ b/modules/network-firewall/outputs.tf @@ -0,0 +1,29 @@ +output "network_firewall_name" { + description = "Network Firewall name" + value = module.network_firewall.network_firewall_name +} + +output "network_firewall_arn" { + description = "Network Firewall ARN" + value = module.network_firewall.network_firewall_arn +} + +output "network_firewall_status" { + description = "Nested list of information about the current status of the Network Firewall" + value = module.network_firewall.network_firewall_status +} + +output "network_firewall_policy_name" { + description = "Network Firewall policy name" + value = module.network_firewall.network_firewall_policy_name +} + +output "network_firewall_policy_arn" { + description = "Network Firewall policy ARN" + value = module.network_firewall.network_firewall_policy_arn +} + +output "az_subnet_endpoint_stats" { + description = "List of objects with each object having three items: AZ, subnet ID, VPC endpoint ID" + value = module.network_firewall.az_subnet_endpoint_stats +} diff --git a/modules/network-firewall/providers.tf b/modules/network-firewall/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/network-firewall/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/network-firewall/remote-state.tf b/modules/network-firewall/remote-state.tf new file mode 100644 index 000000000..59f6d7cbf --- /dev/null +++ b/modules/network-firewall/remote-state.tf @@ -0,0 +1,40 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.vpc_component_name + + context = module.this.context +} + +module "flow_logs_bucket" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.flow_logs_bucket_component_name + + bypass = !local.logging_enabled || var.flow_logs_bucket_component_name == null || var.flow_logs_bucket_component_name == "" + + defaults = { + bucket_id = "" + bucket_arn = "" + } + + context = module.this.context +} + +module "alert_logs_bucket" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.alert_logs_bucket_component_name + + bypass = !local.logging_enabled || var.alert_logs_bucket_component_name == null || var.alert_logs_bucket_component_name == "" + + defaults = { + bucket_id = "" + bucket_arn = "" + } + + context = module.this.context +} diff --git a/modules/network-firewall/variables.tf b/modules/network-firewall/variables.tf new file mode 100644 index 000000000..36d54b7ca --- /dev/null +++ b/modules/network-firewall/variables.tf @@ -0,0 +1,113 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "vpc_component_name" { + type = string + description = "The name of a VPC component where the Network Firewall is provisioned" +} + +variable "network_firewall_name" { + type = string + description = <<-EOT + Friendly name to give the Network Firewall. If not provided, the name will be derived from the context. + Changing the name will cause the Firewall to be deleted and recreated. + EOT + default = null +} + +variable "network_firewall_description" { + type = string + description = "AWS Network Firewall description. If not provided, the Network Firewall name will be used" + default = null +} + +variable "network_firewall_policy_name" { + type = string + description = <<-EOT + Friendly name to give the Network Firewall policy. If not provided, the name will be derived from the context. + Changing the name will cause the policy to be deleted and recreated. + EOT + default = null +} + +variable "policy_stateful_engine_options_rule_order" { + type = string + description = "Indicates how to manage the order of stateful rule evaluation for the policy. Valid values: DEFAULT_ACTION_ORDER, STRICT_ORDER" + default = null +} + +variable "stateful_default_actions" { + type = list(string) + description = "Default stateful actions" + default = ["aws:alert_strict"] +} + +variable "stateless_default_actions" { + type = list(string) + description = "Default stateless actions" + default = ["aws:forward_to_sfe"] +} + +variable "stateless_fragment_default_actions" { + type = list(string) + description = "Default stateless actions for fragmented packets" + default = ["aws:forward_to_sfe"] +} + +variable "stateless_custom_actions" { + type = list(object({ + action_name = string + dimensions = list(string) + })) + description = "Set of configuration blocks describing the custom action definitions that are available for use in the firewall policy's `stateless_default_actions`" + default = [] +} + +variable "delete_protection" { + type = bool + description = "A boolean flag indicating whether it is possible to delete the firewall" + default = false +} + +variable "firewall_policy_change_protection" { + type = bool + description = "A boolean flag indicating whether it is possible to change the associated firewall policy" + default = false +} + +variable "subnet_change_protection" { + type = bool + description = "A boolean flag indicating whether it is possible to change the associated subnet(s)" + default = false +} + +variable "rule_group_config" { + type = any + description = "Rule group configuration. Refer to [networkfirewall_rule_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_rule_group) for configuration details" +} + +variable "logging_enabled" { + type = bool + description = "Flag to enable/disable Network Firewall Flow and Alert Logs" + default = false +} + +variable "flow_logs_bucket_component_name" { + type = string + description = "Flow logs bucket component name" + default = null +} + +variable "alert_logs_bucket_component_name" { + type = string + description = "Alert logs bucket component name" + default = null +} + +variable "firewall_subnet_name" { + type = string + description = "Firewall subnet name" + default = "firewall" +} diff --git a/modules/network-firewall/versions.tf b/modules/network-firewall/versions.tf new file mode 100644 index 000000000..cc73ffd35 --- /dev/null +++ b/modules/network-firewall/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} diff --git a/modules/route53-resolver-dns-firewall/README.md b/modules/route53-resolver-dns-firewall/README.md new file mode 100644 index 000000000..96a638310 --- /dev/null +++ b/modules/route53-resolver-dns-firewall/README.md @@ -0,0 +1,163 @@ +# Component: `route53-resolver-dns-firewall` + +This component is responsible for provisioning [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) +resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging configuration. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +# stacks/catalog/route53-resolver-dns-firewall/defaults.yaml +components: + terraform: + route53-resolver-dns-firewall/defaults: + metadata: + type: abstract + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + firewall_fail_open: "ENABLED" + query_log_enabled: true + logs_bucket_component_name: "route53-resolver-dns-firewall-logs-bucket" + domains_config: + allowed-domains: + # Concat the lists of domains passed in the `domains` field and loaded from the file `domains_file` + # The file is in the `components/terraform/route53-resolver-dns-firewall/config` folder + domains_file: "config/allowed_domains.txt" + domains: [] + blocked-domains: + # Concat the lists of domains passed in the `domains` field and loaded from the file `domains_file` + # The file is in the `components/terraform/route53-resolver-dns-firewall/config` folder + domains_file: "config/blocked_domains.txt" + domains: [] + rule_groups_config: + blocked-and-allowed-domains: + # 'priority' must be between 100 and 9900 exclusive + priority: 101 + rules: + allowed-domains: + firewall_domain_list_name: "allowed-domains" + # 'priority' must be between 100 and 9900 exclusive + priority: 101 + action: "ALLOW" + blocked-domains: + firewall_domain_list_name: "blocked-domains" + # 'priority' must be between 100 and 9900 exclusive + priority: 200 + action: "BLOCK" + block_response: "NXDOMAIN" +``` + +```yaml +# stacks/mixins/stage/dev.yaml +import: + - catalog/route53-resolver-dns-firewall/defaults + +components: + terraform: + route53-resolver-dns-firewall/example: + metadata: + component: route53-resolver-dns-firewall + inherits: + - route53-resolver-dns-firewall/defaults + vars: + name: route53-dns-firewall-example + vpc_component_name: vpc +``` + +Execute the following command to provision the `route53-resolver-dns-firewall/example` component using Atmos: + +```shell +atmos terraform apply route53-resolver-dns-firewall/example -s +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [logs\_bucket](#module\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [route53\_resolver\_dns\_firewall](#module\_route53\_resolver\_dns\_firewall) | cloudposse/route53-resolver-dns-firewall/aws | 0.2.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [domains\_config](#input\_domains\_config) | Map of Route 53 Resolver DNS Firewall domain configurations |
map(object({
domains = optional(list(string))
domains_file = optional(string)
}))
| n/a | yes | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [firewall\_fail\_open](#input\_firewall\_fail\_open) | Determines how Route 53 Resolver handles queries during failures, for example when all traffic that is sent to DNS Firewall fails to receive a reply.
By default, fail open is disabled, which means the failure mode is closed.
This approach favors security over availability. DNS Firewall blocks queries that it is unable to evaluate properly.
If you enable this option, the failure mode is open. This approach favors availability over security.
In this case, DNS Firewall allows queries to proceed if it is unable to properly evaluate them.
Valid values: ENABLED, DISABLED. | `string` | `"ENABLED"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [logs\_bucket\_component\_name](#input\_logs\_bucket\_component\_name) | Flow logs bucket component name | `string` | `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [query\_log\_config\_name](#input\_query\_log\_config\_name) | Route 53 Resolver query log config name. If omitted, the name will be generated by concatenating the ID from the context with the VPC ID | `string` | `null` | no | +| [query\_log\_enabled](#input\_query\_log\_enabled) | Flag to enable/disable Route 53 Resolver query logging | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [rule\_groups\_config](#input\_rule\_groups\_config) | Rule groups and rules configuration |
map(object({
priority = number
mutation_protection = optional(string)
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule
rules = map(object({
action = string
priority = number
block_override_dns_type = optional(string)
block_override_domain = optional(string)
block_override_ttl = optional(number)
block_response = optional(string)
firewall_domain_list_name = string
}))
}))
| n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | The name of a VPC component where the Network Firewall is provisioned | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [domains](#output\_domains) | Route 53 Resolver DNS Firewall domain configurations | +| [query\_log\_config](#output\_query\_log\_config) | Route 53 Resolver query logging configuration | +| [rule\_group\_associations](#output\_rule\_group\_associations) | Route 53 Resolver DNS Firewall rule group associations | +| [rule\_groups](#output\_rule\_groups) | Route 53 Resolver DNS Firewall rule groups | +| [rules](#output\_rules) | Route 53 Resolver DNS Firewall rules | + + +## References + +- [Deploy centralized traffic filtering using AWS Network Firewall](https://aws.amazon.com/blogs/networking-and-content-delivery/deploy-centralized-traffic-filtering-using-aws-network-firewall) +- [AWS Network Firewall – New Managed Firewall Service in VPC](https://aws.amazon.com/blogs/aws/aws-network-firewall-new-managed-firewall-service-in-vpc) +- [Deployment models for AWS Network Firewall](https://aws.amazon.com/blogs/networking-and-content-delivery/deployment-models-for-aws-network-firewall) +- [Deployment models for AWS Network Firewall with VPC routing enhancements](https://aws.amazon.com/blogs/networking-and-content-delivery/deployment-models-for-aws-network-firewall-with-vpc-routing-enhancements) +- [Inspection Deployment Models with AWS Network Firewall](https://d1.awsstatic.com/architecture-diagrams/ArchitectureDiagrams/inspection-deployment-models-with-AWS-network-firewall-ra.pdf) +- [How to deploy AWS Network Firewall by using AWS Firewall Manager](https://aws.amazon.com/blogs/security/how-to-deploy-aws-network-firewall-by-using-aws-firewall-manager) +- [A Deep Dive into AWS Transit Gateway](https://www.youtube.com/watch?v=a55Iud-66q0) +- [Appliance in a shared services VPC](https://docs.aws.amazon.com/vpc/latest/tgw/transit-gateway-appliance-scenario.html) +- [Quotas on Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-entities-resolver) +- [Unified bad hosts](https://github.com/StevenBlack/hosts) +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/route53-resolver-dns-firewall/config/allowed_domains.txt b/modules/route53-resolver-dns-firewall/config/allowed_domains.txt new file mode 100644 index 000000000..d9ca0d0a9 --- /dev/null +++ b/modules/route53-resolver-dns-firewall/config/allowed_domains.txt @@ -0,0 +1 @@ +example.com. diff --git a/modules/route53-resolver-dns-firewall/config/blocked_domains.txt b/modules/route53-resolver-dns-firewall/config/blocked_domains.txt new file mode 100644 index 000000000..b30b0bad9 --- /dev/null +++ b/modules/route53-resolver-dns-firewall/config/blocked_domains.txt @@ -0,0 +1,48 @@ +accesscu.ca. +alterna.ca. +battlerivercreditunion.com. +bayviewnb.com. +belgianalliancecu.mb.ca. +blueshorefinancial.com. +caissepopclare.com. +caseracu.ca. +cccu.ca. +cdcu.com. +chinookfinancial.com. +comsavings.com. +comtechfirecu.com. +conexus.ca. +copperfin.ca. +diamondnorthcu.com. +eaglerivercu.com. +eastcoastcu.ca. +eccu.ca. +ekccu.com. +encompasscu.ca. +entegra.ca. +envisionfinancial.ca. +firstcu.ca. +firstontariocu.com. +fnbc.ca. +frontlinecu.com. +implicity.ca. +innovationcu.ca. +lakelandcreditunion.com. +lambtonfinancial.ca. +lecu.ca. +local183cu.ca. +mtlehman.com. +newrosscreditunion.ca. +nivervillecu.mb.ca. +northsave.com. +noventis.ca. +npscu.ca. +peacehills.com. +prospera.ca. +pscu.ca. +rpcul.com. +sdcu.com. +sprucecu.bc.ca. +stridecu.ca. +sudburycu.com. +synergycu.ca. diff --git a/modules/route53-resolver-dns-firewall/context.tf b/modules/route53-resolver-dns-firewall/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/route53-resolver-dns-firewall/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/route53-resolver-dns-firewall/main.tf b/modules/route53-resolver-dns-firewall/main.tf new file mode 100644 index 000000000..429a71b50 --- /dev/null +++ b/modules/route53-resolver-dns-firewall/main.tf @@ -0,0 +1,25 @@ +locals { + enabled = module.this.enabled + query_log_enabled = local.enabled && var.query_log_enabled + + vpc_outputs = module.vpc.outputs + vpc_id = local.vpc_outputs.vpc_id + + logs_bucket_outputs = module.logs_bucket.outputs + logs_bucket_arn = local.logs_bucket_outputs.bucket_arn +} + +module "route53_resolver_dns_firewall" { + source = "cloudposse/route53-resolver-dns-firewall/aws" + version = "0.2.1" + + vpc_id = local.vpc_id + query_log_destination_arn = local.logs_bucket_arn + query_log_enabled = local.query_log_enabled + firewall_fail_open = var.firewall_fail_open + query_log_config_name = var.query_log_config_name + domains_config = var.domains_config + rule_groups_config = var.rule_groups_config + + context = module.this.context +} diff --git a/modules/route53-resolver-dns-firewall/outputs.tf b/modules/route53-resolver-dns-firewall/outputs.tf new file mode 100644 index 000000000..7190dcdbb --- /dev/null +++ b/modules/route53-resolver-dns-firewall/outputs.tf @@ -0,0 +1,24 @@ +output "query_log_config" { + value = module.route53_resolver_dns_firewall.query_log_config + description = "Route 53 Resolver query logging configuration" +} + +output "domains" { + value = module.route53_resolver_dns_firewall.domains + description = "Route 53 Resolver DNS Firewall domain configurations" +} + +output "rule_groups" { + value = module.route53_resolver_dns_firewall.rule_groups + description = "Route 53 Resolver DNS Firewall rule groups" +} + +output "rule_group_associations" { + value = module.route53_resolver_dns_firewall.rule_group_associations + description = "Route 53 Resolver DNS Firewall rule group associations" +} + +output "rules" { + value = module.route53_resolver_dns_firewall.rules + description = "Route 53 Resolver DNS Firewall rules" +} diff --git a/modules/route53-resolver-dns-firewall/providers.tf b/modules/route53-resolver-dns-firewall/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/route53-resolver-dns-firewall/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/route53-resolver-dns-firewall/remote-state.tf b/modules/route53-resolver-dns-firewall/remote-state.tf new file mode 100644 index 000000000..c4e0d8e01 --- /dev/null +++ b/modules/route53-resolver-dns-firewall/remote-state.tf @@ -0,0 +1,24 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.vpc_component_name + + context = module.this.context +} + +module "logs_bucket" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.logs_bucket_component_name + + bypass = !local.query_log_enabled || var.logs_bucket_component_name == null || var.logs_bucket_component_name == "" + + defaults = { + bucket_id = "" + bucket_arn = "" + } + + context = module.this.context +} diff --git a/modules/route53-resolver-dns-firewall/variables.tf b/modules/route53-resolver-dns-firewall/variables.tf new file mode 100644 index 000000000..c8d905387 --- /dev/null +++ b/modules/route53-resolver-dns-firewall/variables.tf @@ -0,0 +1,68 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "vpc_component_name" { + type = string + description = "The name of a VPC component where the Network Firewall is provisioned" +} + +variable "logs_bucket_component_name" { + type = string + description = "Flow logs bucket component name" + default = null +} + +variable "firewall_fail_open" { + type = string + description = <<-EOF + Determines how Route 53 Resolver handles queries during failures, for example when all traffic that is sent to DNS Firewall fails to receive a reply. + By default, fail open is disabled, which means the failure mode is closed. + This approach favors security over availability. DNS Firewall blocks queries that it is unable to evaluate properly. + If you enable this option, the failure mode is open. This approach favors availability over security. + In this case, DNS Firewall allows queries to proceed if it is unable to properly evaluate them. + Valid values: ENABLED, DISABLED. + EOF + default = "ENABLED" +} + +variable "query_log_enabled" { + type = bool + description = "Flag to enable/disable Route 53 Resolver query logging" + default = false +} + +variable "query_log_config_name" { + type = string + description = "Route 53 Resolver query log config name. If omitted, the name will be generated by concatenating the ID from the context with the VPC ID" + default = null +} + +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_domain_list +variable "domains_config" { + type = map(object({ + domains = optional(list(string)) + domains_file = optional(string) + })) + description = "Map of Route 53 Resolver DNS Firewall domain configurations" +} + +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule_group +variable "rule_groups_config" { + type = map(object({ + priority = number + mutation_protection = optional(string) + # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule + rules = map(object({ + action = string + priority = number + block_override_dns_type = optional(string) + block_override_domain = optional(string) + block_override_ttl = optional(number) + block_response = optional(string) + firewall_domain_list_name = string + })) + })) + description = "Rule groups and rules configuration" +} diff --git a/modules/route53-resolver-dns-firewall/versions.tf b/modules/route53-resolver-dns-firewall/versions.tf new file mode 100644 index 000000000..b5920b7b1 --- /dev/null +++ b/modules/route53-resolver-dns-firewall/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From 6be84c36c403717a4230ec492703dbd2763514b6 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 10 May 2023 11:35:57 -0700 Subject: [PATCH 111/501] upstream `acm` and `datadog-integration` (#666) --- modules/acm/README.md | 1 + modules/acm/main.tf | 2 +- modules/acm/variables.tf | 6 +++ modules/datadog-integration/README.md | 54 ++++-------------------- modules/datadog-integration/main.tf | 12 +++++- modules/datadog-integration/variables.tf | 5 +++ 6 files changed, 32 insertions(+), 48 deletions(-) diff --git a/modules/acm/README.md b/modules/acm/README.md index f6a4a5e03..cff3c3e32 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -97,6 +97,7 @@ components: | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_private\_zone\_enabled](#input\_dns\_private\_zone\_enabled) | Whether to set the zone to public or private | `bool` | `false` | no | | [domain\_name](#input\_domain\_name) | Root domain name | `string` | n/a | yes | +| [enable\_asterisk\_subject\_alternative\_name](#input\_enable\_asterisk\_subject\_alternative\_name) | Enable or disable the use of a wildcard domain in the subject alternative names | `bool` | `true` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | diff --git a/modules/acm/main.tf b/modules/acm/main.tf index 1ac797f07..f7d57b2b8 100644 --- a/modules/acm/main.tf +++ b/modules/acm/main.tf @@ -22,7 +22,7 @@ module "acm" { domain_name = var.domain_name process_domain_validation_options = var.process_domain_validation_options ttl = 300 - subject_alternative_names = concat([format("*.%s", var.domain_name)], var.subject_alternative_names) + subject_alternative_names = concat(var.enable_asterisk_subject_alternative_name ? [format("*.%s", var.domain_name)] : [], var.subject_alternative_names) zone_id = join("", data.aws_route53_zone.default.*.zone_id) context = module.this.context diff --git a/modules/acm/variables.tf b/modules/acm/variables.tf index 4b8212cd8..cb082d7a6 100644 --- a/modules/acm/variables.tf +++ b/modules/acm/variables.tf @@ -70,3 +70,9 @@ variable "certificate_authority_component_key" { default = null description = "Use this component key e.g. `root` or `mgmt` to read from the remote state to get the certificate_authority_arn if using an authority type of SUBORDINATE" } + +variable "enable_asterisk_subject_alternative_name" { + type = bool + default = true + description = "Enable or disable the use of a wildcard domain in the subject alternative names" +} diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index eeb6f1d0c..bc248f97a 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -1,6 +1,6 @@ # Component: `datadog-integration` -This component is responsible for provisioning Datadog AWS integrations. +This component is responsible for provisioning Datadog AWS integrations. See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. @@ -32,7 +32,9 @@ components: ## Providers -No providers. +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | ## Modules @@ -46,7 +48,9 @@ No providers. ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_regions.all](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/regions) | data source | ## Inputs @@ -68,6 +72,7 @@ No resources. | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [included\_regions](#input\_included\_regions) | An array of AWS regions to include in metrics collection | `list(string)` | `[]` | no | | [integrations](#input\_integrations) | List of AWS permission names to apply for different integrations (e.g. 'all', 'core') | `list(string)` |
[
"all"
]
| no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | @@ -91,49 +96,6 @@ No resources. -## FAQ: - -### Stack Errors (Spacelift): - -``` -╷ -│ Error: error creating AWS integration from https://api.datadoghq.com/api/v1/integration/aws: 409 Conflict: {"errors": ["Could not update AWS Integration due to conflicting updates"]} -│ -│ with module.datadog_integration.datadog_integration_aws.integration[0], -│ on .terraform/modules/datadog_integration/main.tf line 18, in resource "datadog_integration_aws" "integration": -│ 18: resource "datadog_integration_aws" "integration" { -│ -╵ -``` - -This can happen when you apply multiple integrations at the same time. Fix is easy though, re-trigger the stack. - -## Enabling Security Audits - -To enable the Datadog compliance capabilities, AWS integration to must have the `SecurityAudit` policy attached to the Datadog IAM role. This is handled by our [https://github.com/cloudposse/terraform-aws-datadog-integration](https://github.com/cloudposse/terraform-aws-datadog-integration) module used - -the by the `datadog-integration` component. - -Attaching the `SecurityAudit` policy allows Datadog to collect information about how AWS resources are configured (used in Datadog Cloud Security Posture Management to read security configuration metadata) - -- Datadog Cloud Security Posture Management (CSPM) makes it easier to assess and visualize the current and historic security posture of cloud environments, automate audit evidence collection, and catch misconfigurations that leave your organization vulnerable to attacks - -- Cloud Security Posture Management (CSPM) can be accessed at [https://app.datadoghq.com/security/compliance/home](https://app.datadoghq.com/security/compliance/home) - -- The process to enable Datadog Cloud Security Posture Management (CSPM) consists of two steps (one automated, the other manual): - -- Enable `SecurityAudit` policy and provision it with terraform - -- In Datadog UI, perform the following manual steps: - -``` -Go to the Datadog AWS integration tile -Click on the AWS account where you wish to enable resource collection -Go to the Resource collection section for that account and check the box "Route resource data to the Cloud Security Posture Management product" -At the bottom left of the tile, click Update Configuration - -``` - ## References * Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-integration) - Cloud Posse's upstream component diff --git a/modules/datadog-integration/main.tf b/modules/datadog-integration/main.tf index 1c4e572f1..72de92237 100644 --- a/modules/datadog-integration/main.tf +++ b/modules/datadog-integration/main.tf @@ -1,3 +1,13 @@ +locals { + use_include_regions = length(var.included_regions) > 0 + all_regions = data.aws_regions.all.names + excluded_list_by_include = setsubtract(local.use_include_regions ? local.all_regions : [], var.included_regions) +} + +data "aws_regions" "all" { + all_regions = true +} + module "datadog_integration" { source = "cloudposse/datadog-integration/aws" version = "1.0.0" @@ -8,7 +18,7 @@ module "datadog_integration" { integrations = var.integrations filter_tags = local.filter_tags host_tags = local.host_tags - excluded_regions = var.excluded_regions + excluded_regions = concat(var.excluded_regions, tolist(local.excluded_list_by_include)) account_specific_namespace_rules = var.account_specific_namespace_rules context = module.this.context diff --git a/modules/datadog-integration/variables.tf b/modules/datadog-integration/variables.tf index 806d68b4d..8ceaf1def 100644 --- a/modules/datadog-integration/variables.tf +++ b/modules/datadog-integration/variables.tf @@ -33,6 +33,11 @@ variable "excluded_regions" { default = [] } +variable "included_regions" { + type = list(string) + description = "An array of AWS regions to include in metrics collection" + default = [] +} variable "account_specific_namespace_rules" { type = map(string) description = "An object, (in the form {\"namespace1\":true/false, \"namespace2\":true/false} ), that enables or disables metric collection for specific AWS namespaces for this AWS account only" From 2d44dc6421b3d0d4905a04ff7aa0f4b4ae1f673a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 11 May 2023 03:27:07 -0400 Subject: [PATCH 112/501] Add `iam-policy` to `ecs-service` (#663) --- modules/ecs-service/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index d5cb5c11d..ab70382d2 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -215,7 +215,7 @@ module "ecs_alb_service_task" { wait_for_steady_state = lookup(var.task, "wait_for_steady_state", true) circuit_breaker_deployment_enabled = lookup(var.task, "circuit_breaker_deployment_enabled", true) circuit_breaker_rollback_enabled = lookup(var.task, "circuit_breaker_rollback_enabled ", true) - task_policy_arns = var.task_policy_arns + task_policy_arns = var.iam_policy_enabled ? concat(var.task_policy_arns, formatlist(aws_iam_policy.default[0].arn)) : var.task_policy_arns ecs_service_enabled = lookup(var.task, "ecs_service_enabled", true) bind_mount_volumes = lookup(var.task, "bind_mount_volumes", []) task_role_arn = lookup(var.task, "task_role_arn", []) From 85659ece28c1a5fb4fec76383a4856f5b4ba9573 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 11 May 2023 13:56:17 -0400 Subject: [PATCH 113/501] Update `vpc-flow-logs` (#649) --- modules/vpc-flow-logs-bucket/README.md | 42 +++---- modules/vpc-flow-logs-bucket/context.tf | 109 +++++++++++++++--- .../vpc-flow-logs-bucket/default.auto.tfvars | 3 - modules/vpc-flow-logs-bucket/main.tf | 3 +- modules/vpc-flow-logs-bucket/variables.tf | 5 - modules/vpc-flow-logs-bucket/versions.tf | 12 +- 6 files changed, 117 insertions(+), 57 deletions(-) delete mode 100644 modules/vpc-flow-logs-bucket/default.auto.tfvars diff --git a/modules/vpc-flow-logs-bucket/README.md b/modules/vpc-flow-logs-bucket/README.md index 23d415431..136801ef0 100644 --- a/modules/vpc-flow-logs-bucket/README.md +++ b/modules/vpc-flow-logs-bucket/README.md @@ -28,10 +28,8 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13.0 | -| [aws](#requirement\_aws) | >= 3.0 | -| [local](#requirement\_local) | >= 1.3 | -| [template](#requirement\_template) | >= 2.2 | +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | ## Providers @@ -41,9 +39,9 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [flow\_logs\_s3\_bucket](#module\_flow\_logs\_s3\_bucket) | cloudposse/vpc-flow-logs-s3-bucket/aws | 0.12.0 | +| [flow\_logs\_s3\_bucket](#module\_flow\_logs\_s3\_bucket) | cloudposse/vpc-flow-logs-s3-bucket/aws | 0.18.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [this](#module\_this) | cloudposse/label/null | 0.24.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -53,33 +51,35 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional tags for appending to tags\_as\_list\_of\_maps. Not added to `tags`. | `map(string)` | `{}` | no | -| [arn\_format](#input\_arn\_format) | ARN format to be used. May be changed to support deployment in GovCloud/China regions | `string` | `"arn:aws"` | no | -| [attributes](#input\_attributes) | Additional attributes (e.g. `1`) | `list(string)` | `[]` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | -| [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [expiration\_days](#input\_expiration\_days) | Number of days after which to expunge the objects | `number` | `90` | no | | [force\_destroy](#input\_force\_destroy) | A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable | `bool` | `false` | no | | [glacier\_transition\_days](#input\_glacier\_transition\_days) | Number of days after which to move the data to the glacier storage tier | `number` | `60` | no | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile to use when importing a resource | `string` | `null` | no | -| [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [lifecycle\_prefix](#input\_lifecycle\_prefix) | Prefix filter. Used to manage object lifecycle events | `string` | `""` | no | | [lifecycle\_rule\_enabled](#input\_lifecycle\_rule\_enabled) | Enable lifecycle events on this bucket | `bool` | `true` | no | | [lifecycle\_tags](#input\_lifecycle\_tags) | Tags filter. Used to manage object lifecycle events | `map(string)` | `{}` | no | -| [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | -| [namespace](#input\_namespace) | Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp' | `string` | `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [noncurrent\_version\_expiration\_days](#input\_noncurrent\_version\_expiration\_days) | Specifies when noncurrent object versions expire | `number` | `90` | no | | [noncurrent\_version\_transition\_days](#input\_noncurrent\_version\_transition\_days) | Specifies when noncurrent object versions transitions | `number` | `30` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [standard\_transition\_days](#input\_standard\_transition\_days) | Number of days to persist in the standard storage tier before moving to the infrequent access tier | `number` | `30` | no | -| [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [traffic\_type](#input\_traffic\_type) | The type of traffic to capture. Valid values: `ACCEPT`, `REJECT`, `ALL` | `string` | `"ALL"` | no | ## Outputs diff --git a/modules/vpc-flow-logs-bucket/context.tf b/modules/vpc-flow-logs-bucket/context.tf index 81f99b4e3..5e0ef8856 100644 --- a/modules/vpc-flow-logs-bucket/context.tf +++ b/modules/vpc-flow-logs-bucket/context.tf @@ -8,6 +8,8 @@ # Cloud Posse's standard configuration inputs suitable for passing # to Cloud Posse modules. # +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# # Modules should access the whole context as `module.this.context` # to get the input variables with nulls for defaults, # for example `context = module.this.context`, @@ -20,10 +22,11 @@ module "this" { source = "cloudposse/label/null" - version = "0.24.1" # requires Terraform >= 0.13.0 + version = "0.25.0" # requires Terraform >= 0.13.0 enabled = var.enabled namespace = var.namespace + tenant = var.tenant environment = var.environment stage = var.stage name = var.name @@ -36,6 +39,8 @@ module "this" { id_length_limit = var.id_length_limit label_key_case = var.label_key_case label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags context = var.context } @@ -47,6 +52,7 @@ variable "context" { default = { enabled = true namespace = null + tenant = null environment = null stage = null name = null @@ -59,6 +65,15 @@ variable "context" { id_length_limit = null label_key_case = null label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] } description = <<-EOT Single object for setting entire context at once. @@ -88,32 +103,42 @@ variable "enabled" { variable "namespace" { type = string default = null - description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" } variable "environment" { type = string default = null - description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" } variable "stage" { type = string default = null - description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" } variable "name" { type = string default = null - description = "Solution name, e.g. 'app' or 'jenkins'" + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT } variable "delimiter" { type = string default = null description = <<-EOT - Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. + Delimiter to be used between ID elements. Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. EOT } @@ -121,36 +146,64 @@ variable "delimiter" { variable "attributes" { type = list(string) default = [] - description = "Additional attributes (e.g. `1`)" + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT } variable "tags" { type = map(string) default = {} - description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT } variable "additional_tag_map" { type = map(string) default = {} - description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT } variable "label_order" { type = list(string) default = null description = <<-EOT - The naming order of the id output and Name tag. + The order in which the labels (ID elements) appear in the `id`. Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 5 elements, but at least one must be present. - EOT + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT } variable "regex_replace_chars" { type = string default = null description = <<-EOT - Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. EOT } @@ -161,7 +214,7 @@ variable "id_length_limit" { description = <<-EOT Limit `id` to this many characters (minimum 6). Set to `0` for unlimited length. - Set to `null` for default, which is `0`. + Set to `null` for keep the existing setting, which defaults to `0`. Does not affect `id_full`. EOT validation { @@ -174,7 +227,8 @@ variable "label_key_case" { type = string default = null description = <<-EOT - The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`. + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. Possible values: `lower`, `title`, `upper`. Default value: `title`. EOT @@ -189,8 +243,11 @@ variable "label_value_case" { type = string default = null description = <<-EOT - The letter case of output label values (also used in `tags` and `id`). + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. Default value: `lower`. EOT @@ -199,4 +256,24 @@ variable "label_value_case" { error_message = "Allowed values: `lower`, `title`, `upper`, `none`." } } + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + #### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/vpc-flow-logs-bucket/default.auto.tfvars b/modules/vpc-flow-logs-bucket/default.auto.tfvars deleted file mode 100644 index 67952b0d1..000000000 --- a/modules/vpc-flow-logs-bucket/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = true diff --git a/modules/vpc-flow-logs-bucket/main.tf b/modules/vpc-flow-logs-bucket/main.tf index 22486216e..1e69db2ae 100644 --- a/modules/vpc-flow-logs-bucket/main.tf +++ b/modules/vpc-flow-logs-bucket/main.tf @@ -1,6 +1,6 @@ module "flow_logs_s3_bucket" { source = "cloudposse/vpc-flow-logs-s3-bucket/aws" - version = "0.12.0" + version = "0.18.0" lifecycle_prefix = var.lifecycle_prefix lifecycle_tags = var.lifecycle_tags @@ -12,7 +12,6 @@ module "flow_logs_s3_bucket" { expiration_days = var.expiration_days traffic_type = var.traffic_type force_destroy = var.force_destroy - arn_format = var.arn_format flow_log_enabled = false context = module.this.context diff --git a/modules/vpc-flow-logs-bucket/variables.tf b/modules/vpc-flow-logs-bucket/variables.tf index f7b9dda6d..c101b778a 100644 --- a/modules/vpc-flow-logs-bucket/variables.tf +++ b/modules/vpc-flow-logs-bucket/variables.tf @@ -63,8 +63,3 @@ variable "traffic_type" { default = "ALL" } -variable "arn_format" { - type = string - default = "arn:aws" - description = "ARN format to be used. May be changed to support deployment in GovCloud/China regions" -} diff --git a/modules/vpc-flow-logs-bucket/versions.tf b/modules/vpc-flow-logs-bucket/versions.tf index 720538dd8..cc73ffd35 100644 --- a/modules/vpc-flow-logs-bucket/versions.tf +++ b/modules/vpc-flow-logs-bucket/versions.tf @@ -1,18 +1,10 @@ terraform { - required_version = ">= 0.13.0" + required_version = ">= 1.0.0" required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.0" - } - template = { - source = "cloudposse/template" - version = ">= 2.2" - } - local = { - source = "hashicorp/local" - version = ">= 1.3" + version = ">= 4.9.0" } } } From e0bdd0b65432a9c50a3152654fe9d0eafa7b8370 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 11 May 2023 10:59:19 -0700 Subject: [PATCH 114/501] `rds` Component readme update (#667) --- modules/rds/README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/modules/rds/README.md b/modules/rds/README.md index 21ab638df..2566c3c0f 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -13,6 +13,35 @@ via specific CIDR blocks or security group ids. Here's an example snippet for how to use this component. +### PostgreSQL + +```yaml +components: + terraform: + rds/defaults: + metadata: + type: abstract + vars: + enabled: true + use_fullname: false + name: my-postgres-db + instance_class: db.t3.micro + database_name: my-postgres-db + # database_user: admin # enable to specify something specific + engine: postgres + engine_version: "15.2" + database_port: 5432 + db_parameter_group: "postgres15" + allocated_storage: 10 #GBs + ssm_enabled: true + client_security_group_enabled: true + ## The following settings allow the database to be accessed from anywhere + # publicly_accessible: true + # use_private_subnets: false + # allowed_cidr_blocks: + # - 0.0.0.0/0 +``` + ### Microsoft SQL ```yaml @@ -41,7 +70,7 @@ components: deletion_protection: false ``` ### Provisioning from a snapshot -The snapshot identifier variable can be added to provision an instance from a snapshot HOWEVER- +The snapshot identifier variable can be added to provision an instance from a snapshot HOWEVER- Keep in mind these instances are provisioned from a unique kms key per rds. For clean terraform runs, you must first provision the key for the destination instance, then copy the snapshot using that kms key. From f59231c196c9f4bc32e3dd224112d4f2ffbf73bd Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 11 May 2023 13:38:41 -0700 Subject: [PATCH 115/501] Remove (broken) root access to EKS clusters (#668) --- modules/eks/cluster/main.tf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 4db5873b7..35a51fad9 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -15,12 +15,13 @@ locals { (local.identity_account_name) = var.aws_teams_rbac[*].aws_team }, { (local.this_account_name) = var.aws_team_roles_rbac[*].aws_team_role - root = ["*"] }) aws_teams_auth = [for role in var.aws_teams_rbac : { - rolearn = module.iam_arns.principals_map[local.identity_account_name][role.aws_team] - username = format("%s-%s", local.identity_account_name, role.aws_team) + rolearn = module.iam_arns.principals_map[local.identity_account_name][role.aws_team] + # Include session name in the username for auditing purposes. + # See https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-iam-roles-when-multiple-users-need-identical-access-to-the-cluster + username = format("%s-%s::{{SessionName}}", local.identity_account_name, role.aws_team) groups = role.groups }] From 09f4a0bd1c0f3f7611f613884038f23e31aea206 Mon Sep 17 00:00:00 2001 From: Piotr Palka Date: Mon, 15 May 2023 17:25:14 +0200 Subject: [PATCH 116/501] EKS terraform module variable type fix (#674) --- modules/eks/actions-runner-controller/README.md | 2 +- modules/eks/actions-runner-controller/variables.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 0ea24740b..fc2530efe 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -461,7 +461,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(string, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(bool, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | | [ssm\_github\_webhook\_secret\_token\_path](#input\_ssm\_github\_webhook\_secret\_token\_path) | The path in SSM to the GitHub Webhook Secret token. | `string` | `""` | no | diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index 0f02798ee..af8c1c414 100755 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -192,7 +192,7 @@ variable "runners" { pull_driven_scaling_enabled = bool labels = list(string) storage = optional(string, null) - pvc_enabled = optional(string, false) + pvc_enabled = optional(bool, false) resources = object({ limits = object({ cpu = string From ef1c92815e57f7a9935b4267d48c98155a462781 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Mon, 15 May 2023 13:37:27 -0400 Subject: [PATCH 117/501] Add `aws-shield` component (#670) --- modules/aws-shield/README.md | 167 ++++++++++++++++++ modules/aws-shield/alb.tf | 14 ++ modules/aws-shield/cloudfront.tf | 14 ++ modules/aws-shield/context.tf | 279 +++++++++++++++++++++++++++++++ modules/aws-shield/eip.tf | 14 ++ modules/aws-shield/main.tf | 22 +++ modules/aws-shield/outputs.tf | 19 +++ modules/aws-shield/providers.tf | 29 ++++ modules/aws-shield/route53.tf | 14 ++ modules/aws-shield/variables.tf | 28 ++++ modules/aws-shield/versions.tf | 10 ++ 11 files changed, 610 insertions(+) create mode 100644 modules/aws-shield/README.md create mode 100644 modules/aws-shield/alb.tf create mode 100644 modules/aws-shield/cloudfront.tf create mode 100644 modules/aws-shield/context.tf create mode 100644 modules/aws-shield/eip.tf create mode 100644 modules/aws-shield/main.tf create mode 100644 modules/aws-shield/outputs.tf create mode 100644 modules/aws-shield/providers.tf create mode 100644 modules/aws-shield/route53.tf create mode 100644 modules/aws-shield/variables.tf create mode 100644 modules/aws-shield/versions.tf diff --git a/modules/aws-shield/README.md b/modules/aws-shield/README.md new file mode 100644 index 000000000..f4b2231ab --- /dev/null +++ b/modules/aws-shield/README.md @@ -0,0 +1,167 @@ +# Component: `aws-shield` + +This component is responsible for enabling AWS Shield Advanced Protection for the following resources: + +* Application Load Balancers (ALBs) +* CloudFront Distributions +* Elastic IPs +* Route53 Hosted Zones + +This component assumes that resources it is configured to protect are not already protected by other components +that have their `xxx_aws_shield_protection_enabled` variable set to `true`. + +This component also requires that the account where the component is being provisioned to has +been [subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/enable-ddos-prem.html). + +## Usage + +**Stack Level**: Global or Regional + +The following snippet shows how to use all of this component's features in a stack configuration: + +```yaml +components: + terraform: + aws-shield: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + route53_zone_names: + - test.ue1.example.net + alb_names: + - k8s-common-2c5f23ff99 + cloudfront_distribution_ids: + - EDFDVBD632BHDS5 + eips: + - 3.214.128.240 + - 35.172.208.150 + - 35.171.70.50 +``` + +A typical global configuration will only include the `route53_zone_names` and `cloudfront_distribution_ids` variables, +as global Route53 Hosted Zones may exist in that account, and because CloudFront is a global AWS service. + +A global stack configuration will not have a VPC, and hence `alb_names` and `eips` should not be defined: + +```yaml +components: + terraform: + aws-shield: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + route53_zone_names: + - test.example.net + cloudfront_distribution_ids: + - EDFDVBD632BHDS5 +``` + +Regional stack configurations will typically make use of all resources except for `cloudfront_distribution_ids`: + +```yaml +components: + terraform: + aws-shield: + settings: + spacelift: + workspace_enabled: true + vars: + route53_zone_names: + - test.ue1.example.net + alb_names: + - k8s-common-2c5f23ff99 + eips: + - 3.214.128.240 + - 35.172.208.150 + - 35.171.70.50 +``` + +Stack configurations which rely on components with a `xxx_aws_shield_protection_enabled` variable should set that variable to `true` +and leave the corresponding variable for this component as empty, relying on that component's AWS Shield Advanced functionality instead. +This leads to more simplified inter-component dependencies and minimizes the need for maintaining the provisioning order during a cold-start. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_shield_protection.alb_shield_protection](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/shield_protection) | resource | +| [aws_shield_protection.cloudfront_shield_protection](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/shield_protection) | resource | +| [aws_shield_protection.eip_shield_protection](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/shield_protection) | resource | +| [aws_shield_protection.route53_zone_protection](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/shield_protection) | resource | +| [aws_alb.alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/alb) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_cloudfront_distribution.cloudfront_distribution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_distribution) | data source | +| [aws_eip.eip](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eip) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_route53_zone.route53_zone](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_names](#input\_alb\_names) | list of ALB names which will be protected with AWS Shield Advanced | `list(string)` | `[]` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [cloudfront\_distribution\_ids](#input\_cloudfront\_distribution\_ids) | list of CloudFront Distribution IDs which will be protected with AWS Shield Advanced | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eips](#input\_eips) | List of Elastic IPs which will be protected with AWS Shield Advanced | `list(string)` | `[]` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [route53\_zone\_names](#input\_route53\_zone\_names) | List of Route53 Hosted Zone names which will be protected with AWS Shield Advanced | `list(string)` | `[]` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [application\_load\_balancer\_protections](#output\_application\_load\_balancer\_protections) | AWS Shield Advanced Protections for ALBs | +| [cloudfront\_distribution\_protections](#output\_cloudfront\_distribution\_protections) | AWS Shield Advanced Protections for CloudFront Distributions | +| [elastic\_ip\_protections](#output\_elastic\_ip\_protections) | AWS Shield Advanced Protections for Elastic IPs | +| [route53\_hosted\_zone\_protections](#output\_route53\_hosted\_zone\_protections) | AWS Shield Advanced Protections for Route53 Hosted Zones | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aws-shield) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/aws-shield/alb.tf b/modules/aws-shield/alb.tf new file mode 100644 index 000000000..e1238519d --- /dev/null +++ b/modules/aws-shield/alb.tf @@ -0,0 +1,14 @@ +data "aws_alb" "alb" { + for_each = local.alb_protection_enabled ? toset(var.alb_names) : [] + + name = each.key +} + +resource "aws_shield_protection" "alb_shield_protection" { + for_each = local.alb_protection_enabled ? data.aws_alb.alb : {} + + name = data.aws_alb.alb[each.key].name + resource_arn = data.aws_alb.alb[each.key].arn + + tags = local.tags +} diff --git a/modules/aws-shield/cloudfront.tf b/modules/aws-shield/cloudfront.tf new file mode 100644 index 000000000..85ae8aebd --- /dev/null +++ b/modules/aws-shield/cloudfront.tf @@ -0,0 +1,14 @@ +data "aws_cloudfront_distribution" "cloudfront_distribution" { + for_each = local.cloudfront_distribution_protection_enabled ? toset(var.cloudfront_distribution_ids) : [] + + id = each.key +} + +resource "aws_shield_protection" "cloudfront_shield_protection" { + for_each = local.cloudfront_distribution_protection_enabled ? data.aws_cloudfront_distribution.cloudfront_distribution : {} + + name = data.aws_cloudfront_distribution.cloudfront_distribution[each.key].domain_name + resource_arn = data.aws_cloudfront_distribution.cloudfront_distribution[each.key].arn + + tags = local.tags +} diff --git a/modules/aws-shield/context.tf b/modules/aws-shield/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/aws-shield/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/aws-shield/eip.tf b/modules/aws-shield/eip.tf new file mode 100644 index 000000000..e4c3afa1f --- /dev/null +++ b/modules/aws-shield/eip.tf @@ -0,0 +1,14 @@ +data "aws_eip" "eip" { + for_each = local.eip_protection_enabled ? toset(var.eips) : [] + + public_ip = each.key +} + +resource "aws_shield_protection" "eip_shield_protection" { + for_each = local.eip_protection_enabled ? data.aws_eip.eip : {} + + name = data.aws_eip.eip[each.key].id + resource_arn = "arn:${local.partition}:ec2:${var.region}:${local.account_id}:eip-allocation/${data.aws_eip.eip[each.key].id}" + + tags = local.tags +} diff --git a/modules/aws-shield/main.tf b/modules/aws-shield/main.tf new file mode 100644 index 000000000..bed51d5f6 --- /dev/null +++ b/modules/aws-shield/main.tf @@ -0,0 +1,22 @@ +locals { + enabled = module.this.enabled + tags = module.this.tags + + account_id = one(data.aws_caller_identity.current[*].account_id) + + # Used to determine correct partition (i.e. - `aws`, `aws-gov`, `aws-cn`, etc.) + partition = one(data.aws_partition.current[*].partition) + + alb_protection_enabled = local.enabled && length(var.alb_names) > 0 + cloudfront_distribution_protection_enabled = local.enabled && length(var.cloudfront_distribution_ids) > 0 + eip_protection_enabled = local.enabled && length(var.eips) > 0 + route53_protection_enabled = local.enabled && length(var.route53_zone_names) > 0 +} + +data "aws_caller_identity" "current" { + count = local.enabled ? 1 : 0 +} + +data "aws_partition" "current" { + count = local.enabled ? 1 : 0 +} diff --git a/modules/aws-shield/outputs.tf b/modules/aws-shield/outputs.tf new file mode 100644 index 000000000..56dede749 --- /dev/null +++ b/modules/aws-shield/outputs.tf @@ -0,0 +1,19 @@ +output "application_load_balancer_protections" { + description = "AWS Shield Advanced Protections for ALBs" + value = aws_shield_protection.alb_shield_protection +} + +output "cloudfront_distribution_protections" { + description = "AWS Shield Advanced Protections for CloudFront Distributions" + value = aws_shield_protection.cloudfront_shield_protection +} + +output "elastic_ip_protections" { + description = "AWS Shield Advanced Protections for Elastic IPs" + value = aws_shield_protection.eip_shield_protection +} + +output "route53_hosted_zone_protections" { + description = "AWS Shield Advanced Protections for Route53 Hosted Zones" + value = aws_shield_protection.route53_zone_protection +} diff --git a/modules/aws-shield/providers.tf b/modules/aws-shield/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/aws-shield/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/aws-shield/route53.tf b/modules/aws-shield/route53.tf new file mode 100644 index 000000000..c73555dd0 --- /dev/null +++ b/modules/aws-shield/route53.tf @@ -0,0 +1,14 @@ +data "aws_route53_zone" "route53_zone" { + for_each = local.route53_protection_enabled ? toset(var.route53_zone_names) : [] + + name = each.key +} + +resource "aws_shield_protection" "route53_zone_protection" { + for_each = local.route53_protection_enabled ? data.aws_route53_zone.route53_zone : {} + + name = data.aws_route53_zone.route53_zone[each.key].name + resource_arn = "arn:${local.partition}:route53:::hostedzone/${data.aws_route53_zone.route53_zone[each.key].id}" + + tags = local.tags +} diff --git a/modules/aws-shield/variables.tf b/modules/aws-shield/variables.tf new file mode 100644 index 000000000..bcf1c1b9e --- /dev/null +++ b/modules/aws-shield/variables.tf @@ -0,0 +1,28 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "alb_names" { + description = "list of ALB names which will be protected with AWS Shield Advanced" + type = list(string) + default = [] +} + +variable "cloudfront_distribution_ids" { + description = "list of CloudFront Distribution IDs which will be protected with AWS Shield Advanced" + type = list(string) + default = [] +} + +variable "eips" { + description = "List of Elastic IPs which will be protected with AWS Shield Advanced" + type = list(string) + default = [] +} + +variable "route53_zone_names" { + description = "List of Route53 Hosted Zone names which will be protected with AWS Shield Advanced" + type = list(string) + default = [] +} diff --git a/modules/aws-shield/versions.tf b/modules/aws-shield/versions.tf new file mode 100644 index 000000000..f33ede77f --- /dev/null +++ b/modules/aws-shield/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} From a25a22967b3cac49838c4e2610d5aac883ef1f81 Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Mon, 15 May 2023 21:54:45 +0300 Subject: [PATCH 118/501] Fixed `route53-resolver-dns-firewall` for the case when logging is disabled (#669) Co-authored-by: Andriy Knysh --- modules/route53-resolver-dns-firewall/remote-state.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/route53-resolver-dns-firewall/remote-state.tf b/modules/route53-resolver-dns-firewall/remote-state.tf index c4e0d8e01..d99ec71df 100644 --- a/modules/route53-resolver-dns-firewall/remote-state.tf +++ b/modules/route53-resolver-dns-firewall/remote-state.tf @@ -13,7 +13,8 @@ module "logs_bucket" { component = var.logs_bucket_component_name - bypass = !local.query_log_enabled || var.logs_bucket_component_name == null || var.logs_bucket_component_name == "" + bypass = !local.query_log_enabled || var.logs_bucket_component_name == null || var.logs_bucket_component_name == "" + ignore_errors = !local.query_log_enabled || var.logs_bucket_component_name == null || var.logs_bucket_component_name == "" defaults = { bucket_id = "" From 1caab1cb300f302e180f68fce330d269c79d5e20 Mon Sep 17 00:00:00 2001 From: Victor Lantier Date: Mon, 15 May 2023 16:47:19 -0300 Subject: [PATCH 119/501] bump config-yaml module version in account component to remove hashicorp template provider (#671) --- modules/account/README.md | 2 +- modules/account/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/account/README.md b/modules/account/README.md index 89aad5421..73428af61 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -350,7 +350,7 @@ atmos terraform apply account --stack gbl-root | [accounts\_service\_control\_policies](#module\_accounts\_service\_control\_policies) | cloudposse/service-control-policies/aws | 0.9.2 | | [organization\_service\_control\_policies](#module\_organization\_service\_control\_policies) | cloudposse/service-control-policies/aws | 0.9.2 | | [organizational\_units\_service\_control\_policies](#module\_organizational\_units\_service\_control\_policies) | cloudposse/service-control-policies/aws | 0.9.2 | -| [service\_control\_policy\_statements\_yaml\_config](#module\_service\_control\_policy\_statements\_yaml\_config) | cloudposse/config/yaml | 1.0.1 | +| [service\_control\_policy\_statements\_yaml\_config](#module\_service\_control\_policy\_statements\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/account/main.tf b/modules/account/main.tf index fcad474b0..093a53647 100644 --- a/modules/account/main.tf +++ b/modules/account/main.tf @@ -80,7 +80,7 @@ locals { # Convert all Service Control Policy statements from YAML config to Terraform list module "service_control_policy_statements_yaml_config" { source = "cloudposse/config/yaml" - version = "1.0.1" + version = "1.0.2" list_config_local_base_path = path.module list_config_paths = var.service_control_policies_config_paths From 6ed0098b15af41875b5abdc3e3adc60e56eaf967 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 15 May 2023 16:00:53 -0400 Subject: [PATCH 120/501] Correct `cloudtrail` Account-Map Reference (#673) --- modules/cloudtrail/README.md | 4 ++-- modules/cloudtrail/cloudtrail-kms-key.tf | 2 +- modules/cloudtrail/remote-state.tf | 14 +++++++++++++- modules/cloudtrail/variables.tf | 6 ------ modules/eks/cluster/main.tf | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/modules/cloudtrail/README.md b/modules/cloudtrail/README.md index d29d991c1..4b71b58d9 100644 --- a/modules/cloudtrail/README.md +++ b/modules/cloudtrail/README.md @@ -45,8 +45,9 @@ components: | Name | Source | Version | |------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | | [cloudtrail](#module\_cloudtrail) | cloudposse/cloudtrail/aws | 0.21.0 | -| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_cloudtrail](#module\_kms\_key\_cloudtrail) | cloudposse/kms-key/aws | 0.12.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -72,7 +73,6 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [audit\_access\_enabled](#input\_audit\_access\_enabled) | If `true`, allows the Audit account access to read Cloudtrail logs directly from S3. This is a requirement for running Athena queries in the Audit account. | `bool` | `false` | no | -| [audit\_account\_name](#input\_audit\_account\_name) | The key used in Account Map to find the Audit account | `string` | `"core-audit"` | no | | [cloudtrail\_bucket\_component\_name](#input\_cloudtrail\_bucket\_component\_name) | The name of the CloudTrail bucket component | `string` | `"cloudtrail-bucket"` | no | | [cloudtrail\_bucket\_environment\_name](#input\_cloudtrail\_bucket\_environment\_name) | The name of the environment where the CloudTrail bucket is provisioned | `string` | n/a | yes | | [cloudtrail\_bucket\_stage\_name](#input\_cloudtrail\_bucket\_stage\_name) | The stage name where the CloudTrail bucket is provisioned | `string` | n/a | yes | diff --git a/modules/cloudtrail/cloudtrail-kms-key.tf b/modules/cloudtrail/cloudtrail-kms-key.tf index ce62d1fec..693cf94f9 100644 --- a/modules/cloudtrail/cloudtrail-kms-key.tf +++ b/modules/cloudtrail/cloudtrail-kms-key.tf @@ -1,6 +1,6 @@ locals { audit_access_enabled = module.this.enabled && var.audit_access_enabled - audit_account_id = module.account_map.outputs.full_account_map[var.audit_account_name] + audit_account_id = module.account_map.outputs.full_account_map[module.account_map.outputs.audit_account_account_name] } module "kms_key_cloudtrail" { diff --git a/modules/cloudtrail/remote-state.tf b/modules/cloudtrail/remote-state.tf index 584b0a1ee..d09bf7925 100644 --- a/modules/cloudtrail/remote-state.tf +++ b/modules/cloudtrail/remote-state.tf @@ -1,6 +1,6 @@ module "cloudtrail_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = var.cloudtrail_bucket_component_name environment = var.cloudtrail_bucket_environment_name @@ -8,3 +8,15 @@ module "cloudtrail_bucket" { context = module.this.context } + +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + component = "account-map" + tenant = module.iam_roles.global_tenant_name + environment = module.iam_roles.global_environment_name + stage = module.iam_roles.global_stage_name + + context = module.this.context +} diff --git a/modules/cloudtrail/variables.tf b/modules/cloudtrail/variables.tf index 2ffc3216b..889cf1db5 100644 --- a/modules/cloudtrail/variables.tf +++ b/modules/cloudtrail/variables.tf @@ -71,9 +71,3 @@ variable "audit_access_enabled" { default = false description = "If `true`, allows the Audit account access to read Cloudtrail logs directly from S3. This is a requirement for running Athena queries in the Audit account." } - -variable "audit_account_name" { - type = string - default = "core-audit" - description = "The key used in Account Map to find the Audit account" -} diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 35a51fad9..ee43fca7b 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -21,7 +21,7 @@ locals { rolearn = module.iam_arns.principals_map[local.identity_account_name][role.aws_team] # Include session name in the username for auditing purposes. # See https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-iam-roles-when-multiple-users-need-identical-access-to-the-cluster - username = format("%s-%s::{{SessionName}}", local.identity_account_name, role.aws_team) + username = format("%s-%s", local.identity_account_name, role.aws_team) groups = role.groups }] From d8125c8c0f9d01f85d2bb107382d80a760ad3720 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 16 May 2023 11:01:33 -0400 Subject: [PATCH 121/501] `eks/alb-controller-ingress-group`: Corrected Tags to pull LB Data Resource (#676) --- modules/eks/alb-controller-ingress-group/default.auto.tfvars | 3 --- modules/eks/alb-controller-ingress-group/main.tf | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 modules/eks/alb-controller-ingress-group/default.auto.tfvars diff --git a/modules/eks/alb-controller-ingress-group/default.auto.tfvars b/modules/eks/alb-controller-ingress-group/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/eks/alb-controller-ingress-group/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/eks/alb-controller-ingress-group/main.tf b/modules/eks/alb-controller-ingress-group/main.tf index c37561fd0..bf482cd09 100644 --- a/modules/eks/alb-controller-ingress-group/main.tf +++ b/modules/eks/alb-controller-ingress-group/main.tf @@ -171,7 +171,7 @@ data "aws_lb" "default" { tags = { "ingress.k8s.aws/resource" = "LoadBalancer" - "ingress.k8s.aws/stack" = var.name + "ingress.k8s.aws/stack" = local.ingress_controller_group_name "elbv2.k8s.aws/cluster" = module.eks.outputs.eks_cluster_id } From c75793bddc41d624370237a033e12531f302bca4 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Thu, 18 May 2023 10:51:47 -0400 Subject: [PATCH 122/501] Fix `datadog` components (#679) --- .../datadog-configuration/modules/datadog_keys/README.md | 1 + modules/datadog-configuration/variables.tf | 1 + modules/datadog-integration/provider-datadog.tf | 2 +- modules/datadog-lambda-forwarder/provider-datadog.tf | 2 +- modules/datadog-logs-archive/provider-datadog.tf | 1 - modules/datadog-monitor/provider-datadog.tf | 2 +- modules/datadog-private-location-ecs/README.md | 2 +- modules/datadog-private-location-ecs/provider-datadog.tf | 2 +- .../datadog-synthetics-private-location/provider-datadog.tf | 2 +- modules/datadog-synthetics/provider-datadog.tf | 6 ++---- 10 files changed, 10 insertions(+), 11 deletions(-) diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index cd9cb611f..d87d11582 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -8,6 +8,7 @@ Useful submodule for other modules to quickly configure the datadog provider module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" context = module.this.context + enabled = true } provider "datadog" { diff --git a/modules/datadog-configuration/variables.tf b/modules/datadog-configuration/variables.tf index c7664670f..2fa2752d5 100644 --- a/modules/datadog-configuration/variables.tf +++ b/modules/datadog-configuration/variables.tf @@ -19,6 +19,7 @@ variable "datadog_site_url" { error_message = "Allowed values: null, `datadoghq.com`, `us3.datadoghq.com`, `us5.datadoghq.com`, `datadoghq.eu`, `ddog-gov.com`." } } + variable "datadog_secrets_store_type" { type = string description = "Secret Store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" diff --git a/modules/datadog-integration/provider-datadog.tf b/modules/datadog-integration/provider-datadog.tf index 8db220f1f..80e2e0738 100644 --- a/modules/datadog-integration/provider-datadog.tf +++ b/modules/datadog-integration/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region context = module.this.context + enabled = true } provider "datadog" { diff --git a/modules/datadog-lambda-forwarder/provider-datadog.tf b/modules/datadog-lambda-forwarder/provider-datadog.tf index 8db220f1f..80e2e0738 100644 --- a/modules/datadog-lambda-forwarder/provider-datadog.tf +++ b/modules/datadog-lambda-forwarder/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region context = module.this.context + enabled = true } provider "datadog" { diff --git a/modules/datadog-logs-archive/provider-datadog.tf b/modules/datadog-logs-archive/provider-datadog.tf index 963dce116..9db2f3065 100644 --- a/modules/datadog-logs-archive/provider-datadog.tf +++ b/modules/datadog-logs-archive/provider-datadog.tf @@ -1,6 +1,5 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region context = module.this.context enabled = true } diff --git a/modules/datadog-monitor/provider-datadog.tf b/modules/datadog-monitor/provider-datadog.tf index 8db220f1f..80e2e0738 100644 --- a/modules/datadog-monitor/provider-datadog.tf +++ b/modules/datadog-monitor/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region context = module.this.context + enabled = true } provider "datadog" { diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index c5948db9d..e12ced256 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -1,4 +1,4 @@ -# Component: `ecs-service` +# Component: `datadog-private-location-ecs` This component is responsible for creating a datadog private location and deploying it to ECS (EC2 / Fargate) diff --git a/modules/datadog-private-location-ecs/provider-datadog.tf b/modules/datadog-private-location-ecs/provider-datadog.tf index 8db220f1f..80e2e0738 100644 --- a/modules/datadog-private-location-ecs/provider-datadog.tf +++ b/modules/datadog-private-location-ecs/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region context = module.this.context + enabled = true } provider "datadog" { diff --git a/modules/datadog-synthetics-private-location/provider-datadog.tf b/modules/datadog-synthetics-private-location/provider-datadog.tf index 8db220f1f..80e2e0738 100644 --- a/modules/datadog-synthetics-private-location/provider-datadog.tf +++ b/modules/datadog-synthetics-private-location/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region context = module.this.context + enabled = true } provider "datadog" { diff --git a/modules/datadog-synthetics/provider-datadog.tf b/modules/datadog-synthetics/provider-datadog.tf index 07eff92ef..80e2e0738 100644 --- a/modules/datadog-synthetics/provider-datadog.tf +++ b/modules/datadog-synthetics/provider-datadog.tf @@ -1,9 +1,7 @@ module "datadog_configuration" { - source = "../datadog-configuration/modules/datadog_keys" - - region = var.region - + source = "../datadog-configuration/modules/datadog_keys" context = module.this.context + enabled = true } provider "datadog" { From d89f5e3b957681796ff093832c760bf9ccca402c Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Thu, 18 May 2023 18:08:30 +0300 Subject: [PATCH 123/501] Introducing AWS Config component (#675) --- modules/aws-config/README.md | 201 +++++++++++++++++++++ modules/aws-config/context.tf | 279 +++++++++++++++++++++++++++++ modules/aws-config/main.tf | 75 ++++++++ modules/aws-config/outputs.tf | 19 ++ modules/aws-config/providers.tf | 41 +++++ modules/aws-config/remote-state.tf | 49 +++++ modules/aws-config/variables.tf | 125 +++++++++++++ modules/aws-config/versions.tf | 15 ++ modules/config-bucket/README.md | 106 +++++++++++ modules/config-bucket/context.tf | 279 +++++++++++++++++++++++++++++ modules/config-bucket/main.tf | 16 ++ modules/config-bucket/outputs.tf | 14 ++ modules/config-bucket/providers.tf | 29 +++ modules/config-bucket/variables.tf | 58 ++++++ modules/config-bucket/versions.tf | 10 ++ 15 files changed, 1316 insertions(+) create mode 100644 modules/aws-config/README.md create mode 100644 modules/aws-config/context.tf create mode 100644 modules/aws-config/main.tf create mode 100644 modules/aws-config/outputs.tf create mode 100644 modules/aws-config/providers.tf create mode 100644 modules/aws-config/remote-state.tf create mode 100644 modules/aws-config/variables.tf create mode 100644 modules/aws-config/versions.tf create mode 100644 modules/config-bucket/README.md create mode 100644 modules/config-bucket/context.tf create mode 100644 modules/config-bucket/main.tf create mode 100644 modules/config-bucket/outputs.tf create mode 100644 modules/config-bucket/providers.tf create mode 100644 modules/config-bucket/variables.tf create mode 100644 modules/config-bucket/versions.tf diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md new file mode 100644 index 000000000..17fb96d63 --- /dev/null +++ b/modules/aws-config/README.md @@ -0,0 +1,201 @@ +# Component: `aws-config` + +This component is responsible for configuring AWS Config. + +AWS Config service enables you to track changes to your AWS resources over time. It continuously monitors and records configuration changes to your AWS resources and provides you with a detailed view of the relationships between those resources. With AWS Config, you can assess, audit, and evaluate the configurations of your AWS resources for compliance, security, and governance purposes. + +Some of the key features of AWS Config include: +- Configuration history: AWS Config maintains a detailed history of changes to your AWS resources, allowing you to see when changes were made, who made them, and what the changes were. +- Configuration snapshots: AWS Config can take periodic snapshots of your AWS resources configurations, giving you a point-in-time view of their configuration. +- Compliance monitoring: AWS Config provides a range of pre-built rules and checks to monitor your resources for compliance with best practices and industry standards. +- Relationship mapping: AWS Config can map the relationships between your AWS resources, enabling you to see how changes to one resource can impact others. +- Notifications and alerts: AWS Config can send notifications and alerts when changes are made to your AWS resources that could impact their compliance or security posture. + +Overall, AWS Config provides you with a powerful toolset to help you monitor and manage the configurations of your AWS resources, ensuring that they remain compliant, secure, and properly configured over time. + +## Prerequisites + +As part of [CIS AWS Foundations 1.20](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.20), this component assumes that a designated support IAM role with the following permissions has been deployed to every account in the organization: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowSupport", + "Effect": "Allow", + "Action": [ + "support:*" + ], + "Resource": "*" + }, + { + "Sid": "AllowTrustedAdvisor", + "Effect": "Allow", + "Action": "trustedadvisor:Describe*", + "Resource": "*" + } + ] +} +``` + +Before deploying this AWS Config component `config-bucket` and `cloudtrail-bucket` should be deployed first. + +## Usage + +**Stack Level**: Regional + +_**NOTE**: Since AWS Config is regional AWS service, this component needs to be deployed to all regions._ + +At the AWS Organizational level, the Components designate an account to be the `central collection account` and a single region to be the `central collection region` so that compliance information can be aggregated into a central location. + +Logs are typically written to the `audit` account and AWS Config deployed into to the `security` account. + +Here's an example snippet for how to use this component: + +```yaml +components: + terraform: + aws-config: + vars: + enabled: true + account_map_tenant: core + az_abbreviation_type: fixed + # In each AWS account, an IAM role should be created in the main region. + # If the main region is set to us-east-1, the value of the var.create_iam_role variable should be true. + # For all other regions, the value of var.create_iam_role should be false. + create_iam_role: false + central_resource_collector_account: core-security + global_resource_collector_region: us-east-1 + config_bucket_env: ue1 + config_bucket_stage: audit + config_bucket_tenant: core + conformance_packs: + - name: Operational-Best-Practices-for-CIS-AWS-v1.4-Level2 + conformance_pack: https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml + parameter_overrides: + AccessKeysRotatedParamMaxAccessKeyAge: '45' + - name: Operational-Best-Practices-for-HIPAA-Security.yaml + conformance_pack: https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-HIPAA-Security.yaml + parameter_overrides: + ... + (etc) +``` + +## Deployment + +Apply to your central region security account + +```sh +atmos terraform plan aws-config-{central-region} --stack core-{central-region}-security -var=create_iam_role=true +``` + +For example when central region is `us-east-1`: + +```sh +atmos terraform plan aws-config-ue1 --stack core-ue1-security -var=create_iam_role=true +``` + +Apply aws-config to all stacks in all stages. + +```sh +atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [aws\_config](#module\_aws\_config) | cloudposse/config/aws | 0.17.0 | +| [aws\_config\_label](#module\_aws\_config\_label) | cloudposse/label/null | 0.25.0 | +| [aws\_team\_roles](#module\_aws\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [config\_bucket](#module\_config\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [conformance\_pack](#module\_conformance\_pack) | cloudposse/config/aws//modules/conformance-pack | 0.17.0 | +| [global\_collector\_region](#module\_global\_collector\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.1.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_partition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | (Optional) The tenant where the account\_map component required by remote-state is deployed. | `string` | `""` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [az\_abbreviation\_type](#input\_az\_abbreviation\_type) | AZ abbreviation type, `fixed` or `short` | `string` | `"fixed"` | no | +| [central\_resource\_collector\_account](#input\_central\_resource\_collector\_account) | The name of the account that is the centralized aggregation account. | `string` | n/a | yes | +| [config\_bucket\_env](#input\_config\_bucket\_env) | The environment of the AWS Config S3 Bucket | `string` | n/a | yes | +| [config\_bucket\_stage](#input\_config\_bucket\_stage) | The stage of the AWS Config S3 Bucket | `string` | n/a | yes | +| [config\_bucket\_tenant](#input\_config\_bucket\_tenant) | (Optional) The tenant of the AWS Config S3 Bucket | `string` | `""` | no | +| [conformance\_packs](#input\_conformance\_packs) | List of conformance packs. Each conformance pack is a map with the following keys: name, conformance\_pack, parameter\_overrides.

For example:
conformance\_packs = [
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level1"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level1.yaml"
parameter\_overrides = {
"AccessKeysRotatedParamMaxAccessKeyAge" = "45"
}
},
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level2"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml"
parameter\_overrides = {
"IamPasswordPolicyParamMaxPasswordAge" = "45"
}
}
]

Complete list of AWS Conformance Packs managed by AWSLabs can be found here:
https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs |
list(object({
name = string
conformance_pack = string
parameter_overrides = map(string)
}))
| `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_iam\_role](#input\_create\_iam\_role) | Flag to indicate whether an IAM Role should be created to grant the proper permissions for AWS Config | `bool` | `false` | no | +| [delegated\_accounts](#input\_delegated\_accounts) | The account IDs of other accounts that will send their AWS Configuration or Security Hub data to this account | `set(string)` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [global\_resource\_collector\_region](#input\_global\_resource\_collector\_region) | The region that collects AWS Config data for global resources such as IAM | `string` | n/a | yes | +| [iam\_role\_arn](#input\_iam\_role\_arn) | The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the
AWS resources associated with the account. This is only used if create\_iam\_role is false.

If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create\_iam\_role to `false`.

See the AWS Docs for further information:
http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html | `string` | `null` | no | +| [iam\_roles\_environment\_name](#input\_iam\_roles\_environment\_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (master) account | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [aws\_config\_configuration\_recorder\_id](#output\_aws\_config\_configuration\_recorder\_id) | The ID of the AWS Config Recorder | +| [aws\_config\_iam\_role](#output\_aws\_config\_iam\_role) | The ARN of the IAM Role used for AWS Config | +| [storage\_bucket\_arn](#output\_storage\_bucket\_arn) | Storage Config bucket ARN | +| [storage\_bucket\_id](#output\_storage\_bucket\_id) | Storage Config bucket ID | + + + +## References +* [AWS Config Documentation](https://docs.aws.amazon.com/config/index.html) +* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aws-config) +* [Conformance Packs documentation](https://docs.aws.amazon.com/config/latest/developerguide/conformance-packs.html) +* [AWS Managed Sample Conformance Packs](https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs) + +[](https://cpco.io/component) diff --git a/modules/aws-config/context.tf b/modules/aws-config/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/aws-config/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/aws-config/main.tf b/modules/aws-config/main.tf new file mode 100644 index 000000000..7c9d01fa4 --- /dev/null +++ b/modules/aws-config/main.tf @@ -0,0 +1,75 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map + s3_bucket = module.config_bucket.outputs + is_global_collector_region = join("", data.aws_region.this[*].name) == var.global_resource_collector_region + create_iam_role = var.create_iam_role && local.is_global_collector_region + config_iam_role_template = "arn:${local.partition}:iam::${join("", data.aws_caller_identity.this[*].account_id)}:role/${module.aws_config_label.id}" + config_iam_role_from_state = local.create_iam_role ? null : join("", module.global_collector_region[*].outputs.aws_config_iam_role) + config_iam_role_external = var.iam_role_arn != null ? var.iam_role_arn : local.config_iam_role_from_state + config_iam_role_arn = local.create_iam_role ? local.config_iam_role_template : local.config_iam_role_external + central_resource_collector_account = local.account_map[var.central_resource_collector_account] + delegated_accounts = var.delegated_accounts != null ? var.delegated_accounts : toset(values(local.account_map)) + partition = join("", data.aws_partition.this[*].partition) +} + +data "aws_caller_identity" "this" { + count = local.enabled ? 1 : 0 +} + +data "aws_region" "this" { + count = local.enabled ? 1 : 0 +} + +data "aws_partition" "this" { + count = local.enabled ? 1 : 0 +} + +module "aws_config_label" { + source = "cloudposse/label/null" + version = "0.25.0" + attributes = ["config"] + + context = module.this.context +} + +module "utils" { + source = "cloudposse/utils/aws" + version = "1.1.0" + + context = module.this.context +} + +module "conformance_pack" { + source = "cloudposse/config/aws//modules/conformance-pack" + version = "0.17.0" + + count = local.enabled ? length(var.conformance_packs) : 0 + + name = var.conformance_packs[count.index].name + conformance_pack = var.conformance_packs[count.index].conformance_pack + parameter_overrides = var.conformance_packs[count.index].parameter_overrides + + depends_on = [ + module.aws_config + ] + + context = module.this.context +} + +module "aws_config" { + source = "cloudposse/config/aws" + version = "0.17.0" + + s3_bucket_id = local.s3_bucket.config_bucket_id + s3_bucket_arn = local.s3_bucket.config_bucket_arn + create_iam_role = local.create_iam_role + iam_role_arn = local.config_iam_role_arn + create_sns_topic = true + + global_resource_collector_region = var.global_resource_collector_region + central_resource_collector_account = local.central_resource_collector_account + child_resource_collector_accounts = local.delegated_accounts + + context = module.this.context +} diff --git a/modules/aws-config/outputs.tf b/modules/aws-config/outputs.tf new file mode 100644 index 000000000..de4d2ceb0 --- /dev/null +++ b/modules/aws-config/outputs.tf @@ -0,0 +1,19 @@ +output "aws_config_configuration_recorder_id" { + value = module.aws_config.aws_config_configuration_recorder_id + description = "The ID of the AWS Config Recorder" +} + +output "aws_config_iam_role" { + description = "The ARN of the IAM Role used for AWS Config" + value = local.config_iam_role_arn +} + +output "storage_bucket_id" { + value = module.aws_config.storage_bucket_id + description = "Storage Config bucket ID" +} + +output "storage_bucket_arn" { + value = module.aws_config.storage_bucket_arn + description = "Storage Config bucket ARN" +} \ No newline at end of file diff --git a/modules/aws-config/providers.tf b/modules/aws-config/providers.tf new file mode 100644 index 000000000..f8513b99d --- /dev/null +++ b/modules/aws-config/providers.tf @@ -0,0 +1,41 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +provider "awsutils" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/aws-config/remote-state.tf b/modules/aws-config/remote-state.tf new file mode 100644 index 000000000..9155fec51 --- /dev/null +++ b/modules/aws-config/remote-state.tf @@ -0,0 +1,49 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + component = "account-map" + tenant = (var.account_map_tenant != "") ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} + +module "config_bucket" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + component = "config-bucket" + tenant = (var.config_bucket_tenant != "") ? var.config_bucket_tenant : module.this.tenant + stage = var.config_bucket_stage + environment = var.config_bucket_env + privileged = false + + context = module.this.context +} + +module "global_collector_region" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + count = !local.enabled || local.is_global_collector_region ? 0 : 1 + + component = "aws-config-${lookup(module.utils.region_az_alt_code_maps["to_${var.az_abbreviation_type}"], var.global_resource_collector_region)}" + stage = module.this.stage + environment = lookup(module.utils.region_az_alt_code_maps["to_${var.az_abbreviation_type}"], var.global_resource_collector_region) + privileged = false + + context = module.this.context +} + +module "aws_team_roles" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + component = "aws-team-roles" + environment = var.iam_roles_environment_name + + context = module.this.context +} diff --git a/modules/aws-config/variables.tf b/modules/aws-config/variables.tf new file mode 100644 index 000000000..96c21e2a2 --- /dev/null +++ b/modules/aws-config/variables.tf @@ -0,0 +1,125 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "account_map_tenant" { + type = string + default = "" + description = "(Optional) The tenant where the account_map component required by remote-state is deployed." +} + +variable "root_account_stage" { + type = string + default = "root" + description = "The stage name for the Organization root (master) account" +} + +variable "global_environment" { + type = string + default = "gbl" + description = "Global environment name" +} + +variable "privileged" { + type = bool + description = "True if the default provider already has access to the backend" + default = false +} + +variable "config_bucket_stage" { + type = string + description = "The stage of the AWS Config S3 Bucket" +} + +variable "config_bucket_env" { + type = string + description = "The environment of the AWS Config S3 Bucket" +} + +variable "config_bucket_tenant" { + type = string + default = "" + description = "(Optional) The tenant of the AWS Config S3 Bucket" +} + +variable "global_resource_collector_region" { + description = "The region that collects AWS Config data for global resources such as IAM" + type = string +} + +variable "central_resource_collector_account" { + description = "The name of the account that is the centralized aggregation account." + type = string +} + +variable "create_iam_role" { + description = "Flag to indicate whether an IAM Role should be created to grant the proper permissions for AWS Config" + type = bool + default = false +} + +variable "az_abbreviation_type" { + type = string + description = "AZ abbreviation type, `fixed` or `short`" + default = "fixed" +} + +variable "iam_role_arn" { + description = <<-DOC + The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the + AWS resources associated with the account. This is only used if create_iam_role is false. + + If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create_iam_role to `false`. + + See the AWS Docs for further information: + http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html + DOC + default = null + type = string +} + +variable "conformance_packs" { + description = <<-DOC + List of conformance packs. Each conformance pack is a map with the following keys: name, conformance_pack, parameter_overrides. + + For example: + conformance_packs = [ + { + name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level1" + conformance_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level1.yaml" + parameter_overrides = { + "AccessKeysRotatedParamMaxAccessKeyAge" = "45" + } + }, + { + name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level2" + conformance_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml" + parameter_overrides = { + "IamPasswordPolicyParamMaxPasswordAge" = "45" + } + } + ] + + Complete list of AWS Conformance Packs managed by AWSLabs can be found here: + https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs + DOC + type = list(object({ + name = string + conformance_pack = string + parameter_overrides = map(string) + })) + default = [] +} + +variable "delegated_accounts" { + description = "The account IDs of other accounts that will send their AWS Configuration or Security Hub data to this account" + type = set(string) + default = null +} + +variable "iam_roles_environment_name" { + type = string + description = "The name of the environment where the IAM roles are provisioned" + default = "gbl" +} diff --git a/modules/aws-config/versions.tf b/modules/aws-config/versions.tf new file mode 100644 index 000000000..65cf14c13 --- /dev/null +++ b/modules/aws-config/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + + awsutils = { + source = "cloudposse/awsutils" + version = ">= 0.16.0" + } + } +} diff --git a/modules/config-bucket/README.md b/modules/config-bucket/README.md new file mode 100644 index 000000000..2a0526b7b --- /dev/null +++ b/modules/config-bucket/README.md @@ -0,0 +1,106 @@ +# Component: `config-bucket` + +This module creates an S3 bucket suitable for storing `AWS Config` data. + +It implements a configurable log retention policy, which allows you to efficiently manage logs across different +storage classes (_e.g._ `Glacier`) and ultimately expire the data altogether. + +It enables server-side encryption by default. +https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html + +It blocks public access to the bucket by default. +https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. It's suggested to apply this component to only the centralized `audit` account. + +```yaml +components: + terraform: + config-bucket: + vars: + enabled: true + name: "config" + noncurrent_version_expiration_days: 180 + noncurrent_version_transition_days: 30 + standard_transition_days: 60 + glacier_transition_days: 180 + expiration_days: 365 +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [config\_bucket](#module\_config\_bucket) | cloudposse/config-storage/aws | 0.8.1 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [access\_log\_bucket\_name](#input\_access\_log\_bucket\_name) | Name of the S3 bucket where s3 access log will be sent to | `string` | `""` | no | +| [acl](#input\_acl) | The canned ACL to apply. We recommend log-delivery-write for compatibility with AWS services | `string` | `"log-delivery-write"` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enable\_glacier\_transition](#input\_enable\_glacier\_transition) | Enables the transition to AWS Glacier (note that this can incur unnecessary costs for huge amount of small files | `bool` | `true` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [expiration\_days](#input\_expiration\_days) | Number of days after which to expunge the objects | `number` | `90` | no | +| [glacier\_transition\_days](#input\_glacier\_transition\_days) | Number of days after which to move the data to the glacier storage tier | `number` | `60` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lifecycle\_rule\_enabled](#input\_lifecycle\_rule\_enabled) | Enable lifecycle events on this bucket | `bool` | `true` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [noncurrent\_version\_expiration\_days](#input\_noncurrent\_version\_expiration\_days) | Specifies when noncurrent object versions expire | `number` | `90` | no | +| [noncurrent\_version\_transition\_days](#input\_noncurrent\_version\_transition\_days) | Specifies when noncurrent object versions transition to a different storage tier | `number` | `30` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [standard\_transition\_days](#input\_standard\_transition\_days) | Number of days to persist in the standard storage tier before moving to the infrequent access tier | `number` | `30` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [config\_bucket\_arn](#output\_config\_bucket\_arn) | Config bucket ARN | +| [config\_bucket\_domain\_name](#output\_config\_bucket\_domain\_name) | Config bucket FQDN | +| [config\_bucket\_id](#output\_config\_bucket\_id) | Config bucket ID | + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/config-bucket) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/config-bucket/context.tf b/modules/config-bucket/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/config-bucket/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/config-bucket/main.tf b/modules/config-bucket/main.tf new file mode 100644 index 000000000..8e8148aed --- /dev/null +++ b/modules/config-bucket/main.tf @@ -0,0 +1,16 @@ +module "config_bucket" { + source = "cloudposse/config-storage/aws" + version = "0.8.1" + + expiration_days = var.expiration_days + force_destroy = false + glacier_transition_days = var.glacier_transition_days + lifecycle_rule_enabled = var.lifecycle_rule_enabled + noncurrent_version_expiration_days = var.noncurrent_version_expiration_days + noncurrent_version_transition_days = var.noncurrent_version_transition_days + sse_algorithm = "AES256" + standard_transition_days = var.standard_transition_days + access_log_bucket_name = var.access_log_bucket_name + + context = module.this.context +} diff --git a/modules/config-bucket/outputs.tf b/modules/config-bucket/outputs.tf new file mode 100644 index 000000000..6f926cba5 --- /dev/null +++ b/modules/config-bucket/outputs.tf @@ -0,0 +1,14 @@ +output "config_bucket_domain_name" { + value = module.config_bucket.bucket_domain_name + description = "Config bucket FQDN" +} + +output "config_bucket_id" { + value = module.config_bucket.bucket_id + description = "Config bucket ID" +} + +output "config_bucket_arn" { + value = module.config_bucket.bucket_arn + description = "Config bucket ARN" +} diff --git a/modules/config-bucket/providers.tf b/modules/config-bucket/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/config-bucket/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/config-bucket/variables.tf b/modules/config-bucket/variables.tf new file mode 100644 index 000000000..0a5ff0a12 --- /dev/null +++ b/modules/config-bucket/variables.tf @@ -0,0 +1,58 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "lifecycle_rule_enabled" { + type = bool + description = "Enable lifecycle events on this bucket" + default = true +} + +variable "noncurrent_version_expiration_days" { + type = number + default = 90 + description = "Specifies when noncurrent object versions expire" +} + +variable "noncurrent_version_transition_days" { + type = number + default = 30 + description = "Specifies when noncurrent object versions transition to a different storage tier" +} + +variable "standard_transition_days" { + type = number + default = 30 + description = "Number of days to persist in the standard storage tier before moving to the infrequent access tier" +} + +variable "glacier_transition_days" { + type = number + default = 60 + description = "Number of days after which to move the data to the glacier storage tier" +} + +variable "enable_glacier_transition" { + type = bool + default = true + description = "Enables the transition to AWS Glacier (note that this can incur unnecessary costs for huge amount of small files" +} + +variable "expiration_days" { + type = number + default = 90 + description = "Number of days after which to expunge the objects" +} + +variable "access_log_bucket_name" { + type = string + default = "" + description = "Name of the S3 bucket where s3 access log will be sent to" +} + +variable "acl" { + type = string + description = "The canned ACL to apply. We recommend log-delivery-write for compatibility with AWS services" + default = "log-delivery-write" +} diff --git a/modules/config-bucket/versions.tf b/modules/config-bucket/versions.tf new file mode 100644 index 000000000..cc73ffd35 --- /dev/null +++ b/modules/config-bucket/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From f974c7d86d6b3011d44e5fc23c41e03f49f77adc Mon Sep 17 00:00:00 2001 From: Veronika Gnilitska <30597968+gberenice@users.noreply.github.com> Date: Thu, 18 May 2023 19:14:50 +0300 Subject: [PATCH 124/501] feat: adds ability to list principals of Lambdas allowed to access ECR (#680) --- modules/ecr/README.md | 3 ++- modules/ecr/main.tf | 3 ++- modules/ecr/variables.tf | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/ecr/README.md b/modules/ecr/README.md index b7052a7ae..9f9b95f94 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -69,7 +69,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [ecr](#module\_ecr) | cloudposse/ecr/aws | 0.35.0 | +| [ecr](#module\_ecr) | cloudposse/ecr/aws | 0.36.0 | | [full\_access](#module\_full\_access) | ../account-map/modules/roles-to-principals | n/a | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [readonly\_access](#module\_readonly\_access) | ../account-map/modules/roles-to-principals | n/a | @@ -109,6 +109,7 @@ components: | [max\_image\_count](#input\_max\_image\_count) | Max number of images to store. Old ones will be deleted to make room for new ones. | `number` | n/a | yes | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [principals\_lambda](#input\_principals\_lambda) | Principal account IDs of Lambdas allowed to consume ECR | `list(string)` | `[]` | no | | [protected\_tags](#input\_protected\_tags) | Tags to refrain from deleting | `list(string)` | `[]` | no | | [read\_only\_account\_role\_map](#input\_read\_only\_account\_role\_map) | Map of `account:[role, role...]` for read-only access. Use `*` for role to grant access to entire account | `map(list(string))` | `{}` | no | | [read\_write\_account\_role\_map](#input\_read\_write\_account\_role\_map) | Map of `account:[role, role...]` for write access. Use `*` for role to grant access to entire account | `map(list(string))` | n/a | yes | diff --git a/modules/ecr/main.tf b/modules/ecr/main.tf index 41f56fff7..e6b5621d1 100644 --- a/modules/ecr/main.tf +++ b/modules/ecr/main.tf @@ -20,7 +20,7 @@ locals { module "ecr" { source = "cloudposse/ecr/aws" - version = "0.35.0" + version = "0.36.0" protected_tags = var.protected_tags enable_lifecycle_policy = var.enable_lifecycle_policy @@ -29,6 +29,7 @@ module "ecr" { max_image_count = var.max_image_count principals_full_access = compact(concat(module.full_access.principals, [local.ecr_user_arn])) principals_readonly_access = module.readonly_access.principals + principals_lambda = var.principals_lambda scan_images_on_push = var.scan_images_on_push use_fullname = false diff --git a/modules/ecr/variables.tf b/modules/ecr/variables.tf index 293d2b51d..45dce3abe 100644 --- a/modules/ecr/variables.tf +++ b/modules/ecr/variables.tf @@ -52,3 +52,9 @@ variable "enable_lifecycle_policy" { type = bool description = "Enable/disable image lifecycle policy" } + +variable "principals_lambda" { + type = list(string) + description = "Principal account IDs of Lambdas allowed to consume ECR" + default = [] +} From 6ded875dbe7d593f313273113566bc63c93ce86c Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Thu, 18 May 2023 15:43:48 -0400 Subject: [PATCH 125/501] Update `vpc` and `eks/cluster` components (#677) --- modules/eks/cluster/README.md | 12 ++++++------ modules/eks/cluster/aws_sso.tf | 2 -- modules/eks/cluster/fargate-profiles.tf | 2 +- modules/eks/cluster/main.tf | 17 ++++++++++++----- .../cluster/modules/node_group_by_az/main.tf | 4 ++-- .../modules/node_group_by_region/main.tf | 1 - modules/eks/cluster/remote-state.tf | 7 +++---- modules/vpc/README.md | 14 ++++++++------ modules/vpc/main.tf | 11 +++++------ modules/vpc/outputs.tf | 10 ++++++++++ modules/vpc/remote-state.tf | 2 +- 11 files changed, 48 insertions(+), 34 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index a893e6ed7..cd989d326 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -1,6 +1,6 @@ # Component: `eks/cluster` -This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups. +This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups and Fargate profiles. :::warning @@ -195,16 +195,16 @@ For example: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.6.0 | -| [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.1.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.7.0 | +| [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.2.0 | | [iam\_arns](#module\_iam\_arns) | ../../account-map/modules/roles-to-principals | n/a | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [karpenter\_label](#module\_karpenter\_label) | cloudposse/label/null | 0.25.0 | | [region\_node\_group](#module\_region\_node\_group) | ./modules/node_group_by_region | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | ## Resources diff --git a/modules/eks/cluster/aws_sso.tf b/modules/eks/cluster/aws_sso.tf index 992b755a0..f13e575c2 100644 --- a/modules/eks/cluster/aws_sso.tf +++ b/modules/eks/cluster/aws_sso.tf @@ -1,7 +1,6 @@ # This is split off into a separate file in the hopes we can drop it altogether in the future, # or else move it into `roles-to-principals`. - locals { # EKS does not accept the actual role ARN of the permission set, @@ -20,7 +19,6 @@ locals { username = format("%s-%s", local.this_account_name, role.aws_sso_permission_set) groups = role.groups }] - } data "aws_iam_roles" "sso_roles" { diff --git a/modules/eks/cluster/fargate-profiles.tf b/modules/eks/cluster/fargate-profiles.tf index d0eb07857..04e0714d2 100644 --- a/modules/eks/cluster/fargate-profiles.tf +++ b/modules/eks/cluster/fargate-profiles.tf @@ -4,7 +4,7 @@ locals { module "fargate_profile" { source = "cloudposse/eks-fargate-profile/aws" - version = "1.1.0" + version = "1.2.0" for_each = local.fargate_profiles diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index ee43fca7b..9b69b547b 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -3,10 +3,7 @@ locals { eks_outputs = module.eks.outputs vpc_outputs = module.vpc.outputs - attributes = flatten(concat(module.this.attributes, [var.color])) - public_subnet_ids = local.vpc_outputs.public_subnet_ids - private_subnet_ids = local.vpc_outputs.private_subnet_ids - vpc_id = local.vpc_outputs.vpc_id + attributes = flatten(concat(module.this.attributes, [var.color])) this_account_name = module.iam_roles.current_account_account_name identity_account_name = module.iam_roles.identity_account_account_name @@ -78,11 +75,21 @@ locals { module.vpc_ingress[k].outputs.vpc_cidr ] ) + + vpc_id = local.vpc_outputs.vpc_id + + # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` + # `az_public_subnets_map` is a map of AZ names to list of public subnet IDs in the AZs + public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k)]) + + # Get only the private subnets that correspond to the AZs provided in `var.availability_zones` + # `az_private_subnets_map` is a map of AZ names to list of private subnet IDs in the AZs + private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k)]) } module "eks_cluster" { source = "cloudposse/eks-cluster/aws" - version = "2.6.0" + version = "2.7.0" region = var.region attributes = local.attributes diff --git a/modules/eks/cluster/modules/node_group_by_az/main.tf b/modules/eks/cluster/modules/node_group_by_az/main.tf index 080e6ccdc..970b315e1 100644 --- a/modules/eks/cluster/modules/node_group_by_az/main.tf +++ b/modules/eks/cluster/modules/node_group_by_az/main.tf @@ -18,7 +18,7 @@ data "aws_subnets" "private" { module "az_abbreviation" { source = "cloudposse/utils/aws" - version = "1.1.0" + version = "1.3.0" } locals { @@ -32,7 +32,7 @@ locals { module "eks_node_group" { source = "cloudposse/eks-node-group/aws" - version = "2.6.0" + version = "2.10.0" enabled = local.enabled diff --git a/modules/eks/cluster/modules/node_group_by_region/main.tf b/modules/eks/cluster/modules/node_group_by_region/main.tf index 9f5c3f9ea..a14a3c7c1 100644 --- a/modules/eks/cluster/modules/node_group_by_region/main.tf +++ b/modules/eks/cluster/modules/node_group_by_region/main.tf @@ -3,7 +3,6 @@ locals { az_list = tolist(local.az_set) } - module "node_group" { for_each = module.this.enabled ? local.az_set : [] diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf index ad18d7487..af1f02457 100644 --- a/modules/eks/cluster/remote-state.tf +++ b/modules/eks/cluster/remote-state.tf @@ -12,7 +12,7 @@ module "iam_arns" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "vpc" @@ -21,7 +21,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" for_each = local.accounts_with_vpc @@ -33,13 +33,12 @@ module "vpc_ingress" { context = module.this.context } - # Yes, this is self-referential. # It obtains the previous state of the cluster so that we can add # to it rather than overwrite it (specifically the aws-auth configMap) module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = var.eks_component_name diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 5e39e192c..5bcfcd350 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -70,14 +70,14 @@ components: | Name | Source | Version | |------|--------|---------| -| [endpoint\_security\_groups](#module\_endpoint\_security\_groups) | cloudposse/security-group/aws | 2.0.0-rc1 | +| [endpoint\_security\_groups](#module\_endpoint\_security\_groups) | cloudposse/security-group/aws | 2.1.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [subnets](#module\_subnets) | cloudposse/dynamic-subnets/aws | 2.0.4 | +| [subnets](#module\_subnets) | cloudposse/dynamic-subnets/aws | 2.3.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [utils](#module\_utils) | cloudposse/utils/aws | 1.1.0 | -| [vpc](#module\_vpc) | cloudposse/vpc/aws | 2.0.0 | -| [vpc\_endpoints](#module\_vpc\_endpoints) | cloudposse/vpc/aws//modules/vpc-endpoints | 2.0.0 | -| [vpc\_flow\_logs\_bucket](#module\_vpc\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | +| [vpc](#module\_vpc) | cloudposse/vpc/aws | 2.1.0 | +| [vpc\_endpoints](#module\_vpc\_endpoints) | cloudposse/vpc/aws//modules/vpc-endpoints | 2.1.0 | +| [vpc\_flow\_logs\_bucket](#module\_vpc\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | ## Resources @@ -143,6 +143,8 @@ components: | Name | Description | |------|-------------| | [availability\_zones](#output\_availability\_zones) | List of Availability Zones where subnets were created | +| [az\_private\_subnets\_map](#output\_az\_private\_subnets\_map) | Map of AZ names to list of private subnet IDs in the AZs | +| [az\_public\_subnets\_map](#output\_az\_public\_subnets\_map) | Map of AZ names to list of public subnet IDs in the AZs | | [interface\_vpc\_endpoints](#output\_interface\_vpc\_endpoints) | List of Interface VPC Endpoints in this VPC. | | [max\_subnet\_count](#output\_max\_subnet\_count) | Maximum allowed number of subnets before all subnet CIDRs need to be recomputed | | [nat\_eip\_protections](#output\_nat\_eip\_protections) | List of AWS Shield Advanced Protections for NAT Elastic IPs. | diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index 7cc4a2c0a..51ce85f10 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -67,12 +67,12 @@ locals { module "utils" { source = "cloudposse/utils/aws" - version = "1.1.0" + version = "1.3.0" } module "vpc" { source = "cloudposse/vpc/aws" - version = "2.0.0" + version = "2.1.0" ipv4_primary_cidr_block = var.ipv4_primary_cidr_block internet_gateway_enabled = var.public_subnets_enabled @@ -99,7 +99,7 @@ module "endpoint_security_groups" { for_each = local.enabled && try(length(var.interface_vpc_endpoints), 0) > 0 ? toset([local.interface_endpoint_security_group_key]) : [] source = "cloudposse/security-group/aws" - version = "2.0.0-rc1" + version = "2.1.0" create_before_destroy = true preserve_security_group_id = false @@ -124,10 +124,9 @@ module "endpoint_security_groups" { context = module.this.context } - module "vpc_endpoints" { source = "cloudposse/vpc/aws//modules/vpc-endpoints" - version = "2.0.0" + version = "2.1.0" enabled = (length(var.interface_vpc_endpoints) + length(var.gateway_vpc_endpoints)) > 0 @@ -140,7 +139,7 @@ module "vpc_endpoints" { module "subnets" { source = "cloudposse/dynamic-subnets/aws" - version = "2.0.4" + version = "2.3.0" availability_zones = local.availability_zones availability_zone_ids = local.availability_zone_ids diff --git a/modules/vpc/outputs.tf b/modules/vpc/outputs.tf index 7459d8ea4..7e8ddd273 100644 --- a/modules/vpc/outputs.tf +++ b/modules/vpc/outputs.tf @@ -118,3 +118,13 @@ output "availability_zones" { description = "List of Availability Zones where subnets were created" value = module.subnets.availability_zones } + +output "az_private_subnets_map" { + description = "Map of AZ names to list of private subnet IDs in the AZs" + value = module.subnets.az_private_subnets_map +} + +output "az_public_subnets_map" { + description = "Map of AZ names to list of public subnet IDs in the AZs" + value = module.subnets.az_public_subnets_map +} diff --git a/modules/vpc/remote-state.tf b/modules/vpc/remote-state.tf index a956d0c8a..3b0148dd0 100644 --- a/modules/vpc/remote-state.tf +++ b/modules/vpc/remote-state.tf @@ -2,7 +2,7 @@ module "vpc_flow_logs_bucket" { count = var.vpc_flow_logs_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "vpc-flow-logs-bucket" environment = var.vpc_flow_logs_bucket_environment_name From aaf8630c1cb9366ceccb4b9082824c66484286dd Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 18 May 2023 13:31:26 -0700 Subject: [PATCH 126/501] `datadog-agent` bugfixes (#681) --- modules/eks/datadog-agent/README.md | 2 +- modules/eks/datadog-agent/main.tf | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index 513e29358..ab6cac485 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -24,7 +24,7 @@ components: create_namespace: true repository: "https://helm.datadoghq.com" chart: "datadog" - chart_version: "3.0.3" + chart_version: "3.29.2" timeout: 1200 wait: true atomic: true diff --git a/modules/eks/datadog-agent/main.tf b/modules/eks/datadog-agent/main.tf index 8f57f4250..8527e6011 100644 --- a/modules/eks/datadog-agent/main.tf +++ b/modules/eks/datadog-agent/main.tf @@ -8,7 +8,9 @@ locals { # combine context tags with passed in datadog_tags # skip name since that won't be relevant for each metric - datadog_tags = toset(distinct(concat([for k, v in module.this.tags : "${lower(k)}:${v}" if lower(k) != "name"], tolist(var.datadog_tags)))) + datadog_tags = toset(distinct(concat([ + for k, v in module.this.tags : "${lower(k)}:${v}" if lower(k) != "name" + ], tolist(var.datadog_tags)))) cluster_checks_enabled = local.enabled && var.cluster_checks_enabled @@ -66,10 +68,7 @@ locals { } module "datadog_configuration" { - source = "../../datadog-configuration/modules/datadog_keys" - - global_environment_name = null - + source = "../../datadog-configuration/modules/datadog_keys" context = module.this.context } From 4b8d96fc7c50342c8db6a9fe748cef7d013c7f1f Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Fri, 19 May 2023 15:53:43 -0400 Subject: [PATCH 127/501] Update `module "datadog_configuration"` modules (#684) --- modules/datadog-configuration/README.md | 1 + modules/datadog-configuration/modules/datadog_keys/README.md | 2 +- modules/datadog-integration/provider-datadog.tf | 2 +- modules/datadog-lambda-forwarder/provider-datadog.tf | 2 +- modules/datadog-logs-archive/provider-datadog.tf | 2 +- modules/datadog-monitor/provider-datadog.tf | 2 +- modules/datadog-private-location-ecs/provider-datadog.tf | 2 +- modules/datadog-synthetics-private-location/provider-datadog.tf | 2 +- modules/datadog-synthetics/provider-datadog.tf | 2 +- modules/ecs-service/datadog-agent.tf | 2 +- modules/opsgenie-team/provider-datadog.tf | 1 - 11 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index 0c3f75405..b1ea30395 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -45,6 +45,7 @@ Here is a snippet of using the `datadog_keys` submodule: ```terraform module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" + enabled = true context = module.this.context } diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index d87d11582..2a6780834 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -7,8 +7,8 @@ Useful submodule for other modules to quickly configure the datadog provider ```hcl module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - context = module.this.context enabled = true + context = module.this.context } provider "datadog" { diff --git a/modules/datadog-integration/provider-datadog.tf b/modules/datadog-integration/provider-datadog.tf index 80e2e0738..0b4e862f8 100644 --- a/modules/datadog-integration/provider-datadog.tf +++ b/modules/datadog-integration/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - context = module.this.context enabled = true + context = module.this.context } provider "datadog" { diff --git a/modules/datadog-lambda-forwarder/provider-datadog.tf b/modules/datadog-lambda-forwarder/provider-datadog.tf index 80e2e0738..0b4e862f8 100644 --- a/modules/datadog-lambda-forwarder/provider-datadog.tf +++ b/modules/datadog-lambda-forwarder/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - context = module.this.context enabled = true + context = module.this.context } provider "datadog" { diff --git a/modules/datadog-logs-archive/provider-datadog.tf b/modules/datadog-logs-archive/provider-datadog.tf index 9db2f3065..56729e3c5 100644 --- a/modules/datadog-logs-archive/provider-datadog.tf +++ b/modules/datadog-logs-archive/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - context = module.this.context enabled = true + context = module.this.context } locals { diff --git a/modules/datadog-monitor/provider-datadog.tf b/modules/datadog-monitor/provider-datadog.tf index 80e2e0738..0b4e862f8 100644 --- a/modules/datadog-monitor/provider-datadog.tf +++ b/modules/datadog-monitor/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - context = module.this.context enabled = true + context = module.this.context } provider "datadog" { diff --git a/modules/datadog-private-location-ecs/provider-datadog.tf b/modules/datadog-private-location-ecs/provider-datadog.tf index 80e2e0738..0b4e862f8 100644 --- a/modules/datadog-private-location-ecs/provider-datadog.tf +++ b/modules/datadog-private-location-ecs/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - context = module.this.context enabled = true + context = module.this.context } provider "datadog" { diff --git a/modules/datadog-synthetics-private-location/provider-datadog.tf b/modules/datadog-synthetics-private-location/provider-datadog.tf index 80e2e0738..0b4e862f8 100644 --- a/modules/datadog-synthetics-private-location/provider-datadog.tf +++ b/modules/datadog-synthetics-private-location/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - context = module.this.context enabled = true + context = module.this.context } provider "datadog" { diff --git a/modules/datadog-synthetics/provider-datadog.tf b/modules/datadog-synthetics/provider-datadog.tf index 80e2e0738..0b4e862f8 100644 --- a/modules/datadog-synthetics/provider-datadog.tf +++ b/modules/datadog-synthetics/provider-datadog.tf @@ -1,7 +1,7 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - context = module.this.context enabled = true + context = module.this.context } provider "datadog" { diff --git a/modules/ecs-service/datadog-agent.tf b/modules/ecs-service/datadog-agent.tf index a36abe1e6..08466f378 100644 --- a/modules/ecs-service/datadog-agent.tf +++ b/modules/ecs-service/datadog-agent.tf @@ -158,6 +158,6 @@ module "datadog_fluent_bit_container_definition" { module "datadog_configuration" { count = var.datadog_agent_sidecar_enabled ? 1 : 0 source = "../datadog-configuration/modules/datadog_keys" - region = var.region + enabled = true context = module.this.context } diff --git a/modules/opsgenie-team/provider-datadog.tf b/modules/opsgenie-team/provider-datadog.tf index a3a1ab557..d70c1da5e 100644 --- a/modules/opsgenie-team/provider-datadog.tf +++ b/modules/opsgenie-team/provider-datadog.tf @@ -2,7 +2,6 @@ module "datadog_configuration" { source = "../datadog-configuration/modules/datadog_keys" - region = var.region enabled = true context = module.this.context } From 50fdaa1a4c603f9e2994698abbbb6f7ff53a3f61 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 19 May 2023 19:54:51 -0400 Subject: [PATCH 128/501] feat: add lambda monitors to datadog-monitor (#686) --- .../monitors/lambda-log-forwarder.yaml | 39 +++++++++++++++++++ .../catalog/monitors/lambda.yaml | 22 +++++++++++ 2 files changed, 61 insertions(+) create mode 100644 modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml create mode 100644 modules/datadog-monitor/catalog/monitors/lambda.yaml diff --git a/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml b/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml new file mode 100644 index 000000000..7c583f437 --- /dev/null +++ b/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml @@ -0,0 +1,39 @@ +# The official Datadog API documentation with available query parameters & alert types: +# https://docs.datadoghq.com/api/v1/monitors/#create-a-monitor + +datadog-lambda-forwarder-config-modification: + name: "(Lambda) ${ stage } - Datadog Lambda Forwarder Config Changed" + type: event-v2 alert + query: | + events("source:amazon_lambda functionname:${tenant}-${environment}-${ stage }-datadog-lambda-forwarder-logs").rollup("count").last("15m") >= 1 + message: | + Configuration has been changed for the datadog lambda forwarder in + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) + by {{ event.tags.username }}. + Event title: {{ event.title }} + Lambda function name: {{ event.tags.functionname }} + Event ID: {{ event.id }} + + escalation_message: "" + tags: + managed-by: Terraform + notify_no_data: false + notify_audit: true + require_full_window: true + enable_logs_sample: false + force_delete: true + include_tags: true + locked: false + renotify_interval: 60 + timeout_h: 1 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 1 + #warning: + #unknown: + #ok: + #critical_recovery: + #warning_recovery: diff --git a/modules/datadog-monitor/catalog/monitors/lambda.yaml b/modules/datadog-monitor/catalog/monitors/lambda.yaml new file mode 100644 index 000000000..9f6754cb7 --- /dev/null +++ b/modules/datadog-monitor/catalog/monitors/lambda.yaml @@ -0,0 +1,22 @@ +# The official Datadog API documentation with available query parameters & alert types: +# https://docs.datadoghq.com/api/v1/monitors/#create-a-monitor + +lambda-errors: + name: AWS Lambda [{{functionname.name}}] has errors + type: query alert + query: sum(last_5m):sum:aws.lambda.errors{*} by {stage,tenant,environment,functionname}.as_count() > 0 + message: | + Lambda {{functionname.name}} in + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) + has {{value}} errors over the last 5 minutes. + tags: [] + threshold_windows: { } + thresholds: + critical: 0 + notify_audit: false + require_full_window: false + notify_no_data: false + renotify_interval: 0 + include_tags: true + evaluation_delay: 900 + new_group_delay: 60 From bd3f21b7a0501640bdebcbf4c4881da819ec26c7 Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Sat, 20 May 2023 22:41:12 +0300 Subject: [PATCH 129/501] Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL (#688) --- modules/s3-bucket/README.md | 2 +- modules/s3-bucket/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index 1bd37e0b9..8d6474458 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -97,7 +97,7 @@ components: | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [bucket\_policy](#module\_bucket\_policy) | cloudposse/iam-policy/aws | 0.4.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [s3\_bucket](#module\_s3\_bucket) | cloudposse/s3-bucket/aws | 3.0.0 | +| [s3\_bucket](#module\_s3\_bucket) | cloudposse/s3-bucket/aws | 3.1.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/s3-bucket/main.tf b/modules/s3-bucket/main.tf index 2035e0b05..e248c35d4 100644 --- a/modules/s3-bucket/main.tf +++ b/modules/s3-bucket/main.tf @@ -37,7 +37,7 @@ module "bucket_policy" { module "s3_bucket" { source = "cloudposse/s3-bucket/aws" - version = "3.0.0" + version = "3.1.1" bucket_name = var.bucket_name From f7a75efa0585257556f1ce36bd547afed4fb714a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 22 May 2023 11:39:44 -0700 Subject: [PATCH 130/501] Corrections to `dms` components (#658) Co-authored-by: Andriy Knysh --- modules/dms/endpoint/README.md | 17 ++++++++++++----- modules/dms/endpoint/main.tf | 19 +++++++++++++++++-- modules/dms/endpoint/variables.tf | 17 +++++++++++++++-- modules/dms/endpoint/versions.tf | 2 +- modules/dms/iam/README.md | 3 +-- modules/dms/iam/versions.tf | 16 +++++++++------- modules/dms/replication-task/main.tf | 6 +++--- 7 files changed, 58 insertions(+), 22 deletions(-) diff --git a/modules/dms/endpoint/README.md b/modules/dms/endpoint/README.md index 2892afc11..05ad66b9d 100644 --- a/modules/dms/endpoint/README.md +++ b/modules/dms/endpoint/README.md @@ -74,12 +74,14 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.2.0 | +| [terraform](#requirement\_terraform) | >= 1.0 | | [aws](#requirement\_aws) | >= 4.26.0 | ## Providers -No providers. +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.26.0 | ## Modules @@ -91,7 +93,10 @@ No providers. ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_ssm_parameter.password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.username](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs @@ -123,7 +128,8 @@ No resources. | [mongodb\_settings](#input\_mongodb\_settings) | Configuration block for MongoDB settings | `map(any)` | `null` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [password](#input\_password) | Password to be used to login to the endpoint database | `string` | `null` | no | +| [password](#input\_password) | Password to be used to login to the endpoint database | `string` | `""` | no | +| [password\_path](#input\_password\_path) | If set, the path in AWS SSM Parameter Store to fetch the password for the DMS admin user | `string` | `""` | no | | [port](#input\_port) | Port used by the endpoint database | `number` | `null` | no | | [redshift\_settings](#input\_redshift\_settings) | Configuration block for Redshift settings | `map(any)` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | @@ -137,7 +143,8 @@ No resources. | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [username](#input\_username) | User name to be used to login to the endpoint database | `string` | `null` | no | +| [username](#input\_username) | User name to be used to login to the endpoint database | `string` | `""` | no | +| [username\_path](#input\_username\_path) | If set, the path in AWS SSM Parameter Store to fetch the username for the DMS admin user | `string` | `""` | no | ## Outputs diff --git a/modules/dms/endpoint/main.tf b/modules/dms/endpoint/main.tf index cdf92bf4b..0e2b52f52 100644 --- a/modules/dms/endpoint/main.tf +++ b/modules/dms/endpoint/main.tf @@ -1,3 +1,18 @@ +locals { + fetch_username = !(length(var.username) > 0) && (length(var.username_path) > 0) ? true : false + fetch_password = !(length(var.password) > 0) && (length(var.password_path) > 0) ? true : false +} + +data "aws_ssm_parameter" "username" { + count = local.fetch_username ? 1 : 0 + name = var.username_path +} + +data "aws_ssm_parameter" "password" { + count = local.fetch_password ? 1 : 0 + name = var.password_path +} + module "dms_endpoint" { source = "cloudposse/dms/aws//modules/dms-endpoint" version = "0.1.1" @@ -7,7 +22,6 @@ module "dms_endpoint" { kms_key_arn = var.kms_key_arn certificate_arn = var.certificate_arn database_name = var.database_name - password = var.password port = var.port extra_connection_attributes = var.extra_connection_attributes secrets_manager_access_role_arn = var.secrets_manager_access_role_arn @@ -15,7 +29,8 @@ module "dms_endpoint" { server_name = var.server_name service_access_role = var.service_access_role ssl_mode = var.ssl_mode - username = var.username + username = local.fetch_username ? data.aws_ssm_parameter.username[0].value : var.username + password = local.fetch_password ? data.aws_ssm_parameter.password[0].value : var.password elasticsearch_settings = var.elasticsearch_settings kafka_settings = var.kafka_settings kinesis_settings = var.kinesis_settings diff --git a/modules/dms/endpoint/variables.tf b/modules/dms/endpoint/variables.tf index 2bc19a1e7..73ab0dc02 100644 --- a/modules/dms/endpoint/variables.tf +++ b/modules/dms/endpoint/variables.tf @@ -34,7 +34,7 @@ variable "database_name" { variable "password" { type = string description = "Password to be used to login to the endpoint database" - default = null + default = "" } variable "port" { @@ -82,7 +82,7 @@ variable "ssl_mode" { variable "username" { type = string description = "User name to be used to login to the endpoint database" - default = null + default = "" } variable "elasticsearch_settings" { @@ -120,3 +120,16 @@ variable "s3_settings" { description = "Configuration block for S3 settings" default = null } + +variable "username_path" { + type = string + description = "If set, the path in AWS SSM Parameter Store to fetch the username for the DMS admin user" + default = "" +} + +variable "password_path" { + type = string + description = "If set, the path in AWS SSM Parameter Store to fetch the password for the DMS admin user" + default = "" +} + diff --git a/modules/dms/endpoint/versions.tf b/modules/dms/endpoint/versions.tf index 463b50e1d..d8daf2ae0 100644 --- a/modules/dms/endpoint/versions.tf +++ b/modules/dms/endpoint/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.2.0" + required_version = ">= 1.0" required_providers { aws = { diff --git a/modules/dms/iam/README.md b/modules/dms/iam/README.md index fe9d4f9ab..8babad2c2 100644 --- a/modules/dms/iam/README.md +++ b/modules/dms/iam/README.md @@ -29,8 +29,7 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [source](#requirement\_source) | hashicorp/aws | -| [version](#requirement\_version) | >= 4.26.0 | +| [aws](#requirement\_aws) | >= 4.26.0 | ## Providers diff --git a/modules/dms/iam/versions.tf b/modules/dms/iam/versions.tf index 1150e407f..d8daf2ae0 100644 --- a/modules/dms/iam/versions.tf +++ b/modules/dms/iam/versions.tf @@ -2,12 +2,14 @@ terraform { required_version = ">= 1.0" required_providers { - source = "hashicorp/aws" - # Using the latest version of the provider since the earlier versions had many issues with DMS replication tasks. - # In particular: - # https://github.com/hashicorp/terraform-provider-aws/pull/24047 - # https://github.com/hashicorp/terraform-provider-aws/pull/23692 - # https://github.com/hashicorp/terraform-provider-aws/pull/13476 - version = ">= 4.26.0" + aws = { + source = "hashicorp/aws" + # Using the latest version of the provider since the earlier versions had many issues with DMS replication tasks. + # In particular: + # https://github.com/hashicorp/terraform-provider-aws/pull/24047 + # https://github.com/hashicorp/terraform-provider-aws/pull/23692 + # https://github.com/hashicorp/terraform-provider-aws/pull/13476 + version = ">= 4.26.0" + } } } diff --git a/modules/dms/replication-task/main.tf b/modules/dms/replication-task/main.tf index 239e8167f..cb6801c1e 100644 --- a/modules/dms/replication-task/main.tf +++ b/modules/dms/replication-task/main.tf @@ -2,9 +2,9 @@ module "dms_replication_task" { source = "cloudposse/dms/aws//modules/dms-replication-task" version = "0.1.1" - replication_instance_arn = module.dms_replication_instance.outputs.replication_instance_arn - source_endpoint_arn = module.dms_endpoint_source.outputs.endpoint_arn - target_endpoint_arn = module.dms_endpoint_target.outputs.endpoint_arn + replication_instance_arn = module.dms_replication_instance.outputs.dms_replication_instance_arn + source_endpoint_arn = module.dms_endpoint_source.outputs.dms_endpoint_arn + target_endpoint_arn = module.dms_endpoint_target.outputs.dms_endpoint_arn start_replication_task = var.start_replication_task migration_type = var.migration_type From 1dbe50fbd78082128468f41f2162051c6253e4c3 Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Wed, 24 May 2023 14:11:50 +0300 Subject: [PATCH 131/501] Managed rules for AWS Config (#690) --- modules/aws-config/README.md | 9 +++++++++ modules/aws-config/main.tf | 1 + modules/aws-config/variables.tf | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index 17fb96d63..c7e5bc47e 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -80,6 +80,14 @@ components: parameter_overrides: ... (etc) + managed_rules: + access-keys-rotated: + identifier: ACCESS_KEYS_ROTATED + description: "Checks whether the active access keys are rotated within the number of days specified in maxAccessKeyAge. The rule is NON_COMPLIANT if the access keys have not been rotated for more than maxAccessKeyAge number of days." + input_parameters: + maxAccessKeyAge: "30" + enabled: true + tags: {} ``` ## Deployment @@ -171,6 +179,7 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [managed\_rules](#input\_managed\_rules) | A list of AWS Managed Rules that should be enabled on the account.

See the following for a list of possible rules to enable:
https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html

Example:
managed_rules = {
access-keys-rotated = {
identifier = "ACCESS_KEYS_ROTATED"
description = "Checks whether the active access keys are rotated within the number of days specified in maxAccessKeyAge. The rule is NON_COMPLIANT if the access keys have not been rotated for more than maxAccessKeyAge number of days."
input_parameters = {
maxAccessKeyAge : "90"
}
enabled = true
tags = {}
}
}
|
map(object({
description = string
identifier = string
input_parameters = any
tags = map(string)
enabled = bool
}))
| `{}` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | diff --git a/modules/aws-config/main.tf b/modules/aws-config/main.tf index 7c9d01fa4..8728f91ba 100644 --- a/modules/aws-config/main.tf +++ b/modules/aws-config/main.tf @@ -65,6 +65,7 @@ module "aws_config" { s3_bucket_arn = local.s3_bucket.config_bucket_arn create_iam_role = local.create_iam_role iam_role_arn = local.config_iam_role_arn + managed_rules = var.managed_rules create_sns_topic = true global_resource_collector_region = var.global_resource_collector_region diff --git a/modules/aws-config/variables.tf b/modules/aws-config/variables.tf index 96c21e2a2..dc991786b 100644 --- a/modules/aws-config/variables.tf +++ b/modules/aws-config/variables.tf @@ -123,3 +123,35 @@ variable "iam_roles_environment_name" { description = "The name of the environment where the IAM roles are provisioned" default = "gbl" } + +variable "managed_rules" { + description = <<-DOC + A list of AWS Managed Rules that should be enabled on the account. + + See the following for a list of possible rules to enable: + https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html + + Example: + ``` + managed_rules = { + access-keys-rotated = { + identifier = "ACCESS_KEYS_ROTATED" + description = "Checks whether the active access keys are rotated within the number of days specified in maxAccessKeyAge. The rule is NON_COMPLIANT if the access keys have not been rotated for more than maxAccessKeyAge number of days." + input_parameters = { + maxAccessKeyAge : "90" + } + enabled = true + tags = {} + } + } + ``` + DOC + type = map(object({ + description = string + identifier = string + input_parameters = any + tags = map(string) + enabled = bool + })) + default = {} +} From 1c86880efa7f72a20c574ba91f32cf298f47fd34 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 25 May 2023 12:05:28 -0700 Subject: [PATCH 132/501] Update ALB controller IAM policy (#696) --- modules/eks/alb-controller/main.tf | 47 +++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/modules/eks/alb-controller/main.tf b/modules/eks/alb-controller/main.tf index bdf493ad7..44be3b361 100644 --- a/modules/eks/alb-controller/main.tf +++ b/modules/eks/alb-controller/main.tf @@ -28,18 +28,32 @@ module "alb_controller" { iam_role_enabled = true # https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.2.1/docs/install/iam_policy.json iam_policy_statements = [ + { + sid = "AllowCreateServiceLinkedRole" + effect = "Allow" + resources = ["*"] + + actions = ["iam:CreateServiceLinkedRole"] + conditions = [ + { + test = "StringEquals" + variable = "AWSServiceName" + values = ["elasticloadbalancing.amazonaws.com"] + } + ] + }, { sid = "AllowManageCompute" effect = "Allow" resources = ["*"] actions = [ - "iam:CreateServiceLinkedRole", "ec2:DescribeAccountAttributes", "ec2:DescribeAddresses", "ec2:DescribeAvailabilityZones", "ec2:DescribeInternetGateways", "ec2:DescribeVpcs", + "ec2:DescribeVpcPeeringConnections", "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeInstances", @@ -259,6 +273,37 @@ module "alb_controller" { } ] }, + { + sid = "AllowAddTagsOnCreate" + effect = "Allow" + + resources = [ + "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*", + ] + + actions = ["elasticloadbalancing:AddTags"] + + conditions = [ + { + test = "StringEquals" + variable = "elasticloadbalancing:CreateAction" + + values = [ + "CreateTargetGroup", + "CreateLoadBalancer", + # See https://github.com/kubernetes-sigs/aws-load-balancer-controller/issues/2692#issuecomment-1426242236 + "CreateListener", + ] + }, + { + test = "Null" + variable = "aws:RequestTag/elbv2.k8s.aws/cluster" + values = ["false"] + } + ] + }, { sid = "AllowRegisterTargets" effect = "Allow" From c5d2f19b4189ee97c1a4b7a333b6bcb637f00c92 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 25 May 2023 15:05:59 -0700 Subject: [PATCH 133/501] EKS FAQ for Addons (#699) Co-authored-by: Nuru Co-authored-by: cloudpossebot --- modules/eks/cluster/README.md | 35 +++++++++++++++++++++++++++++++- modules/eks/cluster/main.tf | 3 ++- modules/eks/cluster/variables.tf | 9 ++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index cd989d326..b584d09af 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -177,6 +177,38 @@ For example: tags: null ``` +### Using Addons + +EKS clusters support “Addons” that can be automatically installed on a cluster. Install these addons with the [`var.addons` input](https://docs.cloudposse.com/components/library/aws/eks/cluster/#input_addons). + +```yaml +addons: + - addon_name: vpc-cni + addon_version: v1.12.6-eksbuild.2 +``` + +Some addons, such as CoreDNS, require at least one node to be fully provisioned first. +See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. +Set `var.addons_depends_on` to `true` to require the Node Groups to be provisioned before addons. + +```yaml +addons_depends_on: true +addons: + - addon_name: coredns + addon_version: v1.25 +``` + +:::warning + +Addons may not be suitable for all use-cases! For example, if you are using Karpenter to provision nodes, +these nodes will never be available before the cluster component is deployed. + +::: + +For more on upgrading these EKS Addons, see +["How to Upgrade EKS Cluster Addons"](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons/) + + ## Requirements @@ -196,7 +228,7 @@ For example: | Name | Source | Version | |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | -| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.7.0 | +| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.8.1 | | [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.2.0 | | [iam\_arns](#module\_iam\_arns) | ../../account-map/modules/roles-to-principals | n/a | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | @@ -227,6 +259,7 @@ For example: |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [addons](#input\_addons) | Manages [`aws_eks_addon`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
list(object({
addon_name = string
addon_version = string
resolve_conflicts = string
service_account_role_arn = string
}))
| `[]` | no | +| [addons\_depends\_on](#input\_addons\_depends\_on) | If set `true`, all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned.
See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. | `bool` | `false` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 9b69b547b..81d871e31 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -89,7 +89,7 @@ locals { module "eks_cluster" { source = "cloudposse/eks-cluster/aws" - version = "2.7.0" + version = "2.8.1" region = var.region attributes = local.attributes @@ -122,6 +122,7 @@ module "eks_cluster" { subnet_ids = var.cluster_private_subnets_only ? local.private_subnet_ids : concat(local.private_subnet_ids, local.public_subnet_ids) vpc_id = local.vpc_id addons = var.addons + addons_depends_on = var.addons_depends_on ? [module.region_node_group] : null kubernetes_config_map_ignore_role_changes = false diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 23a3d55f9..e17e0353a 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -398,3 +398,12 @@ variable "addons" { description = "Manages [`aws_eks_addon`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources" default = [] } + +variable "addons_depends_on" { + type = bool + description = <<-EOT + If set `true`, all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned. + See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. + EOT + default = false +} From 594de4bf2c8b96574506f3f16f413dd5090f35d9 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Sat, 27 May 2023 14:51:49 -0400 Subject: [PATCH 134/501] Fix tags (#701) Co-authored-by: cloudpossebot --- modules/dns-delegated/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dns-delegated/main.tf b/modules/dns-delegated/main.tf index 35390bf03..511bfc2ea 100644 --- a/modules/dns-delegated/main.tf +++ b/modules/dns-delegated/main.tf @@ -76,7 +76,7 @@ resource "aws_shield_protection" "shield_protection" { name = local.aws_route53_zone[each.key].name resource_arn = format("arn:%s:route53:::hostedzone/%s", local.aws_partition, local.aws_route53_zone[each.key].id) - tags = module.this.context + tags = module.this.tags } resource "aws_route53_record" "soa" { From 4f76eed1f2f8097697f0e2b110e07bb8016785dd Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 30 May 2023 09:30:23 -0700 Subject: [PATCH 135/501] Upstream `aws-inspector` (#700) Co-authored-by: Erik Osterman (CEO @ Cloud Posse) Co-authored-by: Andriy Knysh --- modules/aws-inspector/README.md | 105 +++++++++++ modules/aws-inspector/context.tf | 279 +++++++++++++++++++++++++++++ modules/aws-inspector/main.tf | 30 ++++ modules/aws-inspector/outputs.tf | 4 + modules/aws-inspector/providers.tf | 29 +++ modules/aws-inspector/variables.tf | 14 ++ modules/aws-inspector/versions.tf | 10 ++ 7 files changed, 471 insertions(+) create mode 100644 modules/aws-inspector/README.md create mode 100644 modules/aws-inspector/context.tf create mode 100644 modules/aws-inspector/main.tf create mode 100644 modules/aws-inspector/outputs.tf create mode 100644 modules/aws-inspector/providers.tf create mode 100644 modules/aws-inspector/variables.tf create mode 100644 modules/aws-inspector/versions.tf diff --git a/modules/aws-inspector/README.md b/modules/aws-inspector/README.md new file mode 100644 index 000000000..b9d98843c --- /dev/null +++ b/modules/aws-inspector/README.md @@ -0,0 +1,105 @@ +# Component: `aws-inspector` + +This component is responsible for provisioning an [AWS Inspector](https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html) by installing the [Inspector agent](https://repost.aws/knowledge-center/set-up-amazon-inspector) across all EC2 instances and applying the Inspector rules. + +AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security vulnerabilities and deviations from security best practices. + +Here are some key features and functionalities of AWS Inspector: +- **Security Assessments:** AWS Inspector performs security assessments by analyzing the behavior of your resources and identifying potential security vulnerabilities. It examines the network configuration, operating system settings, and installed software to detect common security issues. + +- **Vulnerability Detection:** AWS Inspector uses a predefined set of rules to identify common vulnerabilities, misconfigurations, and security exposures. It leverages industry-standard security best practices and continuously updates its knowledge base to stay current with emerging threats. + +- **Agent-Based Architecture:** AWS Inspector utilizes an agent-based approach, where you install an Inspector agent on your EC2 instances. The agent collects data about the system and its configuration, securely sends it to AWS Inspector, and allows for more accurate and detailed assessments. + +- **Security Findings:** After performing an assessment, AWS Inspector generates detailed findings that highlight security vulnerabilities, including their severity level, impact, and remediation steps. These findings can help you prioritize and address security issues within your AWS environment. + +- **Integration with AWS Services:** AWS Inspector seamlessly integrates with other AWS services, such as AWS CloudFormation, AWS Systems Manager, and AWS Security Hub. This allows you to automate security assessments, manage findings, and centralize security information across your AWS infrastructure. + + + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + aws-inspector: + vars: + enabled: true + enabled_rules: + - cis +``` +The `aws-inspector` component can be included in your Terraform stack configuration. In the provided example, it is enabled with the `enabled` variable set to `true`. The `enabled_rules` variable specifies a list of rules to enable, and in this case, it includes the `cis` rule. +To simplify rule selection, the short forms of the rules are used for the `enabled_rules` key. These short forms automatically retrieve the appropriate ARN for the rule package based on the region being used. You can find a list of available short forms and their corresponding rule packages in the [var.enabled_rules](https://github.com/cloudposse/terraform-aws-inspector#input_enabled_rules) input documentation. + +For a comprehensive list of rules and their corresponding ARNs, you can refer to the [Amazon Inspector ARNs for rules packages](https://docs.aws.amazon.com/inspector/latest/userguide/inspector_rules-arns.html) documentation. This resource provides detailed information on various rules that can be used with AWS Inspector and their unique identifiers (ARNs). + +By customizing the configuration with the appropriate rules, you can tailor the security assessments performed by AWS Inspector to meet the specific requirements and compliance standards of your applications and infrastructure. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [inspector](#module\_inspector) | cloudposse/inspector/aws | 0.2.8 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ssm_association.install_agent](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_association) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [enabled\_rules](#input\_enabled\_rules) | A list of AWS Inspector rules that should run on a periodic basis.

Valid values are `cve`, `cis`, `nr`, `sbp` which map to the appropriate [Inspector rule arns by region](https://docs.aws.amazon.com/inspector/latest/userguide/inspector_rules-arns.html). | `list(string)` | `[]` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [inspector](#output\_inspector) | The AWS Inspector module outputs | + +## References +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component +[](https://cpco.io/component) diff --git a/modules/aws-inspector/context.tf b/modules/aws-inspector/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/aws-inspector/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/aws-inspector/main.tf b/modules/aws-inspector/main.tf new file mode 100644 index 000000000..f046e75ea --- /dev/null +++ b/modules/aws-inspector/main.tf @@ -0,0 +1,30 @@ +locals { + enabled = module.this.enabled +} + +resource "aws_ssm_association" "install_agent" { + count = local.enabled ? 1 : 0 + + # Owned by AWS + # https://docs.aws.amazon.com/inspector/latest/userguide/inspector_installing-uninstalling-agents.html + name = "AmazonInspector-ManageAWSAgent" + + parameters = { + Operation = "Install" + } + + targets { + key = "InstanceIds" + values = ["*"] + } +} + +module "inspector" { + source = "cloudposse/inspector/aws" + version = "0.2.8" + + create_iam_role = true + enabled_rules = var.enabled_rules + + context = module.this.context +} diff --git a/modules/aws-inspector/outputs.tf b/modules/aws-inspector/outputs.tf new file mode 100644 index 000000000..55989c0e4 --- /dev/null +++ b/modules/aws-inspector/outputs.tf @@ -0,0 +1,4 @@ +output "inspector" { + description = "The AWS Inspector module outputs" + value = module.inspector +} diff --git a/modules/aws-inspector/providers.tf b/modules/aws-inspector/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/aws-inspector/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/aws-inspector/variables.tf b/modules/aws-inspector/variables.tf new file mode 100644 index 000000000..6f9616604 --- /dev/null +++ b/modules/aws-inspector/variables.tf @@ -0,0 +1,14 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "enabled_rules" { + type = list(string) + default = [] + description = <<-DOC + A list of AWS Inspector rules that should run on a periodic basis. + + Valid values are `cve`, `cis`, `nr`, `sbp` which map to the appropriate [Inspector rule arns by region](https://docs.aws.amazon.com/inspector/latest/userguide/inspector_rules-arns.html). + DOC +} diff --git a/modules/aws-inspector/versions.tf b/modules/aws-inspector/versions.tf new file mode 100644 index 000000000..cc73ffd35 --- /dev/null +++ b/modules/aws-inspector/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From e417903239be979d794ef665fdbc2e64a8a75f6b Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Wed, 31 May 2023 17:45:06 +0300 Subject: [PATCH 136/501] Introducing GuardDuty (#682) --- modules/guardduty/common/README.md | 156 +++++++++++++ modules/guardduty/common/context.tf | 279 +++++++++++++++++++++++ modules/guardduty/common/main.tf | 35 +++ modules/guardduty/common/outputs.tf | 19 ++ modules/guardduty/common/providers.tf | 41 ++++ modules/guardduty/common/remote-state.tf | 12 + modules/guardduty/common/variables.tf | 128 +++++++++++ modules/guardduty/common/versions.tf | 15 ++ modules/guardduty/root/README.md | 97 ++++++++ modules/guardduty/root/context.tf | 279 +++++++++++++++++++++++ modules/guardduty/root/main.tf | 29 +++ modules/guardduty/root/providers.tf | 3 + modules/guardduty/root/remote-state.tf | 12 + modules/guardduty/root/variables.tf | 34 +++ modules/guardduty/root/versions.tf | 15 ++ 15 files changed, 1154 insertions(+) create mode 100644 modules/guardduty/common/README.md create mode 100644 modules/guardduty/common/context.tf create mode 100644 modules/guardduty/common/main.tf create mode 100644 modules/guardduty/common/outputs.tf create mode 100644 modules/guardduty/common/providers.tf create mode 100644 modules/guardduty/common/remote-state.tf create mode 100644 modules/guardduty/common/variables.tf create mode 100644 modules/guardduty/common/versions.tf create mode 100644 modules/guardduty/root/README.md create mode 100644 modules/guardduty/root/context.tf create mode 100644 modules/guardduty/root/main.tf create mode 100644 modules/guardduty/root/providers.tf create mode 100644 modules/guardduty/root/remote-state.tf create mode 100644 modules/guardduty/root/variables.tf create mode 100644 modules/guardduty/root/versions.tf diff --git a/modules/guardduty/common/README.md b/modules/guardduty/common/README.md new file mode 100644 index 000000000..c672af016 --- /dev/null +++ b/modules/guardduty/common/README.md @@ -0,0 +1,156 @@ +# Component: `guardduty/common` + +This component is responsible for configuring GuardDuty and it should be used in tandem with the [guardduty/root](../root) component. + +AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by continuously monitoring for malicious activities and unauthorized behaviors. GuardDuty analyzes various data sources within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS logs, to detect potential security threats. + +Key features and components of AWS GuardDuty include: + +- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event logs and network traffic data to detect patterns, anomalies, and known attack techniques. + +- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, domains, and other indicators of compromise. + +- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS Lambda for immediate action or custom response workflows. + +- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security policies and practices. + +- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of security incidents and reduces the need for manual intervention. + +- Security findings and reports: GuardDuty provides detailed security findings and reports that include information about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed through the AWS Management Console or retrieved via APIs for further analysis and reporting. + +GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations with an additional layer of security to proactively identify and respond to potential security risks. + +## Usage + +**Stack Level**: Regional + +The example snippet below shows how to use this component: + +```yaml +components: + terraform: + guardduty/common: + metadata: + component: guardduty/common + vars: + enabled: true + account_map_tenant: core + central_resource_collector_account: core-security + admin_delegated: true +``` + +## Deployment + +This set of steps assumes that `var.central_resource_collector_account = "core-security"`. + +1. Apply `guardduty/common` to `core-security` with `var.admin_delegated = false` +2. Apply `guardduty/root` to `core-root` +3. Apply `guardduty/common` to `core-security` with `var.admin_delegated = true` + +Example: + +``` +# Apply guardduty/common to all regions in core-security +atmos terraform apply guardduty/common-ue2 -s core-ue2-security -var=admin_delegated=false +atmos terraform apply guardduty/common-ue1 -s core-ue1-security -var=admin_delegated=false +atmos terraform apply guardduty/common-uw1 -s core-uw1-security -var=admin_delegated=false +# ... other regions + +# Apply guardduty/root to all regions in core-root +atmos terraform apply guardduty/root-ue2 -s core-ue2-root +atmos terraform apply guardduty/root-ue1 -s core-ue1-root +atmos terraform apply guardduty/root-uw1 -s core-uw1-root +# ... other regions + +# Apply guardduty/common to all regions in core-security but with default values for admin_delegated +atmos terraform apply guardduty/common-ue2 -s core-ue2-security +atmos terraform apply guardduty/common-ue1 -s core-ue1-security +atmos terraform apply guardduty/common-uw1 -s core-uw1-security +# ... other regions +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [awsutils](#provider\_awsutils) | >= 0.16.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [guardduty](#module\_guardduty) | cloudposse/guardduty/aws | 0.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [awsutils_guardduty_organization_settings.this](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/resources/guardduty_organization_settings) | resource | +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `""` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the GuardDuty Admininstrator account has been designated from the root account.

This component should be applied with this variable set to false, then the guardduty-root component should be applied
to designate the administrator account, then this component should be applied again with this variable set to `true`. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [central\_resource\_collector\_account](#input\_central\_resource\_collector\_account) | The name of the account that is the centralized aggregation account | `string` | n/a | yes | +| [cloudwatch\_event\_rule\_pattern\_detail\_type](#input\_cloudwatch\_event\_rule\_pattern\_detail\_type) | The detail-type pattern used to match events that will be sent to SNS.

For more information, see:
https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html
https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html
https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings_cloudwatch.html | `string` | `"GuardDuty Finding"` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_sns\_topic](#input\_create\_sns\_topic) | Flag to indicate whether an SNS topic should be created for notifications.
If you want to send findings to a new SNS topic, set this to true and provide a valid configuration for subscribers. | `bool` | `false` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enable\_cloudwatch](#input\_enable\_cloudwatch) | Flag to indicate whether an CloudWatch logging should be enabled for GuardDuty | `bool` | `false` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [finding\_publishing\_frequency](#input\_finding\_publishing\_frequency) | The frequency of notifications sent for finding occurrences. If the detector is a GuardDuty member account, the value
is determined by the GuardDuty master account and cannot be modified, otherwise it defaults to SIX\_HOURS.

For standalone and GuardDuty master accounts, it must be configured in Terraform to enable drift detection.
Valid values for standalone and master accounts: FIFTEEN\_MINUTES, ONE\_HOUR, SIX\_HOURS."

For more information, see:
https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings_cloudwatch.html#guardduty_findings_cloudwatch_notification_frequency | `string` | `null` | no | +| [findings\_notification\_arn](#input\_findings\_notification\_arn) | The ARN for an SNS topic to send findings notifications to. This is only used if create\_sns\_topic is false.
If you want to send findings to an existing SNS topic, set the value of this to the ARN of the existing topic and set
create\_sns\_topic to false. | `string` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [subscribers](#input\_subscribers) | A map of subscription configurations for SNS topics

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription#argument-reference

protocol:
The protocol to use. The possible values for this are: sqs, sms, lambda, application. (http or https are partially
supported, see link) (email is an option but is unsupported in terraform, see link).
endpoint:
The endpoint to send data to, the contents will vary with the protocol. (see link for more information)
endpoint\_auto\_confirms:
Boolean indicating whether the end point is capable of auto confirming subscription e.g., PagerDuty. Default is
false
raw\_message\_delivery:
Boolean indicating whether or not to enable raw message delivery (the original message is directly passed, not wrapped in JSON with the original message in the message property).
Default is false |
map(object({
protocol = string
endpoint = string
endpoint_auto_confirms = bool
raw_message_delivery = bool
}))
| `{}` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [guardduty\_detector\_arn](#output\_guardduty\_detector\_arn) | GuardDuty detector ARN | +| [guardduty\_detector\_id](#output\_guardduty\_detector\_id) | GuardDuty detector ID | +| [sns\_topic\_name](#output\_sns\_topic\_name) | SNS topic name | +| [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | SNS topic subscriptions | + + +## References +* [AWS GuardDuty Documentation](https://aws.amazon.com/guardduty/) +* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/guardduty/common/) + +[](https://cpco.io/component) diff --git a/modules/guardduty/common/context.tf b/modules/guardduty/common/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/guardduty/common/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/guardduty/common/main.tf b/modules/guardduty/common/main.tf new file mode 100644 index 000000000..29420b177 --- /dev/null +++ b/modules/guardduty/common/main.tf @@ -0,0 +1,35 @@ +locals { + enabled = module.this.enabled + create_sns_topic = local.enabled && var.create_sns_topic + account_map = module.account_map.outputs.full_account_map + central_resource_collector_account = local.account_map[var.central_resource_collector_account] + account_id = one(data.aws_caller_identity.this[*].account_id) + is_global_collector_account = local.account_id == local.central_resource_collector_account + member_account_list = [for a in keys(local.account_map) : (local.account_map[a]) if local.account_map[a] != local.account_id] +} + +module "guardduty" { + count = local.enabled && local.is_global_collector_account ? 1 : 0 + source = "cloudposse/guardduty/aws" + version = "0.5.0" + + finding_publishing_frequency = var.finding_publishing_frequency + create_sns_topic = var.create_sns_topic + findings_notification_arn = var.findings_notification_arn + subscribers = var.subscribers + enable_cloudwatch = var.enable_cloudwatch + cloudwatch_event_rule_pattern_detail_type = var.cloudwatch_event_rule_pattern_detail_type + + context = module.this.context +} + +data "aws_caller_identity" "this" { + count = local.enabled ? 1 : 0 +} + +resource "awsutils_guardduty_organization_settings" "this" { + count = local.enabled && var.admin_delegated && local.is_global_collector_account ? 1 : 0 + + member_accounts = local.member_account_list + detector_id = module.guardduty[0].guardduty_detector.id +} diff --git a/modules/guardduty/common/outputs.tf b/modules/guardduty/common/outputs.tf new file mode 100644 index 000000000..a83bf47da --- /dev/null +++ b/modules/guardduty/common/outputs.tf @@ -0,0 +1,19 @@ +output "guardduty_detector_arn" { + value = one(module.guardduty[*].guardduty_detector.arn) + description = "GuardDuty detector ARN" +} + +output "guardduty_detector_id" { + value = one(module.guardduty[*].guardduty_detector.id) + description = "GuardDuty detector ID" +} + +output "sns_topic_name" { + description = "SNS topic name" + value = one(module.guardduty[*].sns_topic.name) +} + +output "sns_topic_subscriptions" { + description = "SNS topic subscriptions" + value = one(module.guardduty[*].sns_topic_subscriptions) +} \ No newline at end of file diff --git a/modules/guardduty/common/providers.tf b/modules/guardduty/common/providers.tf new file mode 100644 index 000000000..28104fbb7 --- /dev/null +++ b/modules/guardduty/common/providers.tf @@ -0,0 +1,41 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +provider "awsutils" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} \ No newline at end of file diff --git a/modules/guardduty/common/remote-state.tf b/modules/guardduty/common/remote-state.tf new file mode 100644 index 000000000..12de8d665 --- /dev/null +++ b/modules/guardduty/common/remote-state.tf @@ -0,0 +1,12 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + component = "account-map" + tenant = (var.account_map_tenant != "") ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} \ No newline at end of file diff --git a/modules/guardduty/common/variables.tf b/modules/guardduty/common/variables.tf new file mode 100644 index 000000000..66eb0465f --- /dev/null +++ b/modules/guardduty/common/variables.tf @@ -0,0 +1,128 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "account_map_tenant" { + type = string + default = "" + description = "The tenant where the `account_map` component required by remote-state is deployed" +} + +variable "root_account_stage" { + type = string + default = "root" + description = "The stage name for the Organization root (management) account" +} + +variable "global_environment" { + type = string + default = "gbl" + description = "Global environment name" +} + +variable "privileged" { + type = bool + description = "True if the default provider already has access to the backend" + default = false +} + +variable "central_resource_collector_account" { + description = "The name of the account that is the centralized aggregation account" + type = string +} + +variable "admin_delegated" { + type = bool + default = true + description = < +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_guardduty_detector.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector) | resource | +| [aws_guardduty_organization_admin_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_admin_account) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `""` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [administrator\_account](#input\_administrator\_account) | The name of the account that is the GuardDuty administrator account | `string` | `null` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +No outputs. + + +## References +* [AWS GuardDuty Documentation](https://aws.amazon.com/guardduty/) +* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/guardduty/root/) + +[](https://cpco.io/component) diff --git a/modules/guardduty/root/context.tf b/modules/guardduty/root/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/guardduty/root/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/guardduty/root/main.tf b/modules/guardduty/root/main.tf new file mode 100644 index 000000000..ac36e53ba --- /dev/null +++ b/modules/guardduty/root/main.tf @@ -0,0 +1,29 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map +} + +module "utils" { + source = "cloudposse/utils/aws" + version = "1.3.0" + + context = module.this.context +} + +resource "aws_guardduty_organization_admin_account" "this" { + count = local.enabled && var.administrator_account != null && var.administrator_account != "" ? 1 : 0 + + admin_account_id = local.account_map[var.administrator_account] +} + +resource "aws_guardduty_detector" "this" { + count = local.enabled && var.administrator_account != null && var.administrator_account != "" ? 1 : 0 + + enable = true + + datasources { + s3_logs { + enable = true + } + } +} diff --git a/modules/guardduty/root/providers.tf b/modules/guardduty/root/providers.tf new file mode 100644 index 000000000..5ff54f0d6 --- /dev/null +++ b/modules/guardduty/root/providers.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = var.region +} \ No newline at end of file diff --git a/modules/guardduty/root/remote-state.tf b/modules/guardduty/root/remote-state.tf new file mode 100644 index 000000000..12de8d665 --- /dev/null +++ b/modules/guardduty/root/remote-state.tf @@ -0,0 +1,12 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + component = "account-map" + tenant = (var.account_map_tenant != "") ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} \ No newline at end of file diff --git a/modules/guardduty/root/variables.tf b/modules/guardduty/root/variables.tf new file mode 100644 index 000000000..7205eb4fd --- /dev/null +++ b/modules/guardduty/root/variables.tf @@ -0,0 +1,34 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "account_map_tenant" { + type = string + default = "" + description = "The tenant where the `account_map` component required by remote-state is deployed" +} + +variable "root_account_stage" { + type = string + default = "root" + description = "The stage name for the Organization root (management) account" +} + +variable "global_environment" { + type = string + default = "gbl" + description = "Global environment name" +} + +variable "privileged" { + type = bool + description = "True if the default provider already has access to the backend" + default = false +} + +variable "administrator_account" { + description = "The name of the account that is the GuardDuty administrator account" + type = string + default = null +} diff --git a/modules/guardduty/root/versions.tf b/modules/guardduty/root/versions.tf new file mode 100644 index 000000000..65cf14c13 --- /dev/null +++ b/modules/guardduty/root/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + + awsutils = { + source = "cloudposse/awsutils" + version = ">= 0.16.0" + } + } +} From be4a8fa81eff77afcfbf1adb31fd778a2f134dbd Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Wed, 31 May 2023 17:49:55 +0300 Subject: [PATCH 137/501] Introducing Security Hub (#683) --- modules/securityhub/common/README.md | 182 ++++++++++++++ modules/securityhub/common/context.tf | 279 +++++++++++++++++++++ modules/securityhub/common/main.tf | 60 +++++ modules/securityhub/common/outputs.tf | 14 ++ modules/securityhub/common/providers.tf | 41 +++ modules/securityhub/common/remote-state.tf | 12 + modules/securityhub/common/variables.tf | 112 +++++++++ modules/securityhub/common/versions.tf | 15 ++ modules/securityhub/root/README.md | 104 ++++++++ modules/securityhub/root/context.tf | 279 +++++++++++++++++++++ modules/securityhub/root/main.tf | 38 +++ modules/securityhub/root/providers.tf | 3 + modules/securityhub/root/remote-state.tf | 12 + modules/securityhub/root/variables.tf | 54 ++++ modules/securityhub/root/versions.tf | 15 ++ 15 files changed, 1220 insertions(+) create mode 100644 modules/securityhub/common/README.md create mode 100644 modules/securityhub/common/context.tf create mode 100644 modules/securityhub/common/main.tf create mode 100644 modules/securityhub/common/outputs.tf create mode 100644 modules/securityhub/common/providers.tf create mode 100644 modules/securityhub/common/remote-state.tf create mode 100644 modules/securityhub/common/variables.tf create mode 100644 modules/securityhub/common/versions.tf create mode 100644 modules/securityhub/root/README.md create mode 100644 modules/securityhub/root/context.tf create mode 100644 modules/securityhub/root/main.tf create mode 100644 modules/securityhub/root/providers.tf create mode 100644 modules/securityhub/root/remote-state.tf create mode 100644 modules/securityhub/root/variables.tf create mode 100644 modules/securityhub/root/versions.tf diff --git a/modules/securityhub/common/README.md b/modules/securityhub/common/README.md new file mode 100644 index 000000000..8d8724f4f --- /dev/null +++ b/modules/securityhub/common/README.md @@ -0,0 +1,182 @@ +# Component: `securityhub/common` + +This component is responsible for configuring Security Hub and it should be used in tandem with the [securityhub/root](../root) component. + +Amazon Security Hub enables users to centrally manage and monitor the security and compliance of their AWS accounts and resources. It aggregates, organizes, and prioritizes security findings from various AWS services, third-party tools, and integrated partner solutions. + +Here are the key features and capabilities of Amazon Security Hub: + +- Centralized security management: Security Hub provides a centralized dashboard where users can view and manage security findings from multiple AWS accounts and regions. This allows for a unified view of the security posture across the entire AWS environment. + +- Automated security checks: Security Hub automatically performs continuous security checks on AWS resources, configurations, and security best practices. It leverages industry standards and compliance frameworks, such as AWS CIS Foundations Benchmark, to identify potential security issues. + +- Integrated partner solutions: Security Hub integrates with a wide range of AWS native services, as well as third-party security products and solutions. This integration enables the ingestion and analysis of security findings from diverse sources, offering a comprehensive security view. + +- Security standards and compliance: Security Hub provides compliance checks against industry standards and regulatory frameworks, such as PCI DSS, HIPAA, and GDPR. It identifies non-compliant resources and provides guidance on remediation actions to ensure adherence to security best practices. + +- Prioritized security findings: Security Hub analyzes and prioritizes security findings based on severity, enabling users to focus on the most critical issues. It assigns severity levels and generates a consolidated view of security alerts, allowing for efficient threat response and remediation. + +- Custom insights and event aggregation: Security Hub supports custom insights, allowing users to create their own rules and filters to focus on specific security criteria or requirements. It also provides event aggregation and correlation capabilities to identify related security findings and potential attack patterns. + +- Integration with other AWS services: Security Hub seamlessly integrates with other AWS services, such as AWS CloudTrail, Amazon GuardDuty, AWS Config, and AWS IAM Access Analyzer. This integration allows for enhanced visibility, automated remediation, and streamlined security operations. + +- Alert notifications and automation: Security Hub supports alert notifications through Amazon SNS, enabling users to receive real-time notifications of security findings. It also facilitates automation and response through integration with AWS Lambda, allowing for automated remediation actions. + +By utilizing Amazon Security Hub, organizations can improve their security posture, gain insights into security risks, and effectively manage security compliance across their AWS accounts and resources. + +## Usage + +**Stack Level**: Regional + +The example snippet below shows how to use this component: + +```yaml +components: + terraform: + securityhub/common: + metadata: + component: securityhub/common + vars: + enabled: true + account_map_tenant: core + central_resource_collector_account: core-security + admin_delegated: false + central_resource_collector_region: us-east-1 + finding_aggregator_enabled: true + create_sns_topic: true + enable_default_standards: false + enabled_standards: + - standards/cis-aws-foundations-benchmark/v/1.4.0 +``` + +## Deployment + +1. Apply `securityhub/common` to all accounts +2. Apply `securityhub/root` to `core-root` account +3. Apply `securityhub/common` to `core-security` with `var.admin_delegated = true` + +Example: + +``` +export regions="use1 use2 usw1 usw2 aps1 apne3 apne2 apne1 apse1 apse2 cac1 euc1 euw1 euw2 euw3 eun1 sae1" + +# apply to core-* + +export stages="artifacts audit auto corp dns identity network security" +for region in ${regions}; do + for stage in ${stages}; do + atmos terraform deploy securityhub/common-${region} -s core-${region}-${stage} || echo "core-${region}-${stage}" >> failures; + done; +done + +# apply to plat-* + +export stages="dev prod sandbox staging" +for region in ${regions}; do + for stage in ${stages}; do + atmos terraform deploy securityhub/common-${region} -s plat-${region}-${stage} || echo "plat-${region}-${stage}" >> failures; + done; +done + +# apply to "core-root" using "superadmin" privileges + +for region in ${regions}; do + atmos terraform deploy securityhub/root-${region} -s core-${region}-root || echo "core-${region}-root" >> failures; +done + +# apply to "core-security" again with "var.admin_delegated=true" + +for region in ${regions}; do + atmos terraform deploy securityhub/common-${region} -s core-${region}-security -var=admin_delegated=true || echo "core-${region}-security" >> failures; +done +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [awsutils](#provider\_awsutils) | >= 0.16.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [security\_hub](#module\_security\_hub) | cloudposse/security-hub/aws | 0.10.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_securityhub_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_account) | resource | +| [aws_securityhub_standards_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription) | resource | +| [awsutils_security_hub_organization_settings.this](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/resources/security_hub_organization_settings) | resource | +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_partition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `""` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the Security Hub Admininstrator account has been designated from the root account.

This component should be applied with this variable set to `false`, then the securityhub/root component should be applied
to designate the administrator account, then this component should be applied again with this variable set to `true`. | `bool` | `false` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [central\_resource\_collector\_account](#input\_central\_resource\_collector\_account) | The name of the account that is the centralized aggregation account | `string` | n/a | yes | +| [central\_resource\_collector\_region](#input\_central\_resource\_collector\_region) | The region that collects findings | `string` | n/a | yes | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_sns\_topic](#input\_create\_sns\_topic) | Flag to indicate whether an SNS topic should be created for notifications | `bool` | `false` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enable\_default\_standards](#input\_enable\_default\_standards) | Flag to indicate whether default standards should be enabled | `bool` | `true` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [enabled\_standards](#input\_enabled\_standards) | A list of standards to enable in the account.

For example:
- standards/aws-foundational-security-best-practices/v/1.0.0
- ruleset/cis-aws-foundations-benchmark/v/1.2.0
- standards/pci-dss/v/3.2.1
- standards/cis-aws-foundations-benchmark/v/1.4.0 | `set(string)` | `[]` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [finding\_aggregator\_enabled](#input\_finding\_aggregator\_enabled) | Flag to indicate whether a finding aggregator should be created

If you want to aggregate findings from one region, set this to `true`.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_finding_aggregator | `bool` | `false` | no | +| [finding\_aggregator\_linking\_mode](#input\_finding\_aggregator\_linking\_mode) | Linking mode to use for the finding aggregator.

The possible values are:
- `ALL_REGIONS` - Aggregate from all regions
- `ALL_REGIONS_EXCEPT_SPECIFIED` - Aggregate from all regions except those specified in `var.finding_aggregator_regions`
- `SPECIFIED_REGIONS` - Aggregate from regions specified in `var.finding_aggregator_regions` | `string` | `"ALL_REGIONS"` | no | +| [finding\_aggregator\_regions](#input\_finding\_aggregator\_regions) | A list of regions to aggregate findings from.

This is only used if `finding_aggregator_enabled` is `true`. | `any` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [enabled\_subscriptions](#output\_enabled\_subscriptions) | A list of subscriptions that have been enabled | +| [sns\_topic\_name](#output\_sns\_topic\_name) | The SNS topic name that was created | +| [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | The SNS topic subscriptions | + + +## References +* [AWS Security Hub Documentation](https://aws.amazon.com/security-hub/) +* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/securityhub/common/) + +[](https://cpco.io/component) diff --git a/modules/securityhub/common/context.tf b/modules/securityhub/common/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/securityhub/common/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/securityhub/common/main.tf b/modules/securityhub/common/main.tf new file mode 100644 index 000000000..cb155dd9a --- /dev/null +++ b/modules/securityhub/common/main.tf @@ -0,0 +1,60 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map + central_resource_collector_account = local.account_map[var.central_resource_collector_account] + account_id = one(data.aws_caller_identity.this[*].account_id) + region_name = one(data.aws_region.this[*].name) + is_global_collector_account = local.central_resource_collector_account == local.account_id + is_collector_region = local.region_name == var.central_resource_collector_region + member_account_list = [for a in keys(local.account_map) : (local.account_map[a]) if local.account_map[a] != local.account_id] + enabled_standards_arns = toset([ + for standard in var.enabled_standards : + format("arn:%s:securityhub:%s::%s", one(data.aws_partition.this[*].partition), length(regexall("ruleset", standard)) == 0 ? one(data.aws_region.this[*].name) : "", standard) + ]) +} + +data "aws_caller_identity" "this" { + count = local.enabled ? 1 : 0 +} + +data "aws_region" "this" { + count = local.enabled ? 1 : 0 +} + +data "aws_partition" "this" { + count = local.enabled ? 1 : 0 +} + +module "security_hub" { + count = local.enabled && local.is_global_collector_account ? 1 : 0 + source = "cloudposse/security-hub/aws" + version = "0.10.0" + + create_sns_topic = var.create_sns_topic + enabled_standards = var.enabled_standards + finding_aggregator_enabled = local.is_collector_region && var.finding_aggregator_enabled + finding_aggregator_linking_mode = var.finding_aggregator_linking_mode + finding_aggregator_regions = var.finding_aggregator_regions + enable_default_standards = var.enable_default_standards + + context = module.this.context +} + +resource "aws_securityhub_account" "this" { + count = local.enabled && !local.is_global_collector_account ? 1 : 0 + + enable_default_standards = var.enable_default_standards +} + +resource "aws_securityhub_standards_subscription" "this" { + for_each = local.enabled && !local.is_global_collector_account ? local.enabled_standards_arns : [] + depends_on = [aws_securityhub_account.this] + standards_arn = each.key +} + +resource "awsutils_security_hub_organization_settings" "this" { + count = local.enabled && local.is_global_collector_account && var.admin_delegated ? 1 : 0 + + member_accounts = local.member_account_list + auto_enable_new_accounts = true +} diff --git a/modules/securityhub/common/outputs.tf b/modules/securityhub/common/outputs.tf new file mode 100644 index 000000000..07000d86d --- /dev/null +++ b/modules/securityhub/common/outputs.tf @@ -0,0 +1,14 @@ +output "enabled_subscriptions" { + description = "A list of subscriptions that have been enabled" + value = local.enabled && local.is_global_collector_account ? module.security_hub[0].enabled_subscriptions : [] +} + +output "sns_topic_name" { + description = "The SNS topic name that was created" + value = local.enabled && local.is_global_collector_account && var.create_sns_topic ? module.security_hub[0].sns_topic.name : null +} + +output "sns_topic_subscriptions" { + description = "The SNS topic subscriptions" + value = local.enabled && local.is_global_collector_account && var.create_sns_topic ? module.security_hub[0].sns_topic_subscriptions : null +} \ No newline at end of file diff --git a/modules/securityhub/common/providers.tf b/modules/securityhub/common/providers.tf new file mode 100644 index 000000000..28104fbb7 --- /dev/null +++ b/modules/securityhub/common/providers.tf @@ -0,0 +1,41 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +provider "awsutils" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} \ No newline at end of file diff --git a/modules/securityhub/common/remote-state.tf b/modules/securityhub/common/remote-state.tf new file mode 100644 index 000000000..5595945d0 --- /dev/null +++ b/modules/securityhub/common/remote-state.tf @@ -0,0 +1,12 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + component = "account-map" + tenant = (var.account_map_tenant != "") ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/securityhub/common/variables.tf b/modules/securityhub/common/variables.tf new file mode 100644 index 000000000..8d96e7eb4 --- /dev/null +++ b/modules/securityhub/common/variables.tf @@ -0,0 +1,112 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "account_map_tenant" { + type = string + default = "" + description = "The tenant where the `account_map` component required by remote-state is deployed" +} + +variable "root_account_stage" { + type = string + default = "root" + description = "The stage name for the Organization root (management) account" +} + +variable "global_environment" { + type = string + default = "gbl" + description = "Global environment name" +} + +variable "privileged" { + type = bool + description = "True if the default provider already has access to the backend" + default = false +} + +variable "central_resource_collector_account" { + description = "The name of the account that is the centralized aggregation account" + type = string +} + +variable "central_resource_collector_region" { + description = "The region that collects findings" + type = string +} + +variable "create_sns_topic" { + description = "Flag to indicate whether an SNS topic should be created for notifications" + type = bool + default = false +} + +variable "enable_default_standards" { + description = "Flag to indicate whether default standards should be enabled" + type = bool + default = true +} + +variable "enabled_standards" { + description = < +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_securityhub_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_account) | resource | +| [aws_securityhub_organization_admin_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_organization_admin_account) | resource | +| [aws_securityhub_standards_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription) | resource | +| [aws_partition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `""` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [administrator\_account](#input\_administrator\_account) | The name of the account that is the Security Hub administrator account | `string` | `null` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enable\_default\_standards](#input\_enable\_default\_standards) | Flag to indicate whether default standards should be enabled | `bool` | `true` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [enabled\_standards](#input\_enabled\_standards) | A list of standards to enable in the account.

For example:
- standards/aws-foundational-security-best-practices/v/1.0.0
- ruleset/cis-aws-foundations-benchmark/v/1.2.0
- standards/pci-dss/v/3.2.1
- standards/cis-aws-foundations-benchmark/v/1.4.0 | `set(string)` | `[]` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +No outputs. + + +## References +* [AWS Security Hub Documentation](https://aws.amazon.com/security-hub/) +* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/securityhub/root/) + +[](https://cpco.io/component) diff --git a/modules/securityhub/root/context.tf b/modules/securityhub/root/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/securityhub/root/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/securityhub/root/main.tf b/modules/securityhub/root/main.tf new file mode 100644 index 000000000..a048d23df --- /dev/null +++ b/modules/securityhub/root/main.tf @@ -0,0 +1,38 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map + enabled_standards_arns = toset([ + for standard in var.enabled_standards : + format("arn:%s:securityhub:%s::%s", one(data.aws_partition.this[*].partition), length(regexall("ruleset", standard)) == 0 ? one(data.aws_region.this[*].name) : "", standard) + ]) +} + +data "aws_partition" "this" { + count = local.enabled ? 1 : 0 +} + +data "aws_region" "this" { + count = local.enabled ? 1 : 0 +} + +resource "aws_securityhub_organization_admin_account" "this" { + count = local.enabled && var.administrator_account != null && var.administrator_account != "" ? 1 : 0 + + admin_account_id = local.account_map[var.administrator_account] +} + +resource "aws_securityhub_account" "this" { + count = local.enabled ? 1 : 0 + + enable_default_standards = var.enable_default_standards + + depends_on = [ + aws_securityhub_organization_admin_account.this + ] +} + +resource "aws_securityhub_standards_subscription" "this" { + for_each = local.enabled ? local.enabled_standards_arns : [] + depends_on = [aws_securityhub_account.this] + standards_arn = each.key +} diff --git a/modules/securityhub/root/providers.tf b/modules/securityhub/root/providers.tf new file mode 100644 index 000000000..5ff54f0d6 --- /dev/null +++ b/modules/securityhub/root/providers.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = var.region +} \ No newline at end of file diff --git a/modules/securityhub/root/remote-state.tf b/modules/securityhub/root/remote-state.tf new file mode 100644 index 000000000..5595945d0 --- /dev/null +++ b/modules/securityhub/root/remote-state.tf @@ -0,0 +1,12 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.2" + + component = "account-map" + tenant = (var.account_map_tenant != "") ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/securityhub/root/variables.tf b/modules/securityhub/root/variables.tf new file mode 100644 index 000000000..d1a0a3662 --- /dev/null +++ b/modules/securityhub/root/variables.tf @@ -0,0 +1,54 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "account_map_tenant" { + type = string + default = "" + description = "The tenant where the `account_map` component required by remote-state is deployed" +} + +variable "root_account_stage" { + type = string + default = "root" + description = "The stage name for the Organization root (management) account" +} + +variable "global_environment" { + type = string + default = "gbl" + description = "Global environment name" +} + +variable "privileged" { + type = bool + description = "True if the default provider already has access to the backend" + default = false +} + +variable "administrator_account" { + description = "The name of the account that is the Security Hub administrator account" + type = string + default = null +} + +variable "enable_default_standards" { + description = "Flag to indicate whether default standards should be enabled" + type = bool + default = true +} + +variable "enabled_standards" { + description = < Date: Wed, 31 May 2023 10:46:10 -0700 Subject: [PATCH 138/501] Transit Gateway `var.connections` Redesign (#685) --- .../tgw/cross-region-hub-connector/README.md | 0 .../tgw/cross-region-hub-connector/context.tf | 0 .../tgw/cross-region-hub-connector/main.tf | 0 .../tgw/cross-region-hub-connector/outputs.tf | 0 .../cross-region-hub-connector/providers.tf | 0 .../remote-state.tf | 0 .../cross-region-hub-connector/variables.tf | 0 .../cross-region-hub-connector/versions.tf | 0 .../tgw/cross-region-spoke/README.md | 0 .../tgw/cross-region-spoke/context.tf | 0 .../tgw/cross-region-spoke/main.tf | 0 .../modules/tgw_routes/context.tf | 0 .../modules/tgw_routes/main.tf | 0 .../modules/tgw_routes/outputs.tf | 0 .../modules/tgw_routes/variables.tf | 0 .../modules/tgw_routes/versions.tf | 0 .../modules/vpc_routes/context.tf | 0 .../modules/vpc_routes/main.tf | 0 .../modules/vpc_routes/outputs.tf | 0 .../modules/vpc_routes/variables.tf | 0 .../modules/vpc_routes/versions.tf | 0 .../tgw/cross-region-spoke/outputs.tf | 0 .../tgw/cross-region-spoke/providers.tf | 0 .../tgw/cross-region-spoke/remote-state.tf | 0 .../cross-region-spoke/routes-home-region.tf | 0 .../cross-region-spoke/routes-this-region.tf | 0 .../tgw/cross-region-spoke/variables.tf | 0 .../tgw/cross-region-spoke/versions.tf | 0 deprecated/tgw/hub/README.md | 121 ++++++++ deprecated/tgw/hub/context.tf | 279 ++++++++++++++++++ deprecated/tgw/hub/main.tf | 35 +++ deprecated/tgw/hub/outputs.tf | 29 ++ deprecated/tgw/hub/providers.tf | 29 ++ deprecated/tgw/hub/remote-state.tf | 62 ++++ deprecated/tgw/hub/variables.tf | 48 +++ deprecated/tgw/hub/versions.tf | 10 + deprecated/tgw/spoke/README.md | 136 +++++++++ deprecated/tgw/spoke/context.tf | 279 ++++++++++++++++++ deprecated/tgw/spoke/main.tf | 49 +++ .../standard_vpc_attachment/context.tf | 279 ++++++++++++++++++ .../modules/standard_vpc_attachment/main.tf | 66 +++++ .../standard_vpc_attachment/outputs.tf | 14 + .../standard_vpc_attachment/variables.tf | 32 ++ .../standard_vpc_attachment/versions.tf | 10 + deprecated/tgw/spoke/outputs.tf | 0 deprecated/tgw/spoke/providers.tf | 70 +++++ deprecated/tgw/spoke/remote-state.tf | 11 + deprecated/tgw/spoke/variables.tf | 27 ++ deprecated/tgw/spoke/versions.tf | 10 + modules/eks/cluster/README.md | 1 + modules/eks/cluster/outputs.tf | 5 + .../eks/efs-controller/default.auto.tfvars | 5 - .../default.auto.tfvars | 3 - .../cross-region-spoke/default.auto.tfvars | 3 - modules/tgw/hub/README.md | 76 +++-- modules/tgw/hub/default.auto.tfvars | 5 - modules/tgw/hub/main.tf | 1 - modules/tgw/hub/remote-state.tf | 46 +-- modules/tgw/hub/variables.tf | 29 +- modules/tgw/spoke/README.md | 49 ++- modules/tgw/spoke/default.auto.tfvars | 5 - modules/tgw/spoke/main.tf | 13 +- .../default.auto.tfvars | 3 - .../modules/standard_vpc_attachment/main.tf | 94 ++++-- .../standard_vpc_attachment/variables.tf | 34 ++- modules/tgw/spoke/variables.tf | 30 +- 66 files changed, 1860 insertions(+), 138 deletions(-) rename {modules => deprecated}/tgw/cross-region-hub-connector/README.md (100%) rename {modules => deprecated}/tgw/cross-region-hub-connector/context.tf (100%) rename {modules => deprecated}/tgw/cross-region-hub-connector/main.tf (100%) rename {modules => deprecated}/tgw/cross-region-hub-connector/outputs.tf (100%) rename {modules => deprecated}/tgw/cross-region-hub-connector/providers.tf (100%) rename {modules => deprecated}/tgw/cross-region-hub-connector/remote-state.tf (100%) rename {modules => deprecated}/tgw/cross-region-hub-connector/variables.tf (100%) rename {modules => deprecated}/tgw/cross-region-hub-connector/versions.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/README.md (100%) rename {modules => deprecated}/tgw/cross-region-spoke/context.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/main.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/tgw_routes/context.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/tgw_routes/main.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/tgw_routes/variables.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/tgw_routes/versions.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/vpc_routes/context.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/vpc_routes/main.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/vpc_routes/variables.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/modules/vpc_routes/versions.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/outputs.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/providers.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/remote-state.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/routes-home-region.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/routes-this-region.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/variables.tf (100%) rename {modules => deprecated}/tgw/cross-region-spoke/versions.tf (100%) create mode 100644 deprecated/tgw/hub/README.md create mode 100644 deprecated/tgw/hub/context.tf create mode 100644 deprecated/tgw/hub/main.tf create mode 100644 deprecated/tgw/hub/outputs.tf create mode 100644 deprecated/tgw/hub/providers.tf create mode 100644 deprecated/tgw/hub/remote-state.tf create mode 100644 deprecated/tgw/hub/variables.tf create mode 100644 deprecated/tgw/hub/versions.tf create mode 100644 deprecated/tgw/spoke/README.md create mode 100644 deprecated/tgw/spoke/context.tf create mode 100644 deprecated/tgw/spoke/main.tf create mode 100644 deprecated/tgw/spoke/modules/standard_vpc_attachment/context.tf create mode 100644 deprecated/tgw/spoke/modules/standard_vpc_attachment/main.tf create mode 100644 deprecated/tgw/spoke/modules/standard_vpc_attachment/outputs.tf create mode 100644 deprecated/tgw/spoke/modules/standard_vpc_attachment/variables.tf create mode 100644 deprecated/tgw/spoke/modules/standard_vpc_attachment/versions.tf create mode 100644 deprecated/tgw/spoke/outputs.tf create mode 100644 deprecated/tgw/spoke/providers.tf create mode 100644 deprecated/tgw/spoke/remote-state.tf create mode 100644 deprecated/tgw/spoke/variables.tf create mode 100644 deprecated/tgw/spoke/versions.tf delete mode 100644 modules/eks/efs-controller/default.auto.tfvars delete mode 100644 modules/tgw/cross-region-hub-connector/default.auto.tfvars delete mode 100644 modules/tgw/cross-region-spoke/default.auto.tfvars delete mode 100644 modules/tgw/hub/default.auto.tfvars delete mode 100644 modules/tgw/spoke/default.auto.tfvars delete mode 100644 modules/tgw/spoke/modules/standard_vpc_attachment/default.auto.tfvars diff --git a/modules/tgw/cross-region-hub-connector/README.md b/deprecated/tgw/cross-region-hub-connector/README.md similarity index 100% rename from modules/tgw/cross-region-hub-connector/README.md rename to deprecated/tgw/cross-region-hub-connector/README.md diff --git a/modules/tgw/cross-region-hub-connector/context.tf b/deprecated/tgw/cross-region-hub-connector/context.tf similarity index 100% rename from modules/tgw/cross-region-hub-connector/context.tf rename to deprecated/tgw/cross-region-hub-connector/context.tf diff --git a/modules/tgw/cross-region-hub-connector/main.tf b/deprecated/tgw/cross-region-hub-connector/main.tf similarity index 100% rename from modules/tgw/cross-region-hub-connector/main.tf rename to deprecated/tgw/cross-region-hub-connector/main.tf diff --git a/modules/tgw/cross-region-hub-connector/outputs.tf b/deprecated/tgw/cross-region-hub-connector/outputs.tf similarity index 100% rename from modules/tgw/cross-region-hub-connector/outputs.tf rename to deprecated/tgw/cross-region-hub-connector/outputs.tf diff --git a/modules/tgw/cross-region-hub-connector/providers.tf b/deprecated/tgw/cross-region-hub-connector/providers.tf similarity index 100% rename from modules/tgw/cross-region-hub-connector/providers.tf rename to deprecated/tgw/cross-region-hub-connector/providers.tf diff --git a/modules/tgw/cross-region-hub-connector/remote-state.tf b/deprecated/tgw/cross-region-hub-connector/remote-state.tf similarity index 100% rename from modules/tgw/cross-region-hub-connector/remote-state.tf rename to deprecated/tgw/cross-region-hub-connector/remote-state.tf diff --git a/modules/tgw/cross-region-hub-connector/variables.tf b/deprecated/tgw/cross-region-hub-connector/variables.tf similarity index 100% rename from modules/tgw/cross-region-hub-connector/variables.tf rename to deprecated/tgw/cross-region-hub-connector/variables.tf diff --git a/modules/tgw/cross-region-hub-connector/versions.tf b/deprecated/tgw/cross-region-hub-connector/versions.tf similarity index 100% rename from modules/tgw/cross-region-hub-connector/versions.tf rename to deprecated/tgw/cross-region-hub-connector/versions.tf diff --git a/modules/tgw/cross-region-spoke/README.md b/deprecated/tgw/cross-region-spoke/README.md similarity index 100% rename from modules/tgw/cross-region-spoke/README.md rename to deprecated/tgw/cross-region-spoke/README.md diff --git a/modules/tgw/cross-region-spoke/context.tf b/deprecated/tgw/cross-region-spoke/context.tf similarity index 100% rename from modules/tgw/cross-region-spoke/context.tf rename to deprecated/tgw/cross-region-spoke/context.tf diff --git a/modules/tgw/cross-region-spoke/main.tf b/deprecated/tgw/cross-region-spoke/main.tf similarity index 100% rename from modules/tgw/cross-region-spoke/main.tf rename to deprecated/tgw/cross-region-spoke/main.tf diff --git a/modules/tgw/cross-region-spoke/modules/tgw_routes/context.tf b/deprecated/tgw/cross-region-spoke/modules/tgw_routes/context.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/tgw_routes/context.tf rename to deprecated/tgw/cross-region-spoke/modules/tgw_routes/context.tf diff --git a/modules/tgw/cross-region-spoke/modules/tgw_routes/main.tf b/deprecated/tgw/cross-region-spoke/modules/tgw_routes/main.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/tgw_routes/main.tf rename to deprecated/tgw/cross-region-spoke/modules/tgw_routes/main.tf diff --git a/modules/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf b/deprecated/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf rename to deprecated/tgw/cross-region-spoke/modules/tgw_routes/outputs.tf diff --git a/modules/tgw/cross-region-spoke/modules/tgw_routes/variables.tf b/deprecated/tgw/cross-region-spoke/modules/tgw_routes/variables.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/tgw_routes/variables.tf rename to deprecated/tgw/cross-region-spoke/modules/tgw_routes/variables.tf diff --git a/modules/tgw/cross-region-spoke/modules/tgw_routes/versions.tf b/deprecated/tgw/cross-region-spoke/modules/tgw_routes/versions.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/tgw_routes/versions.tf rename to deprecated/tgw/cross-region-spoke/modules/tgw_routes/versions.tf diff --git a/modules/tgw/cross-region-spoke/modules/vpc_routes/context.tf b/deprecated/tgw/cross-region-spoke/modules/vpc_routes/context.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/vpc_routes/context.tf rename to deprecated/tgw/cross-region-spoke/modules/vpc_routes/context.tf diff --git a/modules/tgw/cross-region-spoke/modules/vpc_routes/main.tf b/deprecated/tgw/cross-region-spoke/modules/vpc_routes/main.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/vpc_routes/main.tf rename to deprecated/tgw/cross-region-spoke/modules/vpc_routes/main.tf diff --git a/modules/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf b/deprecated/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf rename to deprecated/tgw/cross-region-spoke/modules/vpc_routes/outputs.tf diff --git a/modules/tgw/cross-region-spoke/modules/vpc_routes/variables.tf b/deprecated/tgw/cross-region-spoke/modules/vpc_routes/variables.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/vpc_routes/variables.tf rename to deprecated/tgw/cross-region-spoke/modules/vpc_routes/variables.tf diff --git a/modules/tgw/cross-region-spoke/modules/vpc_routes/versions.tf b/deprecated/tgw/cross-region-spoke/modules/vpc_routes/versions.tf similarity index 100% rename from modules/tgw/cross-region-spoke/modules/vpc_routes/versions.tf rename to deprecated/tgw/cross-region-spoke/modules/vpc_routes/versions.tf diff --git a/modules/tgw/cross-region-spoke/outputs.tf b/deprecated/tgw/cross-region-spoke/outputs.tf similarity index 100% rename from modules/tgw/cross-region-spoke/outputs.tf rename to deprecated/tgw/cross-region-spoke/outputs.tf diff --git a/modules/tgw/cross-region-spoke/providers.tf b/deprecated/tgw/cross-region-spoke/providers.tf similarity index 100% rename from modules/tgw/cross-region-spoke/providers.tf rename to deprecated/tgw/cross-region-spoke/providers.tf diff --git a/modules/tgw/cross-region-spoke/remote-state.tf b/deprecated/tgw/cross-region-spoke/remote-state.tf similarity index 100% rename from modules/tgw/cross-region-spoke/remote-state.tf rename to deprecated/tgw/cross-region-spoke/remote-state.tf diff --git a/modules/tgw/cross-region-spoke/routes-home-region.tf b/deprecated/tgw/cross-region-spoke/routes-home-region.tf similarity index 100% rename from modules/tgw/cross-region-spoke/routes-home-region.tf rename to deprecated/tgw/cross-region-spoke/routes-home-region.tf diff --git a/modules/tgw/cross-region-spoke/routes-this-region.tf b/deprecated/tgw/cross-region-spoke/routes-this-region.tf similarity index 100% rename from modules/tgw/cross-region-spoke/routes-this-region.tf rename to deprecated/tgw/cross-region-spoke/routes-this-region.tf diff --git a/modules/tgw/cross-region-spoke/variables.tf b/deprecated/tgw/cross-region-spoke/variables.tf similarity index 100% rename from modules/tgw/cross-region-spoke/variables.tf rename to deprecated/tgw/cross-region-spoke/variables.tf diff --git a/modules/tgw/cross-region-spoke/versions.tf b/deprecated/tgw/cross-region-spoke/versions.tf similarity index 100% rename from modules/tgw/cross-region-spoke/versions.tf rename to deprecated/tgw/cross-region-spoke/versions.tf diff --git a/deprecated/tgw/hub/README.md b/deprecated/tgw/hub/README.md new file mode 100644 index 000000000..9b88a8c2b --- /dev/null +++ b/deprecated/tgw/hub/README.md @@ -0,0 +1,121 @@ +# Component: `tgw/hub` + +This component is responsible for provisioning an [AWS Transit Gateway](https://aws.amazon.com/transit-gateway) `hub` that acts as a centralized gateway for connecting VPCs from other `spoke` accounts. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to configure and use this component: + +```yaml +components: + terraform: + tgw/hub: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: tgw-hub + eks_component_names: + - eks/cluster-blue + accounts_with_vpc: + - core-auto + - core-corp + - core-network + - plat-dev + - plat-staging + - plat-prod + - plat-sandbox + accounts_with_eks: + - plat-dev + - plat-staging + - plat-prod + - plat-sandbox +``` + +To provision the Transit Gateway and all related resources, run the following commands: + +```sh +atmos terraform plan tgw/hub -s --network +atmos terraform apply tgw/hub -s --network +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.1 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [tgw\_hub](#module\_tgw\_hub) | cloudposse/transit-gateway/aws | 0.9.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_environment\_name](#input\_account\_map\_environment\_name) | The name of the environment where `account_map` is provisioned | `string` | `"gbl"` | no | +| [account\_map\_stage\_name](#input\_account\_map\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | +| [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | +| [accounts\_with\_eks](#input\_accounts\_with\_eks) | Set of account names that have EKS | `set(string)` | n/a | yes | +| [accounts\_with\_vpc](#input\_accounts\_with\_vpc) | Set of account names that have VPC | `set(string)` | n/a | yes | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_names](#input\_eks\_component\_names) | The names of the eks components | `set(string)` |
[
"eks/cluster"
]
| no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [expose\_eks\_sg](#input\_expose\_eks\_sg) | Set true to allow EKS clusters to accept traffic from source accounts | `bool` | `true` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [eks](#output\_eks) | Accounts with EKS and EKSs information | +| [tgw\_config](#output\_tgw\_config) | Transit Gateway config | +| [transit\_gateway\_arn](#output\_transit\_gateway\_arn) | Transit Gateway ARN | +| [transit\_gateway\_id](#output\_transit\_gateway\_id) | Transit Gateway ID | +| [transit\_gateway\_route\_table\_id](#output\_transit\_gateway\_route\_table\_id) | Transit Gateway route table ID | +| [vpcs](#output\_vpcs) | Accounts with VPC and VPCs information | + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/tgw/hub) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/deprecated/tgw/hub/context.tf b/deprecated/tgw/hub/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/deprecated/tgw/hub/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/deprecated/tgw/hub/main.tf b/deprecated/tgw/hub/main.tf new file mode 100644 index 000000000..c18ef7711 --- /dev/null +++ b/deprecated/tgw/hub/main.tf @@ -0,0 +1,35 @@ +# Create the Transit Gateway, route table associations/propagations, and static TGW routes in the `network` account. +# Enable sharing the Transit Gateway with the Organization using Resource Access Manager (RAM). +# If you would like to share resources with your organization or organizational units, +# then you must use the AWS RAM console or CLI command to enable sharing with AWS Organizations. +# When you share resources within your organization, +# AWS RAM does not send invitations to principals. Principals in your organization get access to shared resources without exchanging invitations. +# https://docs.aws.amazon.com/ram/latest/userguide/getting-started-sharing.html + +module "tgw_hub" { + source = "cloudposse/transit-gateway/aws" + version = "0.9.1" + + ram_resource_share_enabled = true + route_keys_enabled = true + + create_transit_gateway = true + create_transit_gateway_route_table = true + create_transit_gateway_vpc_attachment = false + create_transit_gateway_route_table_association_and_propagation = false + + config = {} + + context = module.this.context +} + +locals { + tgw_config = { + existing_transit_gateway_id = module.tgw_hub.transit_gateway_id + existing_transit_gateway_route_table_id = module.tgw_hub.transit_gateway_route_table_id + vpcs = module.vpc + eks = module.eks + expose_eks_sg = var.expose_eks_sg + eks_component_names = var.eks_component_names + } +} diff --git a/deprecated/tgw/hub/outputs.tf b/deprecated/tgw/hub/outputs.tf new file mode 100644 index 000000000..a34a0815d --- /dev/null +++ b/deprecated/tgw/hub/outputs.tf @@ -0,0 +1,29 @@ +output "transit_gateway_arn" { + value = module.tgw_hub.transit_gateway_arn + description = "Transit Gateway ARN" +} + +output "transit_gateway_id" { + value = module.tgw_hub.transit_gateway_id + description = "Transit Gateway ID" +} + +output "transit_gateway_route_table_id" { + value = module.tgw_hub.transit_gateway_route_table_id + description = "Transit Gateway route table ID" +} + +output "vpcs" { + value = module.vpc + description = "Accounts with VPC and VPCs information" +} + +output "eks" { + value = module.eks + description = "Accounts with EKS and EKSs information" +} + +output "tgw_config" { + value = local.tgw_config + description = "Transit Gateway config" +} diff --git a/deprecated/tgw/hub/providers.tf b/deprecated/tgw/hub/providers.tf new file mode 100644 index 000000000..c2419aabb --- /dev/null +++ b/deprecated/tgw/hub/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/deprecated/tgw/hub/remote-state.tf b/deprecated/tgw/hub/remote-state.tf new file mode 100644 index 000000000..775fd7825 --- /dev/null +++ b/deprecated/tgw/hub/remote-state.tf @@ -0,0 +1,62 @@ +locals { + accounts_with_eks = { + for account in var.accounts_with_eks : + account => module.account_map.outputs.account_info_map[account] + } + + accounts_with_vpc = { + for account in var.accounts_with_vpc : + account => module.account_map.outputs.account_info_map[account] + } + + # Create a map of accounts (- or ) and components + eks_remote_states = { + for account_component in setproduct(keys(local.accounts_with_eks), var.eks_component_names) : + join("-", account_component) => { + account = account_component[0] + component = account_component[1] + } + } +} + +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "account-map" + environment = var.account_map_environment_name + stage = var.account_map_stage_name + tenant = coalesce(var.account_map_tenant_name, module.this.tenant) + + context = module.this.context +} + +module "vpc" { + for_each = local.accounts_with_vpc + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "vpc" + stage = each.value.stage + tenant = lookup(each.value, "tenant", null) + + context = module.this.context +} + +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + for_each = local.eks_remote_states + + component = each.value.component + stage = try(split("-", each.value.account)[1], each.value.account) + tenant = try(split("-", each.value.account)[0], null) + + defaults = { + eks_cluster_managed_security_group_id = null + } + + context = module.this.context +} diff --git a/deprecated/tgw/hub/variables.tf b/deprecated/tgw/hub/variables.tf new file mode 100644 index 000000000..889ad0d16 --- /dev/null +++ b/deprecated/tgw/hub/variables.tf @@ -0,0 +1,48 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "accounts_with_vpc" { + type = set(string) + description = "Set of account names that have VPC" +} + +variable "accounts_with_eks" { + type = set(string) + description = "Set of account names that have EKS" +} + +variable "expose_eks_sg" { + type = bool + description = "Set true to allow EKS clusters to accept traffic from source accounts" + default = true +} + +variable "eks_component_names" { + type = set(string) + description = "The names of the eks components" + default = ["eks/cluster"] +} + +variable "account_map_environment_name" { + type = string + description = "The name of the environment where `account_map` is provisioned" + default = "gbl" +} + +variable "account_map_stage_name" { + type = string + description = "The name of the stage where `account_map` is provisioned" + default = "root" +} + +variable "account_map_tenant_name" { + type = string + description = <<-EOT + The name of the tenant where `account_map` is provisioned. + + If the `tenant` label is not used, leave this as `null`. + EOT + default = null +} diff --git a/deprecated/tgw/hub/versions.tf b/deprecated/tgw/hub/versions.tf new file mode 100644 index 000000000..f0e7120a6 --- /dev/null +++ b/deprecated/tgw/hub/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.1" + } + } +} diff --git a/deprecated/tgw/spoke/README.md b/deprecated/tgw/spoke/README.md new file mode 100644 index 000000000..605510858 --- /dev/null +++ b/deprecated/tgw/spoke/README.md @@ -0,0 +1,136 @@ +# Component: `tgw/spoke` + +This component is responsible for provisioning [AWS Transit Gateway](https://aws.amazon.com/transit-gateway) attachments to connect VPCs in a `spoke` account to different accounts through a central `hub`. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to configure and use this component: + +stacks/catalog/tgw/spoke.yaml + +```yaml +components: + terraform: + tgw/spoke-defaults: + metadata: + type: abstract + component: tgw/spoke + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: tgw-spoke + tags: + Team: sre + Service: tgw-spoke + + tgw/spoke: + metadata: + inherits: + - tgw/spoke-defaults + vars: + connections: + - core-network + - core-auto +``` + +stacks/ue2/dev.yaml + +```yaml +import: + - catalog/tgw/spoke + +components: + terraform: + tgw/spoke: + vars: + # use when there is not an EKS cluster in the stack + expose_eks_sg: false + # override default connections + connections: + - core-network + - core-auto + - plat-staging + +``` + +To provision the attachments for a spoke account: + +```sh +atmos terraform plan tgw/spoke -s -- +atmos terraform apply tgw/spoke -s -- +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.1 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [tgw\_hub](#module\_tgw\_hub) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [tgw\_hub\_role](#module\_tgw\_hub\_role) | ../../account-map/modules/iam-roles | n/a | +| [tgw\_hub\_routes](#module\_tgw\_hub\_routes) | cloudposse/transit-gateway/aws | 0.9.1 | +| [tgw\_spoke\_vpc\_attachment](#module\_tgw\_spoke\_vpc\_attachment) | ./modules/standard_vpc_attachment | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [connections](#input\_connections) | List of accounts to connect to | `list(string)` | n/a | yes | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_names](#input\_eks\_component\_names) | The names of the eks components | `set(string)` |
[
"eks/cluster"
]
| no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [expose\_eks\_sg](#input\_expose\_eks\_sg) | Set true to allow EKS clusters to accept traffic from source accounts | `bool` | `true` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [tgw\_hub\_component\_name](#input\_tgw\_hub\_component\_name) | The name of the transit-gateway component | `string` | `"tgw/hub"` | no | +| [tgw\_hub\_environment\_name](#input\_tgw\_hub\_environment\_name) | The name of the environment where `tgw/gateway` is provisioned | `string` | `"ue2"` | no | +| [tgw\_hub\_stage\_name](#input\_tgw\_hub\_stage\_name) | The name of the stage where `tgw/gateway` is provisioned | `string` | `"network"` | no | +| [tgw\_hub\_tenant\_name](#input\_tgw\_hub\_tenant\_name) | The name of the tenant where `tgw/hub` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | + +## Outputs + +No outputs. + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/tgw) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/deprecated/tgw/spoke/context.tf b/deprecated/tgw/spoke/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/deprecated/tgw/spoke/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/deprecated/tgw/spoke/main.tf b/deprecated/tgw/spoke/main.tf new file mode 100644 index 000000000..ff1175909 --- /dev/null +++ b/deprecated/tgw/spoke/main.tf @@ -0,0 +1,49 @@ +# Create the Transit Gateway, route table associations/propagations, and static TGW routes in the `network` account. +# Enable sharing the Transit Gateway with the Organization using Resource Access Manager (RAM). +# If you would like to share resources with your organization or organizational units, +# then you must use the AWS RAM console or CLI command to enable sharing with AWS Organizations. +# When you share resources within your organization, +# AWS RAM does not send invitations to principals. Principals in your organization get access to shared resources without exchanging invitations. +# https://docs.aws.amazon.com/ram/latest/userguide/getting-started-sharing.html + +locals { + spoke_account = module.this.tenant != null ? format("%s-%s", module.this.tenant, module.this.stage) : module.this.stage +} + +module "tgw_hub_routes" { + source = "cloudposse/transit-gateway/aws" + version = "0.9.1" + + providers = { + aws = aws.tgw-hub + } + + ram_resource_share_enabled = false + route_keys_enabled = false + + create_transit_gateway = false + create_transit_gateway_route_table = false + create_transit_gateway_vpc_attachment = false + create_transit_gateway_route_table_association_and_propagation = true + + config = { + (local.spoke_account) = module.tgw_spoke_vpc_attachment.tg_config, + } + + existing_transit_gateway_route_table_id = module.tgw_hub.outputs.transit_gateway_route_table_id + + context = module.this.context +} + +module "tgw_spoke_vpc_attachment" { + source = "./modules/standard_vpc_attachment" + + owning_account = local.spoke_account + + tgw_config = module.tgw_hub.outputs.tgw_config + connections = var.connections + expose_eks_sg = var.expose_eks_sg + eks_component_names = var.eks_component_names + + context = module.this.context +} diff --git a/deprecated/tgw/spoke/modules/standard_vpc_attachment/context.tf b/deprecated/tgw/spoke/modules/standard_vpc_attachment/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/deprecated/tgw/spoke/modules/standard_vpc_attachment/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/deprecated/tgw/spoke/modules/standard_vpc_attachment/main.tf b/deprecated/tgw/spoke/modules/standard_vpc_attachment/main.tf new file mode 100644 index 000000000..ac5df9e8c --- /dev/null +++ b/deprecated/tgw/spoke/modules/standard_vpc_attachment/main.tf @@ -0,0 +1,66 @@ +locals { + vpcs = var.tgw_config.vpcs + own_vpc = local.vpcs[var.owning_account].outputs + connected_accounts = var.connections + + # Create a list of all of the EKS security groups + own_eks_sgs = compact([ + for account_component in setproduct([var.owning_account], var.eks_component_names) : + try(var.tgw_config.eks[join("-", account_component)].outputs.eks_cluster_managed_security_group_id, "") + ]) + + # Create a map of accounts (- or ) and the security group to add ingress rules for + connected_accounts_allow_ingress = { + for account_sg in setproduct(local.connected_accounts, local.own_eks_sgs) : + account_sg[0] => { + account = account_sg[0] + sg = account_sg[1] + } + } + + allowed_cidrs = [ + for k, v in local.vpcs : v.outputs.vpc_cidr + if contains(local.connected_accounts, k) && k != var.owning_account + ] +} + +module "standard_vpc_attachment" { + source = "cloudposse/transit-gateway/aws" + version = "0.9.1" + + existing_transit_gateway_id = var.tgw_config.existing_transit_gateway_id + existing_transit_gateway_route_table_id = var.tgw_config.existing_transit_gateway_route_table_id + + route_keys_enabled = true + create_transit_gateway = false + create_transit_gateway_route_table = false + create_transit_gateway_vpc_attachment = true + create_transit_gateway_route_table_association_and_propagation = false + + config = { + (var.owning_account) = { + vpc_id = local.own_vpc.vpc_id + vpc_cidr = local.own_vpc.vpc_cidr + subnet_ids = local.own_vpc.private_subnet_ids + subnet_route_table_ids = local.own_vpc.private_route_table_ids + route_to = null + static_routes = null + transit_gateway_vpc_attachment_id = null + route_to_cidr_blocks = local.allowed_cidrs + } + } + + context = module.this.context +} + +resource "aws_security_group_rule" "ingress_cidr_blocks" { + for_each = var.expose_eks_sg ? local.connected_accounts_allow_ingress : {} + + description = "Allow inbound traffic from ${each.key}" + type = "ingress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = [local.vpcs[each.value.account].outputs.vpc_cidr] + security_group_id = each.value.sg +} diff --git a/deprecated/tgw/spoke/modules/standard_vpc_attachment/outputs.tf b/deprecated/tgw/spoke/modules/standard_vpc_attachment/outputs.tf new file mode 100644 index 000000000..538a37725 --- /dev/null +++ b/deprecated/tgw/spoke/modules/standard_vpc_attachment/outputs.tf @@ -0,0 +1,14 @@ +output "tg_config" { + ## Fit tg config type https://github.com/cloudposse/terraform-aws-transit-gateway#input_config + value = { + vpc_id = null + vpc_cidr = null + subnet_ids = null + subnet_route_table_ids = null + route_to = null + route_to_cidr_blocks = null + static_routes = null + transit_gateway_vpc_attachment_id = module.standard_vpc_attachment.transit_gateway_vpc_attachment_ids[var.owning_account] + } + description = "Transit Gateway configuration formatted for handling" +} diff --git a/deprecated/tgw/spoke/modules/standard_vpc_attachment/variables.tf b/deprecated/tgw/spoke/modules/standard_vpc_attachment/variables.tf new file mode 100644 index 000000000..d8c88fb3b --- /dev/null +++ b/deprecated/tgw/spoke/modules/standard_vpc_attachment/variables.tf @@ -0,0 +1,32 @@ +variable "owning_account" { + type = string + default = null + description = "The name of the account that owns the VPC being attached" +} + +variable "tgw_config" { + type = object({ + existing_transit_gateway_id = string + existing_transit_gateway_route_table_id = string + vpcs = any + eks = any + }) + description = "Object to pass common data from root module to this submodule. See root module for details" +} + +variable "connections" { + type = list(string) + description = "List of accounts to connect to" +} + +variable "expose_eks_sg" { + type = bool + description = "Set true to allow EKS clusters to accept traffic from source accounts" + default = true +} + +variable "eks_component_names" { + type = set(string) + description = "The names of the eks components" + default = ["eks/cluster"] +} diff --git a/deprecated/tgw/spoke/modules/standard_vpc_attachment/versions.tf b/deprecated/tgw/spoke/modules/standard_vpc_attachment/versions.tf new file mode 100644 index 000000000..f33ede77f --- /dev/null +++ b/deprecated/tgw/spoke/modules/standard_vpc_attachment/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} diff --git a/deprecated/tgw/spoke/outputs.tf b/deprecated/tgw/spoke/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/deprecated/tgw/spoke/providers.tf b/deprecated/tgw/spoke/providers.tf new file mode 100644 index 000000000..bfa49d241 --- /dev/null +++ b/deprecated/tgw/spoke/providers.tf @@ -0,0 +1,70 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} + +provider "aws" { + alias = "tgw-hub" + region = var.region + + assume_role { + role_arn = coalesce(var.import_role_arn, module.tgw_hub_role.terraform_role_arn) + } +} + +variable "tgw_hub_environment_name" { + type = string + description = "The name of the environment where `tgw/gateway` is provisioned" + default = "ue2" +} + +variable "tgw_hub_stage_name" { + type = string + description = "The name of the stage where `tgw/gateway` is provisioned" + default = "network" +} + +variable "tgw_hub_tenant_name" { + type = string + description = <<-EOT + The name of the tenant where `tgw/hub` is provisioned. + + If the `tenant` label is not used, leave this as `null`. + EOT + default = null +} + +module "tgw_hub_role" { + source = "../../account-map/modules/iam-roles" + + stage = var.tgw_hub_stage_name + environment = var.tgw_hub_environment_name + tenant = var.tgw_hub_tenant_name + + context = module.this.context +} diff --git a/deprecated/tgw/spoke/remote-state.tf b/deprecated/tgw/spoke/remote-state.tf new file mode 100644 index 000000000..d074fca2d --- /dev/null +++ b/deprecated/tgw/spoke/remote-state.tf @@ -0,0 +1,11 @@ +module "tgw_hub" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.tgw_hub_component_name + stage = var.tgw_hub_stage_name + environment = var.tgw_hub_environment_name + tenant = var.tgw_hub_tenant_name + + context = module.this.context +} diff --git a/deprecated/tgw/spoke/variables.tf b/deprecated/tgw/spoke/variables.tf new file mode 100644 index 000000000..a34bd9890 --- /dev/null +++ b/deprecated/tgw/spoke/variables.tf @@ -0,0 +1,27 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "connections" { + type = list(string) + description = "List of accounts to connect to" +} + +variable "tgw_hub_component_name" { + type = string + description = "The name of the transit-gateway component" + default = "tgw/hub" +} + +variable "expose_eks_sg" { + type = bool + description = "Set true to allow EKS clusters to accept traffic from source accounts" + default = true +} + +variable "eks_component_names" { + type = set(string) + description = "The names of the eks components" + default = ["eks/cluster"] +} diff --git a/deprecated/tgw/spoke/versions.tf b/deprecated/tgw/spoke/versions.tf new file mode 100644 index 000000000..f0e7120a6 --- /dev/null +++ b/deprecated/tgw/spoke/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.1" + } + } +} diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index b584d09af..b1f8518d4 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -344,6 +344,7 @@ For more on upgrading these EKS Addons, see | [fargate\_profiles](#output\_fargate\_profiles) | Fargate Profiles | | [karpenter\_iam\_role\_arn](#output\_karpenter\_iam\_role\_arn) | Karpenter IAM Role ARN | | [karpenter\_iam\_role\_name](#output\_karpenter\_iam\_role\_name) | Karpenter IAM Role name | +| [vpc\_cidr](#output\_vpc\_cidr) | The CIDR of the VPC where this cluster is deployed. | ## Related How-to Guides diff --git a/modules/eks/cluster/outputs.tf b/modules/eks/cluster/outputs.tf index e32789647..a286038e6 100644 --- a/modules/eks/cluster/outputs.tf +++ b/modules/eks/cluster/outputs.tf @@ -92,3 +92,8 @@ output "fargate_profile_role_names" { description = "Fargate Profile Role names" value = values(module.fargate_profile)[*].eks_fargate_profile_role_name } + +output "vpc_cidr" { + description = "The CIDR of the VPC where this cluster is deployed." + value = local.vpc_outputs.vpc_cidr +} diff --git a/modules/eks/efs-controller/default.auto.tfvars b/modules/eks/efs-controller/default.auto.tfvars deleted file mode 100644 index 5b0464c79..000000000 --- a/modules/eks/efs-controller/default.auto.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - -name = "efs-controller" diff --git a/modules/tgw/cross-region-hub-connector/default.auto.tfvars b/modules/tgw/cross-region-hub-connector/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/tgw/cross-region-hub-connector/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/tgw/cross-region-spoke/default.auto.tfvars b/modules/tgw/cross-region-spoke/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/tgw/cross-region-spoke/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 9b88a8c2b..9d9e6c7ac 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -11,28 +11,62 @@ Here's an example snippet for how to configure and use this component: ```yaml components: terraform: - tgw/hub: - settings: - spacelift: - workspace_enabled: true + tgw/hub/defaults: + metadata: + type: abstract + component: tgw/hub vars: enabled: true name: tgw-hub - eks_component_names: - - eks/cluster-blue - accounts_with_vpc: - - core-auto - - core-corp - - core-network - - plat-dev - - plat-staging - - plat-prod - - plat-sandbox - accounts_with_eks: - - plat-dev - - plat-staging - - plat-prod - - plat-sandbox + expose_eks_sg: false + tags: + Team: sre + Service: tgw-hub + + tgw/hub: + metadata: + inherits: + - tgw/hub/defaults + component: tgw/hub + vars: + connections: + - account: + tenant: core + stage: network + vpc_component_names: + - vpc-dev + - account: + tenant: core + stage: artifacts + - account: + tenant: core + stage: auto + eks_component_names: + - eks/cluster + - account: + tenant: plat + stage: dev + vpc_component_names: + - vpc + - vpc/data/1 + eks_component_names: + - eks/cluster + - account: + tenant: plat + stage: staging + vpc_component_names: + - vpc + - vpc/data/1 + eks_component_names: + - eks/cluster + - account: + tenant: plat + stage: prod + vpc_component_names: + - vpc + - vpc/data/1 + eks_component_names: + - eks/cluster ``` To provision the Transit Gateway and all related resources, run the following commands: @@ -76,14 +110,12 @@ No resources. | [account\_map\_environment\_name](#input\_account\_map\_environment\_name) | The name of the environment where `account_map` is provisioned | `string` | `"gbl"` | no | | [account\_map\_stage\_name](#input\_account\_map\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | | [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | -| [accounts\_with\_eks](#input\_accounts\_with\_eks) | Set of account names that have EKS | `set(string)` | n/a | yes | -| [accounts\_with\_vpc](#input\_accounts\_with\_vpc) | Set of account names that have VPC | `set(string)` | n/a | yes | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [eks\_component\_names](#input\_eks\_component\_names) | The names of the eks components | `set(string)` |
[
"eks/cluster"
]
| no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [expose\_eks\_sg](#input\_expose\_eks\_sg) | Set true to allow EKS clusters to accept traffic from source accounts | `bool` | `true` | no | diff --git a/modules/tgw/hub/default.auto.tfvars b/modules/tgw/hub/default.auto.tfvars deleted file mode 100644 index 94153c134..000000000 --- a/modules/tgw/hub/default.auto.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - -name = "tgw-hub" diff --git a/modules/tgw/hub/main.tf b/modules/tgw/hub/main.tf index c18ef7711..4ab7e5943 100644 --- a/modules/tgw/hub/main.tf +++ b/modules/tgw/hub/main.tf @@ -30,6 +30,5 @@ locals { vpcs = module.vpc eks = module.eks expose_eks_sg = var.expose_eks_sg - eks_component_names = var.eks_component_names } } diff --git a/modules/tgw/hub/remote-state.tf b/modules/tgw/hub/remote-state.tf index 775fd7825..5048794cf 100644 --- a/modules/tgw/hub/remote-state.tf +++ b/modules/tgw/hub/remote-state.tf @@ -1,22 +1,20 @@ locals { - accounts_with_eks = { - for account in var.accounts_with_eks : - account => module.account_map.outputs.account_info_map[account] - } + vpc_connections = flatten([for connection in var.connections : [ + for vpc_component_name in connection.vpc_component_names : { + stage = connection.account.stage + tenant = lookup(connection.account, "tenant", null) + component = vpc_component_name + }] + ]) - accounts_with_vpc = { - for account in var.accounts_with_vpc : - account => module.account_map.outputs.account_info_map[account] - } + eks_connections = flatten([for connection in var.connections : [ + for eks_component_name in connection.eks_component_names : { + stage = connection.account.stage + tenant = lookup(connection.account, "tenant", null) + component = eks_component_name + }] + ]) - # Create a map of accounts (- or ) and components - eks_remote_states = { - for account_component in setproduct(keys(local.accounts_with_eks), var.eks_component_names) : - join("-", account_component) => { - account = account_component[0] - component = account_component[1] - } - } } module "account_map" { @@ -32,12 +30,14 @@ module "account_map" { } module "vpc" { - for_each = local.accounts_with_vpc + for_each = { for c in local.vpc_connections : + (length(c.tenant) > 0 ? "${c.tenant}-${c.stage}-${c.component}" : "${c.stage}-${c.component}") + => c } source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" - component = "vpc" + component = each.value.component stage = each.value.stage tenant = lookup(each.value, "tenant", null) @@ -45,14 +45,16 @@ module "vpc" { } module "eks" { + for_each = { for c in local.eks_connections : + (length(c.tenant) > 0 ? "${c.tenant}-${c.stage}-${c.component}" : "${c.stage}-${c.component}") + => c } + source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" - for_each = local.eks_remote_states - component = each.value.component - stage = try(split("-", each.value.account)[1], each.value.account) - tenant = try(split("-", each.value.account)[0], null) + stage = each.value.stage + tenant = lookup(each.value, "tenant", null) defaults = { eks_cluster_managed_security_group_id = null diff --git a/modules/tgw/hub/variables.tf b/modules/tgw/hub/variables.tf index 889ad0d16..70cd558af 100644 --- a/modules/tgw/hub/variables.tf +++ b/modules/tgw/hub/variables.tf @@ -3,26 +3,27 @@ variable "region" { description = "AWS Region" } -variable "accounts_with_vpc" { - type = set(string) - description = "Set of account names that have VPC" -} - -variable "accounts_with_eks" { - type = set(string) - description = "Set of account names that have EKS" -} - variable "expose_eks_sg" { type = bool description = "Set true to allow EKS clusters to accept traffic from source accounts" default = true } -variable "eks_component_names" { - type = set(string) - description = "The names of the eks components" - default = ["eks/cluster"] +variable "connections" { + type = list(object({ + account = object({ + stage = string + tenant = optional(string, "") + }) + vpc_component_names = optional(list(string), ["vpc"]) + eks_component_names = optional(list(string), []) + })) + description = <<-EOT + A list of objects to define each TGW connections. + + By default, each connection will look for only the default `vpc` component. + EOT + default = [] } variable "account_map_environment_name" { diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index 605510858..c07287ad0 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -17,24 +17,34 @@ components: metadata: type: abstract component: tgw/spoke - settings: - spacelift: - workspace_enabled: true vars: enabled: true name: tgw-spoke tags: Team: sre Service: tgw-spoke + expose_eks_sg: false + tgw_hub_tenant_name: core + tgw_hub_environment_name: ue1 tgw/spoke: metadata: inherits: - tgw/spoke-defaults vars: + # This is what THIS spoke is allowed to connect to. + # since this is deployed to each plat account (dev->prod), + # we allow connections to network and auto. connections: - - core-network - - core-auto + - account: + tenant: core + stage: network + # Set this value if the vpc component has a different name in this account + vpc_component_names: + - vpc-dev + - account: + tenant: core + stage: auto ``` stacks/ue2/dev.yaml @@ -51,10 +61,24 @@ components: expose_eks_sg: false # override default connections connections: - - core-network - - core-auto - - plat-staging - + - account: + tenant: core + stage: network + vpc_component_names: + - vpc-dev + - account: + tenant: core + stage: auto + - account: + tenant: plat + stage: dev + eks_component_names: + - eks/cluster + - account: + tenant: plat + stage: qa + eks_component_names: + - eks/cluster ``` To provision the attachments for a spoke account: @@ -83,7 +107,7 @@ No providers. | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [tgw\_hub](#module\_tgw\_hub) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [tgw\_hub\_role](#module\_tgw\_hub\_role) | ../../account-map/modules/iam-roles | n/a | -| [tgw\_hub\_routes](#module\_tgw\_hub\_routes) | cloudposse/transit-gateway/aws | 0.9.1 | +| [tgw\_hub\_routes](#module\_tgw\_hub\_routes) | cloudposse/transit-gateway/aws | 0.10.0 | | [tgw\_spoke\_vpc\_attachment](#module\_tgw\_spoke\_vpc\_attachment) | ./modules/standard_vpc_attachment | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -97,11 +121,10 @@ No resources. |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [connections](#input\_connections) | List of accounts to connect to | `list(string)` | n/a | yes | +| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [eks\_component\_names](#input\_eks\_component\_names) | The names of the eks components | `set(string)` |
[
"eks/cluster"
]
| no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [expose\_eks\_sg](#input\_expose\_eks\_sg) | Set true to allow EKS clusters to accept traffic from source accounts | `bool` | `true` | no | @@ -114,6 +137,8 @@ No resources. | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [own\_eks\_component\_names](#input\_own\_eks\_component\_names) | The name of the eks components in the owning account. | `list(string)` | `[]` | no | +| [own\_vpc\_component\_name](#input\_own\_vpc\_component\_name) | The name of the vpc component in the owning account. Defaults to "vpc" | `string` | `"vpc"` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/tgw/spoke/default.auto.tfvars b/modules/tgw/spoke/default.auto.tfvars deleted file mode 100644 index 6ebfe4f02..000000000 --- a/modules/tgw/spoke/default.auto.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - -name = "tgw-spoke" diff --git a/modules/tgw/spoke/main.tf b/modules/tgw/spoke/main.tf index ff1175909..607969f5b 100644 --- a/modules/tgw/spoke/main.tf +++ b/modules/tgw/spoke/main.tf @@ -12,7 +12,7 @@ locals { module "tgw_hub_routes" { source = "cloudposse/transit-gateway/aws" - version = "0.9.1" + version = "0.10.0" providers = { aws = aws.tgw-hub @@ -38,12 +38,13 @@ module "tgw_hub_routes" { module "tgw_spoke_vpc_attachment" { source = "./modules/standard_vpc_attachment" - owning_account = local.spoke_account + owning_account = local.spoke_account + own_vpc_component_name = var.own_vpc_component_name + own_eks_component_names = var.own_eks_component_names - tgw_config = module.tgw_hub.outputs.tgw_config - connections = var.connections - expose_eks_sg = var.expose_eks_sg - eks_component_names = var.eks_component_names + tgw_config = module.tgw_hub.outputs.tgw_config + connections = var.connections + expose_eks_sg = var.expose_eks_sg context = module.this.context } diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/default.auto.tfvars b/modules/tgw/spoke/modules/standard_vpc_attachment/default.auto.tfvars deleted file mode 100644 index 67952b0d1..000000000 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = true diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf index ac5df9e8c..1d4fb42a3 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf @@ -1,29 +1,75 @@ locals { - vpcs = var.tgw_config.vpcs - own_vpc = local.vpcs[var.owning_account].outputs - connected_accounts = var.connections + vpcs = var.tgw_config.vpcs + eks = var.tgw_config.eks - # Create a list of all of the EKS security groups - own_eks_sgs = compact([ - for account_component in setproduct([var.owning_account], var.eks_component_names) : - try(var.tgw_config.eks[join("-", account_component)].outputs.eks_cluster_managed_security_group_id, "") - ]) + own_account_vpc_key = "${var.owning_account}-${var.own_vpc_component_name}" + own_vpc = local.vpcs[local.own_account_vpc_key].outputs - # Create a map of accounts (- or ) and the security group to add ingress rules for - connected_accounts_allow_ingress = { - for account_sg in setproduct(local.connected_accounts, local.own_eks_sgs) : - account_sg[0] => { - account = account_sg[0] - sg = account_sg[1] - } + # Create a list of all VPC component keys. Key includes stack + component + # + # Example var.connections + # connections: + # - account: + # tenant: core + # stage: network + # vpc_component_names: + # - vpc-dev + # - account: + # tenant: core + # stage: auto + connected_vpc_component_keys = flatten( + [ + for c in var.connections : + [ + # Default value for c.vpc_component_names is ["vpc"] + for vpc in c.vpc_component_names : + # This component key needs to match the key created by tgw/hub + # See components/terraform/tgw/hub/remote-state.tf + length(c.account.tenant) > 0 ? "${c.account.tenant}-${c.account.stage}-${vpc}" : "${c.account.stage}-${vpc}" + ] + ] + ) + + # Create a list of all EKS component keys. + # Follows same pattern as vpc_component_names + connected_eks_component_keys = flatten( + [ + for c in var.connections : + [ + for eks in c.eks_component_names : + length(c.account.tenant) > 0 ? "${c.account.tenant}-${c.account.stage}-${eks}" : "${c.account.stage}-${eks}" + ] + ] + ) + + # Define a list of all VPCs allowed to access this account's VPC. + # Filter the tgw_config output from tgw/hub for VPCs and pull the CIDR of a VPC if + # (1) this is not the primary VPC that we are connecting to and (2) this VPC key is given as a connection + allowed_vpcs = { + for vpc_key, vpc_remote_state in local.vpcs : + vpc_key => { + cidr = vpc_remote_state.outputs.vpc_cidr + } if vpc_key != local.own_account_vpc_key && contains(local.connected_vpc_component_keys, vpc_key) } - allowed_cidrs = [ - for k, v in local.vpcs : v.outputs.vpc_cidr - if contains(local.connected_accounts, k) && k != var.owning_account - ] + # For each EKS cluster in this account, map the EKS SG to the CIDR for each connected cluster + allowed_eks = merge([ + for own_eks_component in var.own_eks_component_names : + { + for eks_key, eks_remote_state in local.eks : + eks_key => { + # SG of each EKS component in this account + sg_id = local.eks["${var.owning_account}-${own_eks_component}"].outputs.eks_cluster_managed_security_group_id + # CIDR of the remote EKS cluster + cidr = eks_remote_state.outputs.vpc_cidr + } if contains(local.connected_eks_component_keys, eks_key) + } + ]...) + } +# Create a TGW attachment from this account's VPC to the TGW Hub +# This includes a merged list of all CIDRs from allowed VPCs in connected accounts module "standard_vpc_attachment" { source = "cloudposse/transit-gateway/aws" version = "0.9.1" @@ -46,21 +92,23 @@ module "standard_vpc_attachment" { route_to = null static_routes = null transit_gateway_vpc_attachment_id = null - route_to_cidr_blocks = local.allowed_cidrs + route_to_cidr_blocks = [for vpc in local.allowed_vpcs : vpc.cidr] } } context = module.this.context } +# Define a Security Group Rule to allow traffic from +# Expose traffic from EKS VPC CIDRs in other accounts to this accounts EKS cluster SG resource "aws_security_group_rule" "ingress_cidr_blocks" { - for_each = var.expose_eks_sg ? local.connected_accounts_allow_ingress : {} + for_each = var.expose_eks_sg ? local.allowed_eks : {} description = "Allow inbound traffic from ${each.key}" type = "ingress" from_port = 0 to_port = 65535 protocol = "tcp" - cidr_blocks = [local.vpcs[each.value.account].outputs.vpc_cidr] - security_group_id = each.value.sg + cidr_blocks = [each.value.cidr] # CIDR of cluster in other accounts + security_group_id = each.value.sg_id # SG of cluster in this account } diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf index d8c88fb3b..0e43828ac 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf @@ -4,6 +4,18 @@ variable "owning_account" { description = "The name of the account that owns the VPC being attached" } +variable "own_vpc_component_name" { + type = string + default = "vpc" + description = "The name of the vpc component in the owning account. Defaults to \"vpc\"" +} + +variable "own_eks_component_names" { + type = list(string) + default = [] + description = "The name of the eks components in the owning account." +} + variable "tgw_config" { type = object({ existing_transit_gateway_id = string @@ -15,8 +27,20 @@ variable "tgw_config" { } variable "connections" { - type = list(string) - description = "List of accounts to connect to" + type = list(object({ + account = object({ + stage = string + tenant = optional(string, "") + }) + vpc_component_names = optional(list(string), ["vpc"]) + eks_component_names = optional(list(string), []) + })) + description = <<-EOT + A list of objects to define each TGW connections. + + By default, each connection will look for only the default `vpc` component. + EOT + default = [] } variable "expose_eks_sg" { @@ -24,9 +48,3 @@ variable "expose_eks_sg" { description = "Set true to allow EKS clusters to accept traffic from source accounts" default = true } - -variable "eks_component_names" { - type = set(string) - description = "The names of the eks components" - default = ["eks/cluster"] -} diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index a34bd9890..d0908efc4 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -4,8 +4,20 @@ variable "region" { } variable "connections" { - type = list(string) - description = "List of accounts to connect to" + type = list(object({ + account = object({ + stage = string + tenant = optional(string, "") + }) + vpc_component_names = optional(list(string), ["vpc"]) + eks_component_names = optional(list(string), []) + })) + description = <<-EOT + A list of objects to define each TGW connections. + + By default, each connection will look for only the default `vpc` component. + EOT + default = [] } variable "tgw_hub_component_name" { @@ -20,8 +32,14 @@ variable "expose_eks_sg" { default = true } -variable "eks_component_names" { - type = set(string) - description = "The names of the eks components" - default = ["eks/cluster"] +variable "own_vpc_component_name" { + type = string + default = "vpc" + description = "The name of the vpc component in the owning account. Defaults to \"vpc\"" +} + +variable "own_eks_component_names" { + type = list(string) + default = [] + description = "The name of the eks components in the owning account." } From 73d6f510125474ad42bfc1fedf7074442f01070f Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 2 Jun 2023 08:28:07 -0600 Subject: [PATCH 139/501] `.editorconfig` Typo (#704) --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 01eaf8ae6..e282a8e97 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,7 +17,7 @@ indent_style = tab indent_size = 4 [{*.yaml,*.yml,*.md}] -intent_style = space +indent_style = space indent_size = 2 [*.sh] From 675f40bc4ec92993d59040c1efacf53ea8b44a56 Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Fri, 2 Jun 2023 13:01:39 -0500 Subject: [PATCH 140/501] ssm-parameters: support tiers (#705) Co-authored-by: cloudpossebot --- modules/ssm-parameters/README.md | 4 ++-- modules/ssm-parameters/main.tf | 1 + modules/ssm-parameters/variables.tf | 4 +++- modules/ssm-parameters/versions.tf | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/ssm-parameters/README.md b/modules/ssm-parameters/README.md index 01a0b6210..7ffc2a3a6 100644 --- a/modules/ssm-parameters/README.md +++ b/modules/ssm-parameters/README.md @@ -30,7 +30,7 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.2.5 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.0 | | [sops](#requirement\_sops) | >= 0.5, < 1.0 | @@ -76,7 +76,7 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [params](#input\_params) | n/a |
map(object({
value = string
description = string
overwrite = bool
type = string
}))
| n/a | yes | +| [params](#input\_params) | A map of parameter values to write to SSM Parameter Store |
map(object({
value = string
description = string
overwrite = optional(bool, false)
tier = optional(string, "Standard")
type = string
}))
| n/a | yes | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [sops\_source\_file](#input\_sops\_source\_file) | The relative path to the SOPS file which is consumed as the source for creating parameter resources. | `string` | n/a | yes | diff --git a/modules/ssm-parameters/main.tf b/modules/ssm-parameters/main.tf index 2e42a052f..895737987 100644 --- a/modules/ssm-parameters/main.tf +++ b/modules/ssm-parameters/main.tf @@ -26,6 +26,7 @@ resource "aws_ssm_parameter" "destination" { name = each.key description = each.value.description + tier = each.value.tier type = each.value.type key_id = var.kms_arn value = each.value.value diff --git a/modules/ssm-parameters/variables.tf b/modules/ssm-parameters/variables.tf index f0382978f..51733adbc 100644 --- a/modules/ssm-parameters/variables.tf +++ b/modules/ssm-parameters/variables.tf @@ -23,7 +23,9 @@ variable "params" { type = map(object({ value = string description = string - overwrite = bool + overwrite = optional(bool, false) + tier = optional(string, "Standard") type = string })) + description = "A map of parameter values to write to SSM Parameter Store" } diff --git a/modules/ssm-parameters/versions.tf b/modules/ssm-parameters/versions.tf index 6cd6a3381..300f9596d 100644 --- a/modules/ssm-parameters/versions.tf +++ b/modules/ssm-parameters/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.2.5" + required_version = ">= 1.3.0" required_providers { aws = { From 30fa1ff049422aab8d69ce7643efd48b0385866a Mon Sep 17 00:00:00 2001 From: Nuru Date: Sat, 3 Jun 2023 18:18:05 -0700 Subject: [PATCH 141/501] Preserve custom roles when vendoring in updates (#697) --- modules/aws-team-roles/README.md | 52 ++++++++++++++++++- .../aws-team-roles/additional-policy-map.tf | 10 ++++ modules/aws-team-roles/main.tf | 3 +- modules/aws-teams/README.md | 15 ++++-- modules/aws-teams/additional-policy-map.tf | 10 ++++ modules/aws-teams/main.tf | 3 +- 6 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 modules/aws-team-roles/additional-policy-map.tf create mode 100644 modules/aws-teams/additional-policy-map.tf diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index 97577a59c..d5bb97649 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -3,7 +3,57 @@ This component is responsible for provisioning user and system IAM roles outside the `identity` account. It sets them up to be assumed from the "team" roles defined in the `identity` account by [the `aws-teams` component](../aws-teams) and/or the AWS SSO permission sets -defined in [the `aws-sso` component](../aws-sso). +defined in [the `aws-sso` component](../aws-sso), and/or be directly accessible via SAML logins. + + +### Privileges are Granted to Users via IAM Policies + +Each role is granted permissions by attaching a list of IAM policies to the IAM role +via its `role_policy_arns` list. You can configure AWS managed policies by entering the ARNs of the policies +directly into the list, or you can create a custom policy as follows: + +1. Give the policy a name, e.g. `eks-admin`. We will use `NAME` as a placeholder for the name in the instructions below. +2. Create a file in the `aws-teams` directory with the name `policy-NAME.tf`. +3. In that file, create a policy as follows: + + ```hcl + data "aws_iam_policy_document" "NAME" { + # Define the policy here + } + + resource "aws_iam_policy" "NAME" { + name = format("%s-NAME", module.this.id) + policy = data.aws_iam_policy_document.NAME.json + + tags = module.this.tags + } + ``` + +4. Create a file named `additional-policy-map_override.tf` in the `aws-team-roles` directory (if it does not already exist). + This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), meaning its + contents will be merged with the main terraform file, and any locals defined in it will override locals defined in other files. + Having your code in this separate override file makes it possible for the component to provide a placeholder local variable + so that it works without customization, while allowing you to customize the component and still update it without losing your customizations. +5. In that file, redefine the local variable `overridable_additional_custom_policy_map` map as follows: + + ```hcl + locals { + overridable_additional_custom_policy_map = { + NAME = aws_iam_policy.NAME.arn + } + } + ``` + + If you have multiple custom policies, add each one to the map in the form `NAME = aws_iam_policy.NAME.arn`. +6. With that done, you can now attach that policy by adding the name to the `role_policy_arns` list. For example: + + ```yaml + role_policy_arns: + - "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" + - "NAME" + ``` + + ## Usage diff --git a/modules/aws-team-roles/additional-policy-map.tf b/modules/aws-team-roles/additional-policy-map.tf new file mode 100644 index 000000000..5ead699f8 --- /dev/null +++ b/modules/aws-team-roles/additional-policy-map.tf @@ -0,0 +1,10 @@ +locals { + # If you have custom policies, override this declaration by creating + # a file called `additional-policy-map_override.tf`. + # Then add the custom policies to the additional_custom_policy_map in that file. + # See the README for more details. + overridable_additional_custom_policy_map = { + # Example: + # eks_viewer = aws_iam_policy.eks_viewer.arn + } +} diff --git a/modules/aws-team-roles/main.tf b/modules/aws-team-roles/main.tf index cc17c2556..3fd6e669c 100644 --- a/modules/aws-team-roles/main.tf +++ b/modules/aws-team-roles/main.tf @@ -10,12 +10,13 @@ locals { # If you want to create custom policies to add to multiple roles by name, create the policy # using an aws_iam_policy resource and then map it to the name you want to use in the # YAML configuration by adding an entry in `custom_policy_map`. - custom_policy_map = { + supplied_custom_policy_map = { billing_read_only = try(aws_iam_policy.billing_read_only[0].arn, null) billing_admin = try(aws_iam_policy.billing_admin[0].arn, null) support = try(aws_iam_policy.support[0].arn, null) eks_viewer = try(aws_iam_policy.eks_viewer[0].arn, null) } + custom_policy_map = merge(local.supplied_custom_policy_map, local.overridable_additional_custom_policy_map) configured_policies = flatten([for k, v in local.roles_config : v.role_policy_arns]) diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index bfb93f925..c62c76070 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -1,8 +1,14 @@ # Component: `aws-teams` -This component is responsible for provisioning all primary user and system roles into the centralized identity account. -This is expected to be use alongside [the `aws-team-roles` component](../aws-team-roles) to provide -fine grained role delegation across the account hierarchy. +## Purpose + +This component implements the hub of a hub-and-spoke account access hierarchy, where the hub is the `identity` account. +This allows you to assign each user to a single "team" (implemented as an IAM role) in the `identity` account, which they access +via a single authentication (login), which then allows them to access a set of roles in a set of accounts. + +This component is responsible for provisioning team roles in the centralized identity account. +This is expected to be used alongside [the `aws-team-roles` component](../aws-team-roles) +which provisions the roles in the spoke accounts and configures their privileges and which teams can access them. ### Teams Function Like Groups and are Implemented as Roles The "teams" created in the `identity` account by this module can be thought of as access control "groups": @@ -10,6 +16,9 @@ a user who is allowed access one of these teams gets access to a set of roles (a across a set of accounts. Generally, there is nothing else provisioned in the `identity` account, so the teams have limited access to resources in the `identity` account by design. +Privileges for the users within the identity account itself are generally limited, and are configured +via the `role_policy_arns` variable, just as with `aws-team-roles`. See the `aws-team-roles` README for more details. + Teams are implemented as IAM Roles in each account. Access to the "teams" in the `identity` account is controlled by the `aws-saml` and `aws-sso` components. Access to the roles in all the other accounts is controlled by the "assume role" policies of those roles, which allow the "team" diff --git a/modules/aws-teams/additional-policy-map.tf b/modules/aws-teams/additional-policy-map.tf new file mode 100644 index 000000000..9596d2daa --- /dev/null +++ b/modules/aws-teams/additional-policy-map.tf @@ -0,0 +1,10 @@ +locals { + # If you have custom policies, override this declaration by creating + # a file called `additional-policy-map_override.tf`. + # Then add the custom policies to the additional_custom_policy_map in that file. + # See the README in `aws-team-roles` for more details. + overridable_additional_custom_policy_map = { + # Example: + # eks_viewer = aws_iam_policy.eks_viewer.arn + } +} diff --git a/modules/aws-teams/main.tf b/modules/aws-teams/main.tf index adb3606d7..31ecc29cc 100644 --- a/modules/aws-teams/main.tf +++ b/modules/aws-teams/main.tf @@ -6,10 +6,11 @@ locals { # If you want to create custom policies to add to multiple roles by name, create the policy # using an aws_iam_policy resource and then map it to the name you want to use in the # YAML configuration by adding an entry in `custom_policy_map`. - custom_policy_map = { + supplied_custom_policy_map = { team_role_access = aws_iam_policy.team_role_access.arn support = try(aws_iam_policy.support[0].arn, null) } + custom_policy_map = merge(local.supplied_custom_policy_map, local.overridable_additional_custom_policy_map) configured_policies = flatten([for k, v in local.roles_config : v.role_policy_arns]) From 85d0014216b80596737a1616ae025f70a2d5b8e0 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 4 Jun 2023 16:08:15 -0700 Subject: [PATCH 142/501] Update modules for Terraform AWS provider v5 (#707) --- modules/cloudtrail-bucket/README.md | 2 +- modules/cloudtrail-bucket/main.tf | 2 +- modules/datadog-synthetics/README.md | 2 +- modules/datadog-synthetics/remote-state.tf | 2 +- modules/spacelift-worker-pool/README.md | 2 +- modules/spacelift-worker-pool/main.tf | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/cloudtrail-bucket/README.md b/modules/cloudtrail-bucket/README.md index 72a826ba6..a2051a73e 100644 --- a/modules/cloudtrail-bucket/README.md +++ b/modules/cloudtrail-bucket/README.md @@ -38,7 +38,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [cloudtrail\_s3\_bucket](#module\_cloudtrail\_s3\_bucket) | cloudposse/cloudtrail-s3-bucket/aws | 0.23.1 | +| [cloudtrail\_s3\_bucket](#module\_cloudtrail\_s3\_bucket) | cloudposse/cloudtrail-s3-bucket/aws | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/cloudtrail-bucket/main.tf b/modules/cloudtrail-bucket/main.tf index c004facf2..0ad5770fc 100644 --- a/modules/cloudtrail-bucket/main.tf +++ b/modules/cloudtrail-bucket/main.tf @@ -1,6 +1,6 @@ module "cloudtrail_s3_bucket" { source = "cloudposse/cloudtrail-s3-bucket/aws" - version = "0.23.1" + version = "0.25.0" acl = var.acl expiration_days = var.expiration_days diff --git a/modules/datadog-synthetics/README.md b/modules/datadog-synthetics/README.md index 116e85a50..9a08231c6 100644 --- a/modules/datadog-synthetics/README.md +++ b/modules/datadog-synthetics/README.md @@ -147,7 +147,7 @@ No providers. | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [datadog\_synthetics](#module\_datadog\_synthetics) | cloudposse/platform/datadog//modules/synthetics | 1.0.1 | | [datadog\_synthetics\_merge](#module\_datadog\_synthetics\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | -| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [datadog\_synthetics\_yaml\_config](#module\_datadog\_synthetics\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/datadog-synthetics/remote-state.tf b/modules/datadog-synthetics/remote-state.tf index 872949fc8..34a34a447 100644 --- a/modules/datadog-synthetics/remote-state.tf +++ b/modules/datadog-synthetics/remote-state.tf @@ -1,6 +1,6 @@ module "datadog_synthetics_private_location" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = var.synthetics_private_location_component_name diff --git a/modules/spacelift-worker-pool/README.md b/modules/spacelift-worker-pool/README.md index 50da9d239..2fa8022a0 100644 --- a/modules/spacelift-worker-pool/README.md +++ b/modules/spacelift-worker-pool/README.md @@ -97,7 +97,7 @@ the output to the `trusted_role_arns` list for the `spacelift` role in `aws-team | Name | Source | Version | |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.30.1 | +| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.34.1 | | [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_label](#module\_iam\_label) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | diff --git a/modules/spacelift-worker-pool/main.tf b/modules/spacelift-worker-pool/main.tf index 5138fa74f..39d7ce2f0 100644 --- a/modules/spacelift-worker-pool/main.tf +++ b/modules/spacelift-worker-pool/main.tf @@ -86,7 +86,7 @@ module "security_group" { module "autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" - version = "0.30.1" + version = "0.34.1" image_id = var.spacelift_ami_id == null ? join("", data.aws_ami.spacelift.*.image_id) : var.spacelift_ami_id instance_type = var.instance_type From 79aba10b618c5d9af156f41fec61b7de35617708 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 4 Jun 2023 16:11:22 -0700 Subject: [PATCH 143/501] [eks/external-secrets-operator] Normalize variables, update dependencies (#708) --- .../eks/external-secrets-operator/README.md | 40 ++++++++++++++----- .../helm-variables.tf | 18 +++++++-- modules/eks/external-secrets-operator/main.tf | 20 +++++----- .../provider-helm.tf | 2 +- .../eks/external-secrets-operator/versions.tf | 2 +- 5 files changed, 59 insertions(+), 23 deletions(-) diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 2f488bdaa..67cd809b9 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -52,16 +52,36 @@ components: vars: enabled: true name: "external-secrets-operator" + helm_manifest_experiment_enabled: false + chart: "external-secrets" + chart_repository: "https://charts.external-secrets.io" + chart_version: "0.8.3" kubernetes_namespace: "secrets" create_namespace: true timeout: 90 wait: true atomic: true cleanup_on_fail: true + tags: + Team: sre + Service: external-secrets-operator + resources: + limits: + cpu: "100m" + memory: "300Mi" + requests: + cpu: "20m" + memory: "60Mi" parameter_store_paths: - app - rds - + # You can use `chart_values` to set any other chart options. Treat `chart_values` as the root of the doc. + # + # # For example + # --- + # chart_values: + # installCRDs: true + chart_values: {} ``` @@ -72,14 +92,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Modules @@ -87,8 +107,8 @@ components: |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.5.0 | -| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.5.0 | +| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.8.1 | +| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.8.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -108,17 +128,20 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `null` | no | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `null` | no | +| [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | | [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | +| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [description](#input\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | @@ -143,7 +166,6 @@ components: | [rbac\_enabled](#input\_rbac\_enabled) | Service Account for pods. | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [repository](#input\_repository) | Repository URL where to locate the requested chart. | `string` | `null` | no | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/modules/eks/external-secrets-operator/helm-variables.tf b/modules/eks/external-secrets-operator/helm-variables.tf index ca1c6c7b2..4756e3f79 100644 --- a/modules/eks/external-secrets-operator/helm-variables.tf +++ b/modules/eks/external-secrets-operator/helm-variables.tf @@ -3,28 +3,40 @@ variable "kubernetes_namespace" { description = "The namespace to install the release into." } -variable "description" { +variable "chart_description" { type = string description = "Set release description attribute (visible in the history)." default = null } -variable "repository" { +variable "chart_repository" { type = string description = "Repository URL where to locate the requested chart." default = null } +variable "chart" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." + default = null +} + variable "chart_version" { type = string description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." default = null } +variable "chart_values" { + type = any + description = "Additional values to yamlencode as `helm_release` values." + default = {} +} + variable "create_namespace" { type = bool description = "Create the Kubernetes namespace if it does not yet exist" - default = true + default = null } variable "verify" { diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf index 6827768dd..7b8617243 100644 --- a/modules/eks/external-secrets-operator/main.tf +++ b/modules/eks/external-secrets-operator/main.tf @@ -18,14 +18,14 @@ resource "kubernetes_namespace" "default" { # https://external-secrets.io/v0.5.9/guides-getting-started/ module "external_secrets_operator" { source = "cloudposse/helm-release/aws" - version = "0.5.0" + version = "0.8.1" - name = "" # avoids hitting length restrictions on IAM Role names - description = "External Secrets Operator is a Kubernetes operator that integrates external secret management systems including AWS SSM, Parameter Store, Hasicorp Vault, 1Password Secrets Automation, etc. It reads values from external vaults and injects values as a Kubernetes Secret" + name = "" # avoid redundant release name in IAM role: ...-ekc-cluster-external-secrets-operator-external-secrets-operator@secrets + description = var.chart_description - repository = "https://charts.external-secrets.io" - chart = "external-secrets" - chart_version = "0.6.0-rc1" # using RC to address this bug https://github.com/external-secrets/external-secrets/issues/1511 + repository = var.chart_repository + chart = var.chart + chart_version = var.chart_version kubernetes_namespace = join("", kubernetes_namespace.default.*.id) create_namespace = false wait = var.wait @@ -68,7 +68,9 @@ module "external_secrets_operator" { rbac = { create = var.rbac_enabled } - }) + }), + # additional values + yamlencode(var.chart_values) ]) context = module.this.context @@ -76,9 +78,9 @@ module "external_secrets_operator" { module "external_ssm_secrets" { source = "cloudposse/helm-release/aws" - version = "0.5.0" + version = "0.8.1" - name = "ssm" # avoids hitting length restrictions on IAM Role names + name = "ssm" # distinguish from external_secrets_operator description = "This Chart uses creates a SecretStore and ExternalSecret to pull variables (under a given path) from AWS SSM Parameter Store into a Kubernetes secret." chart = "${path.module}/charts/external-ssm-secrets" diff --git a/modules/eks/external-secrets-operator/provider-helm.tf b/modules/eks/external-secrets-operator/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/external-secrets-operator/provider-helm.tf +++ b/modules/eks/external-secrets-operator/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/external-secrets-operator/versions.tf b/modules/eks/external-secrets-operator/versions.tf index b7a1a1986..fb8857fab 100644 --- a/modules/eks/external-secrets-operator/versions.tf +++ b/modules/eks/external-secrets-operator/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.7.1" + version = ">= 2.7.1, != 2.21.0" } } } From e198524c82a6b512ea1b757f9af0ba2c87cc6a72 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 4 Jun 2023 18:59:25 -0700 Subject: [PATCH 144/501] Move `profiles_enabled` logic out of `providers.tf` and into `iam-roles` (#702) --- .../account-map/modules/iam-roles/README.md | 13 +++++---- modules/account-map/modules/iam-roles/main.tf | 3 +- .../account-map/modules/iam-roles/outputs.tf | 28 +++++++++---------- .../modules/iam-roles/variables.tf | 12 +++++--- modules/aws-waf-acl/README.md | 1 + modules/aws-waf-acl/providers.tf | 18 ++++++++++-- modules/cognito/README.md | 3 +- modules/cognito/providers.tf | 17 +++++++++-- modules/dns-delegated/providers.tf | 2 +- modules/dns-primary/providers.tf | 2 +- modules/documentdb/providers.tf | 1 + modules/eks-iam/README.md | 1 + modules/eks-iam/providers.tf | 22 ++++++++++----- modules/elasticache-redis/providers.tf | 1 + modules/mq-broker/README.md | 1 + modules/mq-broker/providers.tf | 16 +++++++++-- modules/sqs-queue/providers.tf | 1 + modules/sso-saml-provider/providers.tf | 1 + modules/vpc-flow-logs-bucket/README.md | 3 +- modules/vpc-flow-logs-bucket/providers.tf | 17 +++++++++-- modules/zscaler/README.md | 1 + modules/zscaler/providers.tf | 17 ++++++++--- 22 files changed, 132 insertions(+), 49 deletions(-) mode change 100755 => 100644 modules/eks-iam/providers.tf mode change 100755 => 100644 modules/mq-broker/providers.tf diff --git a/modules/account-map/modules/iam-roles/README.md b/modules/account-map/modules/iam-roles/README.md index 984c9beaa..c08ecf2a5 100644 --- a/modules/account-map/modules/iam-roles/README.md +++ b/modules/account-map/modules/iam-roles/README.md @@ -2,14 +2,15 @@ This submodule is used by other modules to determine which IAM Roles or AWS CLI Config Profiles to use for various tasks, most commonly -for applying Terraform plans. +for applying Terraform plans. ## Special Configuration Needed In order to avoid having to pass customization information through every module that uses this submodule, if the default configuration does not suit your needs, -you are expected to customize `variables.tf` with the defaults you want to -use in your project. For example, if you are including the `tenant` label -in the designation of your "root" account (your Organization Management Account), -then you should modify `variables.tf` so that `global_tenant_name` defaults -to the appropriate value. +you are expected to add `variables_override.tf` to override the variables with +the defaults you want to use in your project. For example, if you are not using +"core" as the `tenant` portion of your "root" account (your Organization Management Account), +then you should include the `variable "overridable_global_tenant_name"` declaration +in your `variables_override.tf` so that `overridable_global_tenant_name` defaults +to the value you are using (or the empty string if you are not using `tenant` at all). diff --git a/modules/account-map/modules/iam-roles/main.tf b/modules/account-map/modules/iam-roles/main.tf index bab29e15c..e9a95553f 100644 --- a/modules/account-map/modules/iam-roles/main.tf +++ b/modules/account-map/modules/iam-roles/main.tf @@ -22,5 +22,6 @@ module "account_map" { } locals { - account_name = lookup(module.always.descriptors, "account_name", module.always.stage) + account_name = lookup(module.always.descriptors, "account_name", module.always.stage) + profiles_enabled = module.account_map.outputs.profiles_enabled } diff --git a/modules/account-map/modules/iam-roles/outputs.tf b/modules/account-map/modules/iam-roles/outputs.tf index 04b3bb220..252b6a3e2 100644 --- a/modules/account-map/modules/iam-roles/outputs.tf +++ b/modules/account-map/modules/iam-roles/outputs.tf @@ -1,5 +1,5 @@ output "terraform_role_arn" { - value = module.account_map.outputs.terraform_roles[local.account_name] + value = local.profiles_enabled ? null : module.account_map.outputs.terraform_roles[local.account_name] description = "The AWS Role ARN for Terraform to use when provisioning resources in the account, when Role ARNs are in use" } @@ -9,7 +9,7 @@ output "terraform_role_arns" { } output "terraform_profile_name" { - value = module.account_map.outputs.terraform_profiles[local.account_name] + value = local.profiles_enabled ? module.account_map.outputs.terraform_profiles[local.account_name] : null description = "The AWS config profile name for Terraform to use when provisioning resources in the account, when profiles are in use" } @@ -27,17 +27,17 @@ output "org_role_arn" { } output "global_tenant_name" { - value = var.global_tenant_name + value = var.overridable_global_tenant_name description = "The `null-label` `tenant` value used for organization-wide resources" } output "global_environment_name" { - value = var.global_environment_name + value = var.overridable_global_environment_name description = "The `null-label` `environment` value used for regionless (global) resources" } output "global_stage_name" { - value = var.global_stage_name + value = var.overridable_global_stage_name description = "The `null-label` `stage` value for the organization management account (where the `account-map` state is stored)" } @@ -50,22 +50,22 @@ output "current_account_account_name" { } output "dns_terraform_role_arn" { - value = module.account_map.outputs.terraform_roles[module.account_map.outputs.dns_account_account_name] + value = local.profiles_enabled ? null : module.account_map.outputs.terraform_roles[module.account_map.outputs.dns_account_account_name] description = "The AWS Role ARN for Terraform to use to provision DNS Zone delegations, when Role ARNs are in use" } output "dns_terraform_profile_name" { - value = module.account_map.outputs.terraform_profiles[module.account_map.outputs.dns_account_account_name] + value = local.profiles_enabled ? module.account_map.outputs.terraform_profiles[module.account_map.outputs.dns_account_account_name] : null description = "The AWS config profile name for Terraform to use to provision DNS Zone delegations, when profiles are in use" } output "audit_terraform_role_arn" { - value = module.account_map.outputs.terraform_roles[module.account_map.outputs.audit_account_account_name] + value = local.profiles_enabled ? null : module.account_map.outputs.terraform_roles[module.account_map.outputs.audit_account_account_name] description = "The AWS Role ARN for Terraform to use to provision resources in the \"audit\" role account, when Role ARNs are in use" } output "audit_terraform_profile_name" { - value = module.account_map.outputs.terraform_profiles[module.account_map.outputs.audit_account_account_name] + value = local.profiles_enabled ? module.account_map.outputs.terraform_profiles[module.account_map.outputs.audit_account_account_name] : null description = "The AWS config profile name for Terraform to use to provision resources in the \"audit\" role account, when profiles are in use" } @@ -75,26 +75,26 @@ output "identity_account_account_name" { } output "identity_terraform_role_arn" { - value = module.account_map.outputs.terraform_roles[module.account_map.outputs.identity_account_account_name] + value = local.profiles_enabled ? null : module.account_map.outputs.terraform_roles[module.account_map.outputs.identity_account_account_name] description = "The AWS Role ARN for Terraform to use to provision resources in the \"identity\" role account, when Role ARNs are in use" } output "identity_terraform_profile_name" { - value = module.account_map.outputs.terraform_profiles[module.account_map.outputs.identity_account_account_name] + value = local.profiles_enabled ? module.account_map.outputs.terraform_profiles[module.account_map.outputs.identity_account_account_name] : null description = "The AWS config profile name for Terraform to use to provision resources in the \"identity\" role account, when profiles are in use" } output "identity_cicd_role_arn" { - value = module.account_map.outputs.cicd_roles[module.account_map.outputs.identity_account_account_name] + value = local.profiles_enabled ? null : module.account_map.outputs.cicd_roles[module.account_map.outputs.identity_account_account_name] description = "(Deprecated) The AWS Role ARN for CI/CD tools to assume to gain access to other accounts, when Role ARNs are in use" } output "identity_cicd_profile_name" { - value = module.account_map.outputs.cicd_profiles[module.account_map.outputs.identity_account_account_name] + value = local.profiles_enabled ? module.account_map.outputs.cicd_profiles[module.account_map.outputs.identity_account_account_name] : null description = "(Deprecated) The AWS config profile name for CI/CD tools to assume to gain access to other accounts, when profiles are in use" } output "profiles_enabled" { - value = module.account_map.outputs.profiles_enabled + value = local.profiles_enabled description = "When true, use AWS config profiles in Terraform AWS provider configurations. When false, use Role ARNs." } diff --git a/modules/account-map/modules/iam-roles/variables.tf b/modules/account-map/modules/iam-roles/variables.tf index 54967766a..554da715f 100644 --- a/modules/account-map/modules/iam-roles/variables.tf +++ b/modules/account-map/modules/iam-roles/variables.tf @@ -4,20 +4,24 @@ variable "privileged" { default = false } -variable "global_tenant_name" { +## The overridable_* variables in this file provide Cloud Posse defaults. +## Because this module is used in bootstrapping Terraform, we do not configure +## these inputs in the normal way. Instead, to change the values, you should +## add a `variables_override.tf` file and change the default to the value you want. +variable "overridable_global_tenant_name" { type = string description = "The tenant name used for organization-wide resources" default = "core" } -variable "global_environment_name" { +variable "overridable_global_environment_name" { type = string description = "Global environment name" default = "gbl" } -variable "global_stage_name" { +variable "overridable_global_stage_name" { type = string - description = "The stage name for the organization management account (where the `accout-map` state is stored)" + description = "The stage name for the organization management account (where the `account-map` state is stored)" default = "root" } diff --git a/modules/aws-waf-acl/README.md b/modules/aws-waf-acl/README.md index 32cf4f1da..9b78eea97 100644 --- a/modules/aws-waf-acl/README.md +++ b/modules/aws-waf-acl/README.md @@ -84,6 +84,7 @@ components: | [geo\_match\_statement\_rules](#input\_geo\_match\_statement\_rules) | A rule statement used to identify web requests based on country of origin.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [ip\_set\_reference\_statement\_rules](#input\_ip\_set\_reference\_statement\_rules) | A rule statement used to detect web requests coming from particular IP addresses or address ranges.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
arn:
The ARN of the IP Set that this statement references.
ip\_set\_forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
position:
The position in the header to search for the IP address.
Possible values include: `FIRST`, `LAST`, or `ANY`.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/aws-waf-acl/providers.tf b/modules/aws-waf-acl/providers.tf index c6e854450..08ee01b2a 100644 --- a/modules/aws-waf-acl/providers.tf +++ b/modules/aws-waf-acl/providers.tf @@ -1,6 +1,14 @@ provider "aws" { - region = var.region - profile = coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } } module "iam_roles" { @@ -13,3 +21,9 @@ variable "import_profile_name" { default = null description = "AWS Profile name to use when importing a resource" } + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/cognito/README.md b/modules/cognito/README.md index 1531e3d54..09920c701 100644 --- a/modules/cognito/README.md +++ b/modules/cognito/README.md @@ -119,7 +119,8 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [identity\_providers](#input\_identity\_providers) | Cognito Identity Providers configuration | `list(any)` | `[]` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile to use when importing a resource | `string` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/cognito/providers.tf b/modules/cognito/providers.tf index de2e8a327..08ee01b2a 100644 --- a/modules/cognito/providers.tf +++ b/modules/cognito/providers.tf @@ -1,7 +1,14 @@ provider "aws" { region = var.region - profile = coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } } module "iam_roles" { @@ -12,5 +19,11 @@ module "iam_roles" { variable "import_profile_name" { type = string default = null - description = "AWS Profile to use when importing a resource" + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" } diff --git a/modules/dns-delegated/providers.tf b/modules/dns-delegated/providers.tf index dd2b4a1fb..944bbdbf0 100644 --- a/modules/dns-delegated/providers.tf +++ b/modules/dns-delegated/providers.tf @@ -20,7 +20,7 @@ provider "aws" { profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null dynamic "assume_role" { - for_each = var.import_role_arn == null ? (module.iam_roles.terraform_role_arn != null ? [true] : []) : ["import"] + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) } diff --git a/modules/dns-primary/providers.tf b/modules/dns-primary/providers.tf index 447c00328..08ee01b2a 100644 --- a/modules/dns-primary/providers.tf +++ b/modules/dns-primary/providers.tf @@ -4,7 +4,7 @@ provider "aws" { profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null dynamic "assume_role" { - for_each = var.import_role_arn == null ? (module.iam_roles.terraform_role_arn != null ? [true] : []) : ["import"] + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) } diff --git a/modules/documentdb/providers.tf b/modules/documentdb/providers.tf index efa9ede5d..08ee01b2a 100644 --- a/modules/documentdb/providers.tf +++ b/modules/documentdb/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { diff --git a/modules/eks-iam/README.md b/modules/eks-iam/README.md index a3d00ade4..b2f46f7f3 100644 --- a/modules/eks-iam/README.md +++ b/modules/eks-iam/README.md @@ -79,6 +79,7 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters.
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_alias\_name](#input\_kms\_alias\_name) | AWS KMS alias used for encryption/decryption of SSM parameters default is alias used in SSM | `string` | `"alias/aws/ssm"` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/eks-iam/providers.tf b/modules/eks-iam/providers.tf old mode 100755 new mode 100644 index 506e16d2e..08ee01b2a --- a/modules/eks-iam/providers.tf +++ b/modules/eks-iam/providers.tf @@ -1,17 +1,25 @@ provider "aws" { region = var.region - assume_role { - # `terraform import` will not use data from a data source, - # so on import we have to explicitly specify the role - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } } } module "iam_roles" { - source = "../account-map/modules/iam-roles" - stage = var.stage - region = var.region + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" } variable "import_role_arn" { diff --git a/modules/elasticache-redis/providers.tf b/modules/elasticache-redis/providers.tf index efa9ede5d..08ee01b2a 100644 --- a/modules/elasticache-redis/providers.tf +++ b/modules/elasticache-redis/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { diff --git a/modules/mq-broker/README.md b/modules/mq-broker/README.md index 63c518902..7a67934bd 100644 --- a/modules/mq-broker/README.md +++ b/modules/mq-broker/README.md @@ -80,6 +80,7 @@ No resources. | [host\_instance\_type](#input\_host\_instance\_type) | The broker's instance type. e.g. mq.t2.micro or mq.m4.large | `string` | `"mq.t3.micro"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_mq\_key\_arn](#input\_kms\_mq\_key\_arn) | ARN of the AWS KMS key used for Amazon MQ encryption | `string` | `null` | no | | [kms\_ssm\_key\_arn](#input\_kms\_ssm\_key\_arn) | ARN of the AWS KMS key used for SSM encryption | `string` | `"alias/aws/ssm"` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/mq-broker/providers.tf b/modules/mq-broker/providers.tf old mode 100755 new mode 100644 index eb5dcb247..08ee01b2a --- a/modules/mq-broker/providers.tf +++ b/modules/mq-broker/providers.tf @@ -1,8 +1,14 @@ provider "aws" { region = var.region - # `terraform import` will not use data from a data source, so on import we have to explicitly specify the profile - profile = coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } } module "iam_roles" { @@ -15,3 +21,9 @@ variable "import_profile_name" { default = null description = "AWS Profile name to use when importing a resource" } + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/sqs-queue/providers.tf b/modules/sqs-queue/providers.tf index efa9ede5d..08ee01b2a 100644 --- a/modules/sqs-queue/providers.tf +++ b/modules/sqs-queue/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { diff --git a/modules/sso-saml-provider/providers.tf b/modules/sso-saml-provider/providers.tf index efa9ede5d..08ee01b2a 100644 --- a/modules/sso-saml-provider/providers.tf +++ b/modules/sso-saml-provider/providers.tf @@ -2,6 +2,7 @@ provider "aws" { region = var.region profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { for_each = module.iam_roles.profiles_enabled ? [] : ["role"] content { diff --git a/modules/vpc-flow-logs-bucket/README.md b/modules/vpc-flow-logs-bucket/README.md index 136801ef0..c9170ee2b 100644 --- a/modules/vpc-flow-logs-bucket/README.md +++ b/modules/vpc-flow-logs-bucket/README.md @@ -62,7 +62,8 @@ No resources. | [force\_destroy](#input\_force\_destroy) | A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable | `bool` | `false` | no | | [glacier\_transition\_days](#input\_glacier\_transition\_days) | Number of days after which to move the data to the glacier storage tier | `number` | `60` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile to use when importing a resource | `string` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/vpc-flow-logs-bucket/providers.tf b/modules/vpc-flow-logs-bucket/providers.tf index 1aa5d23ec..08ee01b2a 100644 --- a/modules/vpc-flow-logs-bucket/providers.tf +++ b/modules/vpc-flow-logs-bucket/providers.tf @@ -1,8 +1,14 @@ provider "aws" { region = var.region - # `terraform import` will not use data from a data source, so on import we have to explicitly specify the profile - profile = coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } } module "iam_roles" { @@ -13,6 +19,11 @@ module "iam_roles" { variable "import_profile_name" { type = string default = null - description = "AWS Profile to use when importing a resource" + description = "AWS Profile name to use when importing a resource" } +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index b117bda96..0adefc619 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -84,6 +84,7 @@ import: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_type](#input\_instance\_type) | The instance family to use for the ZScaler EC2 instances. | `string` | `"r5n.medium"` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/zscaler/providers.tf b/modules/zscaler/providers.tf index fd46aae55..08ee01b2a 100644 --- a/modules/zscaler/providers.tf +++ b/modules/zscaler/providers.tf @@ -1,10 +1,13 @@ provider "aws" { region = var.region - assume_role { - # `terraform import` will not use data from a data source, - # so on import we have to explicitly specify the role - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } } } @@ -13,6 +16,12 @@ module "iam_roles" { context = module.this.context } +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + variable "import_role_arn" { type = string default = null From e04477b4151ec72dcab2a36c12c9fa99f9fc3202 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 5 Jun 2023 16:02:57 -0400 Subject: [PATCH 145/501] Expand ECR GH OIDC Default Policy (#711) --- .../ecr/github-actions-iam-policy.tf | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/mixins/github-actions-iam-policy/ecr/github-actions-iam-policy.tf b/mixins/github-actions-iam-policy/ecr/github-actions-iam-policy.tf index ce8efa072..69941c2b0 100644 --- a/mixins/github-actions-iam-policy/ecr/github-actions-iam-policy.tf +++ b/mixins/github-actions-iam-policy/ecr/github-actions-iam-policy.tf @@ -1,37 +1,55 @@ locals { - github_actions_iam_policy = join("", data.aws_iam_policy_document.github_actions_iam_policy.*.json) + enabled = module.this.enabled + github_actions_iam_policy = data.aws_iam_policy_document.github_actions_iam_policy.json + ecr_resources_static = [for k, v in module.ecr.repository_arn_map : v] + ecr_resources_wildcard = [for k, v in module.ecr.repository_arn_map : "${v}/*"] + resources = concat(local.ecr_resources_static, local.ecr_resources_wildcard) } data "aws_iam_policy_document" "github_actions_iam_policy" { - count = var.github_actions_iam_role_enabled ? 1 : 0 - - # Permissions copied from https://docs.aws.amazon.com/AmazonECR/latest/userguide/security-iam-awsmanpol.html#security-iam-awsmanpol-AmazonEC2ContainerRegistryPowerUser - # This policy grants administrative permissions that allow IAM users to read and write to repositories, - # but doesn't allow them to delete repositories or change the policy documents that are applied to them. statement { - sid = "AmazonEC2ContainerRegistryPowerUser" + sid = "AllowECRPermissions" effect = "Allow" actions = [ - "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:GetRepositoryPolicy", - "ecr:DescribeRepositories", - "ecr:ListImages", - "ecr:DescribeImages", + "ecr:BatchDeleteImage", "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:DeleteLifecyclePolicy", + "ecr:DescribeImages", + "ecr:DescribeImageScanFindings", + "ecr:DescribeRepositories", + "ecr:GetAuthorizationToken", + "ecr:GetDownloadUrlForLayer", "ecr:GetLifecyclePolicy", "ecr:GetLifecyclePolicyPreview", - "ecr:ListTagsForResource", - "ecr:DescribeImageScanFindings", + "ecr:GetRepositoryPolicy", "ecr:InitiateLayerUpload", - "ecr:UploadLayerPart", - "ecr:CompleteLayerUpload", + "ecr:ListImages", "ecr:PutImage", + "ecr:PutImageScanningConfiguration", + "ecr:PutImageTagMutability", + "ecr:PutLifecyclePolicy", + "ecr:StartImageScan", + "ecr:StartLifecyclePolicyPreview", + "ecr:TagResource", + "ecr:UntagResource", + "ecr:UploadLayerPart", ] + resources = local.resources + } - #bridgecrew:skip=BC_AWS_IAM_57:OK to allow write access to all ECRs because ECRs have their own access policies - # and this policy prohibits the user from making changes to the access policy. + # required as minimum permissions for pushing and logging into a public ECR repository + # https://github.com/aws-actions/amazon-ecr-login#permissions + # https://docs.aws.amazon.com/AmazonECR/latest/public/docker-push-ecr-image.html + statement { + sid = "AllowEcrGetAuthorizationToken" + effect = "Allow" + actions = [ + "ecr:GetAuthorizationToken", + "sts:GetServiceBearerToken" + ] resources = ["*"] } } + From c869cb26e7c7bee9aab7ca891b7cecae52c43a3b Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 5 Jun 2023 15:30:47 -0700 Subject: [PATCH 146/501] Disable helm experiments by default, block Kubernetes provider 2.21.0 (#712) --- mixins/provider-helm.tf | 2 +- modules/datadog-synthetics-private-location/README.md | 4 ++-- .../datadog-synthetics-private-location/provider-helm.tf | 2 +- modules/datadog-synthetics-private-location/versions.tf | 2 +- modules/eks/actions-runner-controller/README.md | 4 ++-- modules/eks/actions-runner-controller/provider-helm.tf | 2 +- modules/eks/actions-runner-controller/versions.tf | 2 +- modules/eks/alb-controller-ingress-class/README.md | 6 +++--- modules/eks/alb-controller-ingress-class/provider-helm.tf | 2 +- modules/eks/alb-controller-ingress-class/versions.tf | 2 +- modules/eks/alb-controller-ingress-group/README.md | 5 ++--- .../eks/alb-controller-ingress-group/provider-kubernetes.tf | 6 ------ modules/eks/alb-controller-ingress-group/versions.tf | 2 +- modules/eks/alb-controller/README.md | 4 ++-- modules/eks/alb-controller/provider-helm.tf | 2 +- modules/eks/alb-controller/versions.tf | 2 +- modules/eks/argocd/README.md | 6 +++--- modules/eks/argocd/provider-helm.tf | 2 +- modules/eks/argocd/versions.tf | 2 +- modules/eks/aws-node-termination-handler/README.md | 6 +++--- modules/eks/aws-node-termination-handler/provider-helm.tf | 2 +- modules/eks/aws-node-termination-handler/versions.tf | 2 +- modules/eks/cert-manager/README.md | 4 ++-- modules/eks/cert-manager/provider-helm.tf | 2 +- modules/eks/cert-manager/versions.tf | 2 +- modules/eks/datadog-agent/README.md | 6 +++--- modules/eks/datadog-agent/provider-helm.tf | 2 +- modules/eks/datadog-agent/versions.tf | 2 +- modules/eks/ebs-controller/README.md | 6 +++--- modules/eks/ebs-controller/provider-helm.tf | 2 +- modules/eks/ebs-controller/versions.tf | 2 +- modules/eks/echo-server/README.md | 4 ++-- modules/eks/echo-server/provider-helm.tf | 2 +- modules/eks/echo-server/versions.tf | 2 +- modules/eks/efs-controller/README.md | 6 +++--- modules/eks/efs-controller/provider-helm.tf | 2 +- modules/eks/efs-controller/versions.tf | 2 +- modules/eks/external-dns/README.md | 4 ++-- modules/eks/external-dns/provider-helm.tf | 2 +- modules/eks/external-dns/versions.tf | 2 +- modules/eks/external-secrets-operator/README.md | 4 ++-- modules/eks/external-secrets-operator/versions.tf | 2 +- modules/eks/idp-roles/README.md | 4 ++-- modules/eks/idp-roles/provider-helm.tf | 2 +- modules/eks/idp-roles/versions.tf | 2 +- modules/eks/karpenter-provisioner/README.md | 6 +++--- modules/eks/karpenter-provisioner/provider-helm.tf | 2 +- modules/eks/karpenter-provisioner/versions.tf | 2 +- modules/eks/karpenter/README.md | 4 ++-- modules/eks/karpenter/provider-helm.tf | 2 +- modules/eks/karpenter/versions.tf | 2 +- modules/eks/metrics-server/README.md | 6 +++--- modules/eks/metrics-server/provider-helm.tf | 2 +- modules/eks/metrics-server/versions.tf | 2 +- modules/eks/redis-operator/README.md | 6 +++--- modules/eks/redis-operator/provider-helm.tf | 2 +- modules/eks/redis-operator/versions.tf | 2 +- modules/eks/redis/README.md | 6 +++--- modules/eks/redis/provider-helm.tf | 2 +- modules/eks/redis/versions.tf | 2 +- modules/eks/reloader/README.md | 6 +++--- modules/eks/reloader/provider-helm.tf | 2 +- modules/eks/reloader/versions.tf | 2 +- 63 files changed, 94 insertions(+), 101 deletions(-) diff --git a/mixins/provider-helm.tf b/mixins/provider-helm.tf index 9bb5edb6f..abe6f9c56 100644 --- a/mixins/provider-helm.tf +++ b/mixins/provider-helm.tf @@ -85,7 +85,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index 800a89558..b642777f0 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -129,7 +129,7 @@ Environment variables: | [aws](#requirement\_aws) | >= 4.0 | | [datadog](#requirement\_datadog) | >= 3.3.0 | | [helm](#requirement\_helm) | >= 2.3.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | | [local](#requirement\_local) | >= 1.3 | | [template](#requirement\_template) | >= 2.0 | @@ -175,7 +175,7 @@ Environment variables: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/datadog-synthetics-private-location/provider-helm.tf b/modules/datadog-synthetics-private-location/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/datadog-synthetics-private-location/provider-helm.tf +++ b/modules/datadog-synthetics-private-location/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/datadog-synthetics-private-location/versions.tf b/modules/datadog-synthetics-private-location/versions.tf index 7ded97311..fe99a3345 100755 --- a/modules/datadog-synthetics-private-location/versions.tf +++ b/modules/datadog-synthetics-private-location/versions.tf @@ -24,7 +24,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.14.0" + version = ">= 2.14.0, != 2.21.0" } } } diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index fc2530efe..4257a005f 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -387,7 +387,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0, != 2.21.0 | ## Providers @@ -436,7 +436,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [existing\_kubernetes\_secret\_name](#input\_existing\_kubernetes\_secret\_name) | If you are going to create the Kubernetes Secret the runner-controller will use
by some means (such as SOPS) outside of this component, set the name of the secret
here and it will be used. In this case, this component will not create a secret
and you can leave the secret-related inputs with their default (empty) values.
The same secret will be used by both the runner-controller and the webhook-server. | `string` | `""` | no | | [github\_app\_id](#input\_github\_app\_id) | The ID of the GitHub App to use for the runner controller. | `string` | `""` | no | | [github\_app\_installation\_id](#input\_github\_app\_installation\_id) | The "Installation ID" of the GitHub App to use for the runner controller. | `string` | `""` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/actions-runner-controller/provider-helm.tf b/modules/eks/actions-runner-controller/provider-helm.tf index 9bb5edb6f..abe6f9c56 100644 --- a/modules/eks/actions-runner-controller/provider-helm.tf +++ b/modules/eks/actions-runner-controller/provider-helm.tf @@ -85,7 +85,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/actions-runner-controller/versions.tf b/modules/eks/actions-runner-controller/versions.tf index 482ed7ae8..f4e52c7b2 100644 --- a/modules/eks/actions-runner-controller/versions.tf +++ b/modules/eks/actions-runner-controller/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.0" + version = ">= 2.0, != 2.21.0" } } } diff --git a/modules/eks/alb-controller-ingress-class/README.md b/modules/eks/alb-controller-ingress-class/README.md index 6d6e07868..1d9a5fc0a 100644 --- a/modules/eks/alb-controller-ingress-class/README.md +++ b/modules/eks/alb-controller-ingress-class/README.md @@ -29,14 +29,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.14.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Modules @@ -69,7 +69,7 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [group](#input\_group) | Group name for default ingress | `string` | `"common"` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/alb-controller-ingress-class/provider-helm.tf b/modules/eks/alb-controller-ingress-class/provider-helm.tf index 9bb5edb6f..abe6f9c56 100644 --- a/modules/eks/alb-controller-ingress-class/provider-helm.tf +++ b/modules/eks/alb-controller-ingress-class/provider-helm.tf @@ -85,7 +85,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/alb-controller-ingress-class/versions.tf b/modules/eks/alb-controller-ingress-class/versions.tf index 45b29866a..48fd8c954 100644 --- a/modules/eks/alb-controller-ingress-class/versions.tf +++ b/modules/eks/alb-controller-ingress-class/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.14.0" + version = ">= 2.14.0, != 2.21.0" } } } diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index d5b4f004c..e0460a54d 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -37,14 +37,14 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Modules @@ -93,7 +93,6 @@ components: | [fixed\_response\_template](#input\_fixed\_response\_template) | Fixed response template to service as a default backend | `string` | `"resources/default-backend.html.tpl"` | no | | [fixed\_response\_vars](#input\_fixed\_response\_vars) | The templatefile vars to use for the fixed response template | `map(any)` |
{
"email": "hello@cloudposse.com"
}
| no | | [global\_accelerator\_enabled](#input\_global\_accelerator\_enabled) | Whether or not Global Accelerator Endpoint Group should be provisioned for the load balancer | `bool` | `false` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/alb-controller-ingress-group/provider-kubernetes.tf b/modules/eks/alb-controller-ingress-group/provider-kubernetes.tf index 00cfd1542..3bc3e6c4c 100644 --- a/modules/eks/alb-controller-ingress-group/provider-kubernetes.tf +++ b/modules/eks/alb-controller-ingress-group/provider-kubernetes.tf @@ -77,12 +77,6 @@ variable "kubeconfig_exec_auth_api_version" { description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" } -variable "helm_manifest_experiment_enabled" { - type = bool - default = true - description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" -} - locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled diff --git a/modules/eks/alb-controller-ingress-group/versions.tf b/modules/eks/alb-controller-ingress-group/versions.tf index 8b70f9f52..5e4bbc1d4 100644 --- a/modules/eks/alb-controller-ingress-group/versions.tf +++ b/modules/eks/alb-controller-ingress-group/versions.tf @@ -8,7 +8,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.7.1" + version = ">= 2.7.1, != 2.21.0" } } } diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 33aa003d7..d5a81d6f3 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -55,7 +55,7 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Providers @@ -105,7 +105,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/alb-controller/provider-helm.tf b/modules/eks/alb-controller/provider-helm.tf index 9bb5edb6f..abe6f9c56 100644 --- a/modules/eks/alb-controller/provider-helm.tf +++ b/modules/eks/alb-controller/provider-helm.tf @@ -85,7 +85,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/alb-controller/versions.tf b/modules/eks/alb-controller/versions.tf index 45b29866a..48fd8c954 100644 --- a/modules/eks/alb-controller/versions.tf +++ b/modules/eks/alb-controller/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.14.0" + version = ">= 2.14.0, != 2.21.0" } } } diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index ecb7ee961..4f04acd98 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -48,7 +48,7 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.6.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.9.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.9.0, != 2.21.0 | ## Providers @@ -56,7 +56,7 @@ components: |------|---------| | [aws](#provider\_aws) | >= 4.0 | | [aws.config\_secrets](#provider\_aws.config\_secrets) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.9.0, != 2.21.0 | ## Modules @@ -127,7 +127,7 @@ components: | [forecastle\_enabled](#input\_forecastle\_enabled) | Toggles Forecastle integration in the deployed chart | `bool` | `false` | no | | [github\_notifications\_enabled](#input\_github\_notifications\_enabled) | Whether or not to enable GitHub deployment and commit status notifications. | `bool` | `false` | no | | [github\_organization](#input\_github\_organization) | GitHub Organization | `string` | n/a | yes | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [host](#input\_host) | Host name to use for ingress and ALB | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/argocd/provider-helm.tf b/modules/eks/argocd/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/argocd/provider-helm.tf +++ b/modules/eks/argocd/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/argocd/versions.tf b/modules/eks/argocd/versions.tf index 23781035b..3e6c990e3 100644 --- a/modules/eks/argocd/versions.tf +++ b/modules/eks/argocd/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.9.0" + version = ">= 2.9.0, != 2.21.0" } } } diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 3d04c1adf..2d142d902 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -46,14 +46,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0, != 2.21.0 | ## Modules @@ -91,7 +91,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/aws-node-termination-handler/provider-helm.tf b/modules/eks/aws-node-termination-handler/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/aws-node-termination-handler/provider-helm.tf +++ b/modules/eks/aws-node-termination-handler/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/aws-node-termination-handler/versions.tf b/modules/eks/aws-node-termination-handler/versions.tf index 71f1c0e3e..14c085342 100644 --- a/modules/eks/aws-node-termination-handler/versions.tf +++ b/modules/eks/aws-node-termination-handler/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.0" + version = ">= 2.0, != 2.21.0" } } } diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index a33f333d7..7baf95adb 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -57,7 +57,7 @@ The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Providers @@ -113,7 +113,7 @@ The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/cert-manager/provider-helm.tf b/modules/eks/cert-manager/provider-helm.tf index 9bb5edb6f..abe6f9c56 100644 --- a/modules/eks/cert-manager/provider-helm.tf +++ b/modules/eks/cert-manager/provider-helm.tf @@ -85,7 +85,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/cert-manager/versions.tf b/modules/eks/cert-manager/versions.tf index 45b29866a..48fd8c954 100644 --- a/modules/eks/cert-manager/versions.tf +++ b/modules/eks/cert-manager/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.14.0" + version = ">= 2.14.0, != 2.21.0" } } } diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index ab6cac485..690db3d29 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -169,7 +169,7 @@ https-checks: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.7 | -| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | | [utils](#requirement\_utils) | >= 0.3.0 | ## Providers @@ -177,7 +177,7 @@ https-checks: | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.14.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Modules @@ -223,7 +223,7 @@ https-checks: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the EKS component. Used to get the remote state | `string` | `"eks/eks"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [iam\_policy\_statements](#input\_iam\_policy\_statements) | IAM policy for the service account. Required if `var.iam_role_enabled` is `true`. This will not do variable replacements. Please see `var.iam_policy_statements_template_path`. | `any` | `{}` | no | | [iam\_role\_enabled](#input\_iam\_role\_enabled) | Whether to create an IAM role. Setting this to `true` will also replace any occurrences of `{service_account_role_arn}` in `var.values_template_path` with the ARN of the IAM role created by this module. | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | diff --git a/modules/eks/datadog-agent/provider-helm.tf b/modules/eks/datadog-agent/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/datadog-agent/provider-helm.tf +++ b/modules/eks/datadog-agent/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/datadog-agent/versions.tf b/modules/eks/datadog-agent/versions.tf index 3990fdb16..e656d97c0 100644 --- a/modules/eks/datadog-agent/versions.tf +++ b/modules/eks/datadog-agent/versions.tf @@ -16,7 +16,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.14.0" + version = ">= 2.14.0, != 2.21.0" } } } diff --git a/modules/eks/ebs-controller/README.md b/modules/eks/ebs-controller/README.md index 812b5ddac..807cb5f75 100644 --- a/modules/eks/ebs-controller/README.md +++ b/modules/eks/ebs-controller/README.md @@ -40,14 +40,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Modules @@ -82,7 +82,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/ebs-controller/provider-helm.tf b/modules/eks/ebs-controller/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/ebs-controller/provider-helm.tf +++ b/modules/eks/ebs-controller/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/ebs-controller/versions.tf b/modules/eks/ebs-controller/versions.tf index b7a1a1986..fb8857fab 100644 --- a/modules/eks/ebs-controller/versions.tf +++ b/modules/eks/ebs-controller/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.7.1" + version = ">= 2.7.1, != 2.21.0" } } } diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index b10dbd9fb..ee13f6343 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -73,7 +73,7 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Providers @@ -118,7 +118,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [hostname\_template](#input\_hostname\_template) | The `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`"
Typically something like `"echo.%[3]v.%[2]v.example.com"`. | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/echo-server/provider-helm.tf b/modules/eks/echo-server/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/echo-server/provider-helm.tf +++ b/modules/eks/echo-server/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/echo-server/versions.tf b/modules/eks/echo-server/versions.tf index b7a1a1986..fb8857fab 100644 --- a/modules/eks/echo-server/versions.tf +++ b/modules/eks/echo-server/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.7.1" + version = ">= 2.7.1, != 2.21.0" } } } diff --git a/modules/eks/efs-controller/README.md b/modules/eks/efs-controller/README.md index d8604dc72..1968a429a 100644 --- a/modules/eks/efs-controller/README.md +++ b/modules/eks/efs-controller/README.md @@ -46,14 +46,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0, != 2.21.0 | ## Modules @@ -93,7 +93,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/efs-controller/provider-helm.tf b/modules/eks/efs-controller/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/efs-controller/provider-helm.tf +++ b/modules/eks/efs-controller/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/efs-controller/versions.tf b/modules/eks/efs-controller/versions.tf index 71f1c0e3e..14c085342 100644 --- a/modules/eks/efs-controller/versions.tf +++ b/modules/eks/efs-controller/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.0" + version = ">= 2.0, != 2.21.0" } } } diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index d950a7bf4..484ced012 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -53,7 +53,7 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Providers @@ -102,7 +102,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/external-dns/provider-helm.tf b/modules/eks/external-dns/provider-helm.tf index 9bb5edb6f..abe6f9c56 100644 --- a/modules/eks/external-dns/provider-helm.tf +++ b/modules/eks/external-dns/provider-helm.tf @@ -85,7 +85,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/external-dns/versions.tf b/modules/eks/external-dns/versions.tf index c8087b1b8..61ea676a2 100644 --- a/modules/eks/external-dns/versions.tf +++ b/modules/eks/external-dns/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.7.1" + version = ">= 2.7.1, != 2.21.0" } } } diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 67cd809b9..0304362c7 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -92,14 +92,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0, != 2.21.0 | ## Modules diff --git a/modules/eks/external-secrets-operator/versions.tf b/modules/eks/external-secrets-operator/versions.tf index fb8857fab..46584b569 100644 --- a/modules/eks/external-secrets-operator/versions.tf +++ b/modules/eks/external-secrets-operator/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.7.1, != 2.21.0" + version = ">= 2.7.1, != 2.21.0, != 2.21.0" } } } diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index 88989b7d6..4f8ed9ea9 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -29,7 +29,7 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Providers @@ -73,7 +73,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/idp-roles/provider-helm.tf b/modules/eks/idp-roles/provider-helm.tf index d04bccf3d..325e24d83 100644 --- a/modules/eks/idp-roles/provider-helm.tf +++ b/modules/eks/idp-roles/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/idp-roles/versions.tf b/modules/eks/idp-roles/versions.tf index 5a442caea..ec64f8a4f 100644 --- a/modules/eks/idp-roles/versions.tf +++ b/modules/eks/idp-roles/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.14.0" + version = ">= 2.14.0, != 2.21.0" } } } diff --git a/modules/eks/karpenter-provisioner/README.md b/modules/eks/karpenter-provisioner/README.md index dbaf1d86e..51ea41783 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/modules/eks/karpenter-provisioner/README.md @@ -101,14 +101,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.14.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Modules @@ -139,7 +139,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/karpenter-provisioner/provider-helm.tf b/modules/eks/karpenter-provisioner/provider-helm.tf index 9bb5edb6f..abe6f9c56 100644 --- a/modules/eks/karpenter-provisioner/provider-helm.tf +++ b/modules/eks/karpenter-provisioner/provider-helm.tf @@ -85,7 +85,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/karpenter-provisioner/versions.tf b/modules/eks/karpenter-provisioner/versions.tf index 57cc9f927..9f0f54df7 100644 --- a/modules/eks/karpenter-provisioner/versions.tf +++ b/modules/eks/karpenter-provisioner/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.14.0" + version = ">= 2.14.0, != 2.21.0" } } } diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index db88f6df2..16e55428d 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -294,7 +294,7 @@ For more details, refer to: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Providers @@ -338,7 +338,7 @@ For more details, refer to: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/karpenter/provider-helm.tf b/modules/eks/karpenter/provider-helm.tf index 9bb5edb6f..abe6f9c56 100644 --- a/modules/eks/karpenter/provider-helm.tf +++ b/modules/eks/karpenter/provider-helm.tf @@ -85,7 +85,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/karpenter/versions.tf b/modules/eks/karpenter/versions.tf index c8087b1b8..61ea676a2 100644 --- a/modules/eks/karpenter/versions.tf +++ b/modules/eks/karpenter/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.7.1" + version = ">= 2.7.1, != 2.21.0" } } } diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index f0d96ef7d..fc7d1a140 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -45,14 +45,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.14.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.14.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Modules @@ -90,7 +90,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/metrics-server/provider-helm.tf b/modules/eks/metrics-server/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/metrics-server/provider-helm.tf +++ b/modules/eks/metrics-server/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/metrics-server/versions.tf b/modules/eks/metrics-server/versions.tf index 57cc9f927..9f0f54df7 100644 --- a/modules/eks/metrics-server/versions.tf +++ b/modules/eks/metrics-server/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.14.0" + version = ">= 2.14.0, != 2.21.0" } } } diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 87fc05ae9..911721247 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -73,14 +73,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0, != 2.21.0 | ## Modules @@ -118,7 +118,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/redis-operator/provider-helm.tf b/modules/eks/redis-operator/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/redis-operator/provider-helm.tf +++ b/modules/eks/redis-operator/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/redis-operator/versions.tf b/modules/eks/redis-operator/versions.tf index 71f1c0e3e..14c085342 100644 --- a/modules/eks/redis-operator/versions.tf +++ b/modules/eks/redis-operator/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.0" + version = ">= 2.0, != 2.21.0" } } } diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index 062927b6f..d92b20bfb 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -79,14 +79,14 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0, != 2.21.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0, != 2.21.0 | ## Modules @@ -124,7 +124,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/redis/provider-helm.tf b/modules/eks/redis/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/redis/provider-helm.tf +++ b/modules/eks/redis/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/redis/versions.tf b/modules/eks/redis/versions.tf index 71f1c0e3e..14c085342 100644 --- a/modules/eks/redis/versions.tf +++ b/modules/eks/redis/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.0" + version = ">= 2.0, != 2.21.0" } } } diff --git a/modules/eks/reloader/README.md b/modules/eks/reloader/README.md index 37c1aeb4e..249881ff5 100644 --- a/modules/eks/reloader/README.md +++ b/modules/eks/reloader/README.md @@ -37,7 +37,7 @@ components: | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | -| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Providers @@ -45,7 +45,7 @@ components: |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | | [helm](#provider\_helm) | >= 2.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Modules @@ -81,7 +81,7 @@ components: | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | diff --git a/modules/eks/reloader/provider-helm.tf b/modules/eks/reloader/provider-helm.tf index 20e4d3837..21cecf145 100644 --- a/modules/eks/reloader/provider-helm.tf +++ b/modules/eks/reloader/provider-helm.tf @@ -79,7 +79,7 @@ variable "kubeconfig_exec_auth_api_version" { variable "helm_manifest_experiment_enabled" { type = bool - default = true + default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" } diff --git a/modules/eks/reloader/versions.tf b/modules/eks/reloader/versions.tf index c8087b1b8..61ea676a2 100644 --- a/modules/eks/reloader/versions.tf +++ b/modules/eks/reloader/versions.tf @@ -12,7 +12,7 @@ terraform { } kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.7.1" + version = ">= 2.7.1, != 2.21.0" } } } From 83f0021cd63c9148b76135fc24ce769d852b39ec Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 7 Jun 2023 13:40:15 -0400 Subject: [PATCH 147/501] feat: New Component `aws-ssosync` (#625) Co-authored-by: cloudpossebot Co-authored-by: Benjamin Smith Co-authored-by: Dan Miller --- modules/aws-ssosync/README.md | 258 ++++++++++++++++++++++++++ modules/aws-ssosync/context.tf | 279 +++++++++++++++++++++++++++++ modules/aws-ssosync/dist/README.md | 211 ++++++++++++++++++++++ modules/aws-ssosync/iam.tf | 44 +++++ modules/aws-ssosync/main.tf | 128 +++++++++++++ modules/aws-ssosync/outputs.tf | 14 ++ modules/aws-ssosync/providers.tf | 27 +++ modules/aws-ssosync/variables.tf | 102 +++++++++++ modules/aws-ssosync/versions.tf | 18 ++ 9 files changed, 1081 insertions(+) create mode 100644 modules/aws-ssosync/README.md create mode 100644 modules/aws-ssosync/context.tf create mode 100644 modules/aws-ssosync/dist/README.md create mode 100644 modules/aws-ssosync/iam.tf create mode 100644 modules/aws-ssosync/main.tf create mode 100644 modules/aws-ssosync/outputs.tf create mode 100644 modules/aws-ssosync/providers.tf create mode 100644 modules/aws-ssosync/variables.tf create mode 100644 modules/aws-ssosync/versions.tf diff --git a/modules/aws-ssosync/README.md b/modules/aws-ssosync/README.md new file mode 100644 index 000000000..e6d085ca6 --- /dev/null +++ b/modules/aws-ssosync/README.md @@ -0,0 +1,258 @@ +# Component: `aws-ssosync` + +Deploys [AWS ssosync](https://github.com/awslabs/ssosync) to sync Google Groups with AWS SSO. + +AWS `ssosync` is a Lambda application that regularly manages Identity Store users. + +This component requires SuperAdmin because it deploys a role in the identity account. + +You need to have setup AWS SSO in root account and delegated identity as your delegated adminstrator. + +## Usage +You should be able to deploy the `aws-ssosync` component to the `[core-]gbl-identity` stack +with `atmos terraform deploy aws-ssosync -s gbl-identity`. + +**Stack Level**: Global +**Deployment**: Must be deployed by super-admin using `atmos` CLI + +The following is an example snippet for how to use this component: + +(`stacks/catalog/aws-ssosync.yaml`) +```yaml +components: + terraform: + aws-ssosync: + backend: + s3: + role_arn: null + settings: + spacelift: + workspace_enabled: false + vars: + enabled: true + name: aws-ssosync + google_admin_email: an-actual-admin@acme.com + ssosync_url_prefix: "https://github.com/Benbentwo/ssosync/releases/download" + ssosync_version: "2.0.2" + google_credentials_ssm_path: "/ssosync" + log_format: text + log_level: debug + schedule_expression: "rate(15 minutes)" +``` + +We recommend following a similar process to what the [AWS ssosync](https://github.com/awslabs/ssosync) +documentation recommends. + +### Clickops + +Overview of steps: +1. Deploy the `aws-sso` component +1. Configure GSuite +1. Deploy the `aws-ssosync` component to the `gbl-identity` stack + +#### Deploy the `aws-sso` component + +Follow the [aws-sso](../aws-sso/) component documentation to deploy the `aws-sso` component. +Once this is done, you'll want to grab a few pieces of information. + +Go to the AWS Single Sign-On console in the region you have set up AWS SSO and +select `Settings`. Click `Enable automatic provisioning`. + +A pop up will appear with URL and the Access Token. The Access Token will only +appear at this stage. You want to copy both of these as a parameter to the ssosync command. + +To pass parameters to the `ssosync` command, you'll need to decide on a path +in SSM Parameter Store, `google_credentials_ssm_path`. + +In SSM Parameter Store on your `identity` account, create a parameter with the +name `/scim_endpoint_url` and the value of the +URL from the previous step. Also create a parameter with the name +`/scim_endpoint_access_token` and the value of the +Access Token from the previous step. + +One more parameter you'll need is your Identity Store ID. +To obtain your Identity store ID, go to the AWS Identity Center console and +select `Settings`. Under the `Identity Source` section, copy the Identity Store ID. +Back in the `identity` account, create a parameter with the name +`/identity_store_id`. + +Lastly, go ahead and [delegate administration](https://docs.aws.amazon.com/singlesignon/latest/userguide/delegated-admin.html) +from the `root` account to the `identity` account + +#### Configure GSuite + +_steps taken directly from [ssosync README.md](https://github.com/awslabs/ssosync/blob/master/README.md#google)_ + +First, you have to setup your API. In the project you want to use go to the +[Console](https://console.developers.google.com/apis) and select *API & Service * > +*Enable APIs and Services*. Search for *Admin SDK* and *Enable* the API. + +You have to perform this +[tutorial](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) +to create a service account that you use to sync your users. Save +the `JSON file` you create during the process and rename it to `google_credentials.json`. + +Head back in to your `identity` account in AWS and create a parameter in SSM +Parameter Store with the name `/google_credentials` and +give it the contents of the `google_credentials.json` file. + +In the domain-wide delegation for the Admin API, you have to specify the +following scopes for the user. + +* https://www.googleapis.com/auth/admin.directory.group.readonly +* https://www.googleapis.com/auth/admin.directory.group.member.readonly +* https://www.googleapis.com/auth/admin.directory.user.readonly + +Back in the Console go to the Dashboard for the API & Services and select +`Enable API and Services`. +In the Search box type `Admin` and select the `Admin SDK` option. Click the +`Enable` button. + +#### Deploy the `aws-ssosync` component + +Make sure that all four of the following SSM parameters exist in the `identity` account: +* `/scim_endpoint_url` +* `/scim_endpoint_access_token` +* `/identity_store_id` +* `/google_credentials` + + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [archive](#requirement\_archive) | >= 2.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [null](#requirement\_null) | >= 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [archive](#provider\_archive) | >= 2.3.0 | +| [aws](#provider\_aws) | >= 4.0 | +| [null](#provider\_null) | >= 3.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [ssosync\_artifact](#module\_ssosync\_artifact) | cloudposse/module-artifact/external | 0.8.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_event_rule.ssosync](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.ssosync](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_lambda_function.ssosync](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.allow_cloudwatch_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [null_resource.extract_my_tgz](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [archive_file.lambda](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | +| [aws_iam_policy_document.ssosync_lambda_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ssosync_lambda_identity_center](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_ssm_parameter.google_credentials](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.identity_store_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.scim_endpoint_access_token](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.scim_endpoint_url](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [architecture](#input\_architecture) | Architecture of the Lambda function | `string` | `"x86_64"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [google\_admin\_email](#input\_google\_admin\_email) | Google Admin email | `string` | n/a | yes | +| [google\_credentials\_ssm\_path](#input\_google\_credentials\_ssm\_path) | SSM Path for `ssosync` secrets | `string` | `"/ssosync"` | no | +| [google\_group\_match](#input\_google\_group\_match) | Google Workspace group filter query parameter, example: 'name:Admin* email:aws-*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups | `string` | `""` | no | +| [google\_user\_match](#input\_google\_user\_match) | Google Workspace user filter query parameter, example: 'name:John* email:admin*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users | `string` | `""` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [ignore\_groups](#input\_ignore\_groups) | Ignore these Google Workspace groups | `string` | `""` | no | +| [ignore\_users](#input\_ignore\_users) | Ignore these Google Workspace users | `string` | `""` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [include\_groups](#input\_include\_groups) | Include only these Google Workspace groups. (Only applicable for sync\_method user\_groups) | `string` | `""` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [log\_format](#input\_log\_format) | Log format for Lambda function logging | `string` | `"json"` | no | +| [log\_level](#input\_log\_level) | Log level for Lambda function logging | `string` | `"warn"` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region where AWS SSO is enabled | `string` | n/a | yes | +| [schedule\_expression](#input\_schedule\_expression) | Schedule for trigger the execution of ssosync (see CloudWatch schedule expressions) | `string` | `"rate(15 minutes)"` | no | +| [ssosync\_url\_prefix](#input\_ssosync\_url\_prefix) | URL prefix for ssosync binary | `string` | `"https://github.com/Benbentwo/ssosync/releases/download"` | no | +| [ssosync\_version](#input\_ssosync\_version) | Version of ssosync to use | `string` | `"v2.0.2"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [sync\_method](#input\_sync\_method) | Sync method to use | `string` | `"groups"` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | ARN of the lambda function | +| [invoke\_arn](#output\_invoke\_arn) | Invoke ARN of the lambda function | +| [qualified\_arn](#output\_qualified\_arn) | ARN identifying your Lambda Function Version (if versioning is enabled via publish = true) | + + +## References + +- [cloudposse/terraform-aws-sso][39] + +[][40] + +[1]: https://docs.aws.amazon.com/singlesignon/latest/userguide/permissionsetsconcept.html +[2]: #requirement%5C_terraform +[3]: #requirement%5C_aws +[4]: #requirement%5C_external +[5]: #requirement%5C_local +[6]: #requirement%5C_template +[7]: #requirement%5C_utils +[8]: #provider%5C_aws +[9]: #module%5C_account%5C_map +[10]: #module%5C_permission%5C_sets +[11]: #module%5C_role%5C_prefix +[12]: #module%5C_sso%5C_account%5C_assignments +[13]: #module%5C_this +[14]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document +[15]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document +[16]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document +[17]: #input%5C_account%5C_assignments +[18]: #input%5C_additional%5C_tag%5C_map +[19]: #input%5C_attributes +[20]: #input%5C_context +[21]: #input%5C_delimiter +[22]: #input%5C_enabled +[23]: #input%5C_environment +[24]: #input%5C_global%5C_environment%5C_name +[25]: #input%5C_iam%5C_primary%5C_roles%5C_stage%5C_name +[26]: #input%5C_id%5C_length%5C_limit +[27]: #input%5C_identity%5C_roles%5C_accessible +[28]: #input%5C_label%5C_key%5C_case +[29]: #input%5C_label%5C_order +[30]: #input%5C_label%5C_value%5C_case +[31]: #input%5C_name +[32]: #input%5C_namespace +[33]: #input%5C_privileged +[34]: #input%5C_regex%5C_replace%5C_chars +[35]: #input%5C_region +[36]: #input%5C_root%5C_account%5C_stage%5C_name +[37]: #input%5C_stage +[38]: #input%5C_tags +[39]: https://github.com/cloudposse/terraform-aws-sso +[40]: https://cpco.io/component diff --git a/modules/aws-ssosync/context.tf b/modules/aws-ssosync/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/aws-ssosync/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/aws-ssosync/dist/README.md b/modules/aws-ssosync/dist/README.md new file mode 100644 index 000000000..82c426e7c --- /dev/null +++ b/modules/aws-ssosync/dist/README.md @@ -0,0 +1,211 @@ +# Fork of AWS SSO Sync +Removes need for ASM Secrets + + +# SSO Sync + +![Github Action](https://github.com/awslabs/ssosync/workflows/main/badge.svg) +![gopherbadger-tag-do-not-edit](https://img.shields.io/badge/Go%20Coverage-42%25-brightgreen.svg?longCache=true&style=flat) +[![Go Report Card](https://goreportcard.com/badge/github.com/awslabs/ssosync)](https://goreportcard.com/report/github.com/awslabs/ssosync) +[![License Apache 2](https://img.shields.io/badge/License-Apache2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) +[![Taylor Swift](https://img.shields.io/badge/secured%20by-taylor%20swift-brightgreen.svg)](https://twitter.com/SwiftOnSecurity) + +> Helping you populate AWS SSO directly with your Google Apps users + +SSO Sync will run on any platform that Go can build for. It is available in the [AWS Serverless Application Repository](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync) + +> :warning: there are breaking changes for versions `>= 0.02` + +> :warning: `>= 1.0.0-rc.5` groups to do not get deleted in AWS SSO when deleted in the Google Directory, and groups are synced by their email address + +> :warning: `>= 2.0.0` this makes use of the **Identity Store API** which means: +* if deploying the lambda from the [AWS Serverless Application Repository](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync) then it needs to be deployed into the [IAM Identity Center delegated administration](https://docs.aws.amazon.com/singlesignon/latest/userguide/delegated-admin.html) account. Technically you could deploy in the management account but we would recommend against this. +* if you are running the project as a cli tool, then the environment will need to be using credentials of a user in the [IAM Identity Center delegated administration](https://docs.aws.amazon.com/singlesignon/latest/userguide/delegated-admin.html) account, with appropriate permissions. + +## Why? + +As per the [AWS SSO](https://aws.amazon.com/single-sign-on/) Homepage: + +> AWS Single Sign-On (SSO) makes it easy to centrally manage access +> to multiple AWS accounts and business applications and provide users +> with single sign-on access to all their assigned accounts and applications +> from one place. + +Key part further down: + +> With AWS SSO, you can create and manage user identities in AWS SSO’s +>identity store, or easily connect to your existing identity source including +> Microsoft Active Directory and **Azure Active Directory (Azure AD)**. + +AWS SSO can use other Identity Providers as well... such as Google Apps for Domains. Although AWS SSO +supports a subset of the SCIM protocol for populating users, it currently only has support for Azure AD. + +This project provides a CLI tool to pull users and groups from Google and push them into AWS SSO. +`ssosync` deals with removing users as well. The heavily commented code provides you with the detail of +what it is going to do. + +### References + + * [SCIM Protocol RFC](https://tools.ietf.org/html/rfc7644) + * [AWS SSO - Connect to Your External Identity Provider](https://docs.aws.amazon.com/singlesignon/latest/userguide/manage-your-identity-source-idp.html) + * [AWS SSO - Automatic Provisioning](https://docs.aws.amazon.com/singlesignon/latest/userguide/provision-automatically.html) + * [AWS IAM Identity Center - Identity Store API](https://docs.aws.amazon.com/singlesignon/latest/IdentityStoreAPIReference/welcome.html) + +## Installation + +The recommended installation is: +* [Setup IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/get-started-enable-identity-center.html), in the management account of your organization +* Created a linked account `Identity` Account from which to manage IAM Identity Center +* [Delegate administration](https://docs.aws.amazon.com/singlesignon/latest/userguide/delegated-admin.html) to the `Identity' account +* Deploy the [SSOSync app](https://console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync) from the AWS Serverless Application Repository + + +You can also: +You can `go get github.com/awslabs/ssosync` or grab a Release binary from the release page. The binary +can be used from your local computer, or you can deploy to AWS Lambda to run on a CloudWatch Event +for regular synchronization. + +## Configuration + +You need a few items of configuration. One side from AWS, and the other +from Google Cloud to allow for API access to each. You should have configured +Google as your Identity Provider for AWS SSO already. + +You will need the files produced by these steps for AWS Lambda deployment as well +as locally running the ssosync tool. + +### Google + +First, you have to setup your API. In the project you want to use go to the [Console](https://console.developers.google.com/apis) and select *API & Services* > *Enable APIs and Services*. Search for *Admin SDK* and *Enable* the API. + +You have to perform this [tutorial](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) to create a service account that you use to sync your users. Save the `JSON file` you create during the process and rename it to `credentials.json`. + +> you can also use the `--google-credentials` parameter to explicitly specify the file with the service credentials. Please, keep this file safe, or store it in the AWS Secrets Manager + +In the domain-wide delegation for the Admin API, you have to specify the following scopes for the user. + +* https://www.googleapis.com/auth/admin.directory.group.readonly +* https://www.googleapis.com/auth/admin.directory.group.member.readonly +* https://www.googleapis.com/auth/admin.directory.user.readonly + +Back in the Console go to the Dashboard for the API & Services and select "Enable API and Services". +In the Search box type `Admin` and select the `Admin SDK` option. Click the `Enable` button. + +You will have to specify the email address of an admin via `--google-admin` to assume this users role in the Directory. + +### AWS + +Go to the AWS Single Sign-On console in the region you have set up AWS SSO and select +Settings. Click `Enable automatic provisioning`. + +A pop up will appear with URL and the Access Token. The Access Token will only appear +at this stage. You want to copy both of these as a parameter to the `ssosync` command. + +Or you specific these as environment variables. + +```bash +SSOSYNC_SCIM_ACCESS_TOKEN= +SSOSYNC_SCIM_ENDPOINT= +``` + +Additionally, authenticate your AWS credentials. Follow this [section](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#:~:text=Creating%20the%20Credentials%20File) to create a Shared Credentials File in the home directory or export your Credentials with Environment Variables. Ensure that the default credentials are for the AWS account you intended to be synced. + +To obtain your `Identity store ID`, go to the AWS Identity Center console and select settings. Under the `Identity Source` section, copy the `Identity store ID`. + +## Local Usage + +```bash +git clone https://github.com/awslabs/ssosync.git +cd ssosync/ +make go-build +``` + +```bash +./ssosync --help +``` + +```bash +A command line tool to enable you to synchronise your Google +Apps (Google Workspace) users to AWS Single Sign-on (AWS SSO) +Complete documentation is available at https://github.com/awslabs/ssosync + +Usage: + ssosync [flags] + +Flags: + -t, --access-token string AWS SSO SCIM API Access Token + -d, --debug enable verbose / debug logging + -e, --endpoint string AWS SSO SCIM API Endpoint + -u, --google-admin string Google Workspace admin user email + -c, --google-credentials string path to Google Workspace credentials file (default "credentials.json") + -g, --group-match string Google Workspace Groups filter query parameter, example: 'name:Admin* email:aws-*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups + -h, --help help for ssosync + --ignore-groups strings ignores these Google Workspace groups + --ignore-users strings ignores these Google Workspace users + --include-groups strings include only these Google Workspace groups, NOTE: only works when --sync-method 'users_groups' + --log-format string log format (default "text") + --log-level string log level (default "info") + -s, --sync-method string Sync method to use (users_groups|groups) (default "groups") + -m, --user-match string Google Workspace Users filter query parameter, example: 'name:John* email:admin*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users + -v, --version version for ssosync + -r, --region AWS region where identity store exists + -i, --identity-store-id AWS Identity Store ID +``` + +The function has `two behaviour` and these are controlled by the `--sync-method` flag, this behavior could be + +1. `groups`: __(default)__ The sync procedure work base on Groups, gets the Google Workspace groups and their members, then creates in AWS SSO the users (members of the Google Workspace groups), then the groups and at the end assign the users to their respective groups. +2. `users_groups`: __(original behavior, previous versions)__ The sync procedure is simple, gets the Google Workspace users and creates these in AWS SSO Users; then gets Google Workspace groups and creates these in AWS SSO Groups and assigns users to belong to the AWS SSO Groups. + +Flags Notes: + +* `--include-groups` only works when `--sync-method` is `users_groups` +* `--ignore-users` works for both `--sync-method` values. Example: `--ignore-users user1@example.com,user2@example.com` or `SSOSYNC_IGNORE_USERS=user1@example.com,user2@example.com` +* `--ignore-groups` works for both `--sync-method` values. Example: --ignore-groups group1@example.com,group1@example.com` or `SSOSYNC_IGNORE_GROUPS=group1@example.com,group1@example.com` +* `--group-match` works for both `--sync-method` values and also in combination with `--ignore-groups` and `--ignore-users`. This is the filter query passed to the [Google Workspace Directory API when search Groups](https://developers.google.com/admin-sdk/directory/v1/guides/search-groups), if the flag is not used, groups are not filtered. +* `--user-match` works for both `--sync-method` values and also in combination with `--ignore-groups` and `--ignore-users`. This is the filter query passed to the [Google Workspace Directory API when search Users](https://developers.google.com/admin-sdk/directory/v1/guides/search-users), if the flag is not used, users are not filtered. + +NOTES: + +1. Depending on the number of users and groups you have, maybe you can get `AWS SSO SCIM API rate limits errors`, and more frequently happens if you execute the sync many times in a short time. +2. Depending on the number of users and groups you have, `--debug` flag generate too much logs lines in your AWS Lambda function. So test it in locally with the `--debug` flag enabled and disable it when you use a AWS Lambda function. + +## AWS Lambda Usage + +NOTE: Using Lambda may incur costs in your AWS account. Please make sure you have checked +the pricing for AWS Lambda and CloudWatch before continuing. + +Running ssosync once means that any changes to your Google directory will not appear in +AWS SSO. To sync. regularly, you can run ssosync via AWS Lambda. + +:warning: You find it in the [AWS Serverless Application Repository](https://eu-west-1.console.aws.amazon.com/lambda/home#/create/app?applicationId=arn:aws:serverlessrepo:us-east-2:004480582608:applications/SSOSync). + +## SAM + +You can use the AWS Serverless Application Model (SAM) to deploy this to your account. + +> Please, install the [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) and [GoReleaser](https://goreleaser.com/install/). + +Specify an Amazon S3 Bucket for the upload with `export S3_BUCKET=` and an S3 prefix with `export S3_PREFIX=`. + +Execute `make package` in the console. Which will package and upload the function to the bucket. You can then use the `packaged.yaml` to configure and deploy the stack in [AWS CloudFormation Console](https://console.aws.amazon.com/cloudformation). + +### Example + +Build + +```bash +aws cloudformation validate-template --template-body file://template.yaml 1>/dev/null && +sam validate && +sam build +``` + +Deploy + +```bash +sam deploy --guided +``` + +## License + +[Apache-2.0](/LICENSE) diff --git a/modules/aws-ssosync/iam.tf b/modules/aws-ssosync/iam.tf new file mode 100644 index 000000000..c6eecd62c --- /dev/null +++ b/modules/aws-ssosync/iam.tf @@ -0,0 +1,44 @@ + +data "aws_iam_policy_document" "ssosync_lambda_assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "ssosync_lambda_identity_center" { + statement { + effect = "Allow" + actions = [ + "identitystore:DeleteUser", + "identitystore:CreateGroup", + "identitystore:CreateGroupMembership", + "identitystore:ListGroups", + "identitystore:ListUsers", + "identitystore:ListGroupMemberships", + "identitystore:IsMemberInGroups", + "identitystore:GetGroupMembershipId", + "identitystore:DeleteGroupMembership", + "identitystore:DeleteGroup", + "secretsmanager:GetSecretValue", + "kms:Decrypt" + ] + resources = ["*"] + } +} + +resource "aws_iam_role" "default" { + count = local.enabled ? 1 : 0 + + name = module.this.id + assume_role_policy = data.aws_iam_policy_document.ssosync_lambda_assume_role.json + + inline_policy { + name = "ssosync_lambda_identity_center" + policy = data.aws_iam_policy_document.ssosync_lambda_identity_center.json + } +} diff --git a/modules/aws-ssosync/main.tf b/modules/aws-ssosync/main.tf new file mode 100644 index 000000000..79b24efdb --- /dev/null +++ b/modules/aws-ssosync/main.tf @@ -0,0 +1,128 @@ +locals { + enabled = module.this.enabled + google_credentials = one(data.aws_ssm_parameter.google_credentials[*].value) + scim_endpoint_url = one(data.aws_ssm_parameter.scim_endpoint_url[*].value) + scim_endpoint_access_token = one(data.aws_ssm_parameter.scim_endpoint_access_token[*].value) + identity_store_id = one(data.aws_ssm_parameter.identity_store_id[*].value) + + ssosync_artifact_url = "${var.ssosync_url_prefix}/${var.ssosync_version}/ssosync_Linux_${var.architecture}.tar.gz" + + download_artifact = "ssosync.tar.gz" +} + +data "aws_ssm_parameter" "google_credentials" { + count = local.enabled ? 1 : 0 + name = "${var.google_credentials_ssm_path}/google_credentials" +} + +data "aws_ssm_parameter" "scim_endpoint_url" { + count = local.enabled ? 1 : 0 + name = "${var.google_credentials_ssm_path}/scim_endpoint_url" +} + +data "aws_ssm_parameter" "scim_endpoint_access_token" { + count = local.enabled ? 1 : 0 + name = "${var.google_credentials_ssm_path}/scim_endpoint_access_token" +} + +data "aws_ssm_parameter" "identity_store_id" { + count = local.enabled ? 1 : 0 + name = "${var.google_credentials_ssm_path}/identity_store_id" +} + + +module "ssosync_artifact" { + count = local.enabled ? 1 : 0 + + source = "cloudposse/module-artifact/external" + version = "0.8.0" + + filename = local.download_artifact + module_name = "ssosync" + module_path = path.module + url = local.ssosync_artifact_url +} + +resource "null_resource" "extract_my_tgz" { + count = local.enabled ? 1 : 0 + + provisioner "local-exec" { + command = "tar -xzf ${local.download_artifact} -C dist" + } + + depends_on = [module.ssosync_artifact] +} + +data "archive_file" "lambda" { + count = local.enabled ? 1 : 0 + + type = "zip" + source_file = "dist/ssosync" + output_path = "ssosync.zip" + + depends_on = [null_resource.extract_my_tgz] +} + + +resource "aws_lambda_function" "ssosync" { + count = local.enabled ? 1 : 0 + + function_name = module.this.id + filename = "ssosync.zip" + source_code_hash = module.ssosync_artifact[0].base64sha256 + description = "Syncs Google Workspace users and groups to AWS SSO" + role = aws_iam_role.default[0].arn + handler = "ssosync" + runtime = "go1.x" + timeout = 300 + memory_size = 128 + + environment { + variables = { + SSOSYNC_LOG_LEVEL = var.log_level + SSOSYNC_LOG_FORMAT = var.log_format + SSOSYNC_GOOGLE_CREDENTIALS = local.google_credentials + SSOSYNC_GOOGLE_ADMIN = var.google_admin_email + SSOSYNC_SCIM_ENDPOINT = local.scim_endpoint_url + SSOSYNC_SCIM_ACCESS_TOKEN = local.scim_endpoint_access_token + SSOSYNC_REGION = var.region + SSOSYNC_IDENTITY_STORE_ID = local.identity_store_id + SSOSYNC_USER_MATCH = var.google_user_match + SSOSYNC_GROUP_MATCH = var.google_group_match + SSOSYNC_SYNC_METHOD = var.sync_method + SSOSYNC_IGNORE_GROUPS = var.ignore_groups + SSOSYNC_IGNORE_USERS = var.ignore_users + SSOSYNC_INCLUDE_GROUPS = var.include_groups + SSOSYNC_LOAD_ASM_SECRETS = false + } + } + depends_on = [null_resource.extract_my_tgz, data.archive_file.lambda] +} + +resource "aws_cloudwatch_event_rule" "ssosync" { + count = var.enabled ? 1 : 0 + + name = module.this.id + description = "Run ssosync on a schedule" + schedule_expression = var.schedule_expression + +} + +resource "aws_cloudwatch_event_target" "ssosync" { + count = var.enabled ? 1 : 0 + + rule = aws_cloudwatch_event_rule.ssosync[0].name + target_id = module.this.id + arn = aws_lambda_function.ssosync[0].arn +} + + +resource "aws_lambda_permission" "allow_cloudwatch_execution" { + count = local.enabled ? 1 : 0 + + statement_id = "AllowExecutionFromCloudWatch" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.ssosync[0].arn + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.ssosync[0].arn +} diff --git a/modules/aws-ssosync/outputs.tf b/modules/aws-ssosync/outputs.tf new file mode 100644 index 000000000..2e9f84bd1 --- /dev/null +++ b/modules/aws-ssosync/outputs.tf @@ -0,0 +1,14 @@ +output "arn" { + description = "ARN of the lambda function" + value = aws_lambda_function.ssosync.arn +} + +output "invoke_arn" { + description = "Invoke ARN of the lambda function" + value = aws_lambda_function.ssosync.invoke_arn +} + +output "qualified_arn" { + description = "ARN identifying your Lambda Function Version (if versioning is enabled via publish = true)" + value = aws_lambda_function.ssosync.qualified_arn +} diff --git a/modules/aws-ssosync/providers.tf b/modules/aws-ssosync/providers.tf new file mode 100644 index 000000000..5736c3cb2 --- /dev/null +++ b/modules/aws-ssosync/providers.tf @@ -0,0 +1,27 @@ +provider "aws" { + region = var.region + + # aws-ssosync, since it creates roles in identity, + # must be run as SuperAdmin, and cannot use "profile" instead of "role_arn" + # even if the components are generally using profiles. + # Note the role_arn is the ARN of the OrganizationAccountAccessRole, not the SAML role. + + dynamic "assume_role" { + for_each = var.import_role_arn == null ? (module.iam_roles.org_role_arn != null ? [true] : []) : ["import"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.org_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + privileged = true + context = module.this.context +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/aws-ssosync/variables.tf b/modules/aws-ssosync/variables.tf new file mode 100644 index 000000000..83ba9c8da --- /dev/null +++ b/modules/aws-ssosync/variables.tf @@ -0,0 +1,102 @@ +variable "region" { + type = string + description = "AWS Region where AWS SSO is enabled" +} + +variable "schedule_expression" { + type = string + description = "Schedule for trigger the execution of ssosync (see CloudWatch schedule expressions)" + default = "rate(15 minutes)" +} + +variable "log_level" { + type = string + description = "Log level for Lambda function logging" + default = "warn" + + validation { + condition = contains(["panic", "fatal", "error", "warn", "info", "debug", "trace"], var.log_level) + error_message = "Allowed values: `panic`, `fatal`, `error`, `warn`, `info`, `debug`, `trace`" + } +} + +variable "log_format" { + type = string + description = "Log format for Lambda function logging" + default = "json" + + validation { + condition = contains(["json", "text"], var.log_format) + error_message = "Allowed values: `json`, `text`" + } +} + +variable "ssosync_url_prefix" { + type = string + description = "URL prefix for ssosync binary" + default = "https://github.com/Benbentwo/ssosync/releases/download" +} + +variable "ssosync_version" { + type = string + description = "Version of ssosync to use" + default = "v2.0.2" +} + +variable "architecture" { + type = string + description = "Architecture of the Lambda function" + default = "x86_64" +} + +variable "google_credentials_ssm_path" { + type = string + description = "SSM Path for `ssosync` secrets" + default = "/ssosync" +} + +variable "google_admin_email" { + type = string + description = "Google Admin email" +} + +variable "google_user_match" { + type = string + description = "Google Workspace user filter query parameter, example: 'name:John* email:admin*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-users" + default = "" +} + +variable "google_group_match" { + type = string + description = "Google Workspace group filter query parameter, example: 'name:Admin* email:aws-*', see: https://developers.google.com/admin-sdk/directory/v1/guides/search-groups" + default = "" +} + +variable "ignore_groups" { + type = string + description = "Ignore these Google Workspace groups" + default = "" +} + +variable "ignore_users" { + type = string + description = "Ignore these Google Workspace users" + default = "" +} + +variable "include_groups" { + type = string + description = "Include only these Google Workspace groups. (Only applicable for sync_method user_groups)" + default = "" +} + +variable "sync_method" { + type = string + description = "Sync method to use" + default = "groups" + + validation { + condition = contains(["groups", "users_groups"], var.sync_method) + error_message = "Allowed values: `groups`, `users_groups`" + } +} diff --git a/modules/aws-ssosync/versions.tf b/modules/aws-ssosync/versions.tf new file mode 100644 index 000000000..990265a57 --- /dev/null +++ b/modules/aws-ssosync/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + archive = { + source = "hashicorp/archive" + version = ">= 2.3.0" + } + null = { + source = "hashicorp/null" + version = ">= 3.0" + } + } +} From f640c3ec74515be5e2e0510df5f011caebe2fbc5 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 8 Jun 2023 16:28:09 -0700 Subject: [PATCH 148/501] Karpenter Node Interruption Handler (#713) --- modules/eks/karpenter/README.md | 48 +++++++-- modules/eks/karpenter/interruption_handler.tf | 97 +++++++++++++++++++ modules/eks/karpenter/main.tf | 38 +++++++- modules/eks/karpenter/variables.tf | 16 +++ 4 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 modules/eks/karpenter/interruption_handler.tf diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 16e55428d..0cc3a2056 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -18,20 +18,19 @@ components: # Base component of all `karpenter` components eks/karpenter: metadata: + component: eks/karpenter type: abstract - settings: - spacelift: - workspace_enabled: true vars: enabled: true + eks_component_name: "eks/cluster" tags: Team: sre Service: karpenter - eks_component_name: eks/cluster name: "karpenter" + # https://github.com/aws/karpenter/tree/main/charts/karpenter + chart_repository: "oci://public.ecr.aws/karpenter" chart: "karpenter" - chart_repository: "https://charts.karpenter.sh" - chart_version: "v0.16.3" + chart_version: "v0.27.5" create_namespace: true kubernetes_namespace: "karpenter" resources: @@ -41,10 +40,17 @@ components: requests: cpu: "100m" memory: "512Mi" - cleanup_on_fail: true - atomic: true + cleanup_on_fail: false + atomic: false wait: true rbac_enabled: true + interruption_handler_enabled: true + chart_values: + serviceMonitor: + enabled: true + replicas: 1 + aws: + enableENILimitedPodDensity: false # Provision `karpenter` component on the blue EKS cluster eks/karpenter-blue: @@ -285,6 +291,24 @@ For more details, refer to: - https://aws.github.io/aws-eks-best-practices/karpenter - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html +## Node Interruption + +Karpenter also supports Node Interruption. If interruption-handling is enabled, Karpenter will watch for upcoming involuntary interruption events that would cause disruption to your workloads. These interruption events include: + +- Spot Interruption Warnings +- Scheduled Change Health Events (Maintenance Events) +- Instance Terminating Events +- Instance Stopping Events + +:::info + +The Node Interruption Handler is not the same as the Node Termination Handler. The latter works fine and cleanly shuts down the node in 2 minutes. The former gets advance notice, so it can have 5-10 minutes to shut down a node. + +::: + +For more details, see refer to [Karpenter docs](https://karpenter.sh/v0.27.5/concepts/deprovisioning/#interruption) + +To enable Node Interruption, set `var.interruption_handler_enabled` to `true`. This will enable a SQS queue and a set of Event Bridge rules to handle interruption with Karpenter. ## Requirements @@ -315,8 +339,14 @@ For more details, refer to: | Name | Type | |------|------| +| [aws_cloudwatch_event_rule.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | | [aws_iam_instance_profile.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_sqs_queue.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource | +| [aws_sqs_queue_policy.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_iam_policy_document.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | ## Inputs @@ -342,6 +372,8 @@ For more details, refer to: | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [interruption\_handler\_enabled](#input\_interruption\_handler\_enabled) | If `true`, deploy a SQS queue and Event Bridge rules to enable interruption handling by Karpenter.

https://karpenter.sh/v0.27.5/concepts/deprovisioning/#interruption | `bool` | `false` | no | +| [interruption\_queue\_message\_retention](#input\_interruption\_queue\_message\_retention) | The message retention in seconds for the interruption handler SQS queue. | `number` | `300` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/karpenter/interruption_handler.tf b/modules/eks/karpenter/interruption_handler.tf new file mode 100644 index 000000000..3f173b98e --- /dev/null +++ b/modules/eks/karpenter/interruption_handler.tf @@ -0,0 +1,97 @@ +locals { + interruption_handler_enabled = local.enabled && var.interruption_handler_enabled + interruption_handler_queue_name = module.this.id + + dns_suffix = data.aws_partition.current.dns_suffix + + events = { + health_event = { + name = "HealthEvent" + description = "Karpenter interrupt - AWS health event" + event_pattern = { + source = ["aws.health"] + detail-type = ["AWS Health Event"] + } + } + spot_interupt = { + name = "SpotInterrupt" + description = "Karpenter interrupt - EC2 spot instance interruption warning" + event_pattern = { + source = ["aws.ec2"] + detail-type = ["EC2 Spot Instance Interruption Warning"] + } + } + instance_rebalance = { + name = "InstanceRebalance" + description = "Karpenter interrupt - EC2 instance rebalance recommendation" + event_pattern = { + source = ["aws.ec2"] + detail-type = ["EC2 Instance Rebalance Recommendation"] + } + } + instance_state_change = { + name = "InstanceStateChange" + description = "Karpenter interrupt - EC2 instance state-change notification" + event_pattern = { + source = ["aws.ec2"] + detail-type = ["EC2 Instance State-change Notification"] + } + } + } +} + +data "aws_partition" "current" {} + +resource "aws_sqs_queue" "interruption_handler" { + count = local.interruption_handler_enabled ? 1 : 0 + + name = local.interruption_handler_queue_name + message_retention_seconds = var.interruption_queue_message_retention + sqs_managed_sse_enabled = true + + tags = module.this.tags +} + +data "aws_iam_policy_document" "interruption_handler" { + count = local.interruption_handler_enabled ? 1 : 0 + + statement { + sid = "SqsWrite" + actions = ["sqs:SendMessage"] + resources = [aws_sqs_queue.interruption_handler[0].arn] + + principals { + type = "Service" + identifiers = [ + "events.${local.dns_suffix}", + "sqs.${local.dns_suffix}", + ] + } + + } +} + +resource "aws_sqs_queue_policy" "interruption_handler" { + count = local.interruption_handler_enabled ? 1 : 0 + + queue_url = aws_sqs_queue.interruption_handler[0].url + policy = data.aws_iam_policy_document.interruption_handler[0].json +} + +resource "aws_cloudwatch_event_rule" "interruption_handler" { + for_each = { for k, v in local.events : k => v if local.interruption_handler_enabled } + + name = "${module.this.id}-${each.value.name}" + description = each.value.description + event_pattern = jsonencode(each.value.event_pattern) + + tags = module.this.tags +} + +resource "aws_cloudwatch_event_target" "interruption_handler" { + for_each = { for k, v in local.events : k => v if local.interruption_handler_enabled } + + rule = aws_cloudwatch_event_rule.interruption_handler[each.key].name + target_id = "KarpenterInterruptionQueueTarget" + arn = aws_sqs_queue.interruption_handler[0].arn +} diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index 9edb24fdf..169393087 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -54,7 +54,7 @@ module "karpenter" { # https://github.com/aws/karpenter/issues/2649 # Apparently the source of truth for the best IAM policy is the `data.aws_iam_policy_document.karpenter_controller` in # https://github.com/terraform-aws-modules/terraform-aws-iam/blob/master/modules/iam-role-for-service-accounts-eks/policies.tf - iam_policy_statements = [ + iam_policy_statements = concat([ { sid = "KarpenterController" effect = "Allow" @@ -92,8 +92,34 @@ module "karpenter" { # Allow Karpenter to read AMI IDs from SSM actions = ["ssm:GetParameter"] resources = ["arn:aws:ssm:*:*:parameter/aws/service/*"] + }, + { + sid = "KarpenterControllerClusterAccess" + effect = "Allow" + actions = [ + "eks:DescribeCluster" + ] + resources = [ + module.eks.outputs.eks_cluster_arn + ] } - ] + ], + local.interruption_handler_enabled ? [ + { + sid = "KarpenterInterruptionHandlerAccess" + effect = "Allow" + actions = [ + "sqs:DeleteMessage", + "sqs:GetQueueUrl", + "sqs:GetQueueAttributes", + "sqs:ReceiveMessage", + ] + resources = [ + aws_sqs_queue.interruption_handler[0].arn + ] + } + ] : [] + ) values = compact([ # standard k8s object settings @@ -117,6 +143,14 @@ module "karpenter" { } } }), + yamlencode( + local.interruption_handler_enabled ? { + settings = { + aws = { + interruptionQueueName = local.interruption_handler_queue_name + } + } + } : {}), # additional values yamlencode(var.chart_values) ]) diff --git a/modules/eks/karpenter/variables.tf b/modules/eks/karpenter/variables.tf index 6aaa6b4fb..2623ce192 100644 --- a/modules/eks/karpenter/variables.tf +++ b/modules/eks/karpenter/variables.tf @@ -91,3 +91,19 @@ variable "eks_component_name" { description = "The name of the eks component" default = "eks/cluster" } + +variable "interruption_handler_enabled" { + type = bool + default = false + description = < Date: Fri, 9 Jun 2023 10:27:43 -0400 Subject: [PATCH 149/501] add new spacelift components (#717) Co-authored-by: cloudpossebot --- README.yaml | 2 + .../spacelift-policy/README.md | 0 .../spacelift-policy/context.tf | 0 .../spacelift-policy/main.tf | 0 .../spacelift-policy/outputs.tf | 0 .../example.trigger.administrative.rego | 0 .../policies/plan.autodeployupdates.rego | 0 .../spacelift-policy/providers.tf | 0 .../spacelift-policy/variables.tf | 0 .../spacelift-policy/versions.tf | 0 .../spacelift-worker-pool/README.md | 0 .../spacelift-worker-pool/context.tf | 0 .../spacelift-worker-pool/data.tf | 0 .../spacelift-worker-pool/iam.tf | 0 .../spacelift-worker-pool/main.tf | 0 .../spacelift-worker-pool/outputs.tf | 0 .../spacelift-worker-pool/providers.tf | 0 .../spacelift-worker-pool/remote-state.tf | 0 .../templates/spacelift@.service | 0 .../templates/user-data.sh | 0 .../spacelift-worker-pool/variables.tf | 0 .../spacelift-worker-pool/versions.tf | 0 deprecated/spacelift/README.md | 446 ++++++++++++++++++ {modules => deprecated}/spacelift/context.tf | 0 .../docs/example-spacelift-config.yml | 0 .../example-stacks_catalog_spacelift.yaml | 0 .../example-stacks_deployed_spacelift.yaml | 0 .../img/Spacelift-Infrastructure-Behavior.png | Bin .../docs/img/Spacelift-Merge-Execution.png | Bin .../docs/img/Spacelift-PR-Changes.png | Bin .../docs/img/Spacelift-PR-Checks.png | Bin .../spacelift/docs/spacectl.md | 0 .../spacelift/docs/spacelift-overview.md | 0 {modules => deprecated}/spacelift/main.tf | 0 {modules => deprecated}/spacelift/outputs.tf | 0 .../spacelift/providers.tf | 0 .../rego-policies/access.default.rego | 0 .../rego-policies/plan.autodeployupdates.rego | 0 .../spacelift/rego-policies/plan.ecr.rego | 0 .../spacelift/spacelift-provider.tf | 0 .../spacelift/variables.tf | 0 {modules => deprecated}/spacelift/versions.tf | 0 .../spacelift-worker-pool/default.auto.tfvars | 3 - modules/spacelift/README.md | 445 ++--------------- modules/spacelift/admin-stack/README.md | 208 ++++++++ modules/spacelift/admin-stack/child-stacks.tf | 130 +++++ modules/spacelift/admin-stack/context.tf | 279 +++++++++++ modules/spacelift/admin-stack/main.tf | 11 + modules/spacelift/admin-stack/outputs.tf | 14 + modules/spacelift/admin-stack/providers.tf | 2 + modules/spacelift/admin-stack/remote-state.tf | 11 + .../spacelift/admin-stack/root-admin-stack.tf | 95 ++++ modules/spacelift/admin-stack/spaces.tf | 31 ++ modules/spacelift/admin-stack/variables.tf | 345 ++++++++++++++ modules/spacelift/admin-stack/versions.tf | 14 + modules/spacelift/admin-stack/workers.tf | 49 ++ modules/spacelift/spaces/README.md | 130 +++++ modules/spacelift/spaces/context.tf | 279 +++++++++++ modules/spacelift/spaces/main.tf | 65 +++ modules/spacelift/spaces/outputs.tf | 7 + modules/spacelift/spaces/providers.tf | 1 + modules/spacelift/spaces/variables.tf | 21 + modules/spacelift/spaces/versions.tf | 14 + modules/spacelift/worker-pool/README.md | 223 +++++++++ modules/spacelift/worker-pool/context.tf | 279 +++++++++++ modules/spacelift/worker-pool/data.tf | 39 ++ modules/spacelift/worker-pool/iam.tf | 87 ++++ modules/spacelift/worker-pool/main.tf | 122 +++++ modules/spacelift/worker-pool/outputs.tf | 89 ++++ modules/spacelift/worker-pool/providers.tf | 36 ++ modules/spacelift/worker-pool/remote-state.tf | 32 ++ .../worker-pool/templates/spacelift@.service | 13 + .../worker-pool/templates/user-data.sh | 115 +++++ modules/spacelift/worker-pool/variables.tf | 292 ++++++++++++ modules/spacelift/worker-pool/versions.tf | 18 + 75 files changed, 3529 insertions(+), 418 deletions(-) rename {modules => deprecated}/spacelift-policy/README.md (100%) rename {modules => deprecated}/spacelift-policy/context.tf (100%) rename {modules => deprecated}/spacelift-policy/main.tf (100%) rename {modules => deprecated}/spacelift-policy/outputs.tf (100%) rename {modules => deprecated}/spacelift-policy/policies/example.trigger.administrative.rego (100%) rename {modules => deprecated}/spacelift-policy/policies/plan.autodeployupdates.rego (100%) rename {modules => deprecated}/spacelift-policy/providers.tf (100%) rename {modules => deprecated}/spacelift-policy/variables.tf (100%) rename {modules => deprecated}/spacelift-policy/versions.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/README.md (100%) rename {modules => deprecated}/spacelift-worker-pool/context.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/data.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/iam.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/main.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/outputs.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/providers.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/remote-state.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/templates/spacelift@.service (100%) rename {modules => deprecated}/spacelift-worker-pool/templates/user-data.sh (100%) rename {modules => deprecated}/spacelift-worker-pool/variables.tf (100%) rename {modules => deprecated}/spacelift-worker-pool/versions.tf (100%) create mode 100644 deprecated/spacelift/README.md rename {modules => deprecated}/spacelift/context.tf (100%) rename {modules => deprecated}/spacelift/docs/example-spacelift-config.yml (100%) rename {modules => deprecated}/spacelift/docs/example-stacks_catalog_spacelift.yaml (100%) rename {modules => deprecated}/spacelift/docs/example-stacks_deployed_spacelift.yaml (100%) rename {modules => deprecated}/spacelift/docs/img/Spacelift-Infrastructure-Behavior.png (100%) rename {modules => deprecated}/spacelift/docs/img/Spacelift-Merge-Execution.png (100%) rename {modules => deprecated}/spacelift/docs/img/Spacelift-PR-Changes.png (100%) rename {modules => deprecated}/spacelift/docs/img/Spacelift-PR-Checks.png (100%) rename {modules => deprecated}/spacelift/docs/spacectl.md (100%) rename {modules => deprecated}/spacelift/docs/spacelift-overview.md (100%) rename {modules => deprecated}/spacelift/main.tf (100%) rename {modules => deprecated}/spacelift/outputs.tf (100%) rename {modules => deprecated}/spacelift/providers.tf (100%) rename {modules => deprecated}/spacelift/rego-policies/access.default.rego (100%) rename {modules => deprecated}/spacelift/rego-policies/plan.autodeployupdates.rego (100%) rename {modules => deprecated}/spacelift/rego-policies/plan.ecr.rego (100%) rename {modules => deprecated}/spacelift/spacelift-provider.tf (100%) rename {modules => deprecated}/spacelift/variables.tf (100%) rename {modules => deprecated}/spacelift/versions.tf (100%) delete mode 100644 modules/spacelift-worker-pool/default.auto.tfvars create mode 100644 modules/spacelift/admin-stack/README.md create mode 100644 modules/spacelift/admin-stack/child-stacks.tf create mode 100644 modules/spacelift/admin-stack/context.tf create mode 100644 modules/spacelift/admin-stack/main.tf create mode 100644 modules/spacelift/admin-stack/outputs.tf create mode 100644 modules/spacelift/admin-stack/providers.tf create mode 100644 modules/spacelift/admin-stack/remote-state.tf create mode 100644 modules/spacelift/admin-stack/root-admin-stack.tf create mode 100644 modules/spacelift/admin-stack/spaces.tf create mode 100644 modules/spacelift/admin-stack/variables.tf create mode 100644 modules/spacelift/admin-stack/versions.tf create mode 100644 modules/spacelift/admin-stack/workers.tf create mode 100644 modules/spacelift/spaces/README.md create mode 100644 modules/spacelift/spaces/context.tf create mode 100644 modules/spacelift/spaces/main.tf create mode 100644 modules/spacelift/spaces/outputs.tf create mode 100644 modules/spacelift/spaces/providers.tf create mode 100644 modules/spacelift/spaces/variables.tf create mode 100644 modules/spacelift/spaces/versions.tf create mode 100644 modules/spacelift/worker-pool/README.md create mode 100644 modules/spacelift/worker-pool/context.tf create mode 100644 modules/spacelift/worker-pool/data.tf create mode 100644 modules/spacelift/worker-pool/iam.tf create mode 100644 modules/spacelift/worker-pool/main.tf create mode 100644 modules/spacelift/worker-pool/outputs.tf create mode 100644 modules/spacelift/worker-pool/providers.tf create mode 100644 modules/spacelift/worker-pool/remote-state.tf create mode 100644 modules/spacelift/worker-pool/templates/spacelift@.service create mode 100644 modules/spacelift/worker-pool/templates/user-data.sh create mode 100644 modules/spacelift/worker-pool/variables.tf create mode 100644 modules/spacelift/worker-pool/versions.tf diff --git a/README.yaml b/README.yaml index 81d5cc503..c5620f4f1 100644 --- a/README.yaml +++ b/README.yaml @@ -145,3 +145,5 @@ contributors: github: "Gowiem" - name: "Yonatan Koren" github: "korenyoni" + - name: "Matt Calhoun" + github: "mcalhoun" diff --git a/modules/spacelift-policy/README.md b/deprecated/spacelift-policy/README.md similarity index 100% rename from modules/spacelift-policy/README.md rename to deprecated/spacelift-policy/README.md diff --git a/modules/spacelift-policy/context.tf b/deprecated/spacelift-policy/context.tf similarity index 100% rename from modules/spacelift-policy/context.tf rename to deprecated/spacelift-policy/context.tf diff --git a/modules/spacelift-policy/main.tf b/deprecated/spacelift-policy/main.tf similarity index 100% rename from modules/spacelift-policy/main.tf rename to deprecated/spacelift-policy/main.tf diff --git a/modules/spacelift-policy/outputs.tf b/deprecated/spacelift-policy/outputs.tf similarity index 100% rename from modules/spacelift-policy/outputs.tf rename to deprecated/spacelift-policy/outputs.tf diff --git a/modules/spacelift-policy/policies/example.trigger.administrative.rego b/deprecated/spacelift-policy/policies/example.trigger.administrative.rego similarity index 100% rename from modules/spacelift-policy/policies/example.trigger.administrative.rego rename to deprecated/spacelift-policy/policies/example.trigger.administrative.rego diff --git a/modules/spacelift-policy/policies/plan.autodeployupdates.rego b/deprecated/spacelift-policy/policies/plan.autodeployupdates.rego similarity index 100% rename from modules/spacelift-policy/policies/plan.autodeployupdates.rego rename to deprecated/spacelift-policy/policies/plan.autodeployupdates.rego diff --git a/modules/spacelift-policy/providers.tf b/deprecated/spacelift-policy/providers.tf similarity index 100% rename from modules/spacelift-policy/providers.tf rename to deprecated/spacelift-policy/providers.tf diff --git a/modules/spacelift-policy/variables.tf b/deprecated/spacelift-policy/variables.tf similarity index 100% rename from modules/spacelift-policy/variables.tf rename to deprecated/spacelift-policy/variables.tf diff --git a/modules/spacelift-policy/versions.tf b/deprecated/spacelift-policy/versions.tf similarity index 100% rename from modules/spacelift-policy/versions.tf rename to deprecated/spacelift-policy/versions.tf diff --git a/modules/spacelift-worker-pool/README.md b/deprecated/spacelift-worker-pool/README.md similarity index 100% rename from modules/spacelift-worker-pool/README.md rename to deprecated/spacelift-worker-pool/README.md diff --git a/modules/spacelift-worker-pool/context.tf b/deprecated/spacelift-worker-pool/context.tf similarity index 100% rename from modules/spacelift-worker-pool/context.tf rename to deprecated/spacelift-worker-pool/context.tf diff --git a/modules/spacelift-worker-pool/data.tf b/deprecated/spacelift-worker-pool/data.tf similarity index 100% rename from modules/spacelift-worker-pool/data.tf rename to deprecated/spacelift-worker-pool/data.tf diff --git a/modules/spacelift-worker-pool/iam.tf b/deprecated/spacelift-worker-pool/iam.tf similarity index 100% rename from modules/spacelift-worker-pool/iam.tf rename to deprecated/spacelift-worker-pool/iam.tf diff --git a/modules/spacelift-worker-pool/main.tf b/deprecated/spacelift-worker-pool/main.tf similarity index 100% rename from modules/spacelift-worker-pool/main.tf rename to deprecated/spacelift-worker-pool/main.tf diff --git a/modules/spacelift-worker-pool/outputs.tf b/deprecated/spacelift-worker-pool/outputs.tf similarity index 100% rename from modules/spacelift-worker-pool/outputs.tf rename to deprecated/spacelift-worker-pool/outputs.tf diff --git a/modules/spacelift-worker-pool/providers.tf b/deprecated/spacelift-worker-pool/providers.tf similarity index 100% rename from modules/spacelift-worker-pool/providers.tf rename to deprecated/spacelift-worker-pool/providers.tf diff --git a/modules/spacelift-worker-pool/remote-state.tf b/deprecated/spacelift-worker-pool/remote-state.tf similarity index 100% rename from modules/spacelift-worker-pool/remote-state.tf rename to deprecated/spacelift-worker-pool/remote-state.tf diff --git a/modules/spacelift-worker-pool/templates/spacelift@.service b/deprecated/spacelift-worker-pool/templates/spacelift@.service similarity index 100% rename from modules/spacelift-worker-pool/templates/spacelift@.service rename to deprecated/spacelift-worker-pool/templates/spacelift@.service diff --git a/modules/spacelift-worker-pool/templates/user-data.sh b/deprecated/spacelift-worker-pool/templates/user-data.sh similarity index 100% rename from modules/spacelift-worker-pool/templates/user-data.sh rename to deprecated/spacelift-worker-pool/templates/user-data.sh diff --git a/modules/spacelift-worker-pool/variables.tf b/deprecated/spacelift-worker-pool/variables.tf similarity index 100% rename from modules/spacelift-worker-pool/variables.tf rename to deprecated/spacelift-worker-pool/variables.tf diff --git a/modules/spacelift-worker-pool/versions.tf b/deprecated/spacelift-worker-pool/versions.tf similarity index 100% rename from modules/spacelift-worker-pool/versions.tf rename to deprecated/spacelift-worker-pool/versions.tf diff --git a/deprecated/spacelift/README.md b/deprecated/spacelift/README.md new file mode 100644 index 000000000..91b5bc9ce --- /dev/null +++ b/deprecated/spacelift/README.md @@ -0,0 +1,446 @@ +# Component: `spacelift` + +This component is responsible for provisioning Spacelift stacks. + +Spacelift is a specialized, Terraform-compatible continuous integration and deployment (CI/CD) platform for +infrastructure-as-code. It's designed and implemented by long-time DevOps practitioners based on previous experience with +large-scale installations - dozens of teams, hundreds of engineers and tens of thousands of cloud resources. + +## Usage + +**Stack Level**: Regional + +This component provisions an administrative Spacelift stack and assigns it to a worker pool. Although +the stack can manage stacks in any region, it should be provisioned in the same region as the worker pool. + +```yaml +components: + terraform: + spacelift/defaults: + metadata: + type: abstract + component: spacelift + settings: + spacelift: + workspace_enabled: true + administrative: true + autodeploy: true + before_init: + - spacelift-configure + - spacelift-write-vars + - spacelift-tf-workspace + before_plan: + - spacelift-configure + before_apply: + - spacelift-configure + component_root: components/terraform/spacelift + description: Spacelift Administrative stack + stack_destructor_enabled: false + # TODO: replace with the name of the worker pool + worker_pool_name: WORKER_POOL_NAME + repository: infra + branch: main + labels: + - folder:admin + # Do not add normal set of child policies to admin stacks + policies_enabled: [] + policies_by_id_enabled: [] + vars: + enabled: true + spacelift_api_endpoint: https://TODO.app.spacelift.io + administrative_stack_drift_detection_enabled: true + administrative_stack_drift_detection_reconcile: true + administrative_stack_drift_detection_schedule: ["0 4 * * *"] + administrative_trigger_policy_enabled: false + autodeploy: false + aws_role_enabled: false + drift_detection_enabled: true + drift_detection_reconcile: true + drift_detection_schedule: ["0 4 * * *"] + external_execution: true + git_repository: infra # TODO: replace with your repository name + git_branch: main + + # List of available default Rego policies to create in Spacelift. + # These policies are defined in the catalog https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies + # These policies will not be attached to Spacelift stacks by default (but will be created in Spacelift, and could be attached to a stack manually). + # For specify policies to attach to each Spacelift stack, use `var.policies_enabled`. + policies_available: + - "git_push.proposed-run" + - "git_push.tracked-run" + - "plan.default" + - "trigger.dependencies" + - "trigger.retries" + + # List of default Rego policies to attach to all Spacelift stacks. + # These policies are defined in the catalog https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies + policies_enabled: + - "git_push.proposed-run" + - "git_push.tracked-run" + - "plan.default" + - "trigger.dependencies" + + # List of custom policy names to attach to all Spacelift stacks + # These policies must exist in `components/terraform/spacelift/rego-policies` + policies_by_name_enabled: [] + + runner_image: 000000000000.dkr.ecr.us-west-2.amazonaws.com/infra #TODO: replace with your ECR repository + spacelift_component_path: components/terraform + stack_config_path_template: stacks/%s.yaml + stack_destructor_enabled: false + worker_pool_name_id_map: + -spacelift-worker-pool: SOMEWORKERPOOLID #TODO: replace with your worker pool ID + infracost_enabled: false # TODO: decide on infracost + terraform_version: "1.3.6" + terraform_version_map: + "1": "1.3.6" + + # These could be moved to $PROJECT_ROOT/.spacelift/config.yml + before_init: + - spacelift-configure + - spacelift-write-vars + - spacelift-tf-workspace + before_plan: + - spacelift-configure + before_apply: + - spacelift-configure + + # Manages policies, admin stacks, and core OU accounts + spacelift: + metadata: + component: spacelift + inherits: + - spacelift/defaults + settings: + spacelift: + policies_by_id_enabled: + # This component also creates this policy so this is omitted prior to the first apply + # then added so it's consistent with all admin stacks. + - trigger-administrative-policy + vars: + enabled: true + # Use context_filters to split up admin stack management + # context_filters: + # stages: + # - artifacts + # - audit + # - auto + # - corp + # - dns + # - identity + # - marketplace + # - network + # - public + # - security + # These are the policies created from https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies + # Make sure to remove the .rego suffix + policies_available: + - git_push.proposed-run + - git_push.tracked-run + - plan.default + - trigger.dependencies + - trigger.retries + # This is to auto deploy launch template image id changes + - plan.warn-on-resource-changes-except-image-id + # This is the global admin policy + - trigger.administrative + # These are the policies added to each spacelift stack created by this admin stack + policies_enabled: + - git_push.proposed-run + - git_push.tracked-run + - plan.default + - trigger.dependencies + # Keep these empty + policies_by_id_enabled: [] + +``` + +## Prerequisites + +### GitHub Integration + +1. The GitHub owner will need to sign up for a [free trial of Spacelift](https://spacelift.io/free-trial.html) +1. Once an account is created take note of the URL - usually its `https://.app.spacelift.io/` +1. Create a Login Policy + + - Click on Policies then Add Policy + - Use the following policy and replace `GITHUBORG` with the GitHub Organization slug and DEV with the GitHub id for the Dev setting up the Spacelift module. + + ```rego + package spacelift + + # See https://docs.spacelift.io/concepts/policy/login-policy for implementation details. + # Note: Login policies don't affect GitHub organization or SSO admins. + # Note 2: Enabling SSO requires that all users have an IdP (G Suite) account, so we'll just use + # GitHub authentication in the meantime while working with external collaborators. + # Map session input data to human friendly variables to use in policy evaluation + + username := input.session.login + member_of := input.session.teams # Input is friendly name, e.g. "SRE" not "sre" or "@GITHUBORG/sre" + GITHUBORG := input.session.member # Is this user a member of the CUSTOMER GitHub org? + + # Define GitHub usernames of non org external collaborators with admin vs. user access + admin_collaborators := { "DEV" } + user_collaborators := { "GITHUBORG" } # Using GITHUBORG as a placeholder to avoid empty set + + # Grant admin access to GITHUBORG org members in the CloudPosse group + admin { + GITHUBORG + member_of[_] == "CloudPosse" + } + + # Grant admin access to non-GITHUBORG org accounts in the admin_collaborators set + admin { + # not GITHUBORG + admin_collaborators[username] + } + + # Grant user access to GITHUBORG org members in the Developers group + # allow { + # GITHUBORG + # member_of[_] == "Developers" + # } + + # Grant user access to non-GITHUBORG org accounts in the user_collaborators set + allow { + not GITHUBORG + user_collaborators[username] + } + + # Deny access to any non-GITHUBORG org accounts who aren't defined in external collaborators sets + deny { + not GITHUBORG + not user_collaborators[username] + not admin_collaborators[username] + } + + # Grant spaces read only user access to all members + space_read[space.id] { + space := input.spaces[_] + GITHUBORG + } + + # Grant spaces write access to GITHUBORG org members in the Developers group + # space_write[space.id] { + # space := input.spaces[_] + # member_of[_] == "Developers" + # } + ``` + +## Spacelift Layout + +[Runtime configuration](https://docs.spacelift.io/concepts/configuration/runtime-configuration) is a piece of setup +that is applied to individual runs instead of being global to the stack. +It's defined in `.spacelift/config.yml` YAML file at the root of your repository. +It is required for Spacelift to work with `atmos`. + +### Create Spacelift helper scripts + +[/rootfs/usr/local/bin/spacelift-tf-workspace](/rootfs/usr/local/bin/spacelift-tf-workspace) manages selecting or creating a Terraform workspace; similar to how `atmos` manages workspaces +during a Terraform run. + +[/rootfs/usr/local/bin/spacelift-write-vars](/rootfs/usr/local/bin/spacelift-write-vars) writes the component config using `atmos` to the `spacelift.auto.tfvars.json` file. + +**NOTE**: make sure they are all executable: + +```bash +chmod +x rootfs/usr/local/bin/spacelift* +``` + +## Bootstrapping + +After creating & linking Spacelift to this repo (see the +[docs](https://docs.spacelift.io/integrations/github)), follow these steps... + +### Deploy the [`spacelift-worker-pool`](../spacelift-worker-pool) Component + +See [`spacelift-worker-pool` README](../spacelift-worker-pool/README.md) for the configuration and deployment needs. + +### Update the `spacelift` catalog + +1. `git_repository` = Name of `infrastructure` repository +1. `git_branch` = Name of main/master branch +1. `worker_pool_name_id_map` = Map of arbitrary names to IDs Spacelift worker pools, +taken from the `worker_pool_id` output of the `spacelift-worker-pool` component. +1. Set `components.terraform.spacelift.settings.spacelift.worker_pool_name` +to the name of the worker pool you want to use for the `spacelift` component, +the name being the key you set in the `worker_pool_name_id_map` map. + + +### Deploy the admin stacks + +Set these ENV vars: + +```bash +export SPACELIFT_API_KEY_ENDPOINT=https://.app.spacelift.io +export SPACELIFT_API_KEY_ID=... +export SPACELIFT_API_KEY_SECRET=... +``` + +The name of the spacelift stack resource will be different depending on the name of the component and the root atmos stack. +This would be the command if the root atmos stack is `core-gbl-auto` and the spacelift component is `spacelift`. + +``` +atmos terraform apply spacelift --stack core-gbl-auto -target 'module.spacelift.module.stacks["core-gbl-auto-spacelift"]' +``` + +Note that this is the only manually operation you need to perform in `geodesic` using `atmos` to create the initial admin stack. +All other infrastructure stacks wil be created in Spacelift by this admin stack. + + +## Pull Request Workflow + +1. Create a new branch & make changes +2. Create a new pull request (targeting the `main` branch) +3. View the modified resources directly in the pull request +4. View the successful Spacelift checks in the pull request +5. Merge the pull request and check the Spacelift job + + +## spacectl + +See docs https://github.com/spaceone-dev/spacectl + +### Install + +``` +⨠ apt install -y spacectl -qq +``` + +Setup a profile + +``` +⨠ spacectl profile login gbl-identity +Enter Spacelift endpoint (eg. https://unicorn.app.spacelift.io/): https://.app.spacelift.io +Select credentials type: 1 for API key, 2 for GitHub access token: 1 +Enter API key ID: 01FKN... +Enter API key secret: +``` + +### Listing stacks + +```bash +spacectl stack list +``` + +Grab all the stack ids (use the JSON output to avoid bad chars) + +```bash +spacectl stack list --output json | jq -r '.[].id' > stacks.txt +``` + +If the latest commit for each stack is desired, run something like this. + +NOTE: remove the `echo` to remove the dry-run functionality + +```bash +cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-current-commit --sha 25dd359749cfe30c76cce19f58e0a33555256afd --id $stack; done +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [aws](#requirement\_aws) | >= 4.0 | +| [spacelift](#requirement\_spacelift) | >= 0.1.31 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.55.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ssm_parameter.spacelift_key_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.spacelift_key_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [administrative\_push\_policy\_enabled](#input\_administrative\_push\_policy\_enabled) | Flag to enable/disable the global administrative push policy | `bool` | `true` | no | +| [administrative\_stack\_drift\_detection\_enabled](#input\_administrative\_stack\_drift\_detection\_enabled) | Flag to enable/disable administrative stack drift detection | `bool` | `true` | no | +| [administrative\_stack\_drift\_detection\_reconcile](#input\_administrative\_stack\_drift\_detection\_reconcile) | Flag to enable/disable administrative stack drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | +| [administrative\_stack\_drift\_detection\_schedule](#input\_administrative\_stack\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the administrative stack | `list(string)` |
[
"0 4 * * *"
]
| no | +| [administrative\_trigger\_policy\_enabled](#input\_administrative\_trigger\_policy\_enabled) | Flag to enable/disable the global administrative trigger policy | `bool` | `true` | no | +| [attachment\_space\_id](#input\_attachment\_space\_id) | Specify the space ID for attachments (e.g. policies, contexts, etc.) | `string` | `"legacy"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [autodeploy](#input\_autodeploy) | Default autodeploy value for all stacks created by this project | `bool` | n/a | yes | +| [aws\_role\_arn](#input\_aws\_role\_arn) | ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment | `string` | `null` | no | +| [aws\_role\_enabled](#input\_aws\_role\_enabled) | Flag to enable/disable Spacelift to use AWS STS to assume the supplied IAM role and put its temporary credentials in the runtime environment | `bool` | `false` | no | +| [aws\_role\_external\_id](#input\_aws\_role\_external\_id) | Custom external ID (works only for private workers). See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html for more details | `string` | `null` | no | +| [aws\_role\_generate\_credentials\_in\_worker](#input\_aws\_role\_generate\_credentials\_in\_worker) | Flag to enable/disable generating AWS credentials in the private worker after assuming the supplied IAM role | `bool` | `false` | no | +| [before\_init](#input\_before\_init) | List of before-init scripts | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [context\_filters](#input\_context\_filters) | Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`. | `map(list(string))` | `{}` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [drift\_detection\_enabled](#input\_drift\_detection\_enabled) | Flag to enable/disable drift detection on the infrastructure stacks | `bool` | `true` | no | +| [drift\_detection\_reconcile](#input\_drift\_detection\_reconcile) | Flag to enable/disable infrastructure stacks drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | +| [drift\_detection\_schedule](#input\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the infrastructure stacks | `list(string)` |
[
"0 4 * * *"
]
| no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [external\_execution](#input\_external\_execution) | Set this to true if you're calling this module from outside of a Spacelift stack (e.g. the `complete` example) | `bool` | `false` | no | +| [git\_branch](#input\_git\_branch) | The Git branch name | `string` | `"main"` | no | +| [git\_commit\_sha](#input\_git\_commit\_sha) | The commit SHA for which to trigger a run. Requires `var.spacelift_run_enabled` to be set to `true` | `string` | `null` | no | +| [git\_repository](#input\_git\_repository) | The Git repository name | `string` | n/a | yes | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [infracost\_enabled](#input\_infracost\_enabled) | Flag to enable/disable infracost. If this is enabled, it will add infracost label to each stack. See [spacelift infracost](https://docs.spacelift.io/vendors/terraform/infracost) docs for more details. | `bool` | `false` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [policies\_available](#input\_policies\_available) | List of available default policies to create in Spacelift (these policies will not be attached to Spacelift stacks by default, use `var.policies_enabled`) | `list(string)` |
[
"git_push.proposed-run",
"git_push.tracked-run",
"plan.default",
"trigger.dependencies",
"trigger.retries"
]
| no | +| [policies\_by\_id\_enabled](#input\_policies\_by\_id\_enabled) | List of existing policy IDs to attach to all Spacelift stacks. These policies must already exist in Spacelift | `list(string)` | `[]` | no | +| [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of existing policy names to attach to all Spacelift stacks. These policies must exist at `modules/spacelift/rego-policies` OR `var.policies_by_name_path`. | `list(string)` | `[]` | no | +| [policies\_by\_name\_path](#input\_policies\_by\_name\_path) | Path to the catalog of external Rego policies. The Rego files must exist in the caller's code at the path. The module will create Spacelift policies from the external Rego definitions | `string` | `""` | no | +| [policies\_enabled](#input\_policies\_enabled) | DEPRECATED: Use `policies_by_id_enabled` instead. List of default policies created by this stack to attach to all Spacelift stacks | `list(string)` | `[]` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [runner\_image](#input\_runner\_image) | Full address & tag of the Spacelift runner image (e.g. on ECR) | `string` | n/a | yes | +| [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | +| [spacelift\_component\_path](#input\_spacelift\_component\_path) | The Spacelift Component Path | `string` | `"components/terraform"` | no | +| [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | +| [spacelift\_stack\_dependency\_enabled](#input\_spacelift\_stack\_dependency\_enabled) | If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached | `bool` | `false` | no | +| [stack\_config\_path\_template](#input\_stack\_config\_path\_template) | Stack config path template | `string` | `"stacks/%s.yaml"` | no | +| [stack\_destructor\_enabled](#input\_stack\_destructor\_enabled) | Flag to enable/disable the stack destructor to destroy the resources of a stack before deleting the stack itself | `bool` | `false` | no | +| [stacks\_space\_id](#input\_stacks\_space\_id) | Override the space ID for all stacks (unless the stack config has `dedicated_space` set to true). Otherwise, it will default to the admin stack's space. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tag\_filters](#input\_tag\_filters) | A map of tags that will filter stack creation by the matching `tags` set in a component `vars` configuration. | `map(string)` | `{}` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [terraform\_version](#input\_terraform\_version) | Default Terraform version for all stacks created by this project | `string` | n/a | yes | +| [terraform\_version\_map](#input\_terraform\_version\_map) | A map to determine which Terraform patch version to use for each minor version | `map(string)` | `{}` | no | +| [worker\_pool\_name\_id\_map](#input\_worker\_pool\_name\_id\_map) | Map of worker pool names to worker pool IDs | `map(any)` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [stacks](#output\_stacks) | Spacelift stacks | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spacelift) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/spacelift/context.tf b/deprecated/spacelift/context.tf similarity index 100% rename from modules/spacelift/context.tf rename to deprecated/spacelift/context.tf diff --git a/modules/spacelift/docs/example-spacelift-config.yml b/deprecated/spacelift/docs/example-spacelift-config.yml similarity index 100% rename from modules/spacelift/docs/example-spacelift-config.yml rename to deprecated/spacelift/docs/example-spacelift-config.yml diff --git a/modules/spacelift/docs/example-stacks_catalog_spacelift.yaml b/deprecated/spacelift/docs/example-stacks_catalog_spacelift.yaml similarity index 100% rename from modules/spacelift/docs/example-stacks_catalog_spacelift.yaml rename to deprecated/spacelift/docs/example-stacks_catalog_spacelift.yaml diff --git a/modules/spacelift/docs/example-stacks_deployed_spacelift.yaml b/deprecated/spacelift/docs/example-stacks_deployed_spacelift.yaml similarity index 100% rename from modules/spacelift/docs/example-stacks_deployed_spacelift.yaml rename to deprecated/spacelift/docs/example-stacks_deployed_spacelift.yaml diff --git a/modules/spacelift/docs/img/Spacelift-Infrastructure-Behavior.png b/deprecated/spacelift/docs/img/Spacelift-Infrastructure-Behavior.png similarity index 100% rename from modules/spacelift/docs/img/Spacelift-Infrastructure-Behavior.png rename to deprecated/spacelift/docs/img/Spacelift-Infrastructure-Behavior.png diff --git a/modules/spacelift/docs/img/Spacelift-Merge-Execution.png b/deprecated/spacelift/docs/img/Spacelift-Merge-Execution.png similarity index 100% rename from modules/spacelift/docs/img/Spacelift-Merge-Execution.png rename to deprecated/spacelift/docs/img/Spacelift-Merge-Execution.png diff --git a/modules/spacelift/docs/img/Spacelift-PR-Changes.png b/deprecated/spacelift/docs/img/Spacelift-PR-Changes.png similarity index 100% rename from modules/spacelift/docs/img/Spacelift-PR-Changes.png rename to deprecated/spacelift/docs/img/Spacelift-PR-Changes.png diff --git a/modules/spacelift/docs/img/Spacelift-PR-Checks.png b/deprecated/spacelift/docs/img/Spacelift-PR-Checks.png similarity index 100% rename from modules/spacelift/docs/img/Spacelift-PR-Checks.png rename to deprecated/spacelift/docs/img/Spacelift-PR-Checks.png diff --git a/modules/spacelift/docs/spacectl.md b/deprecated/spacelift/docs/spacectl.md similarity index 100% rename from modules/spacelift/docs/spacectl.md rename to deprecated/spacelift/docs/spacectl.md diff --git a/modules/spacelift/docs/spacelift-overview.md b/deprecated/spacelift/docs/spacelift-overview.md similarity index 100% rename from modules/spacelift/docs/spacelift-overview.md rename to deprecated/spacelift/docs/spacelift-overview.md diff --git a/modules/spacelift/main.tf b/deprecated/spacelift/main.tf similarity index 100% rename from modules/spacelift/main.tf rename to deprecated/spacelift/main.tf diff --git a/modules/spacelift/outputs.tf b/deprecated/spacelift/outputs.tf similarity index 100% rename from modules/spacelift/outputs.tf rename to deprecated/spacelift/outputs.tf diff --git a/modules/spacelift/providers.tf b/deprecated/spacelift/providers.tf similarity index 100% rename from modules/spacelift/providers.tf rename to deprecated/spacelift/providers.tf diff --git a/modules/spacelift/rego-policies/access.default.rego b/deprecated/spacelift/rego-policies/access.default.rego similarity index 100% rename from modules/spacelift/rego-policies/access.default.rego rename to deprecated/spacelift/rego-policies/access.default.rego diff --git a/modules/spacelift/rego-policies/plan.autodeployupdates.rego b/deprecated/spacelift/rego-policies/plan.autodeployupdates.rego similarity index 100% rename from modules/spacelift/rego-policies/plan.autodeployupdates.rego rename to deprecated/spacelift/rego-policies/plan.autodeployupdates.rego diff --git a/modules/spacelift/rego-policies/plan.ecr.rego b/deprecated/spacelift/rego-policies/plan.ecr.rego similarity index 100% rename from modules/spacelift/rego-policies/plan.ecr.rego rename to deprecated/spacelift/rego-policies/plan.ecr.rego diff --git a/modules/spacelift/spacelift-provider.tf b/deprecated/spacelift/spacelift-provider.tf similarity index 100% rename from modules/spacelift/spacelift-provider.tf rename to deprecated/spacelift/spacelift-provider.tf diff --git a/modules/spacelift/variables.tf b/deprecated/spacelift/variables.tf similarity index 100% rename from modules/spacelift/variables.tf rename to deprecated/spacelift/variables.tf diff --git a/modules/spacelift/versions.tf b/deprecated/spacelift/versions.tf similarity index 100% rename from modules/spacelift/versions.tf rename to deprecated/spacelift/versions.tf diff --git a/modules/spacelift-worker-pool/default.auto.tfvars b/modules/spacelift-worker-pool/default.auto.tfvars deleted file mode 100644 index f711bf5f0..000000000 --- a/modules/spacelift-worker-pool/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -enabled = false - -github_netrc_enabled = true diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index 91b5bc9ce..97a1e8350 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -1,446 +1,61 @@ # Component: `spacelift` -This component is responsible for provisioning Spacelift stacks. - -Spacelift is a specialized, Terraform-compatible continuous integration and deployment (CI/CD) platform for -infrastructure-as-code. It's designed and implemented by long-time DevOps practitioners based on previous experience with -large-scale installations - dozens of teams, hundreds of engineers and tens of thousands of cloud resources. - -## Usage - -**Stack Level**: Regional - -This component provisions an administrative Spacelift stack and assigns it to a worker pool. Although -the stack can manage stacks in any region, it should be provisioned in the same region as the worker pool. - -```yaml -components: - terraform: - spacelift/defaults: - metadata: - type: abstract - component: spacelift - settings: - spacelift: - workspace_enabled: true - administrative: true - autodeploy: true - before_init: - - spacelift-configure - - spacelift-write-vars - - spacelift-tf-workspace - before_plan: - - spacelift-configure - before_apply: - - spacelift-configure - component_root: components/terraform/spacelift - description: Spacelift Administrative stack - stack_destructor_enabled: false - # TODO: replace with the name of the worker pool - worker_pool_name: WORKER_POOL_NAME - repository: infra - branch: main - labels: - - folder:admin - # Do not add normal set of child policies to admin stacks - policies_enabled: [] - policies_by_id_enabled: [] - vars: - enabled: true - spacelift_api_endpoint: https://TODO.app.spacelift.io - administrative_stack_drift_detection_enabled: true - administrative_stack_drift_detection_reconcile: true - administrative_stack_drift_detection_schedule: ["0 4 * * *"] - administrative_trigger_policy_enabled: false - autodeploy: false - aws_role_enabled: false - drift_detection_enabled: true - drift_detection_reconcile: true - drift_detection_schedule: ["0 4 * * *"] - external_execution: true - git_repository: infra # TODO: replace with your repository name - git_branch: main - - # List of available default Rego policies to create in Spacelift. - # These policies are defined in the catalog https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies - # These policies will not be attached to Spacelift stacks by default (but will be created in Spacelift, and could be attached to a stack manually). - # For specify policies to attach to each Spacelift stack, use `var.policies_enabled`. - policies_available: - - "git_push.proposed-run" - - "git_push.tracked-run" - - "plan.default" - - "trigger.dependencies" - - "trigger.retries" - - # List of default Rego policies to attach to all Spacelift stacks. - # These policies are defined in the catalog https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies - policies_enabled: - - "git_push.proposed-run" - - "git_push.tracked-run" - - "plan.default" - - "trigger.dependencies" - - # List of custom policy names to attach to all Spacelift stacks - # These policies must exist in `components/terraform/spacelift/rego-policies` - policies_by_name_enabled: [] - - runner_image: 000000000000.dkr.ecr.us-west-2.amazonaws.com/infra #TODO: replace with your ECR repository - spacelift_component_path: components/terraform - stack_config_path_template: stacks/%s.yaml - stack_destructor_enabled: false - worker_pool_name_id_map: - -spacelift-worker-pool: SOMEWORKERPOOLID #TODO: replace with your worker pool ID - infracost_enabled: false # TODO: decide on infracost - terraform_version: "1.3.6" - terraform_version_map: - "1": "1.3.6" - - # These could be moved to $PROJECT_ROOT/.spacelift/config.yml - before_init: - - spacelift-configure - - spacelift-write-vars - - spacelift-tf-workspace - before_plan: - - spacelift-configure - before_apply: - - spacelift-configure - - # Manages policies, admin stacks, and core OU accounts - spacelift: - metadata: - component: spacelift - inherits: - - spacelift/defaults - settings: - spacelift: - policies_by_id_enabled: - # This component also creates this policy so this is omitted prior to the first apply - # then added so it's consistent with all admin stacks. - - trigger-administrative-policy - vars: - enabled: true - # Use context_filters to split up admin stack management - # context_filters: - # stages: - # - artifacts - # - audit - # - auto - # - corp - # - dns - # - identity - # - marketplace - # - network - # - public - # - security - # These are the policies created from https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies - # Make sure to remove the .rego suffix - policies_available: - - git_push.proposed-run - - git_push.tracked-run - - plan.default - - trigger.dependencies - - trigger.retries - # This is to auto deploy launch template image id changes - - plan.warn-on-resource-changes-except-image-id - # This is the global admin policy - - trigger.administrative - # These are the policies added to each spacelift stack created by this admin stack - policies_enabled: - - git_push.proposed-run - - git_push.tracked-run - - plan.default - - trigger.dependencies - # Keep these empty - policies_by_id_enabled: [] - -``` - -## Prerequisites - -### GitHub Integration - -1. The GitHub owner will need to sign up for a [free trial of Spacelift](https://spacelift.io/free-trial.html) -1. Once an account is created take note of the URL - usually its `https://.app.spacelift.io/` -1. Create a Login Policy - - - Click on Policies then Add Policy - - Use the following policy and replace `GITHUBORG` with the GitHub Organization slug and DEV with the GitHub id for the Dev setting up the Spacelift module. - - ```rego - package spacelift - - # See https://docs.spacelift.io/concepts/policy/login-policy for implementation details. - # Note: Login policies don't affect GitHub organization or SSO admins. - # Note 2: Enabling SSO requires that all users have an IdP (G Suite) account, so we'll just use - # GitHub authentication in the meantime while working with external collaborators. - # Map session input data to human friendly variables to use in policy evaluation - - username := input.session.login - member_of := input.session.teams # Input is friendly name, e.g. "SRE" not "sre" or "@GITHUBORG/sre" - GITHUBORG := input.session.member # Is this user a member of the CUSTOMER GitHub org? - - # Define GitHub usernames of non org external collaborators with admin vs. user access - admin_collaborators := { "DEV" } - user_collaborators := { "GITHUBORG" } # Using GITHUBORG as a placeholder to avoid empty set - - # Grant admin access to GITHUBORG org members in the CloudPosse group - admin { - GITHUBORG - member_of[_] == "CloudPosse" - } - - # Grant admin access to non-GITHUBORG org accounts in the admin_collaborators set - admin { - # not GITHUBORG - admin_collaborators[username] - } - - # Grant user access to GITHUBORG org members in the Developers group - # allow { - # GITHUBORG - # member_of[_] == "Developers" - # } - - # Grant user access to non-GITHUBORG org accounts in the user_collaborators set - allow { - not GITHUBORG - user_collaborators[username] - } - - # Deny access to any non-GITHUBORG org accounts who aren't defined in external collaborators sets - deny { - not GITHUBORG - not user_collaborators[username] - not admin_collaborators[username] - } - - # Grant spaces read only user access to all members - space_read[space.id] { - space := input.spaces[_] - GITHUBORG - } - - # Grant spaces write access to GITHUBORG org members in the Developers group - # space_write[space.id] { - # space := input.spaces[_] - # member_of[_] == "Developers" - # } - ``` - -## Spacelift Layout - -[Runtime configuration](https://docs.spacelift.io/concepts/configuration/runtime-configuration) is a piece of setup -that is applied to individual runs instead of being global to the stack. -It's defined in `.spacelift/config.yml` YAML file at the root of your repository. -It is required for Spacelift to work with `atmos`. - -### Create Spacelift helper scripts - -[/rootfs/usr/local/bin/spacelift-tf-workspace](/rootfs/usr/local/bin/spacelift-tf-workspace) manages selecting or creating a Terraform workspace; similar to how `atmos` manages workspaces -during a Terraform run. - -[/rootfs/usr/local/bin/spacelift-write-vars](/rootfs/usr/local/bin/spacelift-write-vars) writes the component config using `atmos` to the `spacelift.auto.tfvars.json` file. - -**NOTE**: make sure they are all executable: - -```bash -chmod +x rootfs/usr/local/bin/spacelift* -``` +This folder contains a set of components used to manage [Spacelift](https://docs.spacelift.io/) in an +[atmos-opinionated](https://atmos.tools/) way. ## Bootstrapping -After creating & linking Spacelift to this repo (see the -[docs](https://docs.spacelift.io/integrations/github)), follow these steps... - -### Deploy the [`spacelift-worker-pool`](../spacelift-worker-pool) Component +### Environment Setup -See [`spacelift-worker-pool` README](../spacelift-worker-pool/README.md) for the configuration and deployment needs. +Since `spacelift` is designed to automate the plan/apply/destroy cycles of each component instance in an atmos +environment, we have a chicken-and-egg problem where spacelift needs to be configured manually before it can manage the +rest of the infrastructure. This section walks through the initial bootstrapping process. -### Update the `spacelift` catalog +1. First, to authenticate to `spacelift` from outside of the spacelift environment, we are going to use the + [spacectl](https://github.com/spacelift-io/spacectl) command-line interface tool. If you use a + [geodesic](https://github.com/cloudposse/geodesic) shell configured by Cloud Posse, this is already installed. If you + use another method, follow the installation instructions in the [spacectl](https://github.com/spacelift-io/spacectl) + repo. -1. `git_repository` = Name of `infrastructure` repository -1. `git_branch` = Name of main/master branch -1. `worker_pool_name_id_map` = Map of arbitrary names to IDs Spacelift worker pools, -taken from the `worker_pool_id` output of the `spacelift-worker-pool` component. -1. Set `components.terraform.spacelift.settings.spacelift.worker_pool_name` -to the name of the worker pool you want to use for the `spacelift` component, -the name being the key you set in the `worker_pool_name_id_map` map. +1. Now that we have `spacectl` installed, let's configure it for our `spacelift` instance: + 1. Run `spacectl profile login acme` replacing `acme` with your company's name. + 1. Enter your company's spacelift URL when prompted (e.g., https://acme.app.spacelift.io) + 1. Select `for login with a web browser` as your authentication type + 1. Your browser will be opened, and you will log in to spacelift. -### Deploy the admin stacks - -Set these ENV vars: +1. We can now use the `spacectl` CLI to export an environment variable, allowing Terraform to manage spacelift resources. ```bash -export SPACELIFT_API_KEY_ENDPOINT=https://.app.spacelift.io -export SPACELIFT_API_KEY_ID=... -export SPACELIFT_API_KEY_SECRET=... +export SPACELIFT_API_TOKEN=$(spacectl profile export-token) ``` -The name of the spacelift stack resource will be different depending on the name of the component and the root atmos stack. -This would be the command if the root atmos stack is `core-gbl-auto` and the spacelift component is `spacelift`. - -``` -atmos terraform apply spacelift --stack core-gbl-auto -target 'module.spacelift.module.stacks["core-gbl-auto-spacelift"]' -``` - -Note that this is the only manually operation you need to perform in `geodesic` using `atmos` to create the initial admin stack. -All other infrastructure stacks wil be created in Spacelift by this admin stack. - - -## Pull Request Workflow - -1. Create a new branch & make changes -2. Create a new pull request (targeting the `main` branch) -3. View the modified resources directly in the pull request -4. View the successful Spacelift checks in the pull request -5. Merge the pull request and check the Spacelift job - - -## spacectl - -See docs https://github.com/spaceone-dev/spacectl - -### Install - -``` -⨠ apt install -y spacectl -qq -``` +4. Finally, log in to [AWS using Leapp](https://docs.cloudposse.com/howto/geodesic/authenticate-with-leapp) so that you have access to the Terraform state bucket. -Setup a profile +### Applying Components -``` -⨠ spacectl profile login gbl-identity -Enter Spacelift endpoint (eg. https://unicorn.app.spacelift.io/): https://.app.spacelift.io -Select credentials type: 1 for API key, 2 for GitHub access token: 1 -Enter API key ID: 01FKN... -Enter API key secret: -``` +With our environment configured, we can now apply the base components necessary for `spacelift` to manage the rest of the environment. -### Listing stacks +1. Create spacelift [spaces](https://docs.spacelift.io/concepts/spaces/) by configuring and applying the [spaces](./spaces/) component. ```bash -spacectl stack list +atmos terraform apply spacelift/spaces -s infra-gbl-root ``` -Grab all the stack ids (use the JSON output to avoid bad chars) +2. Create one or more spacelift [worker pools](https://docs.spacelift.io/concepts/worker-pools) by configuring and applying the [worker-pool](./worker-pool/) component. ```bash -spacectl stack list --output json | jq -r '.[].id' > stacks.txt +atmos terraform apply spacelift/worker-pool -s core-ue2-auto ``` -If the latest commit for each stack is desired, run something like this. +3. Create the root spacelift [stack](https://docs.spacelift.io/concepts/stack/) by configuring and applying the [admin-stack](./admin-stack/) component. -NOTE: remove the `echo` to remove the dry-run functionality +NOTE: Before running this command, make sure all of your code changes to configure these components have been merged to your `main` branch, and you have `main` checked out. ```bash -cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-current-commit --sha 25dd359749cfe30c76cce19f58e0a33555256afd --id $stack; done +atmos terraform apply spacelift/admin-stack -s infra-gbl-root -var "spacelift_run_enabled=true" -var "commit_sha=$(git rev-parse HEAD)" ``` +Running this command will create the `root` spacelift admin stack, which will, in turn, read your atmos stack config and create all other spacelift admin stacks defined in the config. - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.3 | -| [aws](#requirement\_aws) | >= 4.0 | -| [spacelift](#requirement\_spacelift) | >= 0.1.31 | - -## Providers - -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | >= 4.0 | - -## Modules - -| Name | Source | Version | -|------|--------|---------| -| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.55.0 | -| [this](#module\_this) | cloudposse/label/null | 0.25.0 | - -## Resources - -| Name | Type | -|------|------| -| [aws_ssm_parameter.spacelift_key_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [aws_ssm_parameter.spacelift_key_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [administrative\_push\_policy\_enabled](#input\_administrative\_push\_policy\_enabled) | Flag to enable/disable the global administrative push policy | `bool` | `true` | no | -| [administrative\_stack\_drift\_detection\_enabled](#input\_administrative\_stack\_drift\_detection\_enabled) | Flag to enable/disable administrative stack drift detection | `bool` | `true` | no | -| [administrative\_stack\_drift\_detection\_reconcile](#input\_administrative\_stack\_drift\_detection\_reconcile) | Flag to enable/disable administrative stack drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | -| [administrative\_stack\_drift\_detection\_schedule](#input\_administrative\_stack\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the administrative stack | `list(string)` |
[
"0 4 * * *"
]
| no | -| [administrative\_trigger\_policy\_enabled](#input\_administrative\_trigger\_policy\_enabled) | Flag to enable/disable the global administrative trigger policy | `bool` | `true` | no | -| [attachment\_space\_id](#input\_attachment\_space\_id) | Specify the space ID for attachments (e.g. policies, contexts, etc.) | `string` | `"legacy"` | no | -| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [autodeploy](#input\_autodeploy) | Default autodeploy value for all stacks created by this project | `bool` | n/a | yes | -| [aws\_role\_arn](#input\_aws\_role\_arn) | ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment | `string` | `null` | no | -| [aws\_role\_enabled](#input\_aws\_role\_enabled) | Flag to enable/disable Spacelift to use AWS STS to assume the supplied IAM role and put its temporary credentials in the runtime environment | `bool` | `false` | no | -| [aws\_role\_external\_id](#input\_aws\_role\_external\_id) | Custom external ID (works only for private workers). See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html for more details | `string` | `null` | no | -| [aws\_role\_generate\_credentials\_in\_worker](#input\_aws\_role\_generate\_credentials\_in\_worker) | Flag to enable/disable generating AWS credentials in the private worker after assuming the supplied IAM role | `bool` | `false` | no | -| [before\_init](#input\_before\_init) | List of before-init scripts | `list(string)` | `[]` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [context\_filters](#input\_context\_filters) | Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`. | `map(list(string))` | `{}` | no | -| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [drift\_detection\_enabled](#input\_drift\_detection\_enabled) | Flag to enable/disable drift detection on the infrastructure stacks | `bool` | `true` | no | -| [drift\_detection\_reconcile](#input\_drift\_detection\_reconcile) | Flag to enable/disable infrastructure stacks drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | -| [drift\_detection\_schedule](#input\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the infrastructure stacks | `list(string)` |
[
"0 4 * * *"
]
| no | -| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [external\_execution](#input\_external\_execution) | Set this to true if you're calling this module from outside of a Spacelift stack (e.g. the `complete` example) | `bool` | `false` | no | -| [git\_branch](#input\_git\_branch) | The Git branch name | `string` | `"main"` | no | -| [git\_commit\_sha](#input\_git\_commit\_sha) | The commit SHA for which to trigger a run. Requires `var.spacelift_run_enabled` to be set to `true` | `string` | `null` | no | -| [git\_repository](#input\_git\_repository) | The Git repository name | `string` | n/a | yes | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | -| [infracost\_enabled](#input\_infracost\_enabled) | Flag to enable/disable infracost. If this is enabled, it will add infracost label to each stack. See [spacelift infracost](https://docs.spacelift.io/vendors/terraform/infracost) docs for more details. | `bool` | `false` | no | -| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | -| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | -| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [policies\_available](#input\_policies\_available) | List of available default policies to create in Spacelift (these policies will not be attached to Spacelift stacks by default, use `var.policies_enabled`) | `list(string)` |
[
"git_push.proposed-run",
"git_push.tracked-run",
"plan.default",
"trigger.dependencies",
"trigger.retries"
]
| no | -| [policies\_by\_id\_enabled](#input\_policies\_by\_id\_enabled) | List of existing policy IDs to attach to all Spacelift stacks. These policies must already exist in Spacelift | `list(string)` | `[]` | no | -| [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of existing policy names to attach to all Spacelift stacks. These policies must exist at `modules/spacelift/rego-policies` OR `var.policies_by_name_path`. | `list(string)` | `[]` | no | -| [policies\_by\_name\_path](#input\_policies\_by\_name\_path) | Path to the catalog of external Rego policies. The Rego files must exist in the caller's code at the path. The module will create Spacelift policies from the external Rego definitions | `string` | `""` | no | -| [policies\_enabled](#input\_policies\_enabled) | DEPRECATED: Use `policies_by_id_enabled` instead. List of default policies created by this stack to attach to all Spacelift stacks | `list(string)` | `[]` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [runner\_image](#input\_runner\_image) | Full address & tag of the Spacelift runner image (e.g. on ECR) | `string` | n/a | yes | -| [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | -| [spacelift\_component\_path](#input\_spacelift\_component\_path) | The Spacelift Component Path | `string` | `"components/terraform"` | no | -| [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | -| [spacelift\_stack\_dependency\_enabled](#input\_spacelift\_stack\_dependency\_enabled) | If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached | `bool` | `false` | no | -| [stack\_config\_path\_template](#input\_stack\_config\_path\_template) | Stack config path template | `string` | `"stacks/%s.yaml"` | no | -| [stack\_destructor\_enabled](#input\_stack\_destructor\_enabled) | Flag to enable/disable the stack destructor to destroy the resources of a stack before deleting the stack itself | `bool` | `false` | no | -| [stacks\_space\_id](#input\_stacks\_space\_id) | Override the space ID for all stacks (unless the stack config has `dedicated_space` set to true). Otherwise, it will default to the admin stack's space. | `string` | `null` | no | -| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tag\_filters](#input\_tag\_filters) | A map of tags that will filter stack creation by the matching `tags` set in a component `vars` configuration. | `map(string)` | `{}` | no | -| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [terraform\_version](#input\_terraform\_version) | Default Terraform version for all stacks created by this project | `string` | n/a | yes | -| [terraform\_version\_map](#input\_terraform\_version\_map) | A map to determine which Terraform patch version to use for each minor version | `map(string)` | `{}` | no | -| [worker\_pool\_name\_id\_map](#input\_worker\_pool\_name\_id\_map) | Map of worker pool names to worker pool IDs | `map(any)` | `{}` | no | - -## Outputs - -| Name | Description | -|------|-------------| -| [stacks](#output\_stacks) | Spacelift stacks | - - -## References - -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spacelift) - Cloud Posse's upstream component - -[](https://cpco.io/component) +Each of these non-root spacelift admin stacks will then be triggered, planned, and applied by spacelift, and they, in turn, will create each of the spacelift stacks they are responsible for managing. diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md new file mode 100644 index 000000000..46c43a781 --- /dev/null +++ b/modules/spacelift/admin-stack/README.md @@ -0,0 +1,208 @@ +# Component: `spacelift/admin-stack` + +This component is responsible for creating an administrative [stack](https://docs.spacelift.io/concepts/stack/) and its +corresponding child stacks in the spacelift organization. + +The component uses a series of `context_fiters` to select atmos component instances to manage as child stacks. + +## Usage + +**Stack Level**: Global + +The following are example snippets of how to use this component: + +```yaml +# stacks/orgs/acme/spacelift.yaml +import: + - mixins/region/global-region + - orgs/acme/_defaults + +vars: + tenant: infra + environment: gbl + stage: root + +components: + terraform: + spacelift/admin-stack: + metadata: + settings: + spacelift: + administrative: true + autodeploy: true + before_apply: + - spacelift-configure-paths + before_init: + - spacelift-configure-paths + - spacelift-write-vars + - spacelift-tf-workspace + before_plan: + - spacelift-configure-paths + drift_detection_enabled: true + drift_detection_reconcile: true + drift_detection_schedule: + - 0 4 * * * + labels: + - admin + - folder:admin + manage_state: false + root_administrative: true + labels: + - root-admin + - admin + policies: {} + vars: + administrative: true + branch: main + child_policy_attachments: + - GIT_PUSH Global Administrator + - TRIGGER Global Administrator + context_filters: + administrative: true # This stack is managing all the other admin stacks + root_administrative: false # We don't want this stack to also find itself in the config and add itself a second time + component_root: components/terraform + enabled: true + labels: + - admin-stack-name:root + repository: infra-live + root_admin_stack: true + root_stack_policy_attachments: + - GIT_PUSH Global Administrator + - TRIGGER Global Administrator + runner_image: 000000000000.dkr.ecr.us-east-2.amazonaws.com/acme/infra-live:latest + spacelift_spaces_environment_name: gbl + spacelift_spaces_stage_name: root + spacelift_spaces_tenant_name: infra + terraform_version: "1.3.9" + worker_pool_name: acme-core-ue2-auto-spacelift-default-worker-pool +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [aws](#requirement\_aws) | >= 4.0 | +| [spacelift](#requirement\_spacelift) | >= 0.1.31 | + +## Providers + +| Name | Version | +|------|---------| +| [null](#provider\_null) | n/a | +| [spacelift](#provider\_spacelift) | >= 0.1.31 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [all\_admin\_stacks\_config](#module\_all\_admin\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.0.0 | +| [child\_stack](#module\_child\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.0.0 | +| [child\_stacks\_config](#module\_child\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.0.0 | +| [root\_admin\_stack](#module\_root\_admin\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.0.0 | +| [root\_admin\_stack\_config](#module\_root\_admin\_stack\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.0.0 | +| [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [null_resource.child_stack_parent_precondition](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [null_resource.public_workers_precondition](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [null_resource.spaces_precondition](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [null_resource.workers_precondition](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [spacelift_policy_attachment.root](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/policy_attachment) | resource | +| [spacelift_policies.this](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/policies) | data source | +| [spacelift_stacks.this](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/stacks) | data source | +| [spacelift_worker_pools.this](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/worker_pools) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_stack\_label](#input\_admin\_stack\_label) | Label to use to identify the admin stack when creating the child stacks | `string` | `"admin-stack-name"` | no | +| [administrative](#input\_administrative) | Whether this stack can manage other stacks | `bool` | `false` | no | +| [allow\_public\_workers](#input\_allow\_public\_workers) | Whether to allow public workers to be used for this stack | `bool` | `false` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [autodeploy](#input\_autodeploy) | Controls the Spacelift 'autodeploy' option for a stack | `bool` | `false` | no | +| [autoretry](#input\_autoretry) | Controls the Spacelift 'autoretry' option for a stack | `bool` | `false` | no | +| [aws\_role\_arn](#input\_aws\_role\_arn) | ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment | `string` | `null` | no | +| [aws\_role\_enabled](#input\_aws\_role\_enabled) | Flag to enable/disable Spacelift to use AWS STS to assume the supplied IAM role and put its temporary credentials in the runtime environment | `bool` | `false` | no | +| [aws\_role\_external\_id](#input\_aws\_role\_external\_id) | Custom external ID (works only for private workers). See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html for more details | `string` | `null` | no | +| [aws\_role\_generate\_credentials\_in\_worker](#input\_aws\_role\_generate\_credentials\_in\_worker) | Flag to enable/disable generating AWS credentials in the private worker after assuming the supplied IAM role | `bool` | `true` | no | +| [azure\_devops](#input\_azure\_devops) | Azure DevOps VCS settings | `map(any)` | `null` | no | +| [bitbucket\_cloud](#input\_bitbucket\_cloud) | Bitbucket Cloud VCS settings | `map(any)` | `null` | no | +| [bitbucket\_datacenter](#input\_bitbucket\_datacenter) | Bitbucket Datacenter VCS settings | `map(any)` | `null` | no | +| [branch](#input\_branch) | Specify which branch to use within your infrastructure repo | `string` | `"main"` | no | +| [child\_policy\_attachments](#input\_child\_policy\_attachments) | List of policy attachments to attach to the child stacks created by this module | `set(string)` | `[]` | no | +| [cloudformation](#input\_cloudformation) | CloudFormation-specific configuration. Presence means this Stack is a CloudFormation Stack. | `map(any)` | `null` | no | +| [commit\_sha](#input\_commit\_sha) | The commit SHA for which to trigger a run. Requires `var.spacelift_run_enabled` to be set to `true` | `string` | `null` | no | +| [component\_env](#input\_component\_env) | Map of component ENV variables | `any` | `{}` | no | +| [component\_root](#input\_component\_root) | The path, relative to the root of the repository, where the component can be found | `string` | n/a | yes | +| [component\_vars](#input\_component\_vars) | All Terraform values to be applied to the stack via a mounted file | `any` | `{}` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [context\_attachments](#input\_context\_attachments) | A list of context IDs to attach to this stack | `set(string)` | `[]` | no | +| [context\_filters](#input\_context\_filters) | Context filters to select atmos stacks matching specific criteria to create as children. |
object({
namespaces = optional(list(string), [])
environments = optional(list(string), [])
tenants = optional(list(string), [])
stages = optional(list(string), [])
tags = optional(map(string), {})
administrative = optional(bool)
root_administrative = optional(bool)
})
| n/a | yes | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input\_description) | Specify description of stack | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [drift\_detection\_enabled](#input\_drift\_detection\_enabled) | Flag to enable/disable drift detection on the infrastructure stacks | `bool` | `false` | no | +| [drift\_detection\_reconcile](#input\_drift\_detection\_reconcile) | Flag to enable/disable infrastructure stacks drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `false` | no | +| [drift\_detection\_schedule](#input\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the infrastructure stacks | `list(string)` |
[
"0 4 * * *"
]
| no | +| [drift\_detection\_timezone](#input\_drift\_detection\_timezone) | Timezone in which the schedule is expressed. Defaults to UTC. | `string` | `null` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github\_enterprise](#input\_github\_enterprise) | GitHub Enterprise (self-hosted) VCS settings | `map(any)` | `null` | no | +| [gitlab](#input\_gitlab) | GitLab VCS settings | `map(any)` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels](#input\_labels) | A list of labels for the stack | `list(string)` | `[]` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [local\_preview\_enabled](#input\_local\_preview\_enabled) | Indicates whether local preview runs can be triggered on this Stack | `bool` | `false` | no | +| [manage\_state](#input\_manage\_state) | Flag to enable/disable manage\_state setting in stack | `bool` | `false` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [parent\_space\_id](#input\_parent\_space\_id) | If creating a dedicated space for this stack, specify the ID of the parent space in Spacelift. | `string` | `null` | no | +| [policy\_ids](#input\_policy\_ids) | Set of Rego policy IDs to attach to this stack | `set(string)` | `[]` | no | +| [protect\_from\_deletion](#input\_protect\_from\_deletion) | Flag to enable/disable deletion protection. | `bool` | `false` | no | +| [pulumi](#input\_pulumi) | Pulumi-specific configuration. Presence means this Stack is a Pulumi Stack. | `map(any)` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | The AWS region to use | `string` | `"us-east-1"` | no | +| [repository](#input\_repository) | The name of your infrastructure repo | `string` | n/a | yes | +| [root\_admin\_stack](#input\_root\_admin\_stack) | Flag to indicate if this stack is the root admin stack. In this case, the stack will be created in the root space and will create all the other admin stacks as children. | `bool` | `false` | no | +| [root\_stack\_policy\_attachments](#input\_root\_stack\_policy\_attachments) | List of policy attachments to attach to the root admin stack | `set(string)` | `[]` | no | +| [runner\_image](#input\_runner\_image) | The full image name and tag of the Docker image to use in Spacelift | `string` | `null` | no | +| [showcase](#input\_showcase) | Showcase settings | `map(any)` | `null` | no | +| [space\_id](#input\_space\_id) | Place the stack in the specified space\_id. | `string` | `"root"` | no | +| [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | +| [spacelift\_spaces\_environment\_name](#input\_spacelift\_spaces\_environment\_name) | The environment name of the spacelift spaces component | `string` | `null` | no | +| [spacelift\_spaces\_stage\_name](#input\_spacelift\_spaces\_stage\_name) | The stage name of the spacelift spaces component | `string` | `null` | no | +| [spacelift\_spaces\_tenant\_name](#input\_spacelift\_spaces\_tenant\_name) | The tenant name of the spacelift spaces component | `string` | `null` | no | +| [spacelift\_stack\_dependency\_enabled](#input\_spacelift\_stack\_dependency\_enabled) | If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached | `bool` | `false` | no | +| [stack\_destructor\_enabled](#input\_stack\_destructor\_enabled) | Flag to enable/disable the stack destructor to destroy the resources of the stack before deleting the stack itself | `bool` | `false` | no | +| [stack\_name](#input\_stack\_name) | The name of the Spacelift stack | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [terraform\_smart\_sanitization](#input\_terraform\_smart\_sanitization) | Whether or not to enable [Smart Sanitization](https://docs.spacelift.io/vendors/terraform/resource-sanitization) which will only sanitize values marked as sensitive. | `bool` | `false` | no | +| [terraform\_version](#input\_terraform\_version) | Specify the version of Terraform to use for the stack | `string` | `null` | no | +| [terraform\_version\_map](#input\_terraform\_version\_map) | A map to determine which Terraform patch version to use for each minor version | `map(string)` | `{}` | no | +| [terraform\_workspace](#input\_terraform\_workspace) | Specify the Terraform workspace to use for the stack | `string` | `null` | no | +| [webhook\_enabled](#input\_webhook\_enabled) | Flag to enable/disable the webhook endpoint to which Spacelift sends the POST requests about run state changes | `bool` | `false` | no | +| [webhook\_endpoint](#input\_webhook\_endpoint) | Webhook endpoint to which Spacelift sends the POST requests about run state changes | `string` | `null` | no | +| [webhook\_secret](#input\_webhook\_secret) | Webhook secret used to sign each POST request so you're able to verify that the requests come from Spacelift | `string` | `null` | no | +| [worker\_pool\_name](#input\_worker\_pool\_name) | The atmos stack name of the worker pool. Example: `acme-core-ue2-auto-spacelift-default-worker-pool` | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [child\_stacks](#output\_child\_stacks) | n/a | +| [root\_stack](#output\_root\_stack) | n/a | +| [root\_stack\_id](#output\_root\_stack\_id) | The stack id | + diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf new file mode 100644 index 000000000..316131bc3 --- /dev/null +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -0,0 +1,130 @@ +locals { + child_stacks = { + for k, v in module.child_stacks_config.spacelift_stacks : k => v + if local.enabled == true && + try(v.settings.spacelift.workspace_enabled, false) == true + } + + child_stack_policies = { + for k, v in module.all_admin_stacks_config.spacelift_stacks : k => v.vars.child_policy_attachments + if local.enabled == true && + try(v.settings.spacelift.workspace_enabled, false) == true && + try(v.vars.child_policy_attachments, null) != null + } + + child_policies = local.create_root_admin_stack ? var.child_policy_attachments : try(local.child_stack_policies[local.managed_by], null) + child_policy_ids = try([for item in local.child_policies : local.policies[item]], []) + admin_stack_label = try([for item in var.labels : item if startswith(item, format("${var.admin_stack_label}:"))][0], null) + managed_by = local.create_root_admin_stack ? local.root_admin_stack_name : try(data.spacelift_stacks.this[0].stacks[0].name, null) +} + +data "spacelift_stacks" "this" { + count = ( + local.enabled && + local.create_root_admin_stack == false && + local.admin_stack_label != null + ) ? 1 : 0 + labels { + any_of = [local.admin_stack_label] + } +} + +# Ensure no stacks are configured to use public workers if they are not allowed +resource "null_resource" "child_stack_parent_precondition" { + count = local.enabled ? 1 : 0 + lifecycle { + precondition { + condition = local.create_root_admin_stack ? true : length(data.spacelift_stacks.this[0].stacks) > 0 + error_message = "Please apply this stack's parent before applying this stack." + } + } +} + +# Get all of the stack configurations from the atmos config that matched the context_filters and create a stack +# for each one. +module "child_stacks_config" { + source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" + version = "1.0.0" + + context_filters = var.context_filters + + context = module.this.context +} + +module "child_stack" { + source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" + version = "1.0.0" + + for_each = local.child_stacks + depends_on = [ + null_resource.spaces_precondition, + null_resource.workers_precondition, + spacelift_policy_attachment.root, + null_resource.child_stack_parent_precondition + ] + + administrative = try(each.value.settings.spacelift.administrative, false) + after_apply = try(each.value.settings.spacelift.after_apply, []) + after_destroy = try(each.value.settings.spacelift.after_destroy, []) + after_init = try(each.value.settings.spacelift.after_init, []) + after_perform = try(each.value.settings.spacelift.after_perform, []) + after_plan = try(each.value.settings.spacelift.after_plan, []) + atmos_stack_name = try(each.value.stack, null) + autodeploy = try(each.value.settings.spacelift.autodeploy, false) + autoretry = try(each.value.settings.spacelift.autoretry, false) + aws_role_enabled = try(each.value.settings.aws_role_enabled, var.aws_role_enabled) + aws_role_arn = try(each.value.settings.aws_role_arn, var.aws_role_arn) + aws_role_external_id = try(each.value.settings.aws_role_external_id, var.aws_role_external_id) + aws_role_generate_credentials_in_worker = try(each.value.settings.aws_role_generate_credentials_in_worker, var.aws_role_generate_credentials_in_worker) + before_apply = try(each.value.settings.spacelift.before_apply, []) + before_destroy = try(each.value.settings.spacelift.before_destroy, []) + before_init = try(each.value.settings.spacelift.before_init, []) + before_perform = try(each.value.settings.spacelift.before_perform, []) + before_plan = try(each.value.settings.spacelift.before_plan, []) + branch = try(each.value.branch, var.branch) + commit_sha = var.commit_sha != null ? var.commit_sha : try(each.value.commit_sha, null) + component_env = try(each.value.env, {}) + component_name = try(each.value.component, null) + component_root = try(join("/", [var.component_root, try(each.value.metadata.component, each.value.component)])) + component_vars = try(each.value.vars, null) + context_attachments = try(each.value.context_attachments, []) + description = try(each.value.description, null) + drift_detection_enabled = try(each.value.settings.spacelift.drift_detection_enabled, var.drift_detection_enabled) + drift_detection_reconcile = try(each.value.settings.spacelift.drift_detection_reconcile, var.drift_detection_reconcile) + drift_detection_schedule = try(each.value.settings.spacelift.drift_detection_schedule, var.drift_detection_schedule) + drift_detection_timezone = try(each.value.settings.spacelift.drift_detection_timezone, var.drift_detection_timezone) + labels = concat( + try(each.value.labels, []), + try(each.value.vars.labels, []), + ["managed-by:${local.managed_by}"], + local.create_root_admin_stack ? ["depends-on:${local.root_admin_stack_name}", ""] : [] + ) + local_preview_enabled = try(each.value.local_preview_enabled, var.local_preview_enabled) + manage_state = try(each.value.manage_state, var.manage_state) + policy_ids = try(local.child_policy_ids, []) + protect_from_deletion = try(each.value.settings.spacelift.protect_from_deletion, false) + repository = var.repository + runner_image = try(each.value.settings.spacelift.runner_image, var.runner_image) + space_id = local.spaces[each.value.settings.spacelift.space_name] + spacelift_run_enabled = try(each.value.settings.spacelift.spacelift_run_enabled, var.spacelift_run_enabled) + stack_destructor_enabled = try(each.value.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) + stack_name = try(each.value.settings.spacelift.stack_name, each.key) + terraform_smart_sanitization = try(each.value.terraform_smart_sanitization, false) + terraform_version = lookup(var.terraform_version_map, try(each.value.terraform_version, ""), var.terraform_version) + terraform_workspace = try(each.value.workspace, null) + webhook_enabled = try(each.value.webhook_enabled, var.webhook_enabled) + webhook_endpoint = try(each.value.webhook_endpoint, var.webhook_endpoint) + webhook_secret = try(each.value.webhook_secret, var.webhook_secret) + worker_pool_id = try(local.worker_pools[each.value.worker_pool_name], local.worker_pools[var.worker_pool_name]) + + azure_devops = try(each.value.azure_devops, null) + bitbucket_cloud = try(each.value.bitbucket_cloud, null) + bitbucket_datacenter = try(each.value.bitbucket_datacenter, null) + cloudformation = try(each.value.cloudformation, null) + github_enterprise = try(local.root_admin_stack_config.github_enterprise, null) + gitlab = try(local.root_admin_stack_config.gitlab, null) + pulumi = try(local.root_admin_stack_config.pulumi, null) + showcase = try(local.root_admin_stack_config.showcase, null) + + context = module.this.context +} diff --git a/modules/spacelift/admin-stack/context.tf b/modules/spacelift/admin-stack/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/spacelift/admin-stack/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spacelift/admin-stack/main.tf b/modules/spacelift/admin-stack/main.tf new file mode 100644 index 000000000..03bce1f8f --- /dev/null +++ b/modules/spacelift/admin-stack/main.tf @@ -0,0 +1,11 @@ +locals { + enabled = module.this.enabled + create_root_admin_stack = local.enabled && var.root_admin_stack + root_admin_stack_name = local.create_root_admin_stack ? keys(module.root_admin_stack_config.spacelift_stacks)[0] : null + root_admin_stack_config = local.create_root_admin_stack ? module.root_admin_stack_config.spacelift_stacks[local.root_admin_stack_name] : null + + # Create a map of all the policies {policy_name = policy_id} + policies = { for k, v in data.spacelift_policies.this.policies : v.name => v.id } +} + +data "spacelift_policies" "this" {} diff --git a/modules/spacelift/admin-stack/outputs.tf b/modules/spacelift/admin-stack/outputs.tf new file mode 100644 index 000000000..1c5425ba8 --- /dev/null +++ b/modules/spacelift/admin-stack/outputs.tf @@ -0,0 +1,14 @@ +output "root_stack_id" { + description = "The stack id" + value = local.enabled && local.create_root_admin_stack ? module.root_admin_stack.id : null +} + +output "root_stack" { + value = local.enabled && local.create_root_admin_stack ? module.root_admin_stack : null + sensitive = true +} + +output "child_stacks" { + value = local.enabled ? values(module.child_stack)[*] : null + sensitive = true +} diff --git a/modules/spacelift/admin-stack/providers.tf b/modules/spacelift/admin-stack/providers.tf new file mode 100644 index 000000000..84d03ead5 --- /dev/null +++ b/modules/spacelift/admin-stack/providers.tf @@ -0,0 +1,2 @@ +provider "spacelift" {} + diff --git a/modules/spacelift/admin-stack/remote-state.tf b/modules/spacelift/admin-stack/remote-state.tf new file mode 100644 index 000000000..397a1c65c --- /dev/null +++ b/modules/spacelift/admin-stack/remote-state.tf @@ -0,0 +1,11 @@ +module "spaces" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "spacelift/spaces" + environment = try(var.spacelift_spaces_environment_name, module.this.environment) + stage = try(var.spacelift_spaces_stage_name, module.this.stage) + tenant = try(var.spacelift_spaces_tenant_name, module.this.tenant) + + context = module.this.context +} diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf new file mode 100644 index 000000000..8d3b70f75 --- /dev/null +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -0,0 +1,95 @@ +# The root admin stack is a special stack that is used to manage all of the other admin stacks in the the Spacelift +# organization. This stack is denoted by setting the root_administrative property to true in the atmos config. Only one +# such stack is allowed in the Spacelift organization. +module "root_admin_stack_config" { + source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" + version = "1.0.0" + + enabled = local.create_root_admin_stack + + context_filters = { + root_administrative = true + } +} + +# This gets the atmos stack config for all of the administrative stacks +module "all_admin_stacks_config" { + source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" + version = "1.0.0" + + enabled = local.create_root_admin_stack + context_filters = { + administrative = true + } +} + +module "root_admin_stack" { + source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" + version = "1.0.0" + + enabled = local.create_root_admin_stack + depends_on = [null_resource.spaces_precondition, null_resource.workers_precondition] + + administrative = true + after_apply = try(local.root_admin_stack_config.settings.spacelift.after_apply, []) + after_destroy = try(local.root_admin_stack_config.settings.spacelift.after_destroy, []) + after_init = try(local.root_admin_stack_config.settings.spacelift.after_init, []) + after_perform = try(local.root_admin_stack_config.settings.spacelift.after_perform, []) + after_plan = try(local.root_admin_stack_config.settings.spacelift.after_plan, []) + atmos_stack_name = try(local.root_admin_stack_config.stack, null) + autodeploy = try(local.root_admin_stack_config.settings.spacelift.autodeploy, false) + autoretry = try(local.root_admin_stack_config.settings.spacelift.autoretry, false) + aws_role_enabled = try(local.root_admin_stack_config.settings.aws_role_enabled, var.aws_role_enabled) + aws_role_arn = try(local.root_admin_stack_config.settings.aws_role_arn, var.aws_role_arn) + aws_role_external_id = try(local.root_admin_stack_config.settings.aws_role_external_id, var.aws_role_external_id) + aws_role_generate_credentials_in_worker = try(local.root_admin_stack_config.settings.aws_role_generate_credentials_in_worker, var.aws_role_generate_credentials_in_worker) + before_apply = try(local.root_admin_stack_config.settings.spacelift.before_apply, []) + before_destroy = try(local.root_admin_stack_config.settings.spacelift.before_destroy, []) + before_init = try(local.root_admin_stack_config.settings.spacelift.before_init, []) + before_perform = try(local.root_admin_stack_config.settings.spacelift.before_perform, []) + before_plan = try(local.root_admin_stack_config.settings.spacelift.before_plan, []) + branch = try(local.root_admin_stack_config.branch, var.branch) + commit_sha = var.commit_sha != null ? var.commit_sha : try(local.root_admin_stack_config.commit_sha, null) + component_env = try(local.root_admin_stack_config.env, {}) + component_name = try(local.root_admin_stack_config.component, null) + component_root = try(join("/", [var.component_root, local.root_admin_stack_config.metadata.component]), null) + component_vars = try(local.root_admin_stack_config.vars, null) + context_attachments = try(local.root_admin_stack_config.context_attachments, []) + description = try(local.root_admin_stack_config.description, null) + drift_detection_enabled = try(local.root_admin_stack_config.settings.spacelift.drift_detection_enabled, var.drift_detection_enabled) + drift_detection_reconcile = try(local.root_admin_stack_config.settings.spacelift.drift_detection_reconcile, var.drift_detection_reconcile) + drift_detection_schedule = try(local.root_admin_stack_config.settings.spacelift.drift_detection_schedule, var.drift_detection_schedule) + drift_detection_timezone = try(local.root_admin_stack_config.settings.spacelift.drift_detection_timezone, var.drift_detection_timezone) + labels = concat(try(local.root_admin_stack_config.labels, []), try(var.labels, [])) + local_preview_enabled = try(local.root_admin_stack_config.local_preview_enabled, var.local_preview_enabled) + manage_state = try(local.root_admin_stack_config.manage_state, var.manage_state) + protect_from_deletion = try(local.root_admin_stack_config.settings.spacelift.protect_from_deletion, false) + repository = var.repository + runner_image = try(local.root_admin_stack_config.settings.spacelift.runner_image, var.runner_image) + space_id = "root" + spacelift_run_enabled = coalesce(try(local.root_admin_stack_config.settings.spacelift.spacelift_run_enabled, null), var.spacelift_run_enabled) + stack_destructor_enabled = try(local.root_admin_stack_config.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) + stack_name = var.stack_name != null ? var.stack_name : local.root_admin_stack_name + terraform_smart_sanitization = try(local.root_admin_stack_config.terraform_smart_sanitization, false) + terraform_version = lookup(var.terraform_version_map, try(local.root_admin_stack_config.terraform_version, ""), var.terraform_version) + terraform_workspace = try(local.root_admin_stack_config.workspace, null) + webhook_enabled = try(local.root_admin_stack_config.webhook_enabled, var.webhook_enabled) + webhook_endpoint = try(local.root_admin_stack_config.webhook_endpoint, var.webhook_endpoint) + webhook_secret = try(local.root_admin_stack_config.webhook_secret, var.webhook_secret) + worker_pool_id = local.worker_pools[var.worker_pool_name] + + azure_devops = try(local.root_admin_stack_config.azure_devops, null) + bitbucket_cloud = try(local.root_admin_stack_config.bitbucket_cloud, null) + bitbucket_datacenter = try(local.root_admin_stack_config.bitbucket_datacenter, null) + cloudformation = try(local.root_admin_stack_config.cloudformation, null) + github_enterprise = try(local.root_admin_stack_config.github_enterprise, null) + gitlab = try(local.root_admin_stack_config.gitlab, null) + pulumi = try(local.root_admin_stack_config.pulumi, null) + showcase = try(local.root_admin_stack_config.showcase, null) +} + +resource "spacelift_policy_attachment" "root" { + for_each = var.root_stack_policy_attachments + policy_id = local.policies[each.key] + stack_id = module.root_admin_stack.id +} diff --git a/modules/spacelift/admin-stack/spaces.tf b/modules/spacelift/admin-stack/spaces.tf new file mode 100644 index 000000000..648a93b6a --- /dev/null +++ b/modules/spacelift/admin-stack/spaces.tf @@ -0,0 +1,31 @@ +locals { + # This loops through all of the administrative stacks in the atmos config and extracts the space_name from the + # spacelift.settings metadata. It then creates a set of all of the unique space_names so we can use that to look up + # their IDs from remote state. + unique_spaces_from_config = toset([for k, v in { + for k, v in module.child_stacks_config.spacelift_stacks : k => try(v.settings.spacelift.space_name, "root") + if try(v.settings.spacelift.workspace_enabled, false) == true + } : v if v != "root"]) + + # Create a map of all the unique spaces {space_name = space_id} + spaces = merge(try({ + for k in local.unique_spaces_from_config : k => module.spaces.outputs.spaces[k].id + }, {}), { + root = "root" + }) + + # Create a list of all the spaces that are defined in config but missing from Spacelift + missing_spaces = setunion(setsubtract(local.unique_spaces_from_config, keys(local.spaces))) +} + +# Ensure all of the spaces referenced in the atmos config exist in Spacelift +resource "null_resource" "spaces_precondition" { + count = local.enabled ? 1 : 0 + + lifecycle { + precondition { + condition = length(local.missing_spaces) == 0 + error_message = "Please create the following spaces in Spacelift before running this module: ${join(", ", local.missing_spaces)}" + } + } +} diff --git a/modules/spacelift/admin-stack/variables.tf b/modules/spacelift/admin-stack/variables.tf new file mode 100644 index 000000000..169e062ab --- /dev/null +++ b/modules/spacelift/admin-stack/variables.tf @@ -0,0 +1,345 @@ +variable "admin_stack_label" { + description = "Label to use to identify the admin stack when creating the child stacks" + type = string + default = "admin-stack-name" +} + +variable "administrative" { + type = bool + description = "Whether this stack can manage other stacks" + default = false +} + +variable "allow_public_workers" { + type = bool + description = "Whether to allow public workers to be used for this stack" + default = false +} +variable "autodeploy" { + type = bool + description = "Controls the Spacelift 'autodeploy' option for a stack" + default = false +} + +variable "autoretry" { + type = bool + description = "Controls the Spacelift 'autoretry' option for a stack" + default = false +} + +variable "aws_role_arn" { + type = string + description = "ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment" + default = null +} + +variable "aws_role_enabled" { + type = bool + description = "Flag to enable/disable Spacelift to use AWS STS to assume the supplied IAM role and put its temporary credentials in the runtime environment" + default = false +} + +variable "aws_role_external_id" { + type = string + description = "Custom external ID (works only for private workers). See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html for more details" + default = null +} + +variable "aws_role_generate_credentials_in_worker" { + type = bool + description = "Flag to enable/disable generating AWS credentials in the private worker after assuming the supplied IAM role" + default = true +} + +variable "azure_devops" { + type = map(any) + description = "Azure DevOps VCS settings" + default = null +} + +variable "bitbucket_cloud" { + type = map(any) + description = "Bitbucket Cloud VCS settings" + default = null +} + +variable "bitbucket_datacenter" { + type = map(any) + description = "Bitbucket Datacenter VCS settings" + default = null +} + +variable "branch" { + type = string + description = "Specify which branch to use within your infrastructure repo" + default = "main" +} + +variable "child_policy_attachments" { + description = "List of policy attachments to attach to the child stacks created by this module" + type = set(string) + default = [] +} + +variable "cloudformation" { + type = map(any) + description = "CloudFormation-specific configuration. Presence means this Stack is a CloudFormation Stack." + default = null +} + +variable "commit_sha" { + type = string + description = "The commit SHA for which to trigger a run. Requires `var.spacelift_run_enabled` to be set to `true`" + default = null +} + +variable "component_env" { + type = any + default = {} + description = "Map of component ENV variables" +} + +variable "component_root" { + type = string + description = "The path, relative to the root of the repository, where the component can be found" +} + +variable "component_vars" { + type = any + default = {} + description = "All Terraform values to be applied to the stack via a mounted file" +} + +variable "context_attachments" { + type = set(string) + description = "A list of context IDs to attach to this stack" + default = [] +} + +variable "context_filters" { + description = "Context filters to select atmos stacks matching specific criteria to create as children." + type = object({ + namespaces = optional(list(string), []) + environments = optional(list(string), []) + tenants = optional(list(string), []) + stages = optional(list(string), []) + tags = optional(map(string), {}) + administrative = optional(bool) + root_administrative = optional(bool) + }) +} + +variable "description" { + type = string + description = "Specify description of stack" + default = null +} + +variable "drift_detection_enabled" { + type = bool + description = "Flag to enable/disable drift detection on the infrastructure stacks" + default = false +} + +variable "drift_detection_reconcile" { + type = bool + description = "Flag to enable/disable infrastructure stacks drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift" + default = false +} + +variable "drift_detection_schedule" { + type = list(string) + description = "List of cron expressions to schedule drift detection for the infrastructure stacks" + default = ["0 4 * * *"] +} + +variable "drift_detection_timezone" { + type = string + description = "Timezone in which the schedule is expressed. Defaults to UTC." + default = null +} + +variable "github_enterprise" { + type = map(any) + description = "GitHub Enterprise (self-hosted) VCS settings" + default = null +} + +variable "gitlab" { + type = map(any) + description = "GitLab VCS settings" + default = null +} + +variable "labels" { + type = list(string) + description = "A list of labels for the stack" + default = [] +} + +variable "local_preview_enabled" { + type = bool + description = "Indicates whether local preview runs can be triggered on this Stack" + default = false +} + +variable "manage_state" { + type = bool + description = "Flag to enable/disable manage_state setting in stack" + default = false +} + +variable "parent_space_id" { + type = string + description = "If creating a dedicated space for this stack, specify the ID of the parent space in Spacelift." + default = null +} + +variable "policy_ids" { + type = set(string) + default = [] + description = "Set of Rego policy IDs to attach to this stack" +} + +variable "protect_from_deletion" { + type = bool + description = "Flag to enable/disable deletion protection." + default = false +} + +variable "pulumi" { + type = map(any) + description = "Pulumi-specific configuration. Presence means this Stack is a Pulumi Stack." + default = null +} + +variable "region" { + type = string + description = "The AWS region to use" + default = "us-east-1" +} + +variable "repository" { + type = string + description = "The name of your infrastructure repo" +} + +variable "root_admin_stack" { + description = "Flag to indicate if this stack is the root admin stack. In this case, the stack will be created in the root space and will create all the other admin stacks as children." + type = bool + default = false +} + +variable "root_stack_policy_attachments" { + description = "List of policy attachments to attach to the root admin stack" + type = set(string) + default = [] +} + +variable "runner_image" { + type = string + description = "The full image name and tag of the Docker image to use in Spacelift" + default = null +} + +variable "showcase" { + type = map(any) + description = "Showcase settings" + default = null +} + +variable "space_id" { + type = string + description = "Place the stack in the specified space_id." + default = "root" +} + +variable "spacelift_run_enabled" { + type = bool + description = "Enable/disable creation of the `spacelift_run` resource" + default = false +} + +variable "spacelift_spaces_environment_name" { + type = string + description = "The environment name of the spacelift spaces component" + default = null +} + +variable "spacelift_spaces_stage_name" { + type = string + description = "The stage name of the spacelift spaces component" + default = null +} + +variable "spacelift_spaces_tenant_name" { + type = string + description = "The tenant name of the spacelift spaces component" + default = null +} + +variable "spacelift_stack_dependency_enabled" { + type = bool + description = "If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached" + default = false +} + +variable "stack_destructor_enabled" { + type = bool + description = "Flag to enable/disable the stack destructor to destroy the resources of the stack before deleting the stack itself" + default = false +} + +variable "stack_name" { + type = string + description = "The name of the Spacelift stack" + default = null +} + +variable "terraform_smart_sanitization" { + type = bool + description = "Whether or not to enable [Smart Sanitization](https://docs.spacelift.io/vendors/terraform/resource-sanitization) which will only sanitize values marked as sensitive." + default = false +} + +variable "terraform_version" { + type = string + description = "Specify the version of Terraform to use for the stack" + default = null +} + +variable "terraform_version_map" { + type = map(string) + description = "A map to determine which Terraform patch version to use for each minor version" + default = {} +} + +variable "terraform_workspace" { + type = string + description = "Specify the Terraform workspace to use for the stack" + default = null +} + +variable "webhook_enabled" { + type = bool + description = "Flag to enable/disable the webhook endpoint to which Spacelift sends the POST requests about run state changes" + default = false +} + +variable "webhook_endpoint" { + type = string + description = "Webhook endpoint to which Spacelift sends the POST requests about run state changes" + default = null +} + +variable "webhook_secret" { + type = string + description = "Webhook secret used to sign each POST request so you're able to verify that the requests come from Spacelift" + default = null +} + +variable "worker_pool_name" { + type = string + description = "The atmos stack name of the worker pool. Example: `acme-core-ue2-auto-spacelift-default-worker-pool`" + default = null +} diff --git a/modules/spacelift/admin-stack/versions.tf b/modules/spacelift/admin-stack/versions.tf new file mode 100644 index 000000000..1174cd191 --- /dev/null +++ b/modules/spacelift/admin-stack/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + spacelift = { + source = "spacelift-io/spacelift" + version = ">= 0.1.31" + } + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} diff --git a/modules/spacelift/admin-stack/workers.tf b/modules/spacelift/admin-stack/workers.tf new file mode 100644 index 000000000..2a512152c --- /dev/null +++ b/modules/spacelift/admin-stack/workers.tf @@ -0,0 +1,49 @@ +locals { + # This loops through all of the stacks in the atmos config and extracts the worker_name. It then creates a set of all + # of the unique worker_names so we can use that to make sure that the worker pool exists in Spacelift. + # + # If a worker pool is not defined in the atmos config, then it will default to a fake "public" value so we can + # check below if any stacks are configured to use public workers. + unique_workers_from_config = toset([for k, v in { + for k, v in module.child_stacks_config.spacelift_stacks : k => coalesce(try(v.settings.spacelift.worker_pool_name, var.worker_pool_name), "public") + if try(v.settings.spacelift.workspace_enabled, false) == true + } : v]) + + # Create a map of all the worker pools that exist in spacelift {worker_pool_name = worker_pool_id} + worker_pools = { for k, v in data.spacelift_worker_pools.this.worker_pools : v.name => v.worker_pool_id } + + # Create a list of all the worker pools that are defined in config but missing from Spacelift + missing_workers = setunion(setsubtract(local.unique_workers_from_config, keys(local.worker_pools))) +} + +data "spacelift_worker_pools" "this" { +} + +# Ensure no stacks are configured to use public workers if they are not allowed +resource "null_resource" "public_workers_precondition" { + count = local.enabled ? 1 : 0 + lifecycle { + precondition { + condition = var.allow_public_workers == true || contains(local.missing_workers, "public") == false + error_message = "Use of public workers is not allowed. Please create worker pool(s) in Spacelift and assign all stacks to a worker before running this module." + } + } +} + +# Ensure all of the spaces referenced in the atmos config exist in Spacelift +resource "null_resource" "workers_precondition" { + count = local.enabled ? 1 : 0 + + depends_on = [null_resource.public_workers_precondition] + + lifecycle { + precondition { + condition = (var.allow_public_workers == false && length(local.missing_workers) == 0) || ( + var.allow_public_workers == true && + length(local.missing_workers) == 1 + && contains(local.missing_workers, "public") + ) + error_message = "Please create the following workers in Spacelift before running this module: ${join(", ", local.missing_workers)}" + } + } +} diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md new file mode 100644 index 000000000..a4fcd3502 --- /dev/null +++ b/modules/spacelift/spaces/README.md @@ -0,0 +1,130 @@ +# Component: `spacelift/spaces` + +This component is responsible for creating and managing the [spaces](https://docs.spacelift.io/concepts/spaces/) in the +spacelift organization. + +## Usage + +**Stack Level**: Global + +The following are example snippets of how to use this component: + +```yaml +# stacks/orgs/acme/spacelift.yaml +import: + - mixins/region/global-region + - orgs/acme/_defaults + +vars: + tenant: infra + environment: gbl + stage: root + +components: + terraform: + spacelift/spaces: + settings: + spacelift: + administrative: true + space_name: root + vars: + spaces: + # root is a special space that is the parent of all other spaces and cannot be deleted or renamed. Only the + # policies block is actually consumed by the component to create policies for the root space. + root: + parent_space_id: root + description: The root space + inherit_entities: true + policies: + GIT_PUSH Global Administrator: + type: GIT_PUSH + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/git_push.administrative.rego + TRIGGER Global Administrator: + type: TRIGGER + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/trigger.administrative.rego + GIT_PUSH Proposed Run: + type: GIT_PUSH + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/git_push.proposed-run.rego + GIT_PUSH Tracked Run: + type: GIT_PUSH + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/git_push.tracked-run.rego + PLAN Default: + type: PLAN + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/plan.default.rego + TRIGGER Dependencies: + type: TRIGGER + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/trigger.dependencies.rego + PLAN Warn On Resource Changes Except Image ID: + type: PLAN + body_url: https://raw.githubusercontent.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/%s/catalog/policies/plan.warn-on-resource-changes-except-image-id.rego + core: + parent_space_id: root + description: The space for the core tenant + inherit_entities: true + labels: + - core + plat: + parent_space_id: root + description: The space for platform tenant + inherit_entities: true + labels: + - plat +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [aws](#requirement\_aws) | >= 4.0 | +| [spacelift](#requirement\_spacelift) | >= 0.1.31 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.0.0 | +| [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.0.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [spaces](#input\_spaces) | n/a |
map(object({
parent_space_id = string,
description = optional(string),
inherit_entities = optional(bool, false),
labels = optional(set(string), []),
policies = optional(map(object({
body = optional(string),
body_url = optional(string),
body_url_version = optional(string, "master"),
type = optional(string),
labels = optional(set(string), []),
})), {}),
}))
| n/a | yes | +| [ssm\_params\_enabled](#input\_ssm\_params\_enabled) | Whether to write the IDs of the created spaces to SSM parameters | `bool` | `true` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [policies](#output\_policies) | n/a | +| [spaces](#output\_spaces) | n/a | + diff --git a/modules/spacelift/spaces/context.tf b/modules/spacelift/spaces/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/spacelift/spaces/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spacelift/spaces/main.tf b/modules/spacelift/spaces/main.tf new file mode 100644 index 000000000..c26e0e50d --- /dev/null +++ b/modules/spacelift/spaces/main.tf @@ -0,0 +1,65 @@ +locals { + enabled = module.this.enabled + spaces = local.enabled ? { for item in values(module.space)[*].space : item.name => { + description = item.description + id = item.id + inherit_entities = item.inherit_entities + labels = toset(item.labels) + parent_space_id = item.parent_space_id + } + } : {} + + # Create a map of all the policies {policy_name = policy} + policies = local.enabled ? { for item in distinct(values(module.policy)[*].policy) : item.name => { + id = item.id + type = item.type + labels = toset(item.labels) + space_id = item.space_id + } + } : {} + + policy_inputs = local.enabled ? { + for k, v in var.spaces : k => { + for pn, p in v.policies : pn => { + body = p.body + body_url = p.body_url + body_url_version = p.body_url_version + labels = toset(v.labels) + name = pn + space_id = k == "root" ? "root" : module.space[k].space_id + type = p.type + } + } + } : {} + all_policies_inputs = merge([for k, v in local.policy_inputs : v if length(keys(v)) > 0]...) +} + +module "space" { + source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space" + version = "1.0.0" + + # Create a space for each entry in the `spaces` variable, except for the root space which already exists by default + # and cannot be deleted. + for_each = { for k, v in var.spaces : k => v if k != "root" } + + space_name = each.key + parent_space_id = each.value.parent_space_id + description = each.value.description + inherit_entities_from_parent = each.value.inherit_entities + labels = each.value.labels +} + +module "policy" { + source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy" + version = "1.0.0" + + for_each = local.all_policies_inputs + + policy_name = each.key + body = each.value.body + body_url = each.value.body_url + body_url_version = each.value.body_url_version + type = each.value.type + labels = each.value.labels + space_id = each.value.space_id +} diff --git a/modules/spacelift/spaces/outputs.tf b/modules/spacelift/spaces/outputs.tf new file mode 100644 index 000000000..196858586 --- /dev/null +++ b/modules/spacelift/spaces/outputs.tf @@ -0,0 +1,7 @@ +output "spaces" { + value = local.enabled ? local.spaces : {} +} + +output "policies" { + value = local.enabled ? local.policies : {} +} diff --git a/modules/spacelift/spaces/providers.tf b/modules/spacelift/spaces/providers.tf new file mode 100644 index 000000000..c95d53819 --- /dev/null +++ b/modules/spacelift/spaces/providers.tf @@ -0,0 +1 @@ +provider "spacelift" {} diff --git a/modules/spacelift/spaces/variables.tf b/modules/spacelift/spaces/variables.tf new file mode 100644 index 000000000..d4f90a898 --- /dev/null +++ b/modules/spacelift/spaces/variables.tf @@ -0,0 +1,21 @@ +variable "spaces" { + type = map(object({ + parent_space_id = string, + description = optional(string), + inherit_entities = optional(bool, false), + labels = optional(set(string), []), + policies = optional(map(object({ + body = optional(string), + body_url = optional(string), + body_url_version = optional(string, "master"), + type = optional(string), + labels = optional(set(string), []), + })), {}), + })) +} + +variable "ssm_params_enabled" { + type = bool + description = "Whether to write the IDs of the created spaces to SSM parameters" + default = true +} diff --git a/modules/spacelift/spaces/versions.tf b/modules/spacelift/spaces/versions.tf new file mode 100644 index 000000000..1174cd191 --- /dev/null +++ b/modules/spacelift/spaces/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + spacelift = { + source = "spacelift-io/spacelift" + version = ">= 0.1.31" + } + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md new file mode 100644 index 000000000..b754df85e --- /dev/null +++ b/modules/spacelift/worker-pool/README.md @@ -0,0 +1,223 @@ +# Component: `spacelift/worker-pool` + +This component is responsible for provisioning Spacelift worker pools. + +By default, workers are given pull access to the configured ECR, permission to assume the `spacelift` team role in the +identity account (although you must also configure the `spacelift` team in the identity account to allow the workers to +assume the role via `trusted_role_arns`), and have the following AWS managed IAM policies attached: + +- AmazonSSMManagedInstanceCore +- AutoScalingReadOnlyAccess +- AWSXRayDaemonWriteAccess +- CloudWatchAgentServerPolicy + +Among other things, this allows workers with SSM agent installed to +be accessed via SSM Session Manager. + +```bash +aws ssm start-session --target +``` + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + spacelift-worker-pool: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: "spacelift-worker-pool" + ec2_instance_type: m6i.large + ecr_account_name: corp + ecr_repo_name: infrastructure + spacelift_api_endpoint: https://.app.spacelift.io +``` + +## Configuration + +### Docker Image on ECR + +Build and tag a Docker image for this repository and push to ECR. Ensure the account where this component is deployed +has read-only access to the ECR repository. + +### API Key + +Prior to deployment, the API key must exist in SSM. The key must have admin permissions. + +To generate the key, please follow [these +instructions](https://docs.spacelift.io/integrations/api.html#spacelift-api-key-token). Once generated, write the API +key ID and secret to the SSM key store at the following locations within the same AWS account and region where the +Spacelift worker pool will reside. + +| Key | SSM Path | Type | +| ------- | ----------------------- | -------------- | +| API ID | `/spacelift/key_id` | `SecureString` | +| API Key | `/spacelift/key_secret` | `SecureString` | + +_HINT_: The API key ID is displayed as an upper-case, 16-character alphanumeric value next to the key name in the API +key list. + +Save the keys using `chamber` using the correct profile for where spacelift worker pool is provisioned + +``` +AWS_PROFILE=acme-gbl-auto-admin chamber write spacelift key_id 1234567890123456 +AWS_PROFILE=acme-gbl-auto-admin chamber write spacelift key_secret abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz +``` + +### IAM configuration + +After provisioning the component, you must give the created instance role permission to assume the Spacelift worker +role. This is done by adding `iam_role_arn` from the output to the `trusted_role_arns` list for the `spacelift` role in +`aws-teams`. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [cloudinit](#requirement\_cloudinit) | >= 2.2.0 | +| [spacelift](#requirement\_spacelift) | >= 0.1.2 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | +| [cloudinit](#provider\_cloudinit) | >= 2.2.0 | +| [spacelift](#provider\_spacelift) | >= 0.1.2 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.30.1 | +| [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_label](#module\_iam\_label) | cloudposse/label/null | 0.25.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.0.0-rc1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_instance_profile.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [spacelift_worker_pool.primary](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/worker_pool) | resource | +| [aws_ami.spacelift](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | +| [aws_iam_policy_document.assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_ssm_parameter.spacelift_key_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.spacelift_key_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [cloudinit_config.config](https://registry.terraform.io/providers/hashicorp/cloudinit/latest/docs/data-sources/config) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_environment\_name](#input\_account\_map\_environment\_name) | The name of the environment where `account_map` is provisioned | `string` | `"gbl"` | no | +| [account\_map\_stage\_name](#input\_account\_map\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | +| [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [aws\_config\_file](#input\_aws\_config\_file) | The AWS\_CONFIG\_FILE used by the worker. Can be overridden by `/.spacelift/config.yml`. | `string` | `"/etc/aws-config/aws-config-spacelift"` | no | +| [aws\_profile](#input\_aws\_profile) | The AWS\_PROFILE used by the worker. If not specified, `"${var.namespace}-identity"` will be used.
Can be overridden by `/.spacelift/config.yml`. | `string` | `null` | no | +| [block\_device\_mappings](#input\_block\_device\_mappings) | Specify volumes to attach to the instance besides the volumes specified by the AMI |
list(object({
device_name = string
no_device = bool
virtual_name = string
ebs = object({
delete_on_termination = bool
encrypted = bool
iops = number
kms_key_id = string
snapshot_id = string
volume_size = number
volume_type = string
})
}))
| `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [cpu\_utilization\_high\_threshold\_percent](#input\_cpu\_utilization\_high\_threshold\_percent) | CPU utilization high threshold | `number` | n/a | yes | +| [cpu\_utilization\_low\_threshold\_percent](#input\_cpu\_utilization\_low\_threshold\_percent) | CPU utilization low threshold | `number` | n/a | yes | +| [custom\_spacelift\_ami](#input\_custom\_spacelift\_ami) | Custom spacelift AMI | `bool` | `false` | no | +| [default\_cooldown](#input\_default\_cooldown) | The amount of time, in seconds, after a scaling activity completes before another scaling activity can start | `number` | `300` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [desired\_capacity](#input\_desired\_capacity) | The number of Amazon EC2 instances that should be running in the group, if not set will use `min_size` as value | `number` | `null` | no | +| [ebs\_optimized](#input\_ebs\_optimized) | If true, the launched EC2 instance will be EBS-optimized | `bool` | `false` | no | +| [ecr\_environment\_name](#input\_ecr\_environment\_name) | The name of the environment where `ecr` is provisioned | `string` | `""` | no | +| [ecr\_region](#input\_ecr\_region) | AWS region that contains the ECR infrastructure repo | `string` | `""` | no | +| [ecr\_repo\_name](#input\_ecr\_repo\_name) | ECR repository name | `string` | n/a | yes | +| [ecr\_stage\_name](#input\_ecr\_stage\_name) | The name of the stage where `ecr` is provisioned | `string` | `"artifacts"` | no | +| [ecr\_tenant\_name](#input\_ecr\_tenant\_name) | The name of the tenant where `ecr` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github\_netrc\_enabled](#input\_github\_netrc\_enabled) | Whether to create a GitHub .netrc file so Spacelift can clone private GitHub repositories. | `bool` | `false` | no | +| [github\_netrc\_ssm\_path\_token](#input\_github\_netrc\_ssm\_path\_token) | If `github_netrc` is enabled, this is the SSM path to retrieve the GitHub token. | `string` | `"/github/token"` | no | +| [github\_netrc\_ssm\_path\_user](#input\_github\_netrc\_ssm\_path\_user) | If `github_netrc` is enabled, this is the SSM path to retrieve the GitHub user | `string` | `"/github/user"` | no | +| [health\_check\_grace\_period](#input\_health\_check\_grace\_period) | Time (in seconds) after instance comes into service before checking health | `number` | `300` | no | +| [health\_check\_type](#input\_health\_check\_type) | Controls how health checking is done. Valid values are `EC2` or `ELB` | `string` | `"EC2"` | no | +| [iam\_attributes](#input\_iam\_attributes) | Additional attributes to add to the IDs of the IAM role and policy | `list(string)` | `[]` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [infracost\_api\_token\_ssm\_path](#input\_infracost\_api\_token\_ssm\_path) | This is the SSM path to retrieve and set the INFRACOST\_API\_TOKEN environment variable | `string` | `"/infracost/token"` | no | +| [infracost\_cli\_args](#input\_infracost\_cli\_args) | These are the CLI args passed to infracost | `string` | `""` | no | +| [infracost\_enabled](#input\_infracost\_enabled) | Whether to enable infracost for Spacelift stacks | `bool` | `false` | no | +| [infracost\_warn\_on\_failure](#input\_infracost\_warn\_on\_failure) | A failure executing Infracost, or a non-zero exit code being returned from the command will cause runs to fail. If this is true, this will only warn instead of failing the stack. | `bool` | `true` | no | +| [instance\_refresh](#input\_instance\_refresh) | The instance refresh definition. If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated |
object({
strategy = string
preferences = object({
instance_warmup = number
min_healthy_percentage = number
})
triggers = list(string)
})
| `null` | no | +| [instance\_type](#input\_instance\_type) | EC2 instance type to use for workers | `string` | `"r5n.large"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [max\_size](#input\_max\_size) | The maximum size of the autoscale group | `number` | n/a | yes | +| [min\_size](#input\_min\_size) | The minimum size of the autoscale group | `number` | n/a | yes | +| [mixed\_instances\_policy](#input\_mixed\_instances\_policy) | Policy to use a mixed group of on-demand/spot of different types. Launch template is automatically generated. https://www.terraform.io/docs/providers/aws/r/autoscaling_group.html#mixed_instances_policy-1 |
object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
override = list(object({
instance_type = string
weighted_capacity = number
}))
})
| `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [scale\_down\_cooldown\_seconds](#input\_scale\_down\_cooldown\_seconds) | The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start | `number` | `300` | no | +| [spacelift\_agents\_per\_node](#input\_spacelift\_agents\_per\_node) | Number of Spacelift agents to run on one worker node | `number` | `1` | no | +| [spacelift\_ami\_id](#input\_spacelift\_ami\_id) | AMI ID of Spacelift worker pool image | `string` | `null` | no | +| [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | +| [spacelift\_aws\_account\_id](#input\_spacelift\_aws\_account\_id) | AWS Account ID owned by Spacelift | `string` | `"643313122712"` | no | +| [spacelift\_domain\_name](#input\_spacelift\_domain\_name) | Top-level domain name to use for pulling the launcher binary | `string` | `"spacelift.io"` | no | +| [spacelift\_runner\_image](#input\_spacelift\_runner\_image) | URL of ECR image to use for Spacelift | `string` | `""` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [termination\_policies](#input\_termination\_policies) | A list of policies to decide how the instances in the auto scale group should be terminated. The allowed values are `OldestInstance`, `NewestInstance`, `OldestLaunchConfiguration`, `ClosestToNextInstanceHour`, `Default` | `list(string)` |
[
"OldestLaunchConfiguration"
]
| no | +| [wait\_for\_capacity\_timeout](#input\_wait\_for\_capacity\_timeout) | A maximum duration that Terraform should wait for ASG instances to be healthy before timing out. (See also Waiting for Capacity below.) Setting this to '0' causes Terraform to skip all Capacity Waiting behavior | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [autoscaling\_group\_arn](#output\_autoscaling\_group\_arn) | The ARN for this AutoScaling Group | +| [autoscaling\_group\_default\_cooldown](#output\_autoscaling\_group\_default\_cooldown) | Time between a scaling activity and the succeeding scaling activity | +| [autoscaling\_group\_health\_check\_grace\_period](#output\_autoscaling\_group\_health\_check\_grace\_period) | Time after instance comes into service before checking health | +| [autoscaling\_group\_health\_check\_type](#output\_autoscaling\_group\_health\_check\_type) | `EC2` or `ELB`. Controls how health checking is done | +| [autoscaling\_group\_id](#output\_autoscaling\_group\_id) | The autoscaling group id | +| [autoscaling\_group\_max\_size](#output\_autoscaling\_group\_max\_size) | The maximum size of the autoscale group | +| [autoscaling\_group\_min\_size](#output\_autoscaling\_group\_min\_size) | The minimum size of the autoscale group | +| [autoscaling\_group\_name](#output\_autoscaling\_group\_name) | The autoscaling group name | +| [iam\_role\_arn](#output\_iam\_role\_arn) | Spacelift IAM Role ARN | +| [iam\_role\_id](#output\_iam\_role\_id) | Spacelift IAM Role ID | +| [iam\_role\_name](#output\_iam\_role\_name) | Spacelift IAM Role name | +| [launch\_template\_arn](#output\_launch\_template\_arn) | The ARN of the launch template | +| [launch\_template\_id](#output\_launch\_template\_id) | The ID of the launch template | +| [security\_group\_arn](#output\_security\_group\_arn) | Spacelift Security Group ARN | +| [security\_group\_id](#output\_security\_group\_id) | Spacelift Security Group ID | +| [security\_group\_name](#output\_security\_group\_name) | Spacelift Security Group Name | +| [worker\_pool\_id](#output\_worker\_pool\_id) | Spacelift worker pool ID | +| [worker\_pool\_name](#output\_worker\_pool\_name) | Spacelift worker pool name | + + +## References + +- [cloudposse/terraform-spacelift-cloud-infrastructure-automation](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation) - Cloud Posse's related upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spacelift-worker-pool) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/spacelift/worker-pool/context.tf b/modules/spacelift/worker-pool/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/spacelift/worker-pool/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spacelift/worker-pool/data.tf b/modules/spacelift/worker-pool/data.tf new file mode 100644 index 000000000..b069e0725 --- /dev/null +++ b/modules/spacelift/worker-pool/data.tf @@ -0,0 +1,39 @@ +data "aws_partition" "current" { + count = local.enabled ? 1 : 0 +} + +# The Spacelift always validates its credentials, so we always pass api_key_id and api_key_secret +data "aws_ssm_parameter" "spacelift_key_id" { + name = "/spacelift/key_id" +} + +data "aws_ssm_parameter" "spacelift_key_secret" { + name = "/spacelift/key_secret" +} + +data "aws_ami" "spacelift" { + count = local.enabled && var.spacelift_ami_id == null ? 1 : 0 + + owners = var.custom_spacelift_ami ? ["self"] : [var.spacelift_aws_account_id] + most_recent = true + + filter { + name = "name" + values = ["spacelift-*"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "architecture" + values = ["x86_64"] + } +} diff --git a/modules/spacelift/worker-pool/iam.tf b/modules/spacelift/worker-pool/iam.tf new file mode 100644 index 000000000..f12e59f51 --- /dev/null +++ b/modules/spacelift/worker-pool/iam.tf @@ -0,0 +1,87 @@ +module "iam_label" { + source = "cloudposse/label/null" + version = "0.25.0" + + attributes = var.iam_attributes + + context = module.this.context +} + +data "aws_iam_policy_document" "assume_role_policy" { + count = local.enabled ? 1 : 0 + + statement { + actions = [ + "sts:AssumeRole", + "sts:TagSession", + ] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +locals { + role_arn_template = module.account_map.outputs.iam_role_arn_templates[local.identity_account_name] +} + +data "aws_iam_policy_document" "default" { + count = local.enabled ? 1 : 0 + + statement { + actions = [ + "sts:AssumeRole", + "sts:TagSession", + ] + resources = formatlist(local.role_arn_template, ["spacelift"]) + } + + statement { + actions = ["ecr:GetAuthorizationToken"] + resources = ["*"] + } + + statement { + actions = [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ] + resources = [local.ecr_repo_arn] + } +} + +resource "aws_iam_policy" "default" { + count = local.enabled ? 1 : 0 + + name = module.iam_label.id + policy = join("", data.aws_iam_policy_document.default.*.json) + + tags = module.iam_label.tags +} + +resource "aws_iam_role" "default" { + count = local.enabled ? 1 : 0 + + name = module.iam_label.id + assume_role_policy = join("", data.aws_iam_policy_document.assume_role_policy.*.json) + managed_policy_arns = [ + join("", aws_iam_policy.default.*.arn), + "arn:${join("", data.aws_partition.current.*.partition)}:iam::aws:policy/AutoScalingReadOnlyAccess", + "arn:${join("", data.aws_partition.current.*.partition)}:iam::aws:policy/CloudWatchAgentServerPolicy", + "arn:${join("", data.aws_partition.current.*.partition)}:iam::aws:policy/AmazonSSMManagedInstanceCore", + "arn:${join("", data.aws_partition.current.*.partition)}:iam::aws:policy/AWSXRayDaemonWriteAccess" + ] + + tags = module.iam_label.tags +} + +resource "aws_iam_instance_profile" "default" { + count = local.enabled ? 1 : 0 + + name = module.iam_label.id + role = join("", aws_iam_role.default.*.name) + + tags = module.iam_label.tags +} diff --git a/modules/spacelift/worker-pool/main.tf b/modules/spacelift/worker-pool/main.tf new file mode 100644 index 000000000..5138fa74f --- /dev/null +++ b/modules/spacelift/worker-pool/main.tf @@ -0,0 +1,122 @@ +locals { + enabled = module.this.enabled + vpc_id = module.vpc.outputs.vpc_id + vpc_private_subnet_ids = module.vpc.outputs.private_subnet_ids + identity_account_name = module.account_map.outputs.identity_account_account_name + identity_account_id = module.account_map.outputs.full_account_map[local.identity_account_name] + ecr_repo_arn = module.ecr.outputs.ecr_repo_arn_map[var.ecr_repo_name] + ecr_repo_url = module.ecr.outputs.ecr_repo_url_map[var.ecr_repo_name] + ecr_account_id = element(split(".", local.ecr_repo_url), 0) + ecr_region = coalesce(var.ecr_region, var.region) + spacelift_runner_image = coalesce(var.spacelift_runner_image, local.ecr_repo_url) + userdata_template = "${path.module}/templates/user-data.sh" + spacelift_service_file = "${path.module}/templates/spacelift@.service" + + spacelift_service_config = <<-END + #cloud-config + ${jsonencode({ + write_files = flatten([ + { + path = "/etc/systemd/system/spacelift@.service" + permissions = "0655" + owner = "root:root" + content = file(local.spacelift_service_file) + } + ] + ) +})} +END +} + +resource "spacelift_worker_pool" "primary" { + count = local.enabled ? 1 : 0 + + name = module.this.id + description = "Deployed to ${var.region} within '${join("-", compact([module.this.tenant, module.this.stage]))}' AWS account" +} + +data "cloudinit_config" "config" { + count = local.enabled ? 1 : 0 + + gzip = false + base64_encode = true + + part { + content_type = "text/cloud-config" + filename = "spacelift@.service" + content = local.spacelift_service_config + } + + part { + content_type = "text/x-shellscript" + filename = "user-data.sh" + content = templatefile(local.userdata_template, { + region = var.region + aws_config_file = var.aws_config_file + aws_profile = coalesce(var.aws_profile, "${var.namespace}-identity") + ecr_region = local.ecr_region + ecr_account_id = local.ecr_account_id + spacelift_runner_image = local.spacelift_runner_image + spacelift_worker_pool_private_key = join("", spacelift_worker_pool.primary.*.private_key) + spacelift_worker_pool_config = join("", spacelift_worker_pool.primary.*.config) + spacelift_domain_name = var.spacelift_domain_name + github_netrc_enabled = var.github_netrc_enabled + github_netrc_ssm_path_token = var.github_netrc_ssm_path_token + github_netrc_ssm_path_user = var.github_netrc_ssm_path_user + spacelift_agents_per_node = var.spacelift_agents_per_node + infracost_enabled = var.infracost_enabled + infracost_api_token_ssm_path = var.infracost_api_token_ssm_path + infracost_warn_on_failure = var.infracost_warn_on_failure + infracost_cli_args = var.infracost_cli_args + }) + } +} + +module "security_group" { + source = "cloudposse/security-group/aws" + version = "2.0.0-rc1" + + security_group_description = "Security Group for Spacelift worker pool" + allow_all_egress = true + + vpc_id = local.vpc_id + + context = module.this.context +} + +module "autoscale_group" { + source = "cloudposse/ec2-autoscale-group/aws" + version = "0.30.1" + + image_id = var.spacelift_ami_id == null ? join("", data.aws_ami.spacelift.*.image_id) : var.spacelift_ami_id + instance_type = var.instance_type + mixed_instances_policy = var.mixed_instances_policy + subnet_ids = local.vpc_private_subnet_ids + health_check_type = var.health_check_type + health_check_grace_period = var.health_check_grace_period + user_data_base64 = join("", data.cloudinit_config.config.*.rendered) + associate_public_ip_address = false + block_device_mappings = var.block_device_mappings + iam_instance_profile_name = join("", aws_iam_instance_profile.default.*.name) + security_group_ids = [module.security_group.id] + termination_policies = var.termination_policies + wait_for_capacity_timeout = var.wait_for_capacity_timeout + ebs_optimized = var.ebs_optimized + + min_size = var.min_size + max_size = var.max_size + desired_capacity = var.desired_capacity + + # Auto-scaling policies and CloudWatch metric alarms + autoscaling_policies_enabled = true + default_cooldown = var.default_cooldown + scale_down_cooldown_seconds = var.scale_down_cooldown_seconds + cpu_utilization_high_threshold_percent = var.cpu_utilization_high_threshold_percent + cpu_utilization_low_threshold_percent = var.cpu_utilization_low_threshold_percent + + # The instance refresh definition + # If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated + instance_refresh = var.instance_refresh + + context = module.this.context +} diff --git a/modules/spacelift/worker-pool/outputs.tf b/modules/spacelift/worker-pool/outputs.tf new file mode 100644 index 000000000..fba712d6f --- /dev/null +++ b/modules/spacelift/worker-pool/outputs.tf @@ -0,0 +1,89 @@ +output "worker_pool_id" { + value = join("", spacelift_worker_pool.primary.*.id) + description = "Spacelift worker pool ID" +} + +output "worker_pool_name" { + value = join("", spacelift_worker_pool.primary.*.name) + description = "Spacelift worker pool name" +} + +output "security_group_id" { + description = "Spacelift Security Group ID" + value = module.security_group.id +} + +output "security_group_arn" { + description = "Spacelift Security Group ARN" + value = module.security_group.arn +} + +output "security_group_name" { + description = "Spacelift Security Group Name" + value = module.security_group.name +} + +output "launch_template_id" { + description = "The ID of the launch template" + value = module.autoscale_group.launch_template_id +} + +output "launch_template_arn" { + description = "The ARN of the launch template" + value = module.autoscale_group.launch_template_arn +} + +output "autoscaling_group_id" { + description = "The autoscaling group id" + value = module.autoscale_group.autoscaling_group_id +} + +output "autoscaling_group_name" { + description = "The autoscaling group name" + value = module.autoscale_group.autoscaling_group_name +} + +output "autoscaling_group_arn" { + description = "The ARN for this AutoScaling Group" + value = module.autoscale_group.autoscaling_group_arn +} + +output "autoscaling_group_min_size" { + description = "The minimum size of the autoscale group" + value = module.autoscale_group.autoscaling_group_min_size +} + +output "autoscaling_group_max_size" { + description = "The maximum size of the autoscale group" + value = module.autoscale_group.autoscaling_group_max_size +} + +output "autoscaling_group_default_cooldown" { + description = "Time between a scaling activity and the succeeding scaling activity" + value = module.autoscale_group.autoscaling_group_default_cooldown +} + +output "autoscaling_group_health_check_grace_period" { + description = "Time after instance comes into service before checking health" + value = module.autoscale_group.autoscaling_group_health_check_grace_period +} + +output "autoscaling_group_health_check_type" { + description = "`EC2` or `ELB`. Controls how health checking is done" + value = module.autoscale_group.autoscaling_group_health_check_type +} + +output "iam_role_name" { + value = join("", aws_iam_role.default.*.name) + description = "Spacelift IAM Role name" +} + +output "iam_role_id" { + value = join("", aws_iam_role.default.*.unique_id) + description = "Spacelift IAM Role ID" +} + +output "iam_role_arn" { + value = join("", aws_iam_role.default.*.arn) + description = "Spacelift IAM Role ARN" +} diff --git a/modules/spacelift/worker-pool/providers.tf b/modules/spacelift/worker-pool/providers.tf new file mode 100644 index 000000000..e569b8514 --- /dev/null +++ b/modules/spacelift/worker-pool/providers.tf @@ -0,0 +1,36 @@ +# This provider always validates its credentials, so we always pass api_key_id and api_key_secret +provider "spacelift" { + api_key_endpoint = var.spacelift_api_endpoint + api_key_id = data.aws_ssm_parameter.spacelift_key_id.value + api_key_secret = data.aws_ssm_parameter.spacelift_key_secret.value +} + +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/spacelift/worker-pool/remote-state.tf b/modules/spacelift/worker-pool/remote-state.tf new file mode 100644 index 000000000..5d78adb0d --- /dev/null +++ b/modules/spacelift/worker-pool/remote-state.tf @@ -0,0 +1,32 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "account-map" + environment = coalesce(var.account_map_environment_name, module.this.environment) + stage = var.account_map_stage_name + tenant = coalesce(var.account_map_tenant_name, module.this.tenant) + + context = module.this.context +} + +module "ecr" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "ecr" + environment = coalesce(var.ecr_environment_name, module.this.environment) + stage = var.ecr_stage_name + tenant = coalesce(var.ecr_tenant_name, module.this.tenant) + + context = module.this.context +} + +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "vpc" + + context = module.this.context +} diff --git a/modules/spacelift/worker-pool/templates/spacelift@.service b/modules/spacelift/worker-pool/templates/spacelift@.service new file mode 100644 index 000000000..4985def09 --- /dev/null +++ b/modules/spacelift/worker-pool/templates/spacelift@.service @@ -0,0 +1,13 @@ +[Unit] +Description=Spacelift agent %i + +[Service] +Type=simple +ExecStart=/bin/bash -c "/usr/bin/spacelift-launcher 1>>/var/log/spacelift/info.log 2>>/var/log/spacelift/error.log" +EnvironmentFile=/etc/spacelift/spacelift.env +Restart=always +RestartSec=20 +TimeoutStartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/modules/spacelift/worker-pool/templates/user-data.sh b/modules/spacelift/worker-pool/templates/user-data.sh new file mode 100644 index 000000000..db8ae1cae --- /dev/null +++ b/modules/spacelift/worker-pool/templates/user-data.sh @@ -0,0 +1,115 @@ +#!/bin/bash -e + +spacelift() { ( + set -e + + $(aws --region ${ecr_region} ecr get-login --registry-ids ${ecr_account_id} --no-include-email) + docker pull ${spacelift_runner_image} + + echo "Updating packages (security)" | tee -a /var/log/spacelift/info.log + yum update-minimal --security -y 1>>/var/log/spacelift/info.log 2>>/var/log/spacelift/error.log + +%{ if github_netrc_enabled } + export GITHUB_TOKEN=$(aws ssm get-parameters --region=${region} --name ${github_netrc_ssm_path_token} --with-decryption --query "Parameters[0].Value" --output text) + export GITHUB_USER=$(aws ssm get-parameters --region=${region} --name ${github_netrc_ssm_path_user} --with-decryption --query "Parameters[0].Value" --output text) + + # Allows downloading terraform modules using a GitHub PAT + NETRC_FILE="/root/.netrc" + echo "Creating $NETRC_FILE" + printf "machine github.com\n" > "$NETRC_FILE" + printf "login %s\n" "$GITHUB_USER" >> "$NETRC_FILE" + printf "password %s\n" "$GITHUB_TOKEN" >> "$NETRC_FILE" + echo "Created $NETRC_FILE" + + # Converts ssh clones into https clones to take advantage of the GitHub PAT + ## NOTE: --system cannot be used as HOME is unset during the cloud-init userdata portion + ## so --file has to be passed in manually. + yum install git -y + GIT_CONFIG="/root/.gitconfig" + echo "Creating $GIT_CONFIG" + git config --file $GIT_CONFIG url."https://github.com/".insteadOf "git@github.com:" + git config --file $GIT_CONFIG url."https://github.com/".insteadOf "ssh://git@github.com/" --add + echo "Created $GIT_CONFIG" + yum remove git -y + + # Mount the .netrc and .gitconfig files into the container + export SPACELIFT_WORKER_EXTRA_MOUNTS=$NETRC_FILE:/conf/.netrc,$GIT_CONFIG:/conf/.gitconfig +%{ endif } +%{ if infracost_enabled } + export INFRACOST_API_KEY=$(aws ssm get-parameters --region=${region} --name ${infracost_api_token_ssm_path} --with-decryption --query "Parameters[0].Value" --output text) + export INFRACOST_CLI_ARGS=${infracost_cli_args} + export INFRACOST_WARN_ON_FAILURE=${infracost_warn_on_failure} +%{ endif } + export SPACELIFT_POOL_PRIVATE_KEY=${spacelift_worker_pool_private_key} + export SPACELIFT_TOKEN=${spacelift_worker_pool_config} + # This is a comma separated list of all the environment variables to read from the env file + export SPACELIFT_WHITELIST_ENVS=AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN,AWS_SDK_LOAD_CONFIG,AWS_CONFIG_FILE,AWS_PROFILE,GITHUB_TOKEN,INFRACOST_API_KEY,ATMOS_BASE_PATH,TF_VAR_terraform_user + # This is a comma separated list of all the sensitive environment variables that will show up masked if printed during a run + export SPACELIFT_MASK_ENVS=AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN,GITHUB_TOKEN,INFRACOST_API_KEY + export SPACELIFT_LAUNCHER_LOGS_TIMEOUT=30m + export SPACELIFT_LAUNCHER_RUN_TIMEOUT=120m + # These vars are prefixed with TMP_ so they do not conflict with AWS_ specific vars + export TMP_AWS_SDK_LOAD_CONFIG=true + export TMP_AWS_CONFIG_FILE=${aws_config_file} + export TMP_AWS_PROFILE=${aws_profile} + + echo "Turning on swap" | tee -a /var/log/spacelift/info.log + dd if=/dev/zero of=/swapfile bs=128M count=32 2>/var/log/spacelift/error.log + chmod 600 /swapfile 2>/var/log/spacelift/error.log + mkswap /swapfile 2>/var/log/spacelift/error.log + swapon /swapfile 2>/var/log/spacelift/error.log + swapon -s | tee -a /var/log/spacelift/info.log + + echo "Downloading Spacelift launcher" | tee -a /var/log/spacelift/info.log + curl https://downloads.${spacelift_domain_name}/spacelift-launcher --output /usr/bin/spacelift-launcher 2>>/var/log/spacelift/error.log + + echo "Making the Spacelift launcher executable" | tee -a /var/log/spacelift/info.log + chmod 755 /usr/bin/spacelift-launcher 2>>/var/log/spacelift/error.log + + echo "Retrieving EC2 instance ID" | tee -a /var/log/spacelift/info.log + export SPACELIFT_METADATA_instance_id=$(ec2-metadata --instance-id | cut -d ' ' -f2) + + echo "Retrieving EC2 ASG ID" | tee -a /var/log/spacelift/info.log + export SPACELIFT_METADATA_asg_id=$(aws autoscaling --region=${region} describe-auto-scaling-instances --instance-ids "$SPACELIFT_METADATA_instance_id" | jq -r '.AutoScalingInstances[0].AutoScalingGroupName') + + echo "Preparing Spacelift ENV variables" | tee -a /var/log/spacelift/info.log + env_file="/etc/spacelift/spacelift.env" + sudo mkdir -p "/etc/spacelift" + sudo touch "$env_file" + sudo chmod 744 "$env_file" + printf "SPACELIFT_POOL_PRIVATE_KEY=%s\n" "$SPACELIFT_POOL_PRIVATE_KEY" > "$env_file" + printf "SPACELIFT_TOKEN=%s\n" "$SPACELIFT_TOKEN" >> "$env_file" + printf "SPACELIFT_WHITELIST_ENVS=%s\n" "$SPACELIFT_WHITELIST_ENVS" >> "$env_file" + printf "SPACELIFT_MASK_ENVS=%s\n" "$SPACELIFT_MASK_ENVS" >> "$env_file" + printf "SPACELIFT_LAUNCHER_LOGS_TIMEOUT=%s\n" "$SPACELIFT_LAUNCHER_LOGS_TIMEOUT" >> "$env_file" + printf "SPACELIFT_LAUNCHER_RUN_TIMEOUT=%s\n" "$SPACELIFT_LAUNCHER_RUN_TIMEOUT" >> "$env_file" + printf "SPACELIFT_METADATA_instance_id=%s\n" "$SPACELIFT_METADATA_instance_id" >> "$env_file" + printf "SPACELIFT_METADATA_asg_id=%s\n" "$SPACELIFT_METADATA_asg_id" >> "$env_file" + printf "AWS_SDK_LOAD_CONFIG=%s\n" "$TMP_AWS_SDK_LOAD_CONFIG" >> "$env_file" + printf "AWS_CONFIG_FILE=%s\n" "$TMP_AWS_CONFIG_FILE" >> "$env_file" + printf "AWS_PROFILE=%s\n" "$TMP_AWS_PROFILE" >> "$env_file" + printf "ATMOS_BASE_PATH=%s\n" "/mnt/workspace/source" >> "$env_file" + printf "TF_VAR_terraform_user=%s\n" "spacelift" >> "$env_file" + [[ ! -z "$GITHUB_TOKEN" ]] && printf "GITHUB_TOKEN=%s\n" "$GITHUB_TOKEN" >> "$env_file" + [[ ! -z "$GITHUB_USER" ]] && printf "GITHUB_USER=%s\n" "$GITHUB_USER" >> "$env_file" + [[ ! -z "$SPACELIFT_WORKER_EXTRA_MOUNTS" ]] && printf "SPACELIFT_WORKER_EXTRA_MOUNTS=%s\n" "$SPACELIFT_WORKER_EXTRA_MOUNTS" >> "$env_file" + [[ ! -z "$INFRACOST_API_KEY" ]] && printf "INFRACOST_API_KEY=%s\n" "$INFRACOST_API_KEY" >> "$env_file" + + echo "Enabling Spacelift agent services" | tee -a /var/log/spacelift/info.log + sudo systemctl enable spacelift@{1..${spacelift_agents_per_node}}.service + + echo "Enabling Amazon SSM agent" | tee -a /var/log/spacelift/info.log + sudo systemctl enable amazon-ssm-agent + + echo "Reloading systemd daemon" | tee -a /var/log/spacelift/info.log + sudo systemctl daemon-reload + + echo "Starting Amazon SSM agent" | tee -a /var/log/spacelift/info.log + sudo systemctl start amazon-ssm-agent + + echo "Starting Spacelift agents" | tee -a /var/log/spacelift/info.log + sudo systemctl start spacelift@{1..${spacelift_agents_per_node}}.service + +); } + +spacelift diff --git a/modules/spacelift/worker-pool/variables.tf b/modules/spacelift/worker-pool/variables.tf new file mode 100644 index 000000000..385b759ab --- /dev/null +++ b/modules/spacelift/worker-pool/variables.tf @@ -0,0 +1,292 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "mixed_instances_policy" { + description = "Policy to use a mixed group of on-demand/spot of different types. Launch template is automatically generated. https://www.terraform.io/docs/providers/aws/r/autoscaling_group.html#mixed_instances_policy-1" + + type = object({ + instances_distribution = object({ + on_demand_allocation_strategy = string + on_demand_base_capacity = number + on_demand_percentage_above_base_capacity = number + spot_allocation_strategy = string + spot_instance_pools = number + spot_max_price = string + }) + override = list(object({ + instance_type = string + weighted_capacity = number + })) + }) + default = null +} + +variable "ebs_optimized" { + type = bool + description = "If true, the launched EC2 instance will be EBS-optimized" + default = false +} + +variable "max_size" { + type = number + description = "The maximum size of the autoscale group" +} + +variable "min_size" { + type = number + description = "The minimum size of the autoscale group" +} + +variable "desired_capacity" { + type = number + description = "The number of Amazon EC2 instances that should be running in the group, if not set will use `min_size` as value" + default = null +} + +variable "wait_for_capacity_timeout" { + type = string + description = "A maximum duration that Terraform should wait for ASG instances to be healthy before timing out. (See also Waiting for Capacity below.) Setting this to '0' causes Terraform to skip all Capacity Waiting behavior" +} + +variable "cpu_utilization_high_threshold_percent" { + type = number + description = "CPU utilization high threshold" +} + +variable "cpu_utilization_low_threshold_percent" { + type = number + description = "CPU utilization low threshold" +} + +variable "default_cooldown" { + type = number + description = "The amount of time, in seconds, after a scaling activity completes before another scaling activity can start" + default = 300 +} + +variable "scale_down_cooldown_seconds" { + type = number + default = 300 + description = "The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start" +} + +variable "health_check_type" { + type = string + description = "Controls how health checking is done. Valid values are `EC2` or `ELB`" + default = "EC2" +} + +variable "health_check_grace_period" { + type = number + description = "Time (in seconds) after instance comes into service before checking health" + default = 300 +} + +variable "termination_policies" { + description = "A list of policies to decide how the instances in the auto scale group should be terminated. The allowed values are `OldestInstance`, `NewestInstance`, `OldestLaunchConfiguration`, `ClosestToNextInstanceHour`, `Default`" + type = list(string) + default = ["OldestLaunchConfiguration"] +} + +variable "block_device_mappings" { + description = "Specify volumes to attach to the instance besides the volumes specified by the AMI" + + type = list(object({ + device_name = string + no_device = bool + virtual_name = string + ebs = object({ + delete_on_termination = bool + encrypted = bool + iops = number + kms_key_id = string + snapshot_id = string + volume_size = number + volume_type = string + }) + })) + + default = [] +} + +variable "account_map_environment_name" { + type = string + description = "The name of the environment where `account_map` is provisioned" + default = "gbl" +} + +variable "account_map_stage_name" { + type = string + description = "The name of the stage where `account_map` is provisioned" + default = "root" +} + +variable "account_map_tenant_name" { + type = string + description = <<-EOT + The name of the tenant where `account_map` is provisioned. + + If the `tenant` label is not used, leave this as `null`. + EOT + default = null +} + +variable "ecr_environment_name" { + type = string + description = "The name of the environment where `ecr` is provisioned" + default = "" +} + +variable "ecr_stage_name" { + type = string + description = "The name of the stage where `ecr` is provisioned" + default = "artifacts" +} + +variable "ecr_tenant_name" { + type = string + description = <<-EOT + The name of the tenant where `ecr` is provisioned. + + If the `tenant` label is not used, leave this as `null`. + EOT + default = null +} + + +variable "ecr_region" { + type = string + description = "AWS region that contains the ECR infrastructure repo" + default = "" +} + +variable "ecr_repo_name" { + type = string + description = "ECR repository name" +} + +variable "instance_type" { + type = string + description = "EC2 instance type to use for workers" + default = "r5n.large" +} + +variable "spacelift_runner_image" { + type = string + description = "URL of ECR image to use for Spacelift" + default = "" +} + +variable "spacelift_api_endpoint" { + type = string + description = "The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io)" +} + +variable "spacelift_ami_id" { + type = string + description = "AMI ID of Spacelift worker pool image" + default = null +} + +variable "custom_spacelift_ami" { + type = bool + description = "Custom spacelift AMI" + default = false +} + +variable "spacelift_domain_name" { + type = string + description = "Top-level domain name to use for pulling the launcher binary" + default = "spacelift.io" +} + +variable "iam_attributes" { + type = list(string) + description = "Additional attributes to add to the IDs of the IAM role and policy" + default = [] +} + +variable "instance_refresh" { + description = "The instance refresh definition. If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated" + type = object({ + strategy = string + preferences = object({ + instance_warmup = number + min_healthy_percentage = number + }) + triggers = list(string) + }) + + default = null +} + +variable "github_netrc_enabled" { + type = bool + description = "Whether to create a GitHub .netrc file so Spacelift can clone private GitHub repositories." + default = false +} + +variable "github_netrc_ssm_path_token" { + type = string + description = "If `github_netrc` is enabled, this is the SSM path to retrieve the GitHub token." + default = "/github/token" +} + +variable "github_netrc_ssm_path_user" { + type = string + description = "If `github_netrc` is enabled, this is the SSM path to retrieve the GitHub user" + default = "/github/user" +} + +variable "infracost_enabled" { + type = bool + description = "Whether to enable infracost for Spacelift stacks" + default = false +} + +variable "infracost_api_token_ssm_path" { + type = string + description = "This is the SSM path to retrieve and set the INFRACOST_API_TOKEN environment variable" + default = "/infracost/token" +} + +variable "infracost_cli_args" { + type = string + description = "These are the CLI args passed to infracost" + default = "" +} + +variable "infracost_warn_on_failure" { + type = bool + description = "A failure executing Infracost, or a non-zero exit code being returned from the command will cause runs to fail. If this is true, this will only warn instead of failing the stack." + default = true +} + +variable "aws_config_file" { + type = string + description = "The AWS_CONFIG_FILE used by the worker. Can be overridden by `/.spacelift/config.yml`." + default = "/etc/aws-config/aws-config-spacelift" +} + +variable "aws_profile" { + type = string + description = <<-EOT + The AWS_PROFILE used by the worker. If not specified, `"$${var.namespace}-identity"` will be used. + Can be overridden by `/.spacelift/config.yml`. + EOT + default = null +} + +variable "spacelift_agents_per_node" { + type = number + description = "Number of Spacelift agents to run on one worker node" + default = 1 +} + +variable "spacelift_aws_account_id" { + type = string + description = "AWS Account ID owned by Spacelift" + default = "643313122712" +} diff --git a/modules/spacelift/worker-pool/versions.tf b/modules/spacelift/worker-pool/versions.tf new file mode 100644 index 000000000..1b4e6e5f2 --- /dev/null +++ b/modules/spacelift/worker-pool/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + spacelift = { + source = "spacelift-io/spacelift" + version = ">= 0.1.2" + } + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = ">= 2.2.0" + } + } +} From 31912f911d9b32490954897f1ebced8fb767cba7 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 9 Jun 2023 12:52:26 -0700 Subject: [PATCH 150/501] upstream argocd (#634) Co-authored-by: Dan Miller --- modules/eks/argocd/README.md | 44 ++++- modules/eks/argocd/main.tf | 164 ++++++++++++------ .../argocd/resources/argocd-values.yaml.tpl | 3 +- .../argocd/variables-argocd-notifications.tf | 30 +--- modules/eks/argocd/variables-argocd.tf | 21 +-- 5 files changed, 153 insertions(+), 109 deletions(-) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 4f04acd98..6c7fd3d7a 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -4,8 +4,13 @@ This component is responsible for provisioning [Argo CD](https://argoproj.github Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. -> :warning::warning::warning: Initial install needs run `deploy` two times because first run will create ArgoCD CRDs -> and second run will finish ArgoCD configuration. :warning::warning::warning: +> :warning::warning::warning: ArgoCD CRDs must be installed separately from this component/helm release. :warning::warning::warning: +```shell +kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=" + +# Eg. version v2.4.9 +kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=v2.4.9" +``` ## Usage @@ -40,6 +45,30 @@ components: chart_values: {} ``` +to use google OIDC: + +```yaml + oidc_enabled: true + saml_enabled: false + oidc_providers: + google: + uses_dex: true + type: google + id: google + name: Google + serviceAccountAccess: + enabled: true + key: googleAuth.json + value: /sso/oidc/google/serviceaccount + admin_email: an_actual_user@acme.com + config: + # This filters emails when signing in with Google to only this domain. helpful for picking the right one. + hostedDomains: + - acme.com + clientID: /sso/saml/google/clientid + clientSecret: /sso/saml/google/clientsecret +``` + ## Requirements @@ -69,6 +98,8 @@ components: | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles\_config\_secrets](#module\_iam\_roles\_config\_secrets) | ../../account-map/modules/iam-roles | n/a | +| [oidc\_gsuite\_service\_providers\_providers\_store\_read](#module\_oidc\_gsuite\_service\_providers\_providers\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [oidc\_providers\_store\_read](#module\_oidc\_providers\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.10.0 | | [saml\_sso\_providers](#module\_saml\_sso\_providers) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -76,6 +107,7 @@ components: | Name | Type | |------|------| +| [kubernetes_secret.oidc_gsuite_service_account](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | | [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | | [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | @@ -83,14 +115,12 @@ components: | [aws_ssm_parameter.oidc_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.oidc_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameters_by_path.argocd_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | -| [kubernetes_resources.crd](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/resources) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [admin\_enabled](#input\_admin\_enabled) | Toggles Admin user creation the deployed chart | `bool` | `false` | no | | [alb\_group\_name](#input\_alb\_group\_name) | A name used in annotations to reuse an ALB (e.g. `argocd`) or to generate a new one | `string` | `null` | no | | [alb\_logs\_bucket](#input\_alb\_logs\_bucket) | The name of the bucket for ALB access logs. The bucket must have policy allowing the ELB logging principal | `string` | `""` | no | | [alb\_logs\_prefix](#input\_alb\_logs\_prefix) | `alb_logs_bucket` s3 bucket prefix | `string` | `""` | no | @@ -103,7 +133,6 @@ components: | [argocd\_apps\_chart\_version](#input\_argocd\_apps\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"0.0.3"` | no | | [argocd\_apps\_enabled](#input\_argocd\_apps\_enabled) | Enable argocd apps | `bool` | `true` | no | | [argocd\_create\_namespaces](#input\_argocd\_create\_namespaces) | ArgoCD create namespaces policy | `bool` | `false` | no | -| [argocd\_rbac\_default\_policy](#input\_argocd\_rbac\_default\_policy) | Default ArgoCD RBAC default role.

See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#basic-built-in-roles for more information. | `string` | `"role:readonly"` | no | | [argocd\_rbac\_groups](#input\_argocd\_rbac\_groups) | List of ArgoCD Group Role Assignment strings to be added to the argocd-rbac configmap policy.csv item.
e.g.
[
{
group: idp-group-name,
role: argocd-role-name
},
]
becomes: `g, idp-group-name, role:argocd-role-name`
See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. |
list(object({
group = string,
role = string
}))
| `[]` | no | | [argocd\_rbac\_policies](#input\_argocd\_rbac\_policies) | List of ArgoCD RBAC Permission strings to be added to the argocd-rbac configmap policy.csv item.

See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. | `list(string)` | `[]` | no | | [argocd\_repositories](#input\_argocd\_repositories) | Map of objects defining an `argocd_repo` to configure. The key is the name of the ArgoCD repository. |
map(object({
environment = string # The environment where the `argocd_repo` component is deployed.
stage = string # The stage where the `argocd_repo` component is deployed.
tenant = string # The tenant where the `argocd_repo` component is deployed.
}))
| `{}` | no | @@ -150,12 +179,13 @@ components: | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [notifications\_default\_triggers](#input\_notifications\_default\_triggers) | Default notification Triggers to configure.

See: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/triggers/#default-triggers
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/790438efebf423c2d56cb4b93471f4adb3fcd448/charts/argo-cd/values.yaml#L2841) | `map(list(string))` | `{}` | no | -| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = number
installationID = number
privateKey = optional(string)
}))
# service.webhook.:
service_webhook = optional(map(
object({
url = string
headers = optional(list(
object({
name = string
value = string
})
), [])
basicAuth = optional(object({
username = string
password = string
}))
insecureSkipVerify = optional(bool, false)
})
))
})
| `{}` | no | -| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
webhook = optional(map(
object({
method = optional(string)
path = optional(string)
body = optional(string)
})
))
}))
| `{}` | no | +| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = optional(number)
installationID = optional(number)
privateKey = optional(string)
}))
})
| `null` | no | +| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
}))
| `{}` | no | | [notifications\_triggers](#input\_notifications\_triggers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
map(list(
object({
oncePer = optional(string)
send = list(string)
when = string
})
))
| `{}` | no | | [oidc\_enabled](#input\_oidc\_enabled) | Toggles OIDC integration in the deployed chart | `bool` | `false` | no | | [oidc\_issuer](#input\_oidc\_issuer) | OIDC issuer URL | `string` | `""` | no | | [oidc\_name](#input\_oidc\_name) | Name of the OIDC resource | `string` | `""` | no | +| [oidc\_providers](#input\_oidc\_providers) | OIDC providers components, clientID and clientSecret should be passed as SSM parameters (denoted by leading slash) | `any` | `{}` | no | | [oidc\_rbac\_scopes](#input\_oidc\_rbac\_scopes) | OIDC RBAC scopes to request | `string` | `"[argocd_realm_access]"` | no | | [oidc\_requested\_scopes](#input\_oidc\_requested\_scopes) | Set of OIDC scopes to request | `string` | `"[\"openid\", \"profile\", \"email\", \"groups\"]"` | no | | [rbac\_enabled](#input\_rbac\_enabled) | Enable Service Account for pods. | `bool` | `true` | no | diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 0b2018be4..c5e3a3819 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -1,5 +1,30 @@ locals { - enabled = module.this.enabled + enabled = module.this.enabled +} + +module "oidc_providers_store_read" { + for_each = local.enabled ? var.oidc_providers : {} + source = "cloudposse/ssm-parameter-store/aws" + version = "0.10.0" + + parameter_read = [for k, v in each.value.config : v if try(startswith(v, "/"), false)] +} + +locals { + param_store_values = [for k, v in module.oidc_providers_store_read : module.oidc_providers_store_read[k].map] + vals = flatten([for k, v in local.param_store_values : [for k2, v2 in v : v2]]) + keys = flatten([for k, v in local.param_store_values : [for k2, v2 in v : k2]]) + map = zipmap(local.keys, local.vals) + oidc_providers_merged = { + for name, values in var.oidc_providers : name => merge(values, { + config = { + for k, v in values.config : k => try(startswith(v, "/"), false) ? local.map[v] : v + } + }) + } +} + +locals { kubernetes_namespace = var.kubernetes_namespace count_enabled = local.enabled ? 1 : 0 oidc_enabled = local.enabled && var.oidc_enabled @@ -11,7 +36,7 @@ locals { github_deploy_key = data.aws_ssm_parameter.github_deploy_key[k].value } } : {} - credential_templates = flatten(concat([ + credential_templates = flatten([ for k, v in local.argocd_repositories : [ { name = "configs.credentialTemplates.${k}.url" @@ -20,43 +45,34 @@ locals { }, { name = "configs.credentialTemplates.${k}.sshPrivateKey" - value = nonsensitive(v.github_deploy_key) + value = v.github_deploy_key type = "string" }, ] - ], - [ - for s, v in local.notifications_notifiers_ssm_configs : [ - for k, i in v : [ - { - name = "notifications.secret.items.${s}_${k}" - value = i - type = "string" - } - ] - ] - ])) + ]) regional_service_discovery_domain = "${module.this.environment}.${module.dns_gbl_delegated.outputs.default_domain_name}" host = var.host != "" ? var.host : format("%s.%s", coalesce(var.alb_name, var.name), local.regional_service_discovery_domain) enable_argo_workflows_auth = local.saml_enabled && var.argo_enable_workflows_auth enable_argo_workflows_auth_count = local.enable_argo_workflows_auth ? 1 : 0 argo_workflows_host = "${var.argo_workflows_name}.${local.regional_service_discovery_domain}" - oidc_config_map = local.oidc_enabled ? { + oidc_values = values(local.oidc_providers_merged)[0] + + oidc_config_map = local.oidc_enabled && !lookup(local.oidc_values, "uses_dex", true) ? { server : { config : { "oidc.config" = <<-EOT - name: ${var.oidc_name} - issuer: ${var.oidc_issuer} - clientID: ${local.oidc_client_id} - clientSecret: ${local.oidc_client_secret} + name: ${lookup(local.oidc_values, "name", null)} + issuer: ${lookup(local.oidc_values.config, "issuer", null)} + clientID: ${lookup(local.oidc_values.config, "clientID", null)} + clientSecret: ${lookup(local.oidc_values.config, "clientSecret", null)} requestedScopes: ${var.oidc_requested_scopes} EOT } } } : {} - saml_config_map = local.saml_enabled ? { + dex_config_map = { configs : { params : { "dexserver.disable.tls" = true @@ -68,10 +84,11 @@ locals { ]) } } - } : {} + } dex_config_connectors = yamlencode({ - connectors = [for name, config in(local.enabled ? var.saml_sso_providers : {}) : + connectors = concat([ + for name, config in(local.enabled && local.saml_enabled ? var.saml_sso_providers : {}) : { type = "saml" id = "saml" @@ -81,19 +98,36 @@ locals { caData = base64encode(format("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", module.saml_sso_providers[name].outputs.ca)) redirectURI = format("https://%s/api/dex/callback", local.host) entityIssuer = format("https://%s/api/dex/callback", local.host) - usernameAttr = module.saml_sso_providers[name].outputs.usernameAttr - emailAttr = module.saml_sso_providers[name].outputs.emailAttr - groupsAttr = module.saml_sso_providers[name].outputs.groupsAttr + usernameAttr = "name" + emailAttr = "email" ssoIssuer = module.saml_sso_providers[name].outputs.issuer } } - ] + ], + [ + for name, config in(local.enabled && local.oidc_enabled ? local.oidc_providers_merged : {}) : + { + type = lookup(config, "type", "oidc") + id = lookup(config, "id", name) + name = coalesce(lookup(config, "name", null), name) + config = merge(config.config, { + redirectURI = format("https://%s/api/dex/callback", local.host) + clientID = lookup(config.config, "clientID", null) + clientSecret = lookup(config.config, "clientSecret", null) + serviceAccountFilePath = lookup(config.serviceAccountAccess, "enabled", false) ? "/tmp/oidc/googleAuth.json" : null + adminEmail = lookup(config.serviceAccountAccess, "enabled", false) ? lookup(config.serviceAccountAccess, "admin_email", null) : null + }) + } if lookup(config, "uses_dex", true) + ] + ) }) post_render_script = local.enable_argo_workflows_auth ? "./resources/kustomize/post-render.sh" : null kustomize_files_values = local.enable_argo_workflows_auth ? { __ignore = { - kustomize_files = { for f in fileset("./resources/kustomize", "[^_]*.{sh,yaml}") : f => filesha256("./resources/kustomize/${f}") } + kustomize_files = { + for f in fileset("./resources/kustomize", "[^_]*.{sh,yaml}") : f => filesha256("./resources/kustomize/${f}") + } } } : {} } @@ -105,35 +139,51 @@ data "aws_ssm_parameters_by_path" "argocd_notifications" { } locals { - notifications_notifiers_ssm_path = { for key, value in local.notifications_notifiers_variables : + notifications_notifiers_ssm_path = var.notifications_notifiers == null ? {} : { + for key, value in var.notifications_notifiers : key => format("%s/%s/", var.notifications_notifiers.ssm_path_prefix, key) } - notifications_notifiers_ssm_configs = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : - key => zipmap( + notifications_notifiers_ssm_configs = { + for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : + key => nonsensitive(zipmap( [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], value.values - ) - } - - notifications_notifiers_ssm_configs_keys = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : - key => zipmap( - [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], - [for name in value.names : format("$%s_%s", key, trimprefix(name, local.notifications_notifiers_ssm_path[key]))] - ) + )) } - notifications_notifiers_variables = merge({ for key, value in var.notifications_notifiers : + notifications_notifiers_variables = var.notifications_notifiers == null ? {} : { + for key, value in var.notifications_notifiers : key => { for param_name, param_value in value : param_name => param_value if param_value != null } - if key != "ssm_path_prefix" && key != "service_webhook" - }, - { for key, value in coalesce(var.notifications_notifiers.service_webhook, {}) : - format("service_webhook_%s", key) => { for param_name, param_value in value : param_name => param_value if param_value != null } - }) + if key != "ssm_path_prefix" + } notifications_notifiers = { for key, value in local.notifications_notifiers_variables : - replace(key, "_", ".") => yamlencode(merge(local.notifications_notifiers_ssm_configs_keys[key], value)) + replace(key, "_", ".") => yamlencode(merge(local.notifications_notifiers_ssm_configs[key], value)) + } +} + +#https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/google/#configure-dex +module "oidc_gsuite_service_providers_providers_store_read" { + for_each = { for k, v in var.oidc_providers : k => v if lookup(v.serviceAccountAccess, "enabled", false) } + source = "cloudposse/ssm-parameter-store/aws" + version = "0.10.0" + + parameter_read = [ + for k, v in var.oidc_providers : v.serviceAccountAccess.value + if v.id == "google" && lookup(v.serviceAccountAccess, "enabled", false) + ] +} +resource "kubernetes_secret" "oidc_gsuite_service_account" { + for_each = { for k, v in var.oidc_providers : k => v if lookup(v.serviceAccountAccess, "enabled", false) } + metadata { + name = "argocd-google-groups-json" + namespace = local.kubernetes_namespace + } + + data = { + (each.value.serviceAccountAccess.key) = trimspace(module.oidc_gsuite_service_providers_providers_store_read[each.key].map[each.value.serviceAccountAccess.value]) } } @@ -159,7 +209,7 @@ module "argocd" { service_account_name = module.this.name service_account_namespace = var.kubernetes_namespace - set_sensitive = nonsensitive(local.credential_templates) + set_sensitive = local.credential_templates values = compact([ # standard k8s object settings @@ -177,7 +227,7 @@ module "argocd" { templatefile( "${path.module}/resources/argocd-values.yaml.tpl", { - admin_enabled = var.admin_enabled + admin_enabled = true alb_group_name = var.alb_group_name == null ? "" : var.alb_group_name alb_logs_bucket = var.alb_logs_bucket alb_logs_prefix = var.alb_logs_prefix @@ -193,7 +243,6 @@ module "argocd" { organization = var.github_organization saml_enabled = local.saml_enabled saml_rbac_scopes = var.saml_rbac_scopes - rbac_default_policy = var.argocd_rbac_default_policy rbac_policies = var.argocd_rbac_policies rbac_groups = var.argocd_rbac_groups enable_argo_workflows_auth = local.enable_argo_workflows_auth @@ -221,7 +270,8 @@ module "argocd" { yamlencode( { notifications = { - triggers = { for key, value in var.notifications_triggers : + triggers = { + for key, value in var.notifications_triggers : replace(key, "_", ".") => yamlencode(value) } } @@ -236,22 +286,22 @@ module "argocd" { ), yamlencode(merge( local.oidc_config_map, - local.saml_config_map, + local.dex_config_map, )), yamlencode(local.kustomize_files_values), yamlencode(var.chart_values) ]) context = module.this.context -} -data "kubernetes_resources" "crd" { - api_version = "apiextensions.k8s.io/v1" - kind = "CustomResourceDefinition" - field_selector = "metadata.name==applications.argoproj.io" + depends_on = [ + kubernetes_secret.oidc_gsuite_service_account + ] } module "argocd_apps" { + count = local.enabled && var.argocd_apps_enabled ? 1 : 0 + source = "cloudposse/helm-release/aws" version = "0.3.0" @@ -266,7 +316,7 @@ module "argocd_apps" { atomic = var.atomic cleanup_on_fail = var.cleanup_on_fail timeout = var.timeout - enabled = local.enabled && var.argocd_apps_enabled && length(data.kubernetes_resources.crd.objects) > 0 + enabled = local.enabled && var.argocd_apps_enabled values = compact([ templatefile( "${path.module}/resources/argocd-apps-values.yaml.tpl", diff --git a/modules/eks/argocd/resources/argocd-values.yaml.tpl b/modules/eks/argocd/resources/argocd-values.yaml.tpl index 16ae99c63..c75ddeabd 100644 --- a/modules/eks/argocd/resources/argocd-values.yaml.tpl +++ b/modules/eks/argocd/resources/argocd-values.yaml.tpl @@ -115,7 +115,6 @@ server: return hs rbacConfig: - policy.default: ${rbac_default_policy} policy.csv: | %{ for policy in rbac_policies ~} ${policy} @@ -131,7 +130,7 @@ server: scopes: '${saml_rbac_scopes}' %{ endif ~} - policy.default: role:readonly + policy.default: role:none repoServer: replicas: 2 diff --git a/modules/eks/argocd/variables-argocd-notifications.tf b/modules/eks/argocd/variables-argocd-notifications.tf index 4105c84e4..f03b2791d 100644 --- a/modules/eks/argocd/variables-argocd-notifications.tf +++ b/modules/eks/argocd/variables-argocd-notifications.tf @@ -13,13 +13,6 @@ variable "notifications_templates" { targetURL = string }) })) - webhook = optional(map( - object({ - method = optional(string) - path = optional(string) - body = optional(string) - }) - )) })) default = {} description = <<-EOT @@ -51,29 +44,12 @@ variable "notifications_notifiers" { type = object({ ssm_path_prefix = optional(string, "/argocd/notifications/notifiers") service_github = optional(object({ - appID = number - installationID = number + appID = optional(number) + installationID = optional(number) privateKey = optional(string) })) - # service.webhook.: - service_webhook = optional(map( - object({ - url = string - headers = optional(list( - object({ - name = string - value = string - }) - ), []) - basicAuth = optional(object({ - username = string - password = string - })) - insecureSkipVerify = optional(bool, false) - }) - )) }) - default = {} + default = null description = <<-EOT Notification Triggers to configure. diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index cf8429304..6c0a787ca 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -95,12 +95,6 @@ variable "forecastle_enabled" { default = false } -variable "admin_enabled" { - type = bool - description = "Toggles Admin user creation the deployed chart" - default = false -} - variable "oidc_enabled" { type = bool description = "Toggles OIDC integration in the deployed chart" @@ -171,16 +165,6 @@ variable "argocd_rbac_policies" { EOT } -variable "argocd_rbac_default_policy" { - type = string - default = "role:readonly" - description = <<-EOT - Default ArgoCD RBAC default role. - - See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#basic-built-in-roles for more information. - EOT -} - variable "argocd_rbac_groups" { type = list(object({ group = string, @@ -216,3 +200,8 @@ variable "saml_sso_providers" { description = "SAML SSO providers components" } +variable "oidc_providers" { + type = any + default = {} + description = "OIDC providers components, clientID and clientSecret should be passed as SSM parameters (denoted by leading slash)" +} From e82f7bf6b3c209fd0fd3a24b5bca20a7a89179a2 Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Mon, 12 Jun 2023 17:56:55 +0300 Subject: [PATCH 151/501] Removed list of components from main README.md (#721) --- README.md | 31 +++++-------------------------- README.yaml | 25 +------------------------ 2 files changed, 6 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 7285339e3..a22a5271f 100644 --- a/README.md +++ b/README.md @@ -99,30 +99,7 @@ make rebuild-docs -See each component's README directory for usage. - -| Component | Description | -|-----------|-------------| -|[account](./modules/account) | Provisions the full account hierarchy along with Organizational Units (OUs). | -|[account-map](./modules/account-map) | Provisions information only: it simply populates Terraform state with data (account ids, groups, and roles) that other root modules need via outputs. | -|[account-settings](./modules/account-settings) | Provisions account level settings: IAM password policy, AWS Account Alias, and EBS encryption. | -|[cloudtrail](./modules/cloudtrail) | Provisions cloudtrail auditing in an individual account. | -|[cloudtrail-bucket](./modules/cloudtrail-bucket) | Provisions a bucket for storing cloudtrail logs for auditing purposes. | -|[datadog-integration](./modules/datadog-integration) | Provisions a DataDog <=> AWS integration. | -|[datadog-monitor](./modules/datadog-monitor) | Provisions global DataDog monitors. | -|[dms](./modules/dms) | Provisions AWS DMS resources: DMS IAM roles, DMS endpoints, DMS replication instances, DMS replication tasks. | -|[dns-delegated](./modules/dns-delegated) | Provisions a DNS zone which delegates nameservers to the DNS zone in the primary DNS account. | -|[dns-primary](./modules/dns-primary) | Provisions the primary DNS zones into an AWS account. | -|[ecr](./modules/ecr) | Provisions repositories, lifecycle rules, and permissions for streamlined ECR usage. | -|[efs](./modules/efs) | Provisions an [EFS](https://aws.amazon.com/efs/) Network File System with KMS encryption-at-rest. | -|[eks](./modules/eks) | Provisions an end-to-end EKS Cluster, including managed node groups and [spotinst ocean](https://spot.io/products/ocean/) node pools. | -|[eks-iam](./modules/eks-iam) | Provisions specific [IAM roles for Kubernetes Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). | -|[iam-delegated-roles](./modules/iam-delegated-roles) | Provisions all delegated user and system IAM roles. | -|[iam-primary-roles](./modules/iam-primary-roles) | Provisions all primary user and system roles into the centralized identity account. | -|[sso](./modules/sso) | Provisions SAML metadata into AWS IAM as new SAML providers. | -|[tfstate-backend](./modules/tfstate-backend) | Provisions an S3 Bucket and DynamoDB table that follow security best practices for usage as a Terraform backend. | -|[transit-gateway](./modules/transit-gateway) | Provisions an AWS Transit Gateway to connect various account separated VPCs through a central hub. | -|[vpc](./modules/vpc) | Provisions a VPC and corresponing Subnets. | +Please take a look at each [component's README](./modules) for usage. @@ -326,8 +303,8 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply ### Contributors -| [![Erik Osterman][osterman_avatar]][osterman_homepage]
[Erik Osterman][osterman_homepage] | [![Igor Rodionov][goruha_avatar]][goruha_homepage]
[Igor Rodionov][goruha_homepage] | [![Andriy Knysh][aknysh_avatar]][aknysh_homepage]
[Andriy Knysh][aknysh_homepage] | [![Matt Gowie][Gowiem_avatar]][Gowiem_homepage]
[Matt Gowie][Gowiem_homepage] | [![Yonatan Koren][korenyoni_avatar]][korenyoni_homepage]
[Yonatan Koren][korenyoni_homepage] | -|---|---|---|---|---| +| [![Erik Osterman][osterman_avatar]][osterman_homepage]
[Erik Osterman][osterman_homepage] | [![Igor Rodionov][goruha_avatar]][goruha_homepage]
[Igor Rodionov][goruha_homepage] | [![Andriy Knysh][aknysh_avatar]][aknysh_homepage]
[Andriy Knysh][aknysh_homepage] | [![Matt Gowie][Gowiem_avatar]][Gowiem_homepage]
[Matt Gowie][Gowiem_homepage] | [![Yonatan Koren][korenyoni_avatar]][korenyoni_homepage]
[Yonatan Koren][korenyoni_homepage] | [![Matt Calhoun][mcalhoun_avatar]][mcalhoun_homepage]
[Matt Calhoun][mcalhoun_homepage] | +|---|---|---|---|---|---| [osterman_homepage]: https://github.com/osterman @@ -340,6 +317,8 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply [Gowiem_avatar]: https://img.cloudposse.com/150x150/https://github.com/Gowiem.png [korenyoni_homepage]: https://github.com/korenyoni [korenyoni_avatar]: https://img.cloudposse.com/150x150/https://github.com/korenyoni.png + [mcalhoun_homepage]: https://github.com/mcalhoun + [mcalhoun_avatar]: https://img.cloudposse.com/150x150/https://github.com/mcalhoun.png [![README Footer][readme_footer_img]][readme_footer_link] [![Beacon][beacon]][website] diff --git a/README.yaml b/README.yaml index c5620f4f1..21f2f33fb 100644 --- a/README.yaml +++ b/README.yaml @@ -104,30 +104,7 @@ introduction: |- # How to use this project usage: |- - See each component's README directory for usage. - - | Component | Description | - |-----------|-------------| - |[account](./modules/account) | Provisions the full account hierarchy along with Organizational Units (OUs). | - |[account-map](./modules/account-map) | Provisions information only: it simply populates Terraform state with data (account ids, groups, and roles) that other root modules need via outputs. | - |[account-settings](./modules/account-settings) | Provisions account level settings: IAM password policy, AWS Account Alias, and EBS encryption. | - |[cloudtrail](./modules/cloudtrail) | Provisions cloudtrail auditing in an individual account. | - |[cloudtrail-bucket](./modules/cloudtrail-bucket) | Provisions a bucket for storing cloudtrail logs for auditing purposes. | - |[datadog-integration](./modules/datadog-integration) | Provisions a DataDog <=> AWS integration. | - |[datadog-monitor](./modules/datadog-monitor) | Provisions global DataDog monitors. | - |[dms](./modules/dms) | Provisions AWS DMS resources: DMS IAM roles, DMS endpoints, DMS replication instances, DMS replication tasks. | - |[dns-delegated](./modules/dns-delegated) | Provisions a DNS zone which delegates nameservers to the DNS zone in the primary DNS account. | - |[dns-primary](./modules/dns-primary) | Provisions the primary DNS zones into an AWS account. | - |[ecr](./modules/ecr) | Provisions repositories, lifecycle rules, and permissions for streamlined ECR usage. | - |[efs](./modules/efs) | Provisions an [EFS](https://aws.amazon.com/efs/) Network File System with KMS encryption-at-rest. | - |[eks](./modules/eks) | Provisions an end-to-end EKS Cluster, including managed node groups and [spotinst ocean](https://spot.io/products/ocean/) node pools. | - |[eks-iam](./modules/eks-iam) | Provisions specific [IAM roles for Kubernetes Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). | - |[iam-delegated-roles](./modules/iam-delegated-roles) | Provisions all delegated user and system IAM roles. | - |[iam-primary-roles](./modules/iam-primary-roles) | Provisions all primary user and system roles into the centralized identity account. | - |[sso](./modules/sso) | Provisions SAML metadata into AWS IAM as new SAML providers. | - |[tfstate-backend](./modules/tfstate-backend) | Provisions an S3 Bucket and DynamoDB table that follow security best practices for usage as a Terraform backend. | - |[transit-gateway](./modules/transit-gateway) | Provisions an AWS Transit Gateway to connect various account separated VPCs through a central hub. | - |[vpc](./modules/vpc) | Provisions a VPC and corresponing Subnets. | + Please take a look at each [component's README](./modules) for usage. include: - "docs/targets.md" From 01e88f0f83d0724adf403020eb175c4e80e77566 Mon Sep 17 00:00:00 2001 From: Maksym Vlasov Date: Mon, 12 Jun 2023 20:42:22 +0300 Subject: [PATCH 152/501] chore: Update and add more basic pre-commit hooks (#714) Co-authored-by: Andriy Knysh Co-authored-by: cloudpossebot --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 +- .github/PULL_REQUEST_TEMPLATE.md | 5 +- .pre-commit-config.yaml | 26 +- .../account-map/modules/iam-roles/README.md | 4 +- .../modules/roles-to-principals/README.md | 6 +- deprecated/aws/acm/variables.tf | 1 - deprecated/aws/aws-metrics-role/variables.tf | 1 - deprecated/aws/aws-metrics-role/versions.tf | 1 - deprecated/aws/backing-services/README.md | 2 +- .../aws/backing-services/aurora-postgres.tf | 2 +- deprecated/aws/bootstrap/README.md | 2 +- deprecated/aws/cis-aggregator-auth/output.tf | 1 - deprecated/aws/cis-aggregator/output.tf | 1 - deprecated/aws/cis-executor/output.tf | 1 - deprecated/aws/cis-instances/output.tf | 1 - .../aws/grafana-backing-services/README.md | 2 +- .../aws/keycloak-backing-services/README.md | 16 +- deprecated/aws/kops/README.md | 5 +- deprecated/aws/opsgenie/providers.tf | 0 deprecated/aws/opsgenie/versions.tf | 0 deprecated/aws/root-iam/README.md | 2 +- deprecated/aws/sentry/aurora-postgres.tf | 2 +- deprecated/aws/sentry/variables.tf | 1 - deprecated/aws/sentry/versions.tf | 1 - deprecated/aws/ses/Makefile | 1 - deprecated/aws/sns-topic/main.tf | 1 - deprecated/aws/spotinst-kops/variables.tf | 1 - deprecated/aws/spotinst/main.tf | 1 - deprecated/aws/spotinst/output.tf | 1 - deprecated/aws/spotinst/variables.tf | 1 - deprecated/aws/tfstate-backend/README.md | 4 +- .../tfstate-backend/scripts/force-destroy.sh | 4 +- .../runners/runner/docker-config.json | 2 +- deprecated/github-actions-runner/versions.tf | 2 +- deprecated/iam-primary-roles/README.md | 8 +- deprecated/iam-primary-roles/remote-state.tf | 1 - deprecated/spacelift/README.md | 2 +- .../ecr/github-actions-iam-policy.tf | 1 - .../modules/roles-to-principals/README.md | 6 +- modules/account-settings/README.md | 2 +- modules/alb/variables.tf | 1 - modules/amplify/README.md | 12 +- modules/argocd-repo/templates/.gitignore.tpl | 2 +- modules/argocd-repo/templates/README.md.tpl | 2 +- modules/athena/README.md | 8 +- modules/athena/cloudtrail.tf | 6 +- modules/athena/variables.tf | 1 - modules/aurora-mysql-resources/main.tf | 1 - .../modules/mysql-user/main.tf | 2 +- .../modules/mysql-user/variables.tf | 1 - modules/aurora-mysql-resources/outputs.tf | 1 - modules/aurora-mysql-resources/variables.tf | 1 - modules/aurora-mysql/README.md | 8 +- modules/aurora-mysql/main.tf | 8 +- modules/aurora-mysql/outputs.tf | 1 - modules/aurora-mysql/ssm.tf | 1 - modules/aurora-mysql/variables.tf | 9 +- modules/aurora-postgres-resources/main.tf | 1 - .../modules/postgresql-user/main.tf | 2 +- .../modules/postgresql-user/variables.tf | 1 - modules/aurora-postgres-resources/outputs.tf | 1 - .../aurora-postgres-resources/variables.tf | 1 - modules/aurora-postgres/README.md | 4 +- modules/aurora-postgres/outputs.tf | 1 - modules/aurora-postgres/ssm.tf | 1 - modules/aurora-postgres/variables.tf | 5 +- modules/aws-backup/main.tf | 1 - modules/aws-backup/outputs.tf | 1 - modules/aws-backup/providers.tf | 0 modules/aws-backup/variables.tf | 1 - modules/aws-config/README.md | 10 +- modules/aws-config/outputs.tf | 2 +- modules/aws-config/variables.tf | 6 +- modules/aws-inspector/README.md | 2 +- modules/aws-saml/main.tf | 1 - modules/aws-saml/outputs.tf | 1 - modules/aws-teams/remote-state.tf | 1 - modules/cloudtrail-bucket/variables.tf | 2 +- modules/config-bucket/README.md | 4 +- modules/datadog-integration/README.md | 2 +- modules/datadog-integration/variables.tf | 1 - modules/datadog-lambda-forwarder/README.md | 6 +- modules/datadog-logs-archive/providers.tf | 0 modules/datadog-monitor/versions.tf | 0 .../datadog-private-location-ecs/README.md | 2 +- .../versions.tf | 0 modules/datadog-synthetics/providers.tf | 0 modules/datadog-synthetics/versions.tf | 0 modules/dms/endpoint/variables.tf | 1 - modules/dns-delegated/variables.tf | 1 - modules/documentdb/main.tf | 2 +- modules/documentdb/ssm.tf | 2 +- modules/documentdb/variables.tf | 2 +- modules/dynamodb/outputs.tf | 2 +- modules/dynamodb/variables.tf | 2 +- modules/ec2-client-vpn/README.md | 2 +- modules/ecs/README.md | 2 +- modules/ecs/variables.tf | 1 - .../eks-iam/alb-controller-iam-policy.json | 2 +- modules/eks-iam/alb-controller.tf | 0 modules/eks-iam/autoscaler.tf | 0 modules/eks-iam/cert-manager.tf | 0 modules/eks-iam/default.auto.tfvars | 0 modules/eks-iam/external-dns.tf | 0 modules/eks-iam/main.tf | 0 .../service-account/default.auto.tfvars | 0 .../eks-iam/modules/service-account/main.tf | 0 .../modules/service-account/outputs.tf | 0 .../modules/service-account/variables.tf | 0 modules/eks-iam/outputs.tf | 0 modules/eks-iam/variables.tf | 0 modules/eks-iam/versions.tf | 0 .../templates/horizontalrunnerautoscaler.yaml | 0 .../templates/runnerdeployment.yaml | 4 +- .../charts/actions-runner/values.yaml | 0 modules/eks/actions-runner-controller/main.tf | 1 - .../eks/actions-runner-controller/outputs.tf | 1 - .../actions-runner-controller/providers.tf | 1 - .../actions-runner-controller/variables.tf | 0 .../eks/argocd/resources/kustomize/.gitignore | 2 +- .../resources/kustomize/kustomization.yaml | 2 +- .../eks/argocd/resources/kustomize/patch.yaml | 2 +- .../argocd/resources/kustomize/post-render.sh | 2 +- modules/eks/argocd/variables-argocd-apps.tf | 1 - modules/eks/cert-manager/outputs.tf | 1 - modules/eks/cert-manager/variables.tf | 1 - modules/eks/cluster/README.md | 6 +- modules/eks/echo-server/main.tf | 1 - modules/eks/efs-controller/remote-state.tf | 1 - .../eks/efs-controller/resources/values.yaml | 2 +- modules/eks/eks-without-spotinst/main.tf | 1 - modules/eks/eks-without-spotinst/outputs.tf | 1 - .../eks/external-secrets-operator/README.md | 4 +- .../examples/app-secrets.yaml | 6 +- .../examples/external-secrets.yaml | 4 +- modules/eks/external-secrets-operator/main.tf | 1 - modules/eks/idp-roles/README.md | 2 +- modules/eks/idp-roles/remote-state.tf | 1 - modules/eks/karpenter/README.md | 4 +- modules/eks/karpenter/variables.tf | 2 +- modules/eks/platform/README.md | 8 +- modules/eks/platform/variables.tf | 2 - modules/eks/redis-operator/README.md | 1 - modules/eks/redis/README.md | 1 - modules/github-action-token-rotator/main.tf | 1 - .../github-action-token-rotator/outputs.tf | 1 - modules/github-oidc-provider/README.md | 8 +- .../scripts/get_github_oidc_thumbprint.sh | 2 +- .../modules/graceful_scale_in/outputs.tf | 2 +- .../modules/graceful_scale_in/variables.tf | 2 +- .../templates/amazon-cloudwatch-agent.json | 2 +- .../README.md | 2 +- modules/guardduty/common/outputs.tf | 2 +- modules/guardduty/common/providers.tf | 2 +- modules/guardduty/common/remote-state.tf | 2 +- modules/guardduty/common/variables.tf | 4 +- modules/guardduty/root/providers.tf | 2 +- modules/guardduty/root/remote-state.tf | 2 +- modules/kinesis-stream/README.md | 2 +- modules/lakeformation/README.md | 2 +- modules/lambda/main.tf | 1 - modules/mq-broker/default.auto.tfvars | 0 modules/mq-broker/main.tf | 0 modules/mq-broker/outputs.tf | 0 modules/mq-broker/variables.tf | 0 modules/mq-broker/versions.tf | 0 modules/mwaa/README.md | 2 +- modules/network-firewall/README.md | 6 +- .../modules/escalation/README.md | 1 - modules/opsgenie-team/providers.tf | 1 - modules/opsgenie-team/ssm.tf | 1 - .../route53-resolver-dns-firewall/README.md | 2 +- modules/s3-bucket/variables.tf | 1 - modules/securityhub/common/README.md | 4 +- modules/securityhub/common/outputs.tf | 2 +- modules/securityhub/common/providers.tf | 2 +- modules/securityhub/common/variables.tf | 10 +- modules/securityhub/root/README.md | 2 +- modules/securityhub/root/providers.tf | 2 +- modules/securityhub/root/variables.tf | 2 +- modules/ses/outputs.tf | 2 +- modules/snowflake-account/README.md | 6 +- modules/snowflake-database/README.md | 2 +- modules/snowflake-database/main.tf | 4 +- modules/sns-topic/README.md | 2 +- modules/sns-topic/default.auto.tfvars | 1 - modules/spa-s3-cloudfront/README.md | 4 +- modules/spacelift/README.md | 246 ++++++++++++++++++ modules/spacelift/admin-stack/providers.tf | 1 - modules/sso-saml-provider/README.md | 1 - modules/sso-saml-provider/outputs.tf | 1 - modules/sso/providers.tf | 1 - modules/strongdm/README.md | 2 +- .../charts/strongdm/templates/_helpers.tpl | 1 - modules/strongdm/providers.tf | 0 modules/tgw/hub/README.md | 2 +- modules/tgw/hub/variables.tf | 2 +- modules/tgw/spoke/README.md | 6 +- .../modules/standard_vpc_attachment/main.tf | 2 +- .../standard_vpc_attachment/variables.tf | 2 +- modules/tgw/spoke/variables.tf | 2 +- modules/vpc-flow-logs-bucket/variables.tf | 1 - 204 files changed, 448 insertions(+), 261 deletions(-) mode change 100755 => 100644 deprecated/aws/opsgenie/providers.tf mode change 100755 => 100644 deprecated/aws/opsgenie/versions.tf mode change 100755 => 100644 modules/aws-backup/providers.tf mode change 100755 => 100644 modules/datadog-logs-archive/providers.tf mode change 100755 => 100644 modules/datadog-monitor/versions.tf mode change 100755 => 100644 modules/datadog-synthetics-private-location/versions.tf mode change 100755 => 100644 modules/datadog-synthetics/providers.tf mode change 100755 => 100644 modules/datadog-synthetics/versions.tf mode change 100755 => 100644 modules/eks-iam/alb-controller.tf mode change 100755 => 100644 modules/eks-iam/autoscaler.tf mode change 100755 => 100644 modules/eks-iam/cert-manager.tf mode change 100755 => 100644 modules/eks-iam/default.auto.tfvars mode change 100755 => 100644 modules/eks-iam/external-dns.tf mode change 100755 => 100644 modules/eks-iam/main.tf mode change 100755 => 100644 modules/eks-iam/modules/service-account/default.auto.tfvars mode change 100755 => 100644 modules/eks-iam/modules/service-account/main.tf mode change 100755 => 100644 modules/eks-iam/modules/service-account/outputs.tf mode change 100755 => 100644 modules/eks-iam/modules/service-account/variables.tf mode change 100755 => 100644 modules/eks-iam/outputs.tf mode change 100755 => 100644 modules/eks-iam/variables.tf mode change 100755 => 100644 modules/eks-iam/versions.tf mode change 100755 => 100644 modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml mode change 100755 => 100644 modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml mode change 100755 => 100644 modules/eks/actions-runner-controller/charts/actions-runner/values.yaml mode change 100755 => 100644 modules/eks/actions-runner-controller/main.tf mode change 100755 => 100644 modules/eks/actions-runner-controller/variables.tf mode change 100755 => 100644 modules/mq-broker/default.auto.tfvars mode change 100755 => 100644 modules/mq-broker/main.tf mode change 100755 => 100644 modules/mq-broker/outputs.tf mode change 100755 => 100644 modules/mq-broker/variables.tf mode change 100755 => 100644 modules/mq-broker/versions.tf mode change 100755 => 100644 modules/strongdm/providers.tf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f3df96b5d..baddda8e7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,7 +7,7 @@ assignees: '' --- -Found a bug? Maybe our [Slack Community](https://slack.cloudposse.com) can help. +Found a bug? Maybe our [Slack Community](https://slack.cloudposse.com) can help. [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) @@ -34,4 +34,4 @@ Anything that will help us triage the bug will help. Here are some ideas: - Version [e.g. 10.15] ## Additional Context -Add any other context about the problem here. \ No newline at end of file +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 76ae6d67a..918f371c1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -15,4 +15,4 @@ contact_links: - name: DevOps Accelerator Program url: https://cloudposse.com/accelerate/ about: |- - Own your infrastructure in record time. We build it. You drive it. + Own your infrastructure in record time. We build it. You drive it. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 39a8686f1..44cdd4a43 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,13 +7,13 @@ assignees: '' --- -Have a question? Please checkout our [Slack Community](https://slack.cloudposse.com) or visit our [Slack Archive](https://archive.sweetops.com/). +Have a question? Please checkout our [Slack Community](https://slack.cloudposse.com) or visit our [Slack Archive](https://archive.sweetops.com/). [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) ## Describe the Feature -A clear and concise description of what the bug is. +A clear and concise description of what the bug is. ## Expected Behavior diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4b8f32df3..d443ce673 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,11 +3,10 @@ * Use bullet points to be concise and to the point. ## why -* Provide the justifications for the changes (e.g. business case). +* Provide the justifications for the changes (e.g. business case). * Describe why these changes were made (e.g. why do these commits fix the problem?) * Use bullet points to be concise and to the point. ## references -* Link to any supporting github issues or helpful documentation to add some context (e.g. stackoverflow). +* Link to any supporting github issues or helpful documentation to add some context (e.g. stackoverflow). * Use `closes #123`, if this PR closes a GitHub issue `#123` - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2affe7454..b8cc1f010 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,26 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v4.4.0 hooks: - - id: check-yaml + # Git style + - id: check-added-large-files # prevents giant files from being committed. + - id: forbid-new-submodules # prevents addition of new git submodules. + - id: no-commit-to-branch # don't commit to branch + + # Common errors + - id: trailing-whitespace # trims trailing whitespace. + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline. + - id: check-merge-conflict # checks for files that contain merge conflict strings. + - id: check-executables-have-shebangs # ensures that (non-binary) executables have a shebang. + + # Cross platform + - id: check-case-conflict # checks for files that would conflict in case-insensitive filesystems. + - id: mixed-line-ending # replaces or checks mixed line ending. + args: [--fix=lf] + + # YAML + - id: check-yaml # checks yaml files for parseable syntax. exclude: | (?x)^( deprecated/github-actions-runner/runners/actions-runner/chart/templates/.*.yaml | @@ -10,8 +28,9 @@ repos: modules/strongdm/charts/strongdm/templates/.*.yaml | modules/eks/.*/charts/.*/templates/.*.yaml )$ + - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.75.0 + rev: v1.80.0 hooks: - id: terraform_fmt - id: terraform_docs @@ -25,4 +44,3 @@ repos: types: ["text"] files: (mixins\/.*|bin\/rebuild-mixins-docs\.sh) pass_filenames: false - diff --git a/deprecated/account-map/modules/iam-roles/README.md b/deprecated/account-map/modules/iam-roles/README.md index 984c9beaa..0de665565 100644 --- a/deprecated/account-map/modules/iam-roles/README.md +++ b/deprecated/account-map/modules/iam-roles/README.md @@ -2,13 +2,13 @@ This submodule is used by other modules to determine which IAM Roles or AWS CLI Config Profiles to use for various tasks, most commonly -for applying Terraform plans. +for applying Terraform plans. ## Special Configuration Needed In order to avoid having to pass customization information through every module that uses this submodule, if the default configuration does not suit your needs, -you are expected to customize `variables.tf` with the defaults you want to +you are expected to customize `variables.tf` with the defaults you want to use in your project. For example, if you are including the `tenant` label in the designation of your "root" account (your Organization Management Account), then you should modify `variables.tf` so that `global_tenant_name` defaults diff --git a/deprecated/account-map/modules/roles-to-principals/README.md b/deprecated/account-map/modules/roles-to-principals/README.md index a24094074..82b128d8c 100644 --- a/deprecated/account-map/modules/roles-to-principals/README.md +++ b/deprecated/account-map/modules/roles-to-principals/README.md @@ -1,15 +1,15 @@ # Submodule `roles-to-principals` -This submodule is used by other modules to map short role names and AWS +This submodule is used by other modules to map short role names and AWS SSO Permission Set names in accounts designated by short account names (for example, `terraform` in the `dev` account) to full IAM Role ARNs and -other related tasks. +other related tasks. ## Special Configuration Needed In order to avoid having to pass customization information through every module that uses this submodule, if the default configuration does not suit your needs, -you are expected to customize `variables.tf` with the defaults you want to +you are expected to customize `variables.tf` with the defaults you want to use in your project. For example, if you are including the `tenant` label in the designation of your "root" account (your Organization Management Account), then you should modify `variables.tf` so that `global_tenant_name` defaults diff --git a/deprecated/aws/acm/variables.tf b/deprecated/aws/acm/variables.tf index 0b76b56f9..19985903b 100644 --- a/deprecated/aws/acm/variables.tf +++ b/deprecated/aws/acm/variables.tf @@ -24,4 +24,3 @@ variable "chamber_parameter_name_format" { description = "Format string for combining `chamber` service name and parameter name. It is rare to need to set this." default = "/%s/%s" } - diff --git a/deprecated/aws/aws-metrics-role/variables.tf b/deprecated/aws/aws-metrics-role/variables.tf index 23c98dbf0..5fd6eea75 100644 --- a/deprecated/aws/aws-metrics-role/variables.tf +++ b/deprecated/aws/aws-metrics-role/variables.tf @@ -70,4 +70,3 @@ variable "max_session_duration" { default = 3600 description = "The maximum session duration (in seconds) for the role. Can have a value from 1 hour to 12 hours" } - diff --git a/deprecated/aws/aws-metrics-role/versions.tf b/deprecated/aws/aws-metrics-role/versions.tf index 2502a985d..9498a4a22 100644 --- a/deprecated/aws/aws-metrics-role/versions.tf +++ b/deprecated/aws/aws-metrics-role/versions.tf @@ -6,4 +6,3 @@ terraform { kubernetes = "~> 1.8" } } - diff --git a/deprecated/aws/backing-services/README.md b/deprecated/aws/backing-services/README.md index b51605ac8..a8f8a707e 100644 --- a/deprecated/aws/backing-services/README.md +++ b/deprecated/aws/backing-services/README.md @@ -7,6 +7,6 @@ aws_security_group.default: Error authorizing security group ingress rules: InvalidGroup.NotFound: You have specified two resources that belong to different networks. ``` -### Answer +### Answer Ensure that the VPC peering with the Kops cluster has been setup. diff --git a/deprecated/aws/backing-services/aurora-postgres.tf b/deprecated/aws/backing-services/aurora-postgres.tf index 32af7164d..e11d83fe3 100644 --- a/deprecated/aws/backing-services/aurora-postgres.tf +++ b/deprecated/aws/backing-services/aurora-postgres.tf @@ -4,7 +4,7 @@ variable "postgres_name" { default = "postgres" } -# Don't use `admin` +# Don't use `admin` # Read more: # ("MasterUsername admin cannot be used as it is a reserved word used by the engine") variable "postgres_admin_user" { diff --git a/deprecated/aws/bootstrap/README.md b/deprecated/aws/bootstrap/README.md index 913398ceb..f53ae9f5b 100644 --- a/deprecated/aws/bootstrap/README.md +++ b/deprecated/aws/bootstrap/README.md @@ -1,6 +1,6 @@ # bootstrap -This module provisions an AWS user along with a bootstrap role suitable for bootstrapping an AWS multi-account architecture as found in our [reference architectures](https://github.com/cloudposse/reference-architecutres). +This module provisions an AWS user along with a bootstrap role suitable for bootstrapping an AWS multi-account architecture as found in our [reference architectures](https://github.com/cloudposse/reference-architecutres). These user and role are intended to be used as a **temporary fixture** and should be deprovisioned after all accounts have been provisioned in order to maintain a secure environment. diff --git a/deprecated/aws/cis-aggregator-auth/output.tf b/deprecated/aws/cis-aggregator-auth/output.tf index 8b1378917..e69de29bb 100644 --- a/deprecated/aws/cis-aggregator-auth/output.tf +++ b/deprecated/aws/cis-aggregator-auth/output.tf @@ -1 +0,0 @@ - diff --git a/deprecated/aws/cis-aggregator/output.tf b/deprecated/aws/cis-aggregator/output.tf index 8b1378917..e69de29bb 100644 --- a/deprecated/aws/cis-aggregator/output.tf +++ b/deprecated/aws/cis-aggregator/output.tf @@ -1 +0,0 @@ - diff --git a/deprecated/aws/cis-executor/output.tf b/deprecated/aws/cis-executor/output.tf index 8b1378917..e69de29bb 100644 --- a/deprecated/aws/cis-executor/output.tf +++ b/deprecated/aws/cis-executor/output.tf @@ -1 +0,0 @@ - diff --git a/deprecated/aws/cis-instances/output.tf b/deprecated/aws/cis-instances/output.tf index 8b1378917..e69de29bb 100644 --- a/deprecated/aws/cis-instances/output.tf +++ b/deprecated/aws/cis-instances/output.tf @@ -1 +0,0 @@ - diff --git a/deprecated/aws/grafana-backing-services/README.md b/deprecated/aws/grafana-backing-services/README.md index 02b7e4707..e72d7f22d 100644 --- a/deprecated/aws/grafana-backing-services/README.md +++ b/deprecated/aws/grafana-backing-services/README.md @@ -23,7 +23,7 @@ access to all the cluster's resources through Kubernetes. ### SSL Server Certificate Validation -To get the Aurora MySQL SSL connection to validate: +To get the Aurora MySQL SSL connection to validate: 1. Get the RDS CA from https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem (expires Mar 5 09:11:31 2020 GMT) or successor (consult current RDS documentation) 2. Save it in a `ConfigMap` diff --git a/deprecated/aws/keycloak-backing-services/README.md b/deprecated/aws/keycloak-backing-services/README.md index 1fe240acd..631b02f36 100644 --- a/deprecated/aws/keycloak-backing-services/README.md +++ b/deprecated/aws/keycloak-backing-services/README.md @@ -8,7 +8,7 @@ As of this writing, this only provisions an Aurora MySQL 5.7 database. ### Database encryption -This module, as of this writing, provisions a database that is **not** encrypted. +This module, as of this writing, provisions a database that is **not** encrypted. This means that database backups/snapshots are also unencrypted. The database, and of course the backups, contain secrets that an attacker could use to gain access to anything protected by Keycloak. @@ -22,7 +22,7 @@ key would also be available to someone with the right IAM credentials. As a practical matter, anyone with access to the backups will likely also have access to the encryption key via KMS, or be able to access the database directly after getting the user and password from SSM, or be able to -execute commands in the Keycloak pod/container that expose the secrets. +execute commands in the Keycloak pod/container that expose the secrets. ### SSL Server Certificate Validation @@ -49,23 +49,23 @@ wide access. `kiam-server` will need to be able to assume this role. 2. Enable encryption for the database using this key. -Then the Keycloak deployment (actually `StatefulSet`) will need to be -annotated so that `kiam` grants Keycloak access to this role. +Then the Keycloak deployment (actually `StatefulSet`) will need to be +annotated so that `kiam` grants Keycloak access to this role. ### SSL Server Certificate Validation -To get the RDS MySQL SSL connection to validate: +To get the RDS MySQL SSL connection to validate: 1. Get the RDS CA from https://s3.amazonaws.com/rds-downloads/rds-ca-2015-root.pem expires (Mar 5 09:11:31 2020 GMT) or successor (consult current RDS documentation) -2. Import it into a Java KeyStore (JKS) +2. Import it into a Java KeyStore (JKS) * Run`keytool -importcert -alias MySQLCACert -file ca.pem -keystore truststore -storepass mypassword` in a Keycloak container in order to be sure to get a compatible version of the Java SDK `keytool` 3. Copy the KeyStore into a secret 4. Mount the Secret 5. Set [`JDBC_PARAMS` environment variable](https://github.com/jboss-dockerfiles/keycloak/blob/119fb1f61a477ec217ba71c18c3a71a10e8d5575/server/tools/cli/databases/mysql/change-database.cli#L2 ) to `?clientCertificateKeyStoreUrl=file:///path-to-keystore&clientCertificateKeyStorePassword=mypassword` -6. Note that it would seem to be more appropriate to set to +6. Note that it would seem to be more appropriate to set to `?trustCertificateKeyStoreUrl=file:///path-to-keystore&trustCertificateKeyStorePassword=mypassword` - but the [documentation](https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-using-ssl.html) + but the [documentation](https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-using-ssl.html) [consistently](https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html) says to use the `clientCertificate*` stuff for verifying the server. diff --git a/deprecated/aws/kops/README.md b/deprecated/aws/kops/README.md index b283a050c..a5cd5ae3d 100644 --- a/deprecated/aws/kops/README.md +++ b/deprecated/aws/kops/README.md @@ -1,6 +1,6 @@ # Kubernetes Ops (kops) -This project provisions dependencies for `kops` clusters including the DNS zone, S3 bucket for state storage, SSH keypair. +This project provisions dependencies for `kops` clusters including the DNS zone, S3 bucket for state storage, SSH keypair. It also writes the computed settings to SSM for usage by other modules or tools. @@ -32,7 +32,7 @@ This is roughly the process to get up and running. These instructions assume you 9. Run `make kops/apply` to build the cluster 10. Run `make kops/validate` to view cluster status. Note, it will take ~10 minutes to come online (depending on cluster size) -Once the cluster is online, you can interact with it using `kubectl`. +Once the cluster is online, you can interact with it using `kubectl`. To start, first run this to export `kubecfg` from the `kops` state store (required to access the cluster): ``` @@ -40,4 +40,3 @@ make kops/export ``` Then all the standard `kubectl` commands will work (e.g. `kubectl get nodes`). - diff --git a/deprecated/aws/opsgenie/providers.tf b/deprecated/aws/opsgenie/providers.tf old mode 100755 new mode 100644 diff --git a/deprecated/aws/opsgenie/versions.tf b/deprecated/aws/opsgenie/versions.tf old mode 100755 new mode 100644 diff --git a/deprecated/aws/root-iam/README.md b/deprecated/aws/root-iam/README.md index 623838e72..f021fca9b 100644 --- a/deprecated/aws/root-iam/README.md +++ b/deprecated/aws/root-iam/README.md @@ -1,5 +1,5 @@ # root-iam -This module is responsible for setting up the access groups in the root account. +This module is responsible for setting up the access groups in the root account. If provisioning this during a cold-start process, make sure you have `TF_VAR_aws_assume_role_arn` set to nil. diff --git a/deprecated/aws/sentry/aurora-postgres.tf b/deprecated/aws/sentry/aurora-postgres.tf index e55324833..946b774d3 100644 --- a/deprecated/aws/sentry/aurora-postgres.tf +++ b/deprecated/aws/sentry/aurora-postgres.tf @@ -4,7 +4,7 @@ variable "postgres_name" { default = "postgres" } -# Don't use `admin` +# Don't use `admin` # Read more: # ("MasterUsername admin cannot be used as it is a reserved word used by the engine") variable "postgres_admin_user" { diff --git a/deprecated/aws/sentry/variables.tf b/deprecated/aws/sentry/variables.tf index f3a94cf6f..ab3015b65 100644 --- a/deprecated/aws/sentry/variables.tf +++ b/deprecated/aws/sentry/variables.tf @@ -28,4 +28,3 @@ variable "chamber_service" { default = "kops" description = "`chamber` service name. See [chamber usage](https://github.com/segmentio/chamber#usage) for more details" } - diff --git a/deprecated/aws/sentry/versions.tf b/deprecated/aws/sentry/versions.tf index 2bdc83477..7a4900af9 100644 --- a/deprecated/aws/sentry/versions.tf +++ b/deprecated/aws/sentry/versions.tf @@ -6,4 +6,3 @@ terraform { random = "~> 2.2" } } - diff --git a/deprecated/aws/ses/Makefile b/deprecated/aws/ses/Makefile index a233f0377..ebccc7c29 100644 --- a/deprecated/aws/ses/Makefile +++ b/deprecated/aws/ses/Makefile @@ -15,4 +15,3 @@ apply console destroy graph plan output providers show: init ## Pass arguments through to terraform which do not require remote state get fmt validate version: terraform $@ - diff --git a/deprecated/aws/sns-topic/main.tf b/deprecated/aws/sns-topic/main.tf index 61de64064..1508e7e12 100644 --- a/deprecated/aws/sns-topic/main.tf +++ b/deprecated/aws/sns-topic/main.tf @@ -28,4 +28,3 @@ module "sns_monitoring" { sns_topic_name = module.sns.sns_topic.name sns_topic_alarms_arn = module.sns.sns_topic.arn } - diff --git a/deprecated/aws/spotinst-kops/variables.tf b/deprecated/aws/spotinst-kops/variables.tf index f6b3581dd..2bca98fbf 100644 --- a/deprecated/aws/spotinst-kops/variables.tf +++ b/deprecated/aws/spotinst-kops/variables.tf @@ -203,4 +203,3 @@ variable "roll_batch_size_percentage" { default = 33 description = "Sets the percentage of the instances to deploy in each batch" } - diff --git a/deprecated/aws/spotinst/main.tf b/deprecated/aws/spotinst/main.tf index 5ae2a1e4a..d1d20cf70 100644 --- a/deprecated/aws/spotinst/main.tf +++ b/deprecated/aws/spotinst/main.tf @@ -79,4 +79,3 @@ module "default" { template_url = local.template_url capabilities = var.capabilities } - diff --git a/deprecated/aws/spotinst/output.tf b/deprecated/aws/spotinst/output.tf index 35a12b4b9..57c02e3b2 100644 --- a/deprecated/aws/spotinst/output.tf +++ b/deprecated/aws/spotinst/output.tf @@ -9,4 +9,3 @@ output "id" { output "outputs" { value = module.default.outputs } - diff --git a/deprecated/aws/spotinst/variables.tf b/deprecated/aws/spotinst/variables.tf index 8970db6f5..e16230caf 100644 --- a/deprecated/aws/spotinst/variables.tf +++ b/deprecated/aws/spotinst/variables.tf @@ -104,4 +104,3 @@ variable "override_token" { default = "" description = "Override Spotinst token" } - diff --git a/deprecated/aws/tfstate-backend/README.md b/deprecated/aws/tfstate-backend/README.md index 5a7634dbf..196869467 100644 --- a/deprecated/aws/tfstate-backend/README.md +++ b/deprecated/aws/tfstate-backend/README.md @@ -1,6 +1,6 @@ # Bootstrap Process -Perform these steps in each account, the very first time, in order to setup the tfstate bucket. +Perform these steps in each account, the very first time, in order to setup the tfstate bucket. ## Create @@ -19,7 +19,7 @@ ENV TF_DYNAMODB_TABLE="cpco-staging-terraform-state-lock" ## Destroy -To destroy the state bucket, first make sure all services in the account have already been destroyed. +To destroy the state bucket, first make sure all services in the account have already been destroyed. Then run: ``` diff --git a/deprecated/aws/tfstate-backend/scripts/force-destroy.sh b/deprecated/aws/tfstate-backend/scripts/force-destroy.sh index ac172be9a..af4c5b062 100755 --- a/deprecated/aws/tfstate-backend/scripts/force-destroy.sh +++ b/deprecated/aws/tfstate-backend/scripts/force-destroy.sh @@ -1,5 +1,5 @@ #!/bin/bash - + # Remove all versions and delete markers for each object OBJECT_VERSIONS=$(aws --output text s3api list-object-versions --bucket "$1" | grep -E '^VERSIONS|^DELETEMARKERS') @@ -20,6 +20,6 @@ while read -r OBJECT_VERSION; do aws s3api delete-object --bucket $1 --key $KEY --version-id $VERSION_ID >/dev/null fi done <<< "$OBJECT_VERSIONS" - + # Remove the bucket with --force option to remove any remaining files without versions. aws s3 rb --force s3://$1 diff --git a/deprecated/github-actions-runner/runners/runner/docker-config.json b/deprecated/github-actions-runner/runners/runner/docker-config.json index c267984aa..66da23b90 100644 --- a/deprecated/github-actions-runner/runners/runner/docker-config.json +++ b/deprecated/github-actions-runner/runners/runner/docker-config.json @@ -1,4 +1,4 @@ { "credsStore": "ecr-login", "experimental": "enabled" -} \ No newline at end of file +} diff --git a/deprecated/github-actions-runner/versions.tf b/deprecated/github-actions-runner/versions.tf index f5a0b0074..594f5ada0 100644 --- a/deprecated/github-actions-runner/versions.tf +++ b/deprecated/github-actions-runner/versions.tf @@ -11,4 +11,4 @@ terraform { version = ">= 2.0" } } -} \ No newline at end of file +} diff --git a/deprecated/iam-primary-roles/README.md b/deprecated/iam-primary-roles/README.md index e5fedeed2..f86f57c53 100644 --- a/deprecated/iam-primary-roles/README.md +++ b/deprecated/iam-primary-roles/README.md @@ -35,15 +35,15 @@ components: # Override the default Role for accessing the backend, because SuperAdmin is not allowed to assume that role role_arn: null vars: - # Historically there was a practical difference between the Primary Roles defined in - # `primary_roles_config` and the Delegated Roles defined in `delegated_roles_config`, + # Historically there was a practical difference between the Primary Roles defined in + # `primary_roles_config` and the Delegated Roles defined in `delegated_roles_config`, # but now the difference is mainly for documentation and bookkeeping. - + # `primary_roles_config` is for roles that only appear in the identity account. # A role in the identity account should be thought of as an IAM access group. # By giving someone access to an identity account role, you are actually # giving them access to a set of roles in a set of accounts. - + # delegated_roles_config is for roles that appear in all (or most) accounts. # Delegated roles correspond more closely to job functions. diff --git a/deprecated/iam-primary-roles/remote-state.tf b/deprecated/iam-primary-roles/remote-state.tf index 059497226..9adfe23e8 100644 --- a/deprecated/iam-primary-roles/remote-state.tf +++ b/deprecated/iam-primary-roles/remote-state.tf @@ -21,4 +21,3 @@ module "account_map" { context = module.this.context } - diff --git a/deprecated/spacelift/README.md b/deprecated/spacelift/README.md index 91b5bc9ce..e3f033710 100644 --- a/deprecated/spacelift/README.md +++ b/deprecated/spacelift/README.md @@ -213,7 +213,7 @@ components: not user_collaborators[username] not admin_collaborators[username] } - + # Grant spaces read only user access to all members space_read[space.id] { space := input.spaces[_] diff --git a/mixins/github-actions-iam-policy/ecr/github-actions-iam-policy.tf b/mixins/github-actions-iam-policy/ecr/github-actions-iam-policy.tf index 69941c2b0..20299a468 100644 --- a/mixins/github-actions-iam-policy/ecr/github-actions-iam-policy.tf +++ b/mixins/github-actions-iam-policy/ecr/github-actions-iam-policy.tf @@ -52,4 +52,3 @@ data "aws_iam_policy_document" "github_actions_iam_policy" { resources = ["*"] } } - diff --git a/modules/account-map/modules/roles-to-principals/README.md b/modules/account-map/modules/roles-to-principals/README.md index a24094074..82b128d8c 100644 --- a/modules/account-map/modules/roles-to-principals/README.md +++ b/modules/account-map/modules/roles-to-principals/README.md @@ -1,15 +1,15 @@ # Submodule `roles-to-principals` -This submodule is used by other modules to map short role names and AWS +This submodule is used by other modules to map short role names and AWS SSO Permission Set names in accounts designated by short account names (for example, `terraform` in the `dev` account) to full IAM Role ARNs and -other related tasks. +other related tasks. ## Special Configuration Needed In order to avoid having to pass customization information through every module that uses this submodule, if the default configuration does not suit your needs, -you are expected to customize `variables.tf` with the defaults you want to +you are expected to customize `variables.tf` with the defaults you want to use in your project. For example, if you are including the `tenant` label in the designation of your "root" account (your Organization Management Account), then you should modify `variables.tf` so that `global_tenant_name` defaults diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index 17567e944..4ab36a608 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -6,7 +6,7 @@ This component is responsible for provisioning account level settings: IAM passw **Stack Level**: Global -Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts, +Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts, so create a file `stacks/catalog/account-settings.yaml` with the following content and then import that file in each account's global stack (overriding any parameters as needed): diff --git a/modules/alb/variables.tf b/modules/alb/variables.tf index 4b4a2470e..920443889 100644 --- a/modules/alb/variables.tf +++ b/modules/alb/variables.tf @@ -209,4 +209,3 @@ variable "stickiness" { description = "Target group sticky configuration" default = null } - diff --git a/modules/amplify/README.md b/modules/amplify/README.md index 6c4bd91ac..874344ec0 100644 --- a/modules/amplify/README.md +++ b/modules/amplify/README.md @@ -101,23 +101,23 @@ components: certificate_verification_dns_record_enabled: false ``` -The `amplify/example` YAML configuration defines an Amplify app in AWS. -The app is set up to use the `Next.js` framework with SSR (server-side rendering) and is linked to the +The `amplify/example` YAML configuration defines an Amplify app in AWS. +The app is set up to use the `Next.js` framework with SSR (server-side rendering) and is linked to the GitHub repository "https://github.com/cloudposse/amplify-test2". -The app is set up to have two environments: `main` and `develop`. +The app is set up to have two environments: `main` and `develop`. Each environment has different configuration settings, such as the branch name, framework, and stage. The `main` environment is set up for production, while the `develop` environments is set up for development. -The app is also configured to have custom subdomains for each environment, with prefixes such as `example-prod` and `example-dev`. +The app is also configured to have custom subdomains for each environment, with prefixes such as `example-prod` and `example-dev`. The subdomains are configured to use DNS records, which are enabled through the `subdomains_dns_records_enabled` variable. -The app also has an IAM service role configured with specific IAM actions, and environment variables set up for each environment. +The app also has an IAM service role configured with specific IAM actions, and environment variables set up for each environment. Additionally, the app is configured to use the Atmos Spacelift workspace, as indicated by the `workspace_enabled: true` setting. The `amplify/example` Atmos component extends the `amplify/defaults` component. -The `amplify/example` configuration is imported into the `stacks/mixins/stage/dev.yaml` stack config file to be provisioned +The `amplify/example` configuration is imported into the `stacks/mixins/stage/dev.yaml` stack config file to be provisioned in the `dev` account. ```yaml diff --git a/modules/argocd-repo/templates/.gitignore.tpl b/modules/argocd-repo/templates/.gitignore.tpl index 20c7c32c7..d1086c259 100644 --- a/modules/argocd-repo/templates/.gitignore.tpl +++ b/modules/argocd-repo/templates/.gitignore.tpl @@ -3,4 +3,4 @@ %{ for entry in entries ~} ${entry} -%{ endfor ~} \ No newline at end of file +%{ endfor ~} diff --git a/modules/argocd-repo/templates/README.md.tpl b/modules/argocd-repo/templates/README.md.tpl index 36615a3b5..8593e2d0f 100644 --- a/modules/argocd-repo/templates/README.md.tpl +++ b/modules/argocd-repo/templates/README.md.tpl @@ -11,4 +11,4 @@ them to an `apps/[app name]/` subdirectory in each environment's directory. The `applicationset.yaml` file in each environment directory's `argocd/` subdirectory is referenced by ArgoCD deployment in each environment's dedicated EKS cluster. This ApplicationSet manifest makes use of [Git Generators](https://argocd-applicationset.readthedocs.io/en/stable/Generators-Git/) -in order to dynamically create ArgoCD Application objects based on the manifests in the `apps/[app name]/` directory. \ No newline at end of file +in order to dynamically create ArgoCD Application objects based on the manifests in the `apps/[app name]/` directory. diff --git a/modules/athena/README.md b/modules/athena/README.md index 092c54949..83c1e8c54 100644 --- a/modules/athena/README.md +++ b/modules/athena/README.md @@ -43,7 +43,7 @@ import: components: terraform: - athena/example: + athena/example: metadata: component: athena inherits: @@ -59,7 +59,7 @@ components: ### CloudTrail Integration -Using Athena with CloudTrail logs is a powerful way to enhance your analysis of AWS service activity. This component supports creating +Using Athena with CloudTrail logs is a powerful way to enhance your analysis of AWS service activity. This component supports creating a CloudTrail table for each account and setting up queries to read CloudTrail logs from a centralized location. To set up the CloudTrail Integration, first create the `create` and `alter` queries in Athena with this component. When `var.cloudtrail_database` @@ -71,7 +71,7 @@ import: components: terraform: - athena/audit: + athena/audit: metadata: component: athena inherits: @@ -105,7 +105,7 @@ Once those are created, run the `create` and then the `alter` queries in the AWS :::info Athena runs queries with the permissions of the user executing the query. In order to be able to query CloudTrail logs, -the `audit` account must have access to the KMS key used to encrypt CloudTrails logs. Set `var.audit_access_enabled` to `true` in the `cloudtrail` +the `audit` account must have access to the KMS key used to encrypt CloudTrails logs. Set `var.audit_access_enabled` to `true` in the `cloudtrail` component ::: diff --git a/modules/athena/cloudtrail.tf b/modules/athena/cloudtrail.tf index fc15b111a..2740ca9d2 100644 --- a/modules/athena/cloudtrail.tf +++ b/modules/athena/cloudtrail.tf @@ -26,10 +26,10 @@ useridentity STRUCT< attributes:STRUCT< mfaauthenticated:STRING, creationdate:STRING>, - sessionissuer:STRUCT< + sessionissuer:STRUCT< type:STRING, principalId:STRING, - arn:STRING, + arn:STRING, accountId:STRING, userName:STRING>, ec2RoleDelivery:string, @@ -65,7 +65,7 @@ tlsDetails struct< cipherSuite:string, clientProvidedHostHeader:string> ) -PARTITIONED BY (account string, region string, year string, month string, day string) +PARTITIONED BY (account string, region string, year string, month string, day string) ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe' STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' diff --git a/modules/athena/variables.tf b/modules/athena/variables.tf index cb9c5470a..0fd8582e8 100644 --- a/modules/athena/variables.tf +++ b/modules/athena/variables.tf @@ -109,4 +109,3 @@ variable "cloudtrail_bucket_component_name" { description = "The name of the CloudTrail bucket component" default = "cloudtrail-bucket" } - diff --git a/modules/aurora-mysql-resources/main.tf b/modules/aurora-mysql-resources/main.tf index 6c09f8c4f..f53b112c9 100644 --- a/modules/aurora-mysql-resources/main.tf +++ b/modules/aurora-mysql-resources/main.tf @@ -66,4 +66,3 @@ module "additional_grants" { context = module.this.context } - diff --git a/modules/aurora-mysql-resources/modules/mysql-user/main.tf b/modules/aurora-mysql-resources/modules/mysql-user/main.tf index 06f6e7f81..976684740 100644 --- a/modules/aurora-mysql-resources/modules/mysql-user/main.tf +++ b/modules/aurora-mysql-resources/modules/mysql-user/main.tf @@ -18,7 +18,7 @@ locals { parameter_write = (local.create_db_user && local.save_password_in_ssm) ? [local.db_password_ssm] : [] - # You cannot grant "ALL" to an RDS user because "ALL" includes privileges that Master does not have (because this is a managed database). + # You cannot grant "ALL" to an RDS user because "ALL" includes privileges that Master does not have (because this is a managed database). # Instead, use "ALL PRIVILEGES" # See the full list of available options at https://docs.amazonaws.cn/en_us/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Security.html all_rds_app_grants = [ diff --git a/modules/aurora-mysql-resources/modules/mysql-user/variables.tf b/modules/aurora-mysql-resources/modules/mysql-user/variables.tf index c95f30442..8815038d0 100644 --- a/modules/aurora-mysql-resources/modules/mysql-user/variables.tf +++ b/modules/aurora-mysql-resources/modules/mysql-user/variables.tf @@ -46,4 +46,3 @@ variable "kms_key_id" { default = "alias/aws/rds" description = "KMS key ID, ARN, or alias to use for encrypting MySQL database" } - diff --git a/modules/aurora-mysql-resources/outputs.tf b/modules/aurora-mysql-resources/outputs.tf index 898890f8e..850a1c210 100644 --- a/modules/aurora-mysql-resources/outputs.tf +++ b/modules/aurora-mysql-resources/outputs.tf @@ -7,4 +7,3 @@ output "additional_grants" { value = keys(module.additional_grants) description = "Additional DB users created" } - diff --git a/modules/aurora-mysql-resources/variables.tf b/modules/aurora-mysql-resources/variables.tf index 62490b0c4..142b453ea 100644 --- a/modules/aurora-mysql-resources/variables.tf +++ b/modules/aurora-mysql-resources/variables.tf @@ -77,4 +77,3 @@ variable "additional_grants" { otherwise, passwords will be generated and stored in SSM parameter store under the service's key. EOT } - diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index 73ef2ba1b..8987af5a2 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -1,6 +1,6 @@ # Component: `aurora-mysql` -This component is responsible for provisioning Aurora MySQL RDS clusters. +This component is responsible for provisioning Aurora MySQL RDS clusters. It seeds relevant database information (hostnames, username, password, etc.) into AWS SSM Parameter Store. ## Usage @@ -106,7 +106,7 @@ components: allowed_cidr_blocks: # all automation in primary region (where Spacelift is deployed) - 10.128.0.0/22 - # all corp in the same region as this cluster + # all corp in the same region as this cluster - 10.132.16.0/22 mysql_instance_type: "db.t3.medium" mysql_name: "replica" @@ -243,8 +243,8 @@ Reploying the component should show no changes. For example, `atmos terraform ap | [publicly\_accessible](#input\_publicly\_accessible) | Set to true to create the cluster in a public subnet | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [replication\_source\_identifier](#input\_replication\_source\_identifier) | ARN of a source DB cluster or DB instance if this DB cluster is to be created as a Read Replica.
If this value is empty and replication is enabled, remote state will attempt to find
a matching cluster in the Primary DB Cluster's region | `string` | `""` | no | -| [ssm\_password\_source](#input\_ssm\_password\_source) | If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using
`var.ssm_password_source` and the database username. If this value is not set,
a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. | `string` | `""` | no | +| [replication\_source\_identifier](#input\_replication\_source\_identifier) | ARN of a source DB cluster or DB instance if this DB cluster is to be created as a Read Replica.
If this value is empty and replication is enabled, remote state will attempt to find
a matching cluster in the Primary DB Cluster's region | `string` | `""` | no | +| [ssm\_password\_source](#input\_ssm\_password\_source) | If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using
`var.ssm_password_source` and the database username. If this value is not set,
a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. | `string` | `""` | no | | [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | SSM path prefix | `string` | `"rds"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/modules/aurora-mysql/main.tf b/modules/aurora-mysql/main.tf index 201b489ba..097d6f5c6 100644 --- a/modules/aurora-mysql/main.tf +++ b/modules/aurora-mysql/main.tf @@ -14,15 +14,15 @@ locals { is_read_replica = local.enabled && var.is_read_replica remote_read_replica_enabled = local.is_read_replica && !(length(var.replication_source_identifier) > 0) && length(var.primary_cluster_region) > 0 - # Removing the replicate source attribute from an existing RDS Replicate database managed by Terraform - # should promote the database to a fully standalone database but currently is not supported by Terraform. + # Removing the replicate source attribute from an existing RDS Replicate database managed by Terraform + # should promote the database to a fully standalone database but currently is not supported by Terraform. # Instead, first manually promote with the AWS CLI or console, and then remove the replication source identitier from the Terrafrom state # See https://github.com/hashicorp/terraform-provider-aws/issues/6749 replication_source_identifier = local.remote_read_replica_enabled && !var.is_promoted_read_replica ? module.primary_cluster[0].outputs.aurora_mysql_cluster_arn : var.replication_source_identifier # For encrypted cross-region replica, kmsKeyId should be explicitly specified # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html - # A read replica of an Amazon RDS encrypted instance must be encrypted using the same KMS key as the primary DB instance when both are in the same AWS Region. + # A read replica of an Amazon RDS encrypted instance must be encrypted using the same KMS key as the primary DB instance when both are in the same AWS Region. # If the primary DB instance and read replica are in different AWS Regions, you encrypt the read replica using a KMS key in that AWS Region. kms_key_arn = module.kms_key_rds.key_arn @@ -82,5 +82,3 @@ resource "random_pet" "mysql_db_name" { db_name = var.mysql_db_name } } - - diff --git a/modules/aurora-mysql/outputs.tf b/modules/aurora-mysql/outputs.tf index 319b4aa68..eafb28aeb 100644 --- a/modules/aurora-mysql/outputs.tf +++ b/modules/aurora-mysql/outputs.tf @@ -59,4 +59,3 @@ output "kms_key_arn" { value = module.kms_key_rds.key_arn description = "KMS key ARN for Aurora MySQL" } - diff --git a/modules/aurora-mysql/ssm.tf b/modules/aurora-mysql/ssm.tf index 8dbf1532f..8a6e9220e 100644 --- a/modules/aurora-mysql/ssm.tf +++ b/modules/aurora-mysql/ssm.tf @@ -80,4 +80,3 @@ module "parameter_store_write" { context = module.this.context } - diff --git a/modules/aurora-mysql/variables.tf b/modules/aurora-mysql/variables.tf index 547a34e42..462468951 100644 --- a/modules/aurora-mysql/variables.tf +++ b/modules/aurora-mysql/variables.tf @@ -13,8 +13,8 @@ variable "ssm_password_source" { type = string default = "" description = <<-EOT - If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using - `var.ssm_password_source` and the database username. If this value is not set, + If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using + `var.ssm_password_source` and the database username. If this value is not set, a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. EOT } @@ -167,8 +167,8 @@ variable "eks_component_names" { variable "replication_source_identifier" { type = string description = <<-EOT - ARN of a source DB cluster or DB instance if this DB cluster is to be created as a Read Replica. - If this value is empty and replication is enabled, remote state will attempt to find + ARN of a source DB cluster or DB instance if this DB cluster is to be created as a Read Replica. + If this value is empty and replication is enabled, remote state will attempt to find a matching cluster in the Primary DB Cluster's region EOT default = "" @@ -213,4 +213,3 @@ variable "allow_ingress_from_vpc_accounts" { } EOF } - diff --git a/modules/aurora-postgres-resources/main.tf b/modules/aurora-postgres-resources/main.tf index e9676e600..2144e4e25 100644 --- a/modules/aurora-postgres-resources/main.tf +++ b/modules/aurora-postgres-resources/main.tf @@ -75,4 +75,3 @@ module "additional_grants" { context = module.this.context } - diff --git a/modules/aurora-postgres-resources/modules/postgresql-user/main.tf b/modules/aurora-postgres-resources/modules/postgresql-user/main.tf index 3332bc0f5..494789461 100644 --- a/modules/aurora-postgres-resources/modules/postgresql-user/main.tf +++ b/modules/aurora-postgres-resources/modules/postgresql-user/main.tf @@ -51,7 +51,7 @@ resource "postgresql_grant" "default" { schema = var.grants[count.index].schema object_type = var.grants[count.index].object_type - # Conditionally set the privileges to either the explicit list of database privileges + # Conditionally set the privileges to either the explicit list of database privileges # or schema privileges if this is a db grant or a schema grant respectively. # We can determine this is a schema grant if a schema is given privileges = contains(var.grants[count.index].grant, "ALL") ? ((length(var.grants[count.index].schema) > 0) ? local.all_privileges_schema : local.all_privileges_database) : var.grants[count.index].grant diff --git a/modules/aurora-postgres-resources/modules/postgresql-user/variables.tf b/modules/aurora-postgres-resources/modules/postgresql-user/variables.tf index 60e8c663e..2cd745796 100644 --- a/modules/aurora-postgres-resources/modules/postgresql-user/variables.tf +++ b/modules/aurora-postgres-resources/modules/postgresql-user/variables.tf @@ -45,4 +45,3 @@ variable "kms_key_id" { default = "alias/aws/rds" description = "KMS key ID, ARN, or alias to use for encrypting the database" } - diff --git a/modules/aurora-postgres-resources/outputs.tf b/modules/aurora-postgres-resources/outputs.tf index 885e61f76..c2a4080b1 100644 --- a/modules/aurora-postgres-resources/outputs.tf +++ b/modules/aurora-postgres-resources/outputs.tf @@ -17,4 +17,3 @@ output "additional_grants" { value = keys(module.additional_grants) description = "Additional grants" } - diff --git a/modules/aurora-postgres-resources/variables.tf b/modules/aurora-postgres-resources/variables.tf index a1fcfc76d..d723638b0 100644 --- a/modules/aurora-postgres-resources/variables.tf +++ b/modules/aurora-postgres-resources/variables.tf @@ -98,4 +98,3 @@ variable "additional_schemas" { If no database is given, the schema will use the database used by the provider configuration EOT } - diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index f1a115217..94fbf3c4b 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -1,6 +1,6 @@ # Component: `aurora-postgres` -This component is responsible for provisioning Aurora Postgres RDS clusters. +This component is responsible for provisioning Aurora Postgres RDS clusters. It seeds relevant database information (hostnames, username, password, etc.) into AWS SSM Parameter Store. ## Usage @@ -184,7 +184,7 @@ components: | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Normally AWS makes a snapshot of the database before deleting it. Set this to `true` in order to skip this.
NOTE: The final snapshot has a name derived from the cluster name. If you delete a cluster, get a final snapshot,
then create a cluster of the same name, its final snapshot will fail with a name collision unless you delete
the previous final snapshot first. | `bool` | `false` | no | | [snapshot\_identifier](#input\_snapshot\_identifier) | Specifies whether or not to create this cluster from a snapshot | `string` | `null` | no | -| [ssm\_password\_source](#input\_ssm\_password\_source) | If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using
`var.ssm_password_source` and the database username. If this value is not set,
a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. | `string` | `""` | no | +| [ssm\_password\_source](#input\_ssm\_password\_source) | If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using
`var.ssm_password_source` and the database username. If this value is not set,
a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. | `string` | `""` | no | | [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | Top level SSM path prefix (without leading or trailing slash) | `string` | `"aurora-postgres"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [storage\_encrypted](#input\_storage\_encrypted) | Specifies whether the DB cluster is encrypted | `bool` | `true` | no | diff --git a/modules/aurora-postgres/outputs.tf b/modules/aurora-postgres/outputs.tf index c28db2be9..6f740c835 100644 --- a/modules/aurora-postgres/outputs.tf +++ b/modules/aurora-postgres/outputs.tf @@ -47,4 +47,3 @@ output "kms_key_arn" { value = module.kms_key_rds.key_arn description = "KMS key ARN for Aurora Postgres" } - diff --git a/modules/aurora-postgres/ssm.tf b/modules/aurora-postgres/ssm.tf index 8d006e569..16d10a637 100644 --- a/modules/aurora-postgres/ssm.tf +++ b/modules/aurora-postgres/ssm.tf @@ -84,4 +84,3 @@ module "parameter_store_write" { context = module.this.context } - diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 4eb03408c..1681670ff 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -287,9 +287,8 @@ variable "ssm_password_source" { type = string default = "" description = <<-EOT - If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using - `var.ssm_password_source` and the database username. If this value is not set, + If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using + `var.ssm_password_source` and the database username. If this value is not set, a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. EOT } - diff --git a/modules/aws-backup/main.tf b/modules/aws-backup/main.tf index 5045ba198..e44cde2ac 100644 --- a/modules/aws-backup/main.tf +++ b/modules/aws-backup/main.tf @@ -24,4 +24,3 @@ module "backup" { context = module.this.context } - diff --git a/modules/aws-backup/outputs.tf b/modules/aws-backup/outputs.tf index 77d3b198c..69fa779e5 100644 --- a/modules/aws-backup/outputs.tf +++ b/modules/aws-backup/outputs.tf @@ -22,4 +22,3 @@ output "backup_selection_id" { value = module.backup.backup_selection_id description = "Backup Selection ID" } - diff --git a/modules/aws-backup/providers.tf b/modules/aws-backup/providers.tf old mode 100755 new mode 100644 diff --git a/modules/aws-backup/variables.tf b/modules/aws-backup/variables.tf index b22993433..1ec30ed17 100644 --- a/modules/aws-backup/variables.tf +++ b/modules/aws-backup/variables.tf @@ -92,4 +92,3 @@ variable "iam_role_enabled" { description = "Whether or not to create a new IAM Role and Policy Attachment" default = true } - diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index c7e5bc47e..588d37e93 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -58,11 +58,11 @@ components: terraform: aws-config: vars: - enabled: true + enabled: true account_map_tenant: core az_abbreviation_type: fixed - # In each AWS account, an IAM role should be created in the main region. - # If the main region is set to us-east-1, the value of the var.create_iam_role variable should be true. + # In each AWS account, an IAM role should be created in the main region. + # If the main region is set to us-east-1, the value of the var.create_iam_role variable should be true. # For all other regions, the value of var.create_iam_role should be false. create_iam_role: false central_resource_collector_account: core-security @@ -73,7 +73,7 @@ components: conformance_packs: - name: Operational-Best-Practices-for-CIS-AWS-v1.4-Level2 conformance_pack: https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml - parameter_overrides: + parameter_overrides: AccessKeysRotatedParamMaxAccessKeyAge: '45' - name: Operational-Best-Practices-for-HIPAA-Security.yaml conformance_pack: https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-HIPAA-Security.yaml @@ -170,7 +170,7 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | | [global\_resource\_collector\_region](#input\_global\_resource\_collector\_region) | The region that collects AWS Config data for global resources such as IAM | `string` | n/a | yes | -| [iam\_role\_arn](#input\_iam\_role\_arn) | The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the
AWS resources associated with the account. This is only used if create\_iam\_role is false.

If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create\_iam\_role to `false`.

See the AWS Docs for further information:
http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html | `string` | `null` | no | +| [iam\_role\_arn](#input\_iam\_role\_arn) | The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the
AWS resources associated with the account. This is only used if create\_iam\_role is false.

If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create\_iam\_role to `false`.

See the AWS Docs for further information:
http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html | `string` | `null` | no | | [iam\_roles\_environment\_name](#input\_iam\_roles\_environment\_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | diff --git a/modules/aws-config/outputs.tf b/modules/aws-config/outputs.tf index de4d2ceb0..a7a14c8ec 100644 --- a/modules/aws-config/outputs.tf +++ b/modules/aws-config/outputs.tf @@ -16,4 +16,4 @@ output "storage_bucket_id" { output "storage_bucket_arn" { value = module.aws_config.storage_bucket_arn description = "Storage Config bucket ARN" -} \ No newline at end of file +} diff --git a/modules/aws-config/variables.tf b/modules/aws-config/variables.tf index dc991786b..cf8d17c8d 100644 --- a/modules/aws-config/variables.tf +++ b/modules/aws-config/variables.tf @@ -67,12 +67,12 @@ variable "az_abbreviation_type" { variable "iam_role_arn" { description = <<-DOC - The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the + The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the AWS resources associated with the account. This is only used if create_iam_role is false. If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create_iam_role to `false`. - - See the AWS Docs for further information: + + See the AWS Docs for further information: http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html DOC default = null diff --git a/modules/aws-inspector/README.md b/modules/aws-inspector/README.md index b9d98843c..bfd7f22fb 100644 --- a/modules/aws-inspector/README.md +++ b/modules/aws-inspector/README.md @@ -2,7 +2,7 @@ This component is responsible for provisioning an [AWS Inspector](https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html) by installing the [Inspector agent](https://repost.aws/knowledge-center/set-up-amazon-inspector) across all EC2 instances and applying the Inspector rules. -AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security vulnerabilities and deviations from security best practices. +AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security vulnerabilities and deviations from security best practices. Here are some key features and functionalities of AWS Inspector: - **Security Assessments:** AWS Inspector performs security assessments by analyzing the behavior of your resources and identifying potential security vulnerabilities. It examines the network configuration, operating system settings, and installed software to detect common security issues. diff --git a/modules/aws-saml/main.tf b/modules/aws-saml/main.tf index 361e80d60..1aca411d9 100644 --- a/modules/aws-saml/main.tf +++ b/modules/aws-saml/main.tf @@ -41,4 +41,3 @@ data "aws_iam_policy_document" "saml_provider_assume" { } } } - diff --git a/modules/aws-saml/outputs.tf b/modules/aws-saml/outputs.tf index b98deba72..89ed29cdf 100644 --- a/modules/aws-saml/outputs.tf +++ b/modules/aws-saml/outputs.tf @@ -15,4 +15,3 @@ output "saml_provider_assume_role_policy" { value = one(data.aws_iam_policy_document.saml_provider_assume[*].json) description = "JSON \"assume role\" policy document to use for roles allowed to log in via SAML" } - diff --git a/modules/aws-teams/remote-state.tf b/modules/aws-teams/remote-state.tf index a2a12059c..067704fe9 100644 --- a/modules/aws-teams/remote-state.tf +++ b/modules/aws-teams/remote-state.tf @@ -25,4 +25,3 @@ module "account_map" { context = module.this.context } - diff --git a/modules/cloudtrail-bucket/variables.tf b/modules/cloudtrail-bucket/variables.tf index cd51dc857..350b12fc3 100644 --- a/modules/cloudtrail-bucket/variables.tf +++ b/modules/cloudtrail-bucket/variables.tf @@ -58,4 +58,4 @@ variable "acl" { will need to be set to 'private' during creation, but you can update normally after. EOT default = "log-delivery-write" -} \ No newline at end of file +} diff --git a/modules/config-bucket/README.md b/modules/config-bucket/README.md index 2a0526b7b..a4a723fe5 100644 --- a/modules/config-bucket/README.md +++ b/modules/config-bucket/README.md @@ -1,8 +1,8 @@ # Component: `config-bucket` This module creates an S3 bucket suitable for storing `AWS Config` data. - -It implements a configurable log retention policy, which allows you to efficiently manage logs across different + +It implements a configurable log retention policy, which allows you to efficiently manage logs across different storage classes (_e.g._ `Glacier`) and ultimately expire the data altogether. It enables server-side encryption by default. diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index bc248f97a..3adb8917d 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -1,6 +1,6 @@ # Component: `datadog-integration` -This component is responsible for provisioning Datadog AWS integrations. +This component is responsible for provisioning Datadog AWS integrations. See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. diff --git a/modules/datadog-integration/variables.tf b/modules/datadog-integration/variables.tf index 8ceaf1def..8d79434dd 100644 --- a/modules/datadog-integration/variables.tf +++ b/modules/datadog-integration/variables.tf @@ -49,4 +49,3 @@ variable "context_host_and_filter_tags" { description = "Automatically add host and filter tags for these context keys" default = ["namespace", "tenant", "stage"] } - diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index cee21da66..830a4b233 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -1,7 +1,7 @@ # Component: `datadog-lambda-forwarder` -This component is responsible for provision all the necessary infrastructure to -deploy [Datadog Lambda forwarders](https://github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring). +This component is responsible for provision all the necessary infrastructure to +deploy [Datadog Lambda forwarders](https://github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring). ## Usage @@ -33,7 +33,7 @@ components: filter_pattern: "" eks-cluster: # Use either `name` or `name_prefix` with `name_suffix` - # If `name_prefix` with `name_suffix` are used, the final `name` will be constructed using `name_prefix` + context + `name_suffix`, + # If `name_prefix` with `name_suffix` are used, the final `name` will be constructed using `name_prefix` + context + `name_suffix`, # e.g. "/aws/eks/eg-ue2-prod-eks-cluster/cluster" name_prefix: "/aws/eks/" name_suffix: "eks-cluster/cluster" diff --git a/modules/datadog-logs-archive/providers.tf b/modules/datadog-logs-archive/providers.tf old mode 100755 new mode 100644 diff --git a/modules/datadog-monitor/versions.tf b/modules/datadog-monitor/versions.tf old mode 100755 new mode 100644 diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index e12ced256..65cf193c9 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -50,7 +50,7 @@ components: compatibilities: - EC2 - FARGATE - - FARGATE_SPOT + - FARGATE_SPOT log_configuration: logDriver: awslogs options: {} diff --git a/modules/datadog-synthetics-private-location/versions.tf b/modules/datadog-synthetics-private-location/versions.tf old mode 100755 new mode 100644 diff --git a/modules/datadog-synthetics/providers.tf b/modules/datadog-synthetics/providers.tf old mode 100755 new mode 100644 diff --git a/modules/datadog-synthetics/versions.tf b/modules/datadog-synthetics/versions.tf old mode 100755 new mode 100644 diff --git a/modules/dms/endpoint/variables.tf b/modules/dms/endpoint/variables.tf index 73ab0dc02..e02be501d 100644 --- a/modules/dms/endpoint/variables.tf +++ b/modules/dms/endpoint/variables.tf @@ -132,4 +132,3 @@ variable "password_path" { description = "If set, the path in AWS SSM Parameter Store to fetch the password for the DMS admin user" default = "" } - diff --git a/modules/dns-delegated/variables.tf b/modules/dns-delegated/variables.tf index 7fa904eb0..14d557918 100644 --- a/modules/dns-delegated/variables.tf +++ b/modules/dns-delegated/variables.tf @@ -89,4 +89,3 @@ variable "dns_soa_config" { EOT default = "awsdns-hostmaster.amazon.com. 1 7200 900 1209600 60" } - diff --git a/modules/documentdb/main.tf b/modules/documentdb/main.tf index c4971fdbc..378325fea 100644 --- a/modules/documentdb/main.tf +++ b/modules/documentdb/main.tf @@ -35,4 +35,4 @@ module "documentdb_cluster" { zone_id = try(module.dns_delegated.outputs.default_dns_zone_id, module.dns_gbl_delegated.outputs.default_dns_zone_id) context = module.this.context -} \ No newline at end of file +} diff --git a/modules/documentdb/ssm.tf b/modules/documentdb/ssm.tf index dd8837d8e..44e22b4c7 100644 --- a/modules/documentdb/ssm.tf +++ b/modules/documentdb/ssm.tf @@ -29,4 +29,4 @@ resource "aws_ssm_parameter" "master_password" { name = "/${module.this.name}/master_password" type = "SecureString" value = join("", random_password.master_password.*.result) -} \ No newline at end of file +} diff --git a/modules/documentdb/variables.tf b/modules/documentdb/variables.tf index f846ce9d9..d7e25d04c 100644 --- a/modules/documentdb/variables.tf +++ b/modules/documentdb/variables.tf @@ -120,4 +120,4 @@ variable "eks_security_group_ingress_enabled" { type = bool description = "Whether to add the Security Group managed by the EKS cluster in the same regional stack to the ingress allowlist of the DocumentDB cluster." default = true -} \ No newline at end of file +} diff --git a/modules/dynamodb/outputs.tf b/modules/dynamodb/outputs.tf index 9a51d6747..2423a2250 100644 --- a/modules/dynamodb/outputs.tf +++ b/modules/dynamodb/outputs.tf @@ -31,4 +31,4 @@ output "table_stream_arn" { output "table_stream_label" { value = module.dynamodb_table.table_stream_label description = "DynamoDB table stream label" -} \ No newline at end of file +} diff --git a/modules/dynamodb/variables.tf b/modules/dynamodb/variables.tf index 43bfb0bcf..59f19c149 100644 --- a/modules/dynamodb/variables.tf +++ b/modules/dynamodb/variables.tf @@ -166,4 +166,4 @@ variable "replicas" { type = list(string) default = [] description = "List of regions to create a replica table in" -} \ No newline at end of file +} diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index a9d5ea7b5..8af2aba11 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -23,7 +23,7 @@ components: retention_in_days: 7 organization_name: acme split_tunnel: true - availability_zones: + availability_zones: - us-west-2a - us-west-2b - us-west-2c diff --git a/modules/ecs/README.md b/modules/ecs/README.md index adfe47cd4..af36ff928 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -34,7 +34,7 @@ components: capacity_providers_ec2: default: instance_type: t3.medium - max_size: 2 + max_size: 2 ``` diff --git a/modules/ecs/variables.tf b/modules/ecs/variables.tf index 3db6a1b03..89498b493 100644 --- a/modules/ecs/variables.tf +++ b/modules/ecs/variables.tf @@ -275,4 +275,3 @@ variable "default_capacity_strategy" { weights = {} } } - diff --git a/modules/eks-iam/alb-controller-iam-policy.json b/modules/eks-iam/alb-controller-iam-policy.json index d981ab244..ad188df25 100644 --- a/modules/eks-iam/alb-controller-iam-policy.json +++ b/modules/eks-iam/alb-controller-iam-policy.json @@ -188,4 +188,4 @@ "Resource": "*" } ] -} \ No newline at end of file +} diff --git a/modules/eks-iam/alb-controller.tf b/modules/eks-iam/alb-controller.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/autoscaler.tf b/modules/eks-iam/autoscaler.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/cert-manager.tf b/modules/eks-iam/cert-manager.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/default.auto.tfvars b/modules/eks-iam/default.auto.tfvars old mode 100755 new mode 100644 diff --git a/modules/eks-iam/external-dns.tf b/modules/eks-iam/external-dns.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/main.tf b/modules/eks-iam/main.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/modules/service-account/default.auto.tfvars b/modules/eks-iam/modules/service-account/default.auto.tfvars old mode 100755 new mode 100644 diff --git a/modules/eks-iam/modules/service-account/main.tf b/modules/eks-iam/modules/service-account/main.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/modules/service-account/outputs.tf b/modules/eks-iam/modules/service-account/outputs.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/modules/service-account/variables.tf b/modules/eks-iam/modules/service-account/variables.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/outputs.tf b/modules/eks-iam/outputs.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/variables.tf b/modules/eks-iam/variables.tf old mode 100755 new mode 100644 diff --git a/modules/eks-iam/versions.tf b/modules/eks-iam/versions.tf old mode 100755 new mode 100644 diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml old mode 100755 new mode 100644 diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml old mode 100755 new mode 100644 index b96f9da41..5a63e96f7 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -14,9 +14,9 @@ spec: requests: # EFS is not actually storage constrained, but this storage request is # required. 100Gi is a ballpark for how much we initially request, but this - # may grow. We are responsible for docker pruning this periodically to + # may grow. We are responsible for docker pruning this periodically to # save space. - storage: 100Gi + storage: 100Gi {{- end }} --- apiVersion: actions.summerwind.dev/v1alpha1 diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/values.yaml old mode 100755 new mode 100644 diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf old mode 100755 new mode 100644 index e62e9298e..70597ce6c --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -232,4 +232,3 @@ module "actions_runner" { depends_on = [module.actions_runner_controller] } - diff --git a/modules/eks/actions-runner-controller/outputs.tf b/modules/eks/actions-runner-controller/outputs.tf index e96163492..2de292578 100644 --- a/modules/eks/actions-runner-controller/outputs.tf +++ b/modules/eks/actions-runner-controller/outputs.tf @@ -12,4 +12,3 @@ output "webhook_payload_url" { value = local.webhook_enabled ? format("https://${var.webhook.hostname_template}", var.tenant, var.stage, var.environment) : null description = "Payload URL for GitHub webhook" } - diff --git a/modules/eks/actions-runner-controller/providers.tf b/modules/eks/actions-runner-controller/providers.tf index 80d153743..74ff8e62c 100644 --- a/modules/eks/actions-runner-controller/providers.tf +++ b/modules/eks/actions-runner-controller/providers.tf @@ -26,4 +26,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf old mode 100755 new mode 100644 diff --git a/modules/eks/argocd/resources/kustomize/.gitignore b/modules/eks/argocd/resources/kustomize/.gitignore index 5d23e393d..3b808ddbe 100644 --- a/modules/eks/argocd/resources/kustomize/.gitignore +++ b/modules/eks/argocd/resources/kustomize/.gitignore @@ -1 +1 @@ -_*.yaml \ No newline at end of file +_*.yaml diff --git a/modules/eks/argocd/resources/kustomize/kustomization.yaml b/modules/eks/argocd/resources/kustomize/kustomization.yaml index 5894a3624..540b59ba4 100644 --- a/modules/eks/argocd/resources/kustomize/kustomization.yaml +++ b/modules/eks/argocd/resources/kustomize/kustomization.yaml @@ -8,4 +8,4 @@ patches: - path: patch.yaml target: kind: Deployment - labelSelector: "app.kubernetes.io/part-of=argocd,app.kubernetes.io/component=dex-server" \ No newline at end of file + labelSelector: "app.kubernetes.io/part-of=argocd,app.kubernetes.io/component=dex-server" diff --git a/modules/eks/argocd/resources/kustomize/patch.yaml b/modules/eks/argocd/resources/kustomize/patch.yaml index 3a4ce55dd..b37182533 100644 --- a/modules/eks/argocd/resources/kustomize/patch.yaml +++ b/modules/eks/argocd/resources/kustomize/patch.yaml @@ -1,3 +1,3 @@ - op: add path: /metadata/annotations/secret.reloader.stakater.com~1reload - value: argo-workflows-sso \ No newline at end of file + value: argo-workflows-sso diff --git a/modules/eks/argocd/resources/kustomize/post-render.sh b/modules/eks/argocd/resources/kustomize/post-render.sh index 8ae41288f..d54756e00 100755 --- a/modules/eks/argocd/resources/kustomize/post-render.sh +++ b/modules/eks/argocd/resources/kustomize/post-render.sh @@ -23,4 +23,4 @@ cd "$SCRIPT_DIR" cat <&0 > "$ALL_YAML" -"$KUSTOMIZE_INSTALL_DIR/kustomize" build --reorder none . \ No newline at end of file +"$KUSTOMIZE_INSTALL_DIR/kustomize" build --reorder none . diff --git a/modules/eks/argocd/variables-argocd-apps.tf b/modules/eks/argocd/variables-argocd-apps.tf index 39e67baad..77a8d42bd 100644 --- a/modules/eks/argocd/variables-argocd-apps.tf +++ b/modules/eks/argocd/variables-argocd-apps.tf @@ -27,4 +27,3 @@ variable "argocd_apps_enabled" { description = "Enable argocd apps" default = true } - diff --git a/modules/eks/cert-manager/outputs.tf b/modules/eks/cert-manager/outputs.tf index 88830b0be..d9ef29c9a 100644 --- a/modules/eks/cert-manager/outputs.tf +++ b/modules/eks/cert-manager/outputs.tf @@ -7,4 +7,3 @@ output "cert_manager_issuer_metadata" { value = try(one(module.cert_manager_issuer.metadata), null) description = "Block status of the deployed release" } - diff --git a/modules/eks/cert-manager/variables.tf b/modules/eks/cert-manager/variables.tf index c55b1f972..175f4c7b6 100644 --- a/modules/eks/cert-manager/variables.tf +++ b/modules/eks/cert-manager/variables.tf @@ -163,4 +163,3 @@ variable "eks_component_name" { description = "The name of the eks component" default = "eks/cluster" } - diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index b1f8518d4..2fe3de580 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -187,7 +187,7 @@ addons: addon_version: v1.12.6-eksbuild.2 ``` -Some addons, such as CoreDNS, require at least one node to be fully provisioned first. +Some addons, such as CoreDNS, require at least one node to be fully provisioned first. See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. Set `var.addons_depends_on` to `true` to require the Node Groups to be provisioned before addons. @@ -201,11 +201,11 @@ addons: :::warning Addons may not be suitable for all use-cases! For example, if you are using Karpenter to provision nodes, -these nodes will never be available before the cluster component is deployed. +these nodes will never be available before the cluster component is deployed. ::: -For more on upgrading these EKS Addons, see +For more on upgrading these EKS Addons, see ["How to Upgrade EKS Cluster Addons"](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons/) diff --git a/modules/eks/echo-server/main.tf b/modules/eks/echo-server/main.tf index 4f662eff7..ede55ddb1 100644 --- a/modules/eks/echo-server/main.tf +++ b/modules/eks/echo-server/main.tf @@ -57,4 +57,3 @@ module "echo_server" { context = module.this.context } - diff --git a/modules/eks/efs-controller/remote-state.tf b/modules/eks/efs-controller/remote-state.tf index 0a78c6203..cedf32782 100644 --- a/modules/eks/efs-controller/remote-state.tf +++ b/modules/eks/efs-controller/remote-state.tf @@ -15,4 +15,3 @@ module "eks" { context = module.this.context } - diff --git a/modules/eks/efs-controller/resources/values.yaml b/modules/eks/efs-controller/resources/values.yaml index 48dd69eb0..b3fa9c76b 100644 --- a/modules/eks/efs-controller/resources/values.yaml +++ b/modules/eks/efs-controller/resources/values.yaml @@ -1,3 +1,3 @@ controller: serviceAccount: - create: "true" \ No newline at end of file + create: "true" diff --git a/modules/eks/eks-without-spotinst/main.tf b/modules/eks/eks-without-spotinst/main.tf index 866ff4ee9..84b740754 100644 --- a/modules/eks/eks-without-spotinst/main.tf +++ b/modules/eks/eks-without-spotinst/main.tf @@ -125,4 +125,3 @@ module "eks_cluster" { context = module.this.context } - diff --git a/modules/eks/eks-without-spotinst/outputs.tf b/modules/eks/eks-without-spotinst/outputs.tf index 80b367532..25b80ec72 100644 --- a/modules/eks/eks-without-spotinst/outputs.tf +++ b/modules/eks/eks-without-spotinst/outputs.tf @@ -67,4 +67,3 @@ output "eks_node_group_statuses" { description = "Status of the EKS Node Group" value = compact([for group in local.node_groups : group.eks_node_group_status]) } - diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 0304362c7..aa101f86f 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -7,7 +7,7 @@ In practice, this means apps will define an `ExternalSecret` that pulls all env ``` # Part of the charts in `/releases -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: app-secrets @@ -72,7 +72,7 @@ components: requests: cpu: "20m" memory: "60Mi" - parameter_store_paths: + parameter_store_paths: - app - rds # You can use `chart_values` to set any other chart options. Treat `chart_values` as the root of the doc. diff --git a/modules/eks/external-secrets-operator/examples/app-secrets.yaml b/modules/eks/external-secrets-operator/examples/app-secrets.yaml index 48e296f26..fcf509bcc 100644 --- a/modules/eks/external-secrets-operator/examples/app-secrets.yaml +++ b/modules/eks/external-secrets-operator/examples/app-secrets.yaml @@ -1,8 +1,8 @@ # example to fetch all secrets underneath the `/app` prefix (service). -# Keys are rewritten within the K8S Secret to be predictable and omit the +# Keys are rewritten within the K8S Secret to be predictable and omit the # prefix. -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: app-secrets @@ -21,4 +21,4 @@ spec: rewrite: - regexp: source: "/app/(.*)" - target: "$1" \ No newline at end of file + target: "$1" diff --git a/modules/eks/external-secrets-operator/examples/external-secrets.yaml b/modules/eks/external-secrets-operator/examples/external-secrets.yaml index 18a346538..8d042bb06 100644 --- a/modules/eks/external-secrets-operator/examples/external-secrets.yaml +++ b/modules/eks/external-secrets-operator/examples/external-secrets.yaml @@ -1,6 +1,6 @@ # example to fetch a single secret from our Parameter Store `SecretStore` -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: single-secret @@ -15,4 +15,4 @@ spec: data: - secretKey: good_secret remoteRef: - key: /app/good_secret \ No newline at end of file + key: /app/good_secret diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf index 7b8617243..93cea7a6f 100644 --- a/modules/eks/external-secrets-operator/main.tf +++ b/modules/eks/external-secrets-operator/main.tf @@ -119,4 +119,3 @@ module "external_ssm_secrets" { module.external_secrets_operator, ] } - diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index 4f8ed9ea9..cf38511d4 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -111,4 +111,4 @@ components: ## References -* https://kubernetes.io/docs/reference/access-authn-authz/authentication/ +* https://kubernetes.io/docs/reference/access-authn-authz/authentication/ diff --git a/modules/eks/idp-roles/remote-state.tf b/modules/eks/idp-roles/remote-state.tf index af8f64247..ac55ba94c 100644 --- a/modules/eks/idp-roles/remote-state.tf +++ b/modules/eks/idp-roles/remote-state.tf @@ -6,4 +6,3 @@ module "eks" { context = module.this.context } - diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 0cc3a2056..e18c77c69 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -300,7 +300,7 @@ Karpenter also supports Node Interruption. If interruption-handling is enabled, - Instance Terminating Events - Instance Stopping Events -:::info +:::info The Node Interruption Handler is not the same as the Node Termination Handler. The latter works fine and cleanly shuts down the node in 2 minutes. The former gets advance notice, so it can have 5-10 minutes to shut down a node. @@ -372,7 +372,7 @@ To enable Node Interruption, set `var.interruption_handler_enabled` to `true`. T | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | -| [interruption\_handler\_enabled](#input\_interruption\_handler\_enabled) | If `true`, deploy a SQS queue and Event Bridge rules to enable interruption handling by Karpenter.

https://karpenter.sh/v0.27.5/concepts/deprovisioning/#interruption | `bool` | `false` | no | +| [interruption\_handler\_enabled](#input\_interruption\_handler\_enabled) | If `true`, deploy a SQS queue and Event Bridge rules to enable interruption handling by Karpenter.

https://karpenter.sh/v0.27.5/concepts/deprovisioning/#interruption | `bool` | `false` | no | | [interruption\_queue\_message\_retention](#input\_interruption\_queue\_message\_retention) | The message retention in seconds for the interruption handler SQS queue. | `number` | `300` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | diff --git a/modules/eks/karpenter/variables.tf b/modules/eks/karpenter/variables.tf index 2623ce192..829a9b6ac 100644 --- a/modules/eks/karpenter/variables.tf +++ b/modules/eks/karpenter/variables.tf @@ -96,7 +96,7 @@ variable "interruption_handler_enabled" { type = bool default = false description = < ## Requirements diff --git a/modules/eks/platform/variables.tf b/modules/eks/platform/variables.tf index 66a2710f6..cd4bc163b 100644 --- a/modules/eks/platform/variables.tf +++ b/modules/eks/platform/variables.tf @@ -33,5 +33,3 @@ variable "platform_environment" { description = "Platform environment" default = "default" } - - diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 911721247..8e4e88558 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -158,4 +158,3 @@ components: ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/redis-operator) - Cloud Posse's upstream component - diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index d92b20bfb..513763944 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -164,4 +164,3 @@ components: ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/redis) - Cloud Posse's upstream component - diff --git a/modules/github-action-token-rotator/main.tf b/modules/github-action-token-rotator/main.tf index e62cf837d..5e43bd00b 100644 --- a/modules/github-action-token-rotator/main.tf +++ b/modules/github-action-token-rotator/main.tf @@ -19,4 +19,3 @@ module "github_action_token_rotator" { context = module.this.context } - diff --git a/modules/github-action-token-rotator/outputs.tf b/modules/github-action-token-rotator/outputs.tf index 9b6358cc8..d21f0e031 100644 --- a/modules/github-action-token-rotator/outputs.tf +++ b/modules/github-action-token-rotator/outputs.tf @@ -2,4 +2,3 @@ output "github_action_token_rotator" { value = module.github_action_token_rotator description = "GitHub action token rotator module outputs." } - diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index 523f8e007..67f17fadf 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -26,11 +26,11 @@ components: ## Configuring the Github OIDC Provider -This component was created to add the Github OIDC provider so that Github Actions can safely assume roles -without the need to store static credentials in the environment. +This component was created to add the Github OIDC provider so that Github Actions can safely assume roles +without the need to store static credentials in the environment. The details of the GitHub OIDC provider are hard coded in the component, however at some point the provider's thumbprint may change, at which point you can use -[scripts/get_github_oidc_thumbprint.sh](./scripts/get_github_oidc_thumbprint.sh) +[scripts/get_github_oidc_thumbprint.sh](./scripts/get_github_oidc_thumbprint.sh) to get the new thumbprint and add it to the list in `var.thumbprint_list`. ## FAQ @@ -43,7 +43,7 @@ The following error is very common if the GitHub workflow is missing proper perm Error: User: arn:aws:sts::***:assumed-role/acme-core-use1-auto-actions-runner@actions-runner-system/token-file-web-identity is not authorized to perform: sts:TagSession on resource: arn:aws:iam::999999999999:role/acme-plat-use1-dev-gha ``` -In order to use a web identity, GitHub Action pipelines must have the following permission. +In order to use a web identity, GitHub Action pipelines must have the following permission. See [GitHub Action documentation for more](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-permissions-settings). ```yaml diff --git a/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh b/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh index 326ebb28c..90d630d02 100755 --- a/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh +++ b/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh @@ -2,7 +2,7 @@ ######################################################################################################################## # This script downloads the certificate information from $GITHUB_OIDC_HOST, extracts the certificate material, then uses -# the openssl command to calculate the thumbprint. It is meant to be called manually and the output used to populate +# the openssl command to calculate the thumbprint. It is meant to be called manually and the output used to populate # the `thumbprint_list` variable in the terraform configuration for this module. ######################################################################################################################## GITHUB_OIDC_HOST="token.actions.githubusercontent.com" diff --git a/modules/github-runners/modules/graceful_scale_in/outputs.tf b/modules/github-runners/modules/graceful_scale_in/outputs.tf index fd24132a8..a87e00fa0 100644 --- a/modules/github-runners/modules/graceful_scale_in/outputs.tf +++ b/modules/github-runners/modules/graceful_scale_in/outputs.tf @@ -16,4 +16,4 @@ output "autoscaling_lifecycle_hook_name" { output "ssm_document_arn" { description = "The ARN of the SSM document." value = join("", aws_ssm_document.default.*.arn) -} \ No newline at end of file +} diff --git a/modules/github-runners/modules/graceful_scale_in/variables.tf b/modules/github-runners/modules/graceful_scale_in/variables.tf index ac97b5568..1aee59ef2 100644 --- a/modules/github-runners/modules/graceful_scale_in/variables.tf +++ b/modules/github-runners/modules/graceful_scale_in/variables.tf @@ -6,4 +6,4 @@ variable "autoscaling_group_name" { variable "command" { description = "Command to run on EC2 instance shutdown." type = string -} \ No newline at end of file +} diff --git a/modules/github-runners/templates/amazon-cloudwatch-agent.json b/modules/github-runners/templates/amazon-cloudwatch-agent.json index 2372a61d8..d6e5d2e16 100644 --- a/modules/github-runners/templates/amazon-cloudwatch-agent.json +++ b/modules/github-runners/templates/amazon-cloudwatch-agent.json @@ -134,4 +134,4 @@ }, "force_flush_interval": 15 } -} \ No newline at end of file +} diff --git a/modules/global-accelerator-endpoint-group/README.md b/modules/global-accelerator-endpoint-group/README.md index debecb5e4..d0420d9fb 100644 --- a/modules/global-accelerator-endpoint-group/README.md +++ b/modules/global-accelerator-endpoint-group/README.md @@ -6,7 +6,7 @@ This component assumes that the `global-accelerator` component has already been ## Usage -**Stack Level**: Regional +**Stack Level**: Regional Here are some example snippets for how to use this component: diff --git a/modules/guardduty/common/outputs.tf b/modules/guardduty/common/outputs.tf index a83bf47da..0a527a355 100644 --- a/modules/guardduty/common/outputs.tf +++ b/modules/guardduty/common/outputs.tf @@ -16,4 +16,4 @@ output "sns_topic_name" { output "sns_topic_subscriptions" { description = "SNS topic subscriptions" value = one(module.guardduty[*].sns_topic_subscriptions) -} \ No newline at end of file +} diff --git a/modules/guardduty/common/providers.tf b/modules/guardduty/common/providers.tf index 28104fbb7..83b416f43 100644 --- a/modules/guardduty/common/providers.tf +++ b/modules/guardduty/common/providers.tf @@ -38,4 +38,4 @@ variable "import_role_arn" { type = string default = null description = "IAM Role ARN to use when importing a resource" -} \ No newline at end of file +} diff --git a/modules/guardduty/common/remote-state.tf b/modules/guardduty/common/remote-state.tf index 12de8d665..5595945d0 100644 --- a/modules/guardduty/common/remote-state.tf +++ b/modules/guardduty/common/remote-state.tf @@ -9,4 +9,4 @@ module "account_map" { privileged = var.privileged context = module.this.context -} \ No newline at end of file +} diff --git a/modules/guardduty/common/variables.tf b/modules/guardduty/common/variables.tf index 66eb0465f..9304a7205 100644 --- a/modules/guardduty/common/variables.tf +++ b/modules/guardduty/common/variables.tf @@ -39,7 +39,7 @@ variable "admin_delegated" { A flag to indicate if the GuardDuty Admininstrator account has been designated from the root account. This component should be applied with this variable set to false, then the guardduty-root component should be applied - to designate the administrator account, then this component should be applied again with this variable set to `true`. + to designate the administrator account, then this component should be applied again with this variable set to `true`. DOC } @@ -125,4 +125,4 @@ variable "cloudwatch_event_rule_pattern_detail_type" { DOC type = string default = "GuardDuty Finding" -} \ No newline at end of file +} diff --git a/modules/guardduty/root/providers.tf b/modules/guardduty/root/providers.tf index 5ff54f0d6..dc58d9a25 100644 --- a/modules/guardduty/root/providers.tf +++ b/modules/guardduty/root/providers.tf @@ -1,3 +1,3 @@ provider "aws" { region = var.region -} \ No newline at end of file +} diff --git a/modules/guardduty/root/remote-state.tf b/modules/guardduty/root/remote-state.tf index 12de8d665..5595945d0 100644 --- a/modules/guardduty/root/remote-state.tf +++ b/modules/guardduty/root/remote-state.tf @@ -9,4 +9,4 @@ module "account_map" { privileged = var.privileged context = module.this.context -} \ No newline at end of file +} diff --git a/modules/kinesis-stream/README.md b/modules/kinesis-stream/README.md index 9cd47fef6..1cdfdbbe6 100644 --- a/modules/kinesis-stream/README.md +++ b/modules/kinesis-stream/README.md @@ -33,7 +33,7 @@ import: components: terraform: - kinesis-example: + kinesis-example: metadata: component: kinesis-stream inherits: diff --git a/modules/lakeformation/README.md b/modules/lakeformation/README.md index 236959e64..bbddf27a5 100644 --- a/modules/lakeformation/README.md +++ b/modules/lakeformation/README.md @@ -32,7 +32,7 @@ import: components: terraform: - lakeformation-example: + lakeformation-example: metadata: component: lakeformation inherits: diff --git a/modules/lambda/main.tf b/modules/lambda/main.tf index 043303c12..d2944c185 100644 --- a/modules/lambda/main.tf +++ b/modules/lambda/main.tf @@ -86,4 +86,3 @@ module "lambda" { context = module.this.context } - diff --git a/modules/mq-broker/default.auto.tfvars b/modules/mq-broker/default.auto.tfvars old mode 100755 new mode 100644 diff --git a/modules/mq-broker/main.tf b/modules/mq-broker/main.tf old mode 100755 new mode 100644 diff --git a/modules/mq-broker/outputs.tf b/modules/mq-broker/outputs.tf old mode 100755 new mode 100644 diff --git a/modules/mq-broker/variables.tf b/modules/mq-broker/variables.tf old mode 100755 new mode 100644 diff --git a/modules/mq-broker/versions.tf b/modules/mq-broker/versions.tf old mode 100755 new mode 100644 diff --git a/modules/mwaa/README.md b/modules/mwaa/README.md index 6122cbf90..01d21278a 100644 --- a/modules/mwaa/README.md +++ b/modules/mwaa/README.md @@ -152,4 +152,4 @@ components: * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component -[](https://cpco.io/component) \ No newline at end of file +[](https://cpco.io/component) diff --git a/modules/network-firewall/README.md b/modules/network-firewall/README.md index c7229393f..4db5de917 100644 --- a/modules/network-firewall/README.md +++ b/modules/network-firewall/README.md @@ -1,6 +1,6 @@ # Component: `network-firewall` -This component is responsible for provisioning [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, +This component is responsible for provisioning [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, including Network Firewall, firewall policy, rule groups, and logging configuration. ## Usage @@ -11,7 +11,7 @@ Example of a Network Firewall with stateful 5-tuple rules: :::info -The "5-tuple" means the five items (columns) that each rule (row, or tuple) in a firewall policy uses to define whether to block or allow traffic: +The "5-tuple" means the five items (columns) that each rule (row, or tuple) in a firewall policy uses to define whether to block or allow traffic: source and destination IP, source and destination port, and protocol. Refer to [Standard stateful rule groups in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-basic.html) @@ -90,7 +90,7 @@ Example of a Network Firewall with [Suricata](https://suricata.readthedocs.io/en :::info -For [Suricata](https://suricata.io/) rule group type, you provide match and action settings in a string, in a Suricata compatible specification. +For [Suricata](https://suricata.io/) rule group type, you provide match and action settings in a string, in a Suricata compatible specification. The specification fully defines what the stateful rules engine looks for in a traffic flow and the action to take on the packets in a flow that matches the inspection criteria. Refer to [Suricata compatible rule strings in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-suricata.html) diff --git a/modules/opsgenie-team/modules/escalation/README.md b/modules/opsgenie-team/modules/escalation/README.md index d97a36c3c..1d64e1962 100644 --- a/modules/opsgenie-team/modules/escalation/README.md +++ b/modules/opsgenie-team/modules/escalation/README.md @@ -89,4 +89,3 @@ module "escalation" { | [escalation\_id](#output\_escalation\_id) | The ID of the Opsgenie Escalation | | [escalation\_name](#output\_escalation\_name) | Name of the Opsgenie Escalation | - diff --git a/modules/opsgenie-team/providers.tf b/modules/opsgenie-team/providers.tf index 643bee068..265107f7b 100644 --- a/modules/opsgenie-team/providers.tf +++ b/modules/opsgenie-team/providers.tf @@ -35,4 +35,3 @@ data "aws_ssm_parameter" "opsgenie_api_key" { provider "opsgenie" { api_key = join("", data.aws_ssm_parameter.opsgenie_api_key[*].value) } - diff --git a/modules/opsgenie-team/ssm.tf b/modules/opsgenie-team/ssm.tf index fd39d4571..452963f68 100644 --- a/modules/opsgenie-team/ssm.tf +++ b/modules/opsgenie-team/ssm.tf @@ -15,4 +15,3 @@ variable "ssm_path" { default = "opsgenie" description = "SSM path" } - diff --git a/modules/route53-resolver-dns-firewall/README.md b/modules/route53-resolver-dns-firewall/README.md index 96a638310..7f5939a20 100644 --- a/modules/route53-resolver-dns-firewall/README.md +++ b/modules/route53-resolver-dns-firewall/README.md @@ -1,6 +1,6 @@ # Component: `route53-resolver-dns-firewall` -This component is responsible for provisioning [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) +This component is responsible for provisioning [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging configuration. ## Usage diff --git a/modules/s3-bucket/variables.tf b/modules/s3-bucket/variables.tf index 9f51d70e9..bbbb425e4 100644 --- a/modules/s3-bucket/variables.tf +++ b/modules/s3-bucket/variables.tf @@ -389,4 +389,3 @@ variable "iam_policy_statements" { description = "Map of IAM policy statements to use in the bucket policy." default = {} } - diff --git a/modules/securityhub/common/README.md b/modules/securityhub/common/README.md index 8d8724f4f..2a13b4779 100644 --- a/modules/securityhub/common/README.md +++ b/modules/securityhub/common/README.md @@ -146,8 +146,8 @@ done | [enabled\_standards](#input\_enabled\_standards) | A list of standards to enable in the account.

For example:
- standards/aws-foundational-security-best-practices/v/1.0.0
- ruleset/cis-aws-foundations-benchmark/v/1.2.0
- standards/pci-dss/v/3.2.1
- standards/cis-aws-foundations-benchmark/v/1.4.0 | `set(string)` | `[]` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [finding\_aggregator\_enabled](#input\_finding\_aggregator\_enabled) | Flag to indicate whether a finding aggregator should be created

If you want to aggregate findings from one region, set this to `true`.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_finding_aggregator | `bool` | `false` | no | -| [finding\_aggregator\_linking\_mode](#input\_finding\_aggregator\_linking\_mode) | Linking mode to use for the finding aggregator.

The possible values are:
- `ALL_REGIONS` - Aggregate from all regions
- `ALL_REGIONS_EXCEPT_SPECIFIED` - Aggregate from all regions except those specified in `var.finding_aggregator_regions`
- `SPECIFIED_REGIONS` - Aggregate from regions specified in `var.finding_aggregator_regions` | `string` | `"ALL_REGIONS"` | no | -| [finding\_aggregator\_regions](#input\_finding\_aggregator\_regions) | A list of regions to aggregate findings from.

This is only used if `finding_aggregator_enabled` is `true`. | `any` | `null` | no | +| [finding\_aggregator\_linking\_mode](#input\_finding\_aggregator\_linking\_mode) | Linking mode to use for the finding aggregator.

The possible values are:
- `ALL_REGIONS` - Aggregate from all regions
- `ALL_REGIONS_EXCEPT_SPECIFIED` - Aggregate from all regions except those specified in `var.finding_aggregator_regions`
- `SPECIFIED_REGIONS` - Aggregate from regions specified in `var.finding_aggregator_regions` | `string` | `"ALL_REGIONS"` | no | +| [finding\_aggregator\_regions](#input\_finding\_aggregator\_regions) | A list of regions to aggregate findings from.

This is only used if `finding_aggregator_enabled` is `true`. | `any` | `null` | no | | [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | diff --git a/modules/securityhub/common/outputs.tf b/modules/securityhub/common/outputs.tf index 07000d86d..e96f886b2 100644 --- a/modules/securityhub/common/outputs.tf +++ b/modules/securityhub/common/outputs.tf @@ -11,4 +11,4 @@ output "sns_topic_name" { output "sns_topic_subscriptions" { description = "The SNS topic subscriptions" value = local.enabled && local.is_global_collector_account && var.create_sns_topic ? module.security_hub[0].sns_topic_subscriptions : null -} \ No newline at end of file +} diff --git a/modules/securityhub/common/providers.tf b/modules/securityhub/common/providers.tf index 28104fbb7..83b416f43 100644 --- a/modules/securityhub/common/providers.tf +++ b/modules/securityhub/common/providers.tf @@ -38,4 +38,4 @@ variable "import_role_arn" { type = string default = null description = "IAM Role ARN to use when importing a resource" -} \ No newline at end of file +} diff --git a/modules/securityhub/common/variables.tf b/modules/securityhub/common/variables.tf index 8d96e7eb4..bfde69fe0 100644 --- a/modules/securityhub/common/variables.tf +++ b/modules/securityhub/common/variables.tf @@ -52,7 +52,7 @@ variable "enable_default_standards" { variable "enabled_standards" { description = <-spacelift-worker-pool: SOMEWORKERPOOLID #TODO: replace with your worker pool ID + infracost_enabled: false # TODO: decide on infracost + terraform_version: "1.3.6" + terraform_version_map: + "1": "1.3.6" + + # These could be moved to $PROJECT_ROOT/.spacelift/config.yml + before_init: + - spacelift-configure + - spacelift-write-vars + - spacelift-tf-workspace + before_plan: + - spacelift-configure + before_apply: + - spacelift-configure + + # Manages policies, admin stacks, and core OU accounts + spacelift: + metadata: + component: spacelift + inherits: + - spacelift/defaults + settings: + spacelift: + policies_by_id_enabled: + # This component also creates this policy so this is omitted prior to the first apply + # then added so it's consistent with all admin stacks. + - trigger-administrative-policy + vars: + enabled: true + # Use context_filters to split up admin stack management + # context_filters: + # stages: + # - artifacts + # - audit + # - auto + # - corp + # - dns + # - identity + # - marketplace + # - network + # - public + # - security + # These are the policies created from https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies + # Make sure to remove the .rego suffix + policies_available: + - git_push.proposed-run + - git_push.tracked-run + - plan.default + - trigger.dependencies + - trigger.retries + # This is to auto deploy launch template image id changes + - plan.warn-on-resource-changes-except-image-id + # This is the global admin policy + - trigger.administrative + # These are the policies added to each spacelift stack created by this admin stack + policies_enabled: + - git_push.proposed-run + - git_push.tracked-run + - plan.default + - trigger.dependencies + # Keep these empty + policies_by_id_enabled: [] + +``` + +## Prerequisites + +### GitHub Integration + +1. The GitHub owner will need to sign up for a [free trial of Spacelift](https://spacelift.io/free-trial.html) +1. Once an account is created take note of the URL - usually its `https://.app.spacelift.io/` +1. Create a Login Policy + + - Click on Policies then Add Policy + - Use the following policy and replace `GITHUBORG` with the GitHub Organization slug and DEV with the GitHub id for the Dev setting up the Spacelift module. + + ```rego + package spacelift + + # See https://docs.spacelift.io/concepts/policy/login-policy for implementation details. + # Note: Login policies don't affect GitHub organization or SSO admins. + # Note 2: Enabling SSO requires that all users have an IdP (G Suite) account, so we'll just use + # GitHub authentication in the meantime while working with external collaborators. + # Map session input data to human friendly variables to use in policy evaluation + + username := input.session.login + member_of := input.session.teams # Input is friendly name, e.g. "SRE" not "sre" or "@GITHUBORG/sre" + GITHUBORG := input.session.member # Is this user a member of the CUSTOMER GitHub org? + + # Define GitHub usernames of non org external collaborators with admin vs. user access + admin_collaborators := { "DEV" } + user_collaborators := { "GITHUBORG" } # Using GITHUBORG as a placeholder to avoid empty set + + # Grant admin access to GITHUBORG org members in the CloudPosse group + admin { + GITHUBORG + member_of[_] == "CloudPosse" + } + + # Grant admin access to non-GITHUBORG org accounts in the admin_collaborators set + admin { + # not GITHUBORG + admin_collaborators[username] + } + + # Grant user access to GITHUBORG org members in the Developers group + # allow { + # GITHUBORG + # member_of[_] == "Developers" + # } + + # Grant user access to non-GITHUBORG org accounts in the user_collaborators set + allow { + not GITHUBORG + user_collaborators[username] + } + + # Deny access to any non-GITHUBORG org accounts who aren't defined in external collaborators sets + deny { + not GITHUBORG + not user_collaborators[username] + not admin_collaborators[username] + } + + # Grant spaces read only user access to all members + space_read[space.id] { + space := input.spaces[_] + GITHUBORG + } + + # Grant spaces write access to GITHUBORG org members in the Developers group + # space_write[space.id] { + # space := input.spaces[_] + # member_of[_] == "Developers" + # } + ``` + +## Spacelift Layout + +[Runtime configuration](https://docs.spacelift.io/concepts/configuration/runtime-configuration) is a piece of setup +that is applied to individual runs instead of being global to the stack. +It's defined in `.spacelift/config.yml` YAML file at the root of your repository. +It is required for Spacelift to work with `atmos`. + +### Create Spacelift helper scripts + +[/rootfs/usr/local/bin/spacelift-tf-workspace](/rootfs/usr/local/bin/spacelift-tf-workspace) manages selecting or creating a Terraform workspace; similar to how `atmos` manages workspaces +during a Terraform run. + +[/rootfs/usr/local/bin/spacelift-write-vars](/rootfs/usr/local/bin/spacelift-write-vars) writes the component config using `atmos` to the `spacelift.auto.tfvars.json` file. + +**NOTE**: make sure they are all executable: + +```bash +chmod +x rootfs/usr/local/bin/spacelift* +``` This folder contains a set of components used to manage [Spacelift](https://docs.spacelift.io/) in an [atmos-opinionated](https://atmos.tools/) way. diff --git a/modules/spacelift/admin-stack/providers.tf b/modules/spacelift/admin-stack/providers.tf index 84d03ead5..c95d53819 100644 --- a/modules/spacelift/admin-stack/providers.tf +++ b/modules/spacelift/admin-stack/providers.tf @@ -1,2 +1 @@ provider "spacelift" {} - diff --git a/modules/sso-saml-provider/README.md b/modules/sso-saml-provider/README.md index dffff96d5..bab429ff0 100644 --- a/modules/sso-saml-provider/README.md +++ b/modules/sso-saml-provider/README.md @@ -19,4 +19,3 @@ components: enabled: true ssm_path_prefix: "/sso/saml/google" ``` - diff --git a/modules/sso-saml-provider/outputs.tf b/modules/sso-saml-provider/outputs.tf index 971d6f05a..b855bfdb3 100644 --- a/modules/sso-saml-provider/outputs.tf +++ b/modules/sso-saml-provider/outputs.tf @@ -30,4 +30,3 @@ output "groupsAttr" { value = local.enabled ? var.groupsAttr : null description = "Groups attribute" } - diff --git a/modules/sso/providers.tf b/modules/sso/providers.tf index 0f428a1a7..c4f45ca75 100644 --- a/modules/sso/providers.tf +++ b/modules/sso/providers.tf @@ -28,4 +28,3 @@ variable "import_role_arn" { default = null description = "IAM Role ARN to use when importing a resource" } - diff --git a/modules/strongdm/README.md b/modules/strongdm/README.md index dc234635e..dff63ec23 100644 --- a/modules/strongdm/README.md +++ b/modules/strongdm/README.md @@ -1,6 +1,6 @@ # Component: `strongdm` -This component provisions [strongDM](https://www.strongdm.com/) gateway, relay and roles +This component provisions [strongDM](https://www.strongdm.com/) gateway, relay and roles ## Usage diff --git a/modules/strongdm/charts/strongdm/templates/_helpers.tpl b/modules/strongdm/charts/strongdm/templates/_helpers.tpl index e5f798fa4..efd52cd75 100644 --- a/modules/strongdm/charts/strongdm/templates/_helpers.tpl +++ b/modules/strongdm/charts/strongdm/templates/_helpers.tpl @@ -81,4 +81,3 @@ Create the name of the controller service account to use {{ default "default" .Values.serviceAccount.name }} {{- end -}} {{- end -}} - diff --git a/modules/strongdm/providers.tf b/modules/strongdm/providers.tf old mode 100755 new mode 100644 diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 9d9e6c7ac..867a34c45 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -112,7 +112,7 @@ No resources. | [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | +| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/tgw/hub/variables.tf b/modules/tgw/hub/variables.tf index 70cd558af..6b5603c83 100644 --- a/modules/tgw/hub/variables.tf +++ b/modules/tgw/hub/variables.tf @@ -19,7 +19,7 @@ variable "connections" { eks_component_names = optional(list(string), []) })) description = <<-EOT - A list of objects to define each TGW connections. + A list of objects to define each TGW connections. By default, each connection will look for only the default `vpc` component. EOT diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index c07287ad0..afeafd088 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -32,7 +32,7 @@ components: inherits: - tgw/spoke-defaults vars: - # This is what THIS spoke is allowed to connect to. + # This is what THIS spoke is allowed to connect to. # since this is deployed to each plat account (dev->prod), # we allow connections to network and auto. connections: @@ -40,7 +40,7 @@ components: tenant: core stage: network # Set this value if the vpc component has a different name in this account - vpc_component_names: + vpc_component_names: - vpc-dev - account: tenant: core @@ -121,7 +121,7 @@ No resources. |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | +| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf index 1d4fb42a3..7e09fe853 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf @@ -30,7 +30,7 @@ locals { ] ) - # Create a list of all EKS component keys. + # Create a list of all EKS component keys. # Follows same pattern as vpc_component_names connected_eks_component_keys = flatten( [ diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf index 0e43828ac..79d6384a0 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf @@ -36,7 +36,7 @@ variable "connections" { eks_component_names = optional(list(string), []) })) description = <<-EOT - A list of objects to define each TGW connections. + A list of objects to define each TGW connections. By default, each connection will look for only the default `vpc` component. EOT diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index d0908efc4..6d4075445 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -13,7 +13,7 @@ variable "connections" { eks_component_names = optional(list(string), []) })) description = <<-EOT - A list of objects to define each TGW connections. + A list of objects to define each TGW connections. By default, each connection will look for only the default `vpc` component. EOT diff --git a/modules/vpc-flow-logs-bucket/variables.tf b/modules/vpc-flow-logs-bucket/variables.tf index c101b778a..6b87353ed 100644 --- a/modules/vpc-flow-logs-bucket/variables.tf +++ b/modules/vpc-flow-logs-bucket/variables.tf @@ -62,4 +62,3 @@ variable "traffic_type" { description = "The type of traffic to capture. Valid values: `ACCEPT`, `REJECT`, `ALL`" default = "ALL" } - From 1b338fe664e5debc5bbac30cfe42003f7458575a Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 12 Jun 2023 16:41:22 -0700 Subject: [PATCH 153/501] Enable `terraform plan` access via dynamic Terraform roles (#715) --- deprecated/spacelift/README.md | 2 - deprecated/spacelift/providers.tf | 20 +- {modules => deprecated}/sso/README.md | 0 {modules => deprecated}/sso/context.tf | 0 {modules => deprecated}/sso/main.tf | 0 .../sso/modules/okta-user/context.tf | 0 .../sso/modules/okta-user/main.tf | 0 .../sso/modules/okta-user/outputs.tf | 0 .../sso/modules/okta-user/variables.tf | 0 {modules => deprecated}/sso/outputs.tf | 0 {modules => deprecated}/sso/providers.tf | 0 {modules => deprecated}/sso/variables.tf | 0 {modules => deprecated}/sso/versions.tf | 0 mixins/README.md | 5 + mixins/provider-awsutils.mixin.tf | 21 ++ mixins/provider-helm.tf | 2 +- mixins/providers.depth-1.tf | 19 ++ mixins/providers.depth-2.tf | 19 ++ mixins/providers.tf | 29 --- modules/account-map/README.md | 18 +- modules/account-map/account-info.tftmpl | 5 +- modules/account-map/dynamic-roles.tf | 95 +++++++++ modules/account-map/main.tf | 58 +----- modules/account-map/modules/iam-roles/main.tf | 56 ++++- .../account-map/modules/iam-roles/outputs.tf | 37 ++-- .../modules/iam-roles/providers.tf | 11 + .../account-map/modules/iam-roles/versions.tf | 10 + .../modules/roles-to-principals/README.md | 13 +- .../modules/roles-to-principals/main.tf | 8 +- .../modules/roles-to-principals/variables.tf | 12 +- .../modules/team-assume-role-policy/README.md | 2 +- .../github-assume-role-policy.mixin.tf | 2 +- .../modules/team-assume-role-policy/main.tf | 63 ++++-- modules/account-map/outputs.tf | 33 ++- modules/account-map/remote-state.tf | 2 +- modules/account-map/variables.tf | 15 ++ modules/account-map/versions.tf | 6 +- modules/account-quotas/README.md | 2 - modules/account-quotas/providers.tf | 20 +- modules/account-settings/README.md | 2 - modules/account-settings/providers.tf | 25 +-- modules/acm/README.md | 2 - modules/acm/providers.tf | 20 +- modules/alb/README.md | 2 - modules/alb/providers.tf | 20 +- modules/amplify/README.md | 2 - modules/amplify/providers.tf | 20 +- modules/argocd-repo/README.md | 2 - modules/argocd-repo/provider-github.tf | 5 + modules/argocd-repo/providers.tf | 26 +-- modules/athena/README.md | 2 - modules/athena/providers.tf | 20 +- modules/aurora-mysql-resources/README.md | 2 - modules/aurora-mysql-resources/providers.tf | 20 +- modules/aurora-mysql/README.md | 2 - modules/aurora-mysql/providers.tf | 20 +- modules/aurora-postgres-resources/README.md | 2 - .../aurora-postgres-resources/providers.tf | 20 +- modules/aurora-postgres/README.md | 2 - modules/aurora-postgres/providers.tf | 20 +- modules/aws-backup/README.md | 2 - modules/aws-backup/providers.tf | 20 +- modules/aws-config/README.md | 2 - modules/aws-config/provider-awsutils.mixin.tf | 14 ++ modules/aws-config/providers.tf | 34 +-- modules/aws-inspector/README.md | 2 - modules/aws-inspector/providers.tf | 20 +- modules/aws-shield/README.md | 2 - modules/aws-shield/providers.tf | 20 +- modules/aws-sso/README.md | 58 +++++- modules/aws-sso/additional-permission-sets.tf | 10 + modules/aws-sso/main.tf | 1 + modules/aws-sso/providers.tf | 24 +-- modules/aws-ssosync/README.md | 7 +- modules/aws-ssosync/providers.tf | 22 +- modules/aws-team-roles/README.md | 4 +- .../aws-team-roles/additional-policy-map.tf | 2 +- modules/aws-team-roles/main.tf | 5 +- modules/aws-team-roles/remote-state.tf | 2 +- modules/aws-teams/README.md | 23 +- modules/aws-teams/additional-policy-map.tf | 2 +- modules/aws-teams/main.tf | 1 - modules/aws-teams/outputs.tf | 2 +- modules/aws-teams/policy-support.tf | 56 ----- modules/aws-teams/remote-state.tf | 9 +- modules/aws-waf-acl/README.md | 2 - modules/aws-waf-acl/providers.tf | 20 +- modules/bastion/README.md | 2 - modules/bastion/providers.tf | 20 +- modules/cloudtrail-bucket/README.md | 2 - modules/cloudtrail-bucket/providers.tf | 20 +- modules/cloudtrail/README.md | 2 - modules/cloudtrail/providers.tf | 20 +- modules/cloudwatch-logs/README.md | 2 - modules/cloudwatch-logs/providers.tf | 20 +- modules/cognito/README.md | 2 - modules/cognito/providers.tf | 20 +- modules/config-bucket/README.md | 2 - modules/config-bucket/providers.tf | 20 +- modules/datadog-configuration/README.md | 2 - .../datadog-configuration/provider-datadog.tf | 23 ++ modules/datadog-configuration/providers.tf | 41 +--- modules/datadog-integration/README.md | 2 - modules/datadog-integration/providers.tf | 20 +- modules/datadog-lambda-forwarder/README.md | 2 - modules/datadog-lambda-forwarder/providers.tf | 20 +- modules/datadog-logs-archive/providers.tf | 20 +- modules/datadog-monitor/README.md | 2 - modules/datadog-monitor/providers.tf | 20 +- .../datadog-private-location-ecs/README.md | 2 - .../datadog-private-location-ecs/providers.tf | 20 +- .../README.md | 2 - .../provider-helm.tf | 26 ++- .../providers.tf | 20 +- modules/datadog-synthetics/README.md | 2 - modules/datadog-synthetics/providers.tf | 20 +- modules/dms/endpoint/README.md | 2 - modules/dms/endpoint/providers.tf | 20 +- modules/dms/iam/README.md | 2 - modules/dms/iam/providers.tf | 20 +- modules/dms/replication-instance/README.md | 2 - modules/dms/replication-instance/providers.tf | 20 +- modules/dms/replication-task/README.md | 2 - modules/dms/replication-task/providers.tf | 20 +- modules/dns-delegated/README.md | 2 - .../dns-delegated/providers-dns-primary.tf | 16 ++ modules/dns-delegated/providers.tf | 36 +--- modules/dns-primary/README.md | 2 - modules/dns-primary/providers.tf | 20 +- modules/documentdb/README.md | 2 - modules/documentdb/providers.tf | 20 +- modules/dynamodb/README.md | 2 - modules/dynamodb/providers.tf | 20 +- modules/ec2-client-vpn/README.md | 2 - .../ec2-client-vpn/provider-awsutils.mixin.tf | 14 ++ modules/ec2-client-vpn/providers.tf | 33 +-- modules/ecr/README.md | 2 - modules/ecr/providers.tf | 20 +- modules/ecs-service/README.md | 2 - modules/ecs-service/providers.tf | 20 +- modules/ecs/README.md | 2 - modules/ecs/providers.tf | 20 +- modules/efs/README.md | 2 - modules/efs/providers.tf | 20 +- modules/eks-iam/README.md | 2 - modules/eks-iam/providers.tf | 20 +- .../eks/actions-runner-controller/README.md | 2 - .../provider-helm.tf | 2 +- .../actions-runner-controller/providers.tf | 21 +- .../alb-controller-ingress-class/README.md | 2 - .../provider-helm.tf | 2 +- .../alb-controller-ingress-class/providers.tf | 20 +- .../alb-controller-ingress-group/README.md | 2 - .../provider-kubernetes.tf | 2 +- .../alb-controller-ingress-group/providers.tf | 20 +- modules/eks/alb-controller/README.md | 2 - modules/eks/alb-controller/provider-helm.tf | 2 +- modules/eks/alb-controller/providers.tf | 20 +- modules/eks/argocd/README.md | 2 - modules/eks/argocd/provider-helm.tf | 26 ++- modules/eks/argocd/provider-secrets.tf | 22 ++ modules/eks/argocd/providers.tf | 34 +-- .../aws-node-termination-handler/README.md | 2 - .../provider-helm.tf | 26 ++- .../aws-node-termination-handler/providers.tf | 21 +- modules/eks/cert-manager/README.md | 2 - modules/eks/cert-manager/provider-helm.tf | 2 +- modules/eks/cert-manager/providers.tf | 20 +- modules/eks/cluster/README.md | 2 +- modules/eks/cluster/main.tf | 4 +- modules/eks/cluster/providers.tf | 13 +- modules/eks/cluster/variables.tf | 6 + modules/eks/datadog-agent/README.md | 2 - modules/eks/datadog-agent/provider-helm.tf | 26 ++- modules/eks/datadog-agent/providers.tf | 23 +- modules/eks/ebs-controller/README.md | 4 - modules/eks/ebs-controller/provider-helm.tf | 26 ++- modules/eks/ebs-controller/providers.tf | 33 +-- modules/eks/echo-server/README.md | 4 - modules/eks/echo-server/provider-helm.tf | 26 ++- modules/eks/echo-server/providers.tf | 33 +-- modules/eks/efs-controller/README.md | 2 - modules/eks/efs-controller/provider-helm.tf | 26 ++- modules/eks/efs-controller/providers.tf | 20 +- modules/eks/eks-without-spotinst/README.md | 1 - modules/eks/eks-without-spotinst/providers.tf | 13 +- modules/eks/external-dns/README.md | 2 - modules/eks/external-dns/provider-helm.tf | 2 +- modules/eks/external-dns/providers.tf | 20 +- .../eks/external-secrets-operator/README.md | 18 +- .../examples/app-secrets.yaml | 24 +-- .../examples/external-secrets.yaml | 8 +- .../provider-helm.tf | 26 ++- .../external-secrets-operator/providers.tf | 33 +-- modules/eks/idp-roles/README.md | 6 +- modules/eks/idp-roles/provider-helm.tf | 28 ++- modules/eks/idp-roles/providers.tf | 33 +-- modules/eks/karpenter-provisioner/README.md | 2 - .../karpenter-provisioner/provider-helm.tf | 2 +- .../eks/karpenter-provisioner/providers.tf | 20 +- modules/eks/karpenter/README.md | 42 +--- modules/eks/karpenter/provider-helm.tf | 2 +- modules/eks/karpenter/providers.tf | 20 +- modules/eks/metrics-server/README.md | 2 - modules/eks/metrics-server/provider-helm.tf | 26 ++- modules/eks/metrics-server/providers.tf | 21 +- modules/eks/platform/README.md | 2 - modules/eks/platform/providers.tf | 22 +- modules/eks/redis-operator/README.md | 2 - modules/eks/redis-operator/provider-helm.tf | 26 ++- modules/eks/redis-operator/providers.tf | 21 +- modules/eks/redis/README.md | 2 - modules/eks/redis/provider-helm.tf | 26 ++- modules/eks/redis/providers.tf | 21 +- modules/eks/reloader/README.md | 2 - modules/eks/reloader/provider-helm.tf | 26 ++- modules/eks/reloader/providers.tf | 20 +- modules/elasticache-redis/README.md | 2 - modules/elasticache-redis/providers.tf | 20 +- modules/elasticsearch/README.md | 2 - modules/elasticsearch/providers.tf | 20 +- modules/github-action-token-rotator/README.md | 2 - .../github-action-token-rotator/providers.tf | 20 +- modules/github-oidc-provider/README.md | 1 - modules/github-oidc-provider/providers.tf | 21 +- modules/github-runners/README.md | 2 - modules/github-runners/providers.tf | 20 +- .../README.md | 2 - .../providers.tf | 20 +- modules/global-accelerator/README.md | 2 - modules/global-accelerator/providers.tf | 20 +- modules/guardduty/common/README.md | 2 - modules/guardduty/common/providers.tf | 34 +-- modules/guardduty/root/README.md | 1 + modules/guardduty/root/providers.tf | 16 ++ modules/iam-role/README.md | 2 - modules/iam-role/providers.tf | 20 +- modules/iam-service-linked-roles/README.md | 2 - modules/iam-service-linked-roles/providers.tf | 20 +- modules/kinesis-stream/README.md | 2 - modules/kinesis-stream/providers.tf | 20 +- modules/kms/providers.tf | 20 +- modules/lakeformation/README.md | 2 - modules/lakeformation/providers.tf | 20 +- modules/lambda/README.md | 2 - modules/lambda/providers.tf | 20 +- modules/mq-broker/README.md | 2 - modules/mq-broker/providers.tf | 20 +- modules/mwaa/README.md | 2 - modules/mwaa/providers.tf | 20 +- modules/network-firewall/README.md | 2 - modules/network-firewall/providers.tf | 20 +- modules/opsgenie-team/README.md | 2 - modules/opsgenie-team/provider-opsgenie.tf | 8 + modules/opsgenie-team/providers.tf | 30 +-- modules/rds/README.md | 2 - modules/rds/providers.tf | 20 +- modules/redshift/README.md | 2 - modules/redshift/providers.tf | 20 +- .../route53-resolver-dns-firewall/README.md | 2 - .../providers.tf | 20 +- modules/s3-bucket/README.md | 2 - modules/s3-bucket/providers.tf | 20 +- modules/securityhub/common/README.md | 2 - modules/securityhub/common/providers.tf | 34 +-- modules/securityhub/root/README.md | 1 + modules/securityhub/root/providers.tf | 16 ++ modules/ses/README.md | 2 - modules/ses/provider-awsutils.mixin.tf | 14 ++ modules/ses/providers.tf | 35 +--- modules/sftp/README.md | 1 - modules/sftp/provider-awsutils.mixin.tf | 14 ++ modules/sftp/providers.tf | 27 +-- modules/snowflake-account/README.md | 2 - .../snowflake-account/provider-snowflake.tf | 12 ++ modules/snowflake-account/providers.tf | 34 +-- modules/snowflake-database/README.md | 2 - .../snowflake-database/provider-snowflake.tf | 18 ++ modules/snowflake-database/providers.tf | 40 +--- modules/sns-topic/README.md | 2 - modules/sns-topic/providers.tf | 20 +- modules/spa-s3-cloudfront/README.md | 2 - .../provider-other-regions.tf | 34 +++ modules/spa-s3-cloudfront/providers.tf | 47 +---- modules/spacelift/README.md | 197 +++++++++++++++--- modules/spacelift/worker-pool/README.md | 2 - .../worker-pool/provider-spacelift.tf | 6 + modules/spacelift/worker-pool/providers.tf | 27 +-- modules/sqs-queue/README.md | 2 - modules/sqs-queue/providers.tf | 20 +- modules/ssm-parameters/README.md | 2 - modules/ssm-parameters/providers.tf | 20 +- modules/sso-saml-provider/providers.tf | 20 +- modules/sso/default.auto.tfvars | 3 - modules/strongdm/README.md | 3 +- modules/strongdm/provider-strongdm.tf | 26 +++ modules/strongdm/providers.tf | 35 +--- modules/strongdm/variables.tf | 2 +- modules/tfstate-backend/README.md | 66 ++++-- modules/tfstate-backend/iam.tf | 12 +- modules/tfstate-backend/main.tf | 11 +- modules/tfstate-backend/variables.tf | 2 +- modules/tfstate-backend/versions.tf | 4 + modules/tgw/hub/README.md | 2 - modules/tgw/hub/providers.tf | 20 +- modules/tgw/spoke/README.md | 2 - modules/tgw/spoke/provider-hub.tf | 47 +++++ modules/tgw/spoke/providers.tf | 61 +----- modules/vpc-flow-logs-bucket/README.md | 2 - modules/vpc-flow-logs-bucket/providers.tf | 20 +- modules/vpc-peering/README.md | 2 - modules/vpc-peering/provider-accepter.tf | 9 + modules/vpc-peering/providers.tf | 31 +-- modules/vpc/README.md | 2 - modules/vpc/providers.tf | 20 +- modules/waf/README.md | 2 - modules/waf/providers.tf | 20 +- modules/zscaler/README.md | 2 - modules/zscaler/providers.tf | 20 +- rootfs/usr/local/bin/aws-config | 15 +- 320 files changed, 1865 insertions(+), 2717 deletions(-) rename {modules => deprecated}/sso/README.md (100%) rename {modules => deprecated}/sso/context.tf (100%) rename {modules => deprecated}/sso/main.tf (100%) rename {modules => deprecated}/sso/modules/okta-user/context.tf (100%) rename {modules => deprecated}/sso/modules/okta-user/main.tf (100%) rename {modules => deprecated}/sso/modules/okta-user/outputs.tf (100%) rename {modules => deprecated}/sso/modules/okta-user/variables.tf (100%) rename {modules => deprecated}/sso/outputs.tf (100%) rename {modules => deprecated}/sso/providers.tf (100%) rename {modules => deprecated}/sso/variables.tf (100%) rename {modules => deprecated}/sso/versions.tf (100%) create mode 100644 mixins/provider-awsutils.mixin.tf create mode 100644 mixins/providers.depth-1.tf create mode 100644 mixins/providers.depth-2.tf delete mode 100644 mixins/providers.tf create mode 100644 modules/account-map/dynamic-roles.tf create mode 100644 modules/account-map/modules/iam-roles/providers.tf create mode 100644 modules/account-map/modules/iam-roles/versions.tf create mode 100644 modules/argocd-repo/provider-github.tf create mode 100644 modules/aws-config/provider-awsutils.mixin.tf create mode 100644 modules/aws-sso/additional-permission-sets.tf delete mode 100644 modules/aws-teams/policy-support.tf create mode 100644 modules/datadog-configuration/provider-datadog.tf create mode 100644 modules/dns-delegated/providers-dns-primary.tf create mode 100644 modules/ec2-client-vpn/provider-awsutils.mixin.tf create mode 100644 modules/eks/argocd/provider-secrets.tf create mode 100644 modules/opsgenie-team/provider-opsgenie.tf create mode 100644 modules/ses/provider-awsutils.mixin.tf create mode 100644 modules/sftp/provider-awsutils.mixin.tf create mode 100644 modules/snowflake-account/provider-snowflake.tf create mode 100644 modules/snowflake-database/provider-snowflake.tf create mode 100644 modules/spa-s3-cloudfront/provider-other-regions.tf create mode 100644 modules/spacelift/worker-pool/provider-spacelift.tf delete mode 100644 modules/sso/default.auto.tfvars create mode 100644 modules/strongdm/provider-strongdm.tf create mode 100644 modules/tgw/spoke/provider-hub.tf create mode 100644 modules/vpc-peering/provider-accepter.tf diff --git a/deprecated/spacelift/README.md b/deprecated/spacelift/README.md index e3f033710..993d9e337 100644 --- a/deprecated/spacelift/README.md +++ b/deprecated/spacelift/README.md @@ -400,8 +400,6 @@ cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-cur | [git\_commit\_sha](#input\_git\_commit\_sha) | The commit SHA for which to trigger a run. Requires `var.spacelift_run_enabled` to be set to `true` | `string` | `null` | no | | [git\_repository](#input\_git\_repository) | The Git repository name | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [infracost\_enabled](#input\_infracost\_enabled) | Flag to enable/disable infracost. If this is enabled, it will add infracost label to each stack. See [spacelift infracost](https://docs.spacelift.io/vendors/terraform/infracost) docs for more details. | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/deprecated/spacelift/providers.tf b/deprecated/spacelift/providers.tf index 08ee01b2a..54257fd20 100644 --- a/deprecated/spacelift/providers.tf +++ b/deprecated/spacelift/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/sso/README.md b/deprecated/sso/README.md similarity index 100% rename from modules/sso/README.md rename to deprecated/sso/README.md diff --git a/modules/sso/context.tf b/deprecated/sso/context.tf similarity index 100% rename from modules/sso/context.tf rename to deprecated/sso/context.tf diff --git a/modules/sso/main.tf b/deprecated/sso/main.tf similarity index 100% rename from modules/sso/main.tf rename to deprecated/sso/main.tf diff --git a/modules/sso/modules/okta-user/context.tf b/deprecated/sso/modules/okta-user/context.tf similarity index 100% rename from modules/sso/modules/okta-user/context.tf rename to deprecated/sso/modules/okta-user/context.tf diff --git a/modules/sso/modules/okta-user/main.tf b/deprecated/sso/modules/okta-user/main.tf similarity index 100% rename from modules/sso/modules/okta-user/main.tf rename to deprecated/sso/modules/okta-user/main.tf diff --git a/modules/sso/modules/okta-user/outputs.tf b/deprecated/sso/modules/okta-user/outputs.tf similarity index 100% rename from modules/sso/modules/okta-user/outputs.tf rename to deprecated/sso/modules/okta-user/outputs.tf diff --git a/modules/sso/modules/okta-user/variables.tf b/deprecated/sso/modules/okta-user/variables.tf similarity index 100% rename from modules/sso/modules/okta-user/variables.tf rename to deprecated/sso/modules/okta-user/variables.tf diff --git a/modules/sso/outputs.tf b/deprecated/sso/outputs.tf similarity index 100% rename from modules/sso/outputs.tf rename to deprecated/sso/outputs.tf diff --git a/modules/sso/providers.tf b/deprecated/sso/providers.tf similarity index 100% rename from modules/sso/providers.tf rename to deprecated/sso/providers.tf diff --git a/modules/sso/variables.tf b/deprecated/sso/variables.tf similarity index 100% rename from modules/sso/variables.tf rename to deprecated/sso/variables.tf diff --git a/modules/sso/versions.tf b/deprecated/sso/versions.tf similarity index 100% rename from modules/sso/versions.tf rename to deprecated/sso/versions.tf diff --git a/mixins/README.md b/mixins/README.md index b09a0f5a5..04e20811d 100644 --- a/mixins/README.md +++ b/mixins/README.md @@ -34,6 +34,11 @@ configuration, specifying which component the resources belong to. It's important to note that all modules and resources within the component then need to use `module.introspection.context` and `module.introspection.tags`, respectively, rather than `module.this.context` and `module.this.tags`. +## Mixin: `provider-awsutils.mixin.tf` + +This mixin is meant to be added to a terraform module that wants to use the awsutils provider. +It assumes the standard `providers.tf` file is present in the module. + ## Mixin: `sops.mixin.tf` This mixin is meant to be added to Terraform EKS components which are used in a cluster where sops-secrets-operator (see: https://github.com/isindir/sops-secrets-operator) diff --git a/mixins/provider-awsutils.mixin.tf b/mixins/provider-awsutils.mixin.tf new file mode 100644 index 000000000..9df2a64a0 --- /dev/null +++ b/mixins/provider-awsutils.mixin.tf @@ -0,0 +1,21 @@ +# <-- BEGIN DOC --> +# +# This mixin is meant to be added to a terraform module that wants to use the awsutils provider. +# It assumes the standard `providers.tf` file is present in the module. +# +# <-- END DOC --> + +provider "awsutils" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} diff --git a/mixins/provider-helm.tf b/mixins/provider-helm.tf index abe6f9c56..64459d4f4 100644 --- a/mixins/provider-helm.tf +++ b/mixins/provider-helm.tf @@ -101,7 +101,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/mixins/providers.depth-1.tf b/mixins/providers.depth-1.tf new file mode 100644 index 000000000..54257fd20 --- /dev/null +++ b/mixins/providers.depth-1.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/mixins/providers.depth-2.tf b/mixins/providers.depth-2.tf new file mode 100644 index 000000000..45d458575 --- /dev/null +++ b/mixins/providers.depth-2.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/mixins/providers.tf b/mixins/providers.tf deleted file mode 100644 index 08ee01b2a..000000000 --- a/mixins/providers.tf +++ /dev/null @@ -1,29 +0,0 @@ -provider "aws" { - region = var.region - - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -module "iam_roles" { - source = "../account-map/modules/iam-roles" - context = module.this.context -} - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/account-map/README.md b/modules/account-map/README.md index a90f26e03..26ddf9d91 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -52,9 +52,10 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [terraform](#requirement\_terraform) | >= 1.2.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [local](#requirement\_local) | >= 1.3 | +| [utils](#requirement\_utils) | >= 1.8.0 | ## Providers @@ -62,12 +63,14 @@ components: |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | | [local](#provider\_local) | >= 1.3 | +| [utils](#provider\_utils) | >= 1.8.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [accounts](#module\_accounts) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [accounts](#module\_accounts) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [atmos](#module\_atmos) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -77,6 +80,8 @@ components: | [local_file.account_info](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource | | [aws_organizations_organization.organization](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/organizations_organization) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [utils_describe_stacks.team_roles](https://registry.terraform.io/providers/cloudposse/utils/latest/docs/data-sources/describe_stacks) | data source | +| [utils_describe_stacks.teams](https://registry.terraform.io/providers/cloudposse/utils/latest/docs/data-sources/describe_stacks) | data source | ## Inputs @@ -112,6 +117,8 @@ components: | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [terraform\_dynamic\_role\_enabled](#input\_terraform\_dynamic\_role\_enabled) | If true, the IAM role Terraform will assume will depend on the identity of the user running terraform | `bool` | `false` | no | +| [terraform\_role\_name\_map](#input\_terraform\_role\_name\_map) | Mapping of Terraform action (plan or apply) to aws-team-role name to assume for that action | `map(string)` |
{
"apply": "terraform",
"plan": "planner"
}
| no | ## Outputs @@ -122,13 +129,9 @@ components: | [artifacts\_account\_account\_name](#output\_artifacts\_account\_account\_name) | The short name for the artifacts account | | [audit\_account\_account\_name](#output\_audit\_account\_account\_name) | The short name for the audit account | | [aws\_partition](#output\_aws\_partition) | The AWS "partition" to use when constructing resource ARNs | -| [cicd\_profiles](#output\_cicd\_profiles) | A list of all SSO profiles used by cicd platforms | -| [cicd\_roles](#output\_cicd\_roles) | A list of all IAM roles used by cicd platforms | | [dns\_account\_account\_name](#output\_dns\_account\_account\_name) | The short name for the primary DNS account | | [eks\_accounts](#output\_eks\_accounts) | A list of all accounts in the AWS Organization that contain EKS clusters | | [full\_account\_map](#output\_full\_account\_map) | The map of account name to account ID (number). | -| [helm\_profiles](#output\_helm\_profiles) | A list of all SSO profiles used to run helm updates | -| [helm\_roles](#output\_helm\_roles) | A list of all IAM roles used to run helm updates | | [iam\_role\_arn\_templates](#output\_iam\_role\_arn\_templates) | Map of accounts to corresponding IAM Role ARN templates | | [identity\_account\_account\_name](#output\_identity\_account\_account\_name) | The short name for the account holding primary IAM roles | | [non\_eks\_accounts](#output\_non\_eks\_accounts) | A list of all accounts in the AWS Organization that do not contain EKS clusters | @@ -136,7 +139,10 @@ components: | [profiles\_enabled](#output\_profiles\_enabled) | Whether or not to enable profiles instead of roles for the backend | | [root\_account\_account\_name](#output\_root\_account\_account\_name) | The short name for the root account | | [root\_account\_aws\_name](#output\_root\_account\_aws\_name) | The name of the root account as reported by AWS | +| [terraform\_access\_map](#output\_terraform\_access\_map) | Mapping of team Role ARN to map of account name to terraform action role ARN to assume | +| [terraform\_dynamic\_role\_enabled](#output\_terraform\_dynamic\_role\_enabled) | True if dynamic role for Terraform is enabled | | [terraform\_profiles](#output\_terraform\_profiles) | A list of all SSO profiles used to run terraform updates | +| [terraform\_role\_name\_map](#output\_terraform\_role\_name\_map) | Mapping of Terraform action (plan or apply) to aws-team-role name to assume for that action | | [terraform\_roles](#output\_terraform\_roles) | A list of all IAM roles used to run terraform updates | diff --git a/modules/account-map/account-info.tftmpl b/modules/account-map/account-info.tftmpl index c0b8ce87b..2bc82fe76 100644 --- a/modules/account-map/account-info.tftmpl +++ b/modules/account-map/account-info.tftmpl @@ -68,10 +68,11 @@ function _account-roles() { printf "%s\n" "$${!account_roles[@]}" | sort } function account-roles() { - printf "$${CONFIG_NAMESPACE:+$${CONFIG_NAMESPACE}-}%s\n" $(_account-roles) + for role in $(_account-roles); do + printf "$${CONFIG_NAMESPACE:+$${CONFIG_NAMESPACE}: }%s -> $${CONFIG_NAMESPACE:+$${CONFIG_NAMESPACE}-}%s\n" $role "$${account_roles[$role]}" + done } - ########### non-template helpers ########### functions+=("account-profile") diff --git a/modules/account-map/dynamic-roles.tf b/modules/account-map/dynamic-roles.tf new file mode 100644 index 000000000..48f1e4545 --- /dev/null +++ b/modules/account-map/dynamic-roles.tf @@ -0,0 +1,95 @@ + +data "utils_describe_stacks" "teams" { + count = local.dynamic_role_enabled ? 1 : 0 + + components = ["aws-teams"] + component_types = ["terraform"] + sections = ["vars"] +} + +data "utils_describe_stacks" "team_roles" { + count = local.dynamic_role_enabled ? 1 : 0 + + components = ["aws-team-roles"] + component_types = ["terraform"] + sections = ["vars"] +} + +locals { + dynamic_role_enabled = module.this.enabled && var.terraform_dynamic_role_enabled + + apply_role = var.terraform_role_name_map.apply + plan_role = var.terraform_role_name_map.plan + + # zero-based index showing position of the namespace in the stack name + stack_namespace_index = try(index(module.this.normalized_context.descriptor_formats.stack.labels, "namespace"), -1) + stack_has_namespace = local.stack_namespace_index >= 0 + stack_account_map = { for k, v in module.atmos : k => lookup(v.descriptors, "account_name", v.stage) } + + # We would like to use code like this: + # teams_stacks = local.dynamic_role_enabled ? { for k, v ... } : {} + # but that generates an error: "Inconsistent conditional result types" + # See https://github.com/hashicorp/terraform/issues/33303 + # To work around this, we have "empty" values that depend on the condition. + empty_map = { + true = null + false = {} + } + empty = local.empty_map[local.dynamic_role_enabled] + + # ASSUMPTIONS: The stack pattern is the same for all accounts and uses the same delimiter as null-label + teams_stacks = local.dynamic_role_enabled ? { + for k, v in yamldecode(data.utils_describe_stacks.teams[0].output) : k => v if !local.stack_has_namespace || try(split(module.this.delimiter, k)[local.stack_namespace_index] == module.this.namespace, false) + } : local.empty + + teams_vars = { for k, v in local.teams_stacks : k => v.components.terraform.aws-teams.vars } + teams_config = local.dynamic_role_enabled ? values(local.teams_vars)[0].teams_config : local.empty + team_names = [for k, v in local.teams_config : k if try(v.enabled, true)] + team_arns = { for team_name in local.team_names : team_name => format(local.iam_role_arn_templates[local.account_role_map.identity], team_name) } + + team_roles_stacks = local.dynamic_role_enabled ? { + for k, v in yamldecode(data.utils_describe_stacks.team_roles[0].output) : k => v if !local.stack_has_namespace || try(split(module.this.delimiter, k)[local.stack_namespace_index] == module.this.namespace, false) + } : local.empty + + team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform.aws-team-roles.vars } + + stack_planners = { for k, v in local.team_roles_vars : k => v.roles[local.plan_role].trusted_teams if try(length(v.roles[local.plan_role].trusted_teams), 0) > 0 && try(v.roles[local.plan_role].enabled, true) } + stack_terraformers = { for k, v in local.team_roles_vars : k => v.roles[local.apply_role].trusted_teams if try(length(v.roles[local.apply_role].trusted_teams), 0) > 0 && try(v.roles[local.apply_role].enabled, true) } + + all_team_vars = merge(local.teams_vars, local.team_roles_vars) + + team_planners = { for team in local.team_names : team => { + for stack, trusted in local.stack_planners : local.stack_account_map[stack] => "plan" if contains(trusted, team) + } } + team_terraformers = { for team in local.team_names : team => { + for stack, trusted in local.stack_terraformers : local.stack_account_map[stack] => "apply" if contains(trusted, team) + } } + + role_arn_terraform_access = { for team in local.team_names : local.team_arns[team] => merge(local.team_planners[team], local.team_terraformers[team]) } +} + +module "atmos" { + # local.all_team_vars is empty map when dynamic_role_enabled is false + for_each = local.all_team_vars + + source = "cloudposse/label/null" + version = "0.25.0" + + enabled = true + namespace = lookup(each.value, "namespace", null) + tenant = lookup(each.value, "tenant", null) + environment = lookup(each.value, "environment", null) + stage = lookup(each.value, "stage", null) + name = lookup(each.value, "name", null) + delimiter = lookup(each.value, "delimiter", null) + attributes = lookup(each.value, "attributes", []) + tags = lookup(each.value, "tags", {}) + additional_tag_map = lookup(each.value, "additional_tag_map", {}) + label_order = lookup(each.value, "label_order", []) + regex_replace_chars = lookup(each.value, "regex_replace_chars", null) + id_length_limit = lookup(each.value, "id_length_limit", null) + label_key_case = lookup(each.value, "label_key_case", null) + label_value_case = lookup(each.value, "label_value_case", null) + descriptor_formats = lookup(each.value, "descriptor_formats", {}) + labels_as_tags = lookup(each.value, "labels_as_tags", []) +} diff --git a/modules/account-map/main.tf b/modules/account-map/main.tf index a28e8a4b0..e465a3e88 100644 --- a/modules/account-map/main.tf +++ b/modules/account-map/main.tf @@ -57,15 +57,10 @@ locals { terraform_roles = { - for name, info in local.account_info_map : name => - format(local.iam_role_arn_templates[name], - (contains([ - var.root_account_account_name, - var.identity_account_account_name - ], name) ? "admin" : "terraform") - ) + for name, info in local.account_info_map : name => format(local.iam_role_arn_templates[name], "terraform") } + // legacy support for `aws` config profiles, where root and identity accounts' terraform profiles are not for Terraforming terraform_profiles = { for name, info in local.account_info_map : name => format(var.profile_template, compact( [ @@ -80,53 +75,4 @@ locals { ] )...) } - - helm_roles = { - for name, info in local.account_info_map : name => - format(local.iam_role_arn_templates[name], - (contains([ - var.root_account_account_name, - var.identity_account_account_name - ], name) ? "admin" : "helm") - ) - - } - - helm_profiles = { - for name, info in local.account_info_map : name => format(var.profile_template, compact( - [ - module.this.namespace, - lookup(info, "tenant", ""), - module.this.environment, - info.stage, - (contains([ - var.root_account_account_name, - var.identity_account_account_name - ], name) ? "admin" : "helm") - ] - )...) - } - - cicd_roles = { - for name, info in local.account_info_map : name => - format(local.iam_role_arn_templates[name], - (contains([ - var.root_account_account_name - ], name) ? "admin" : "cicd") - ) - } - - cicd_profiles = { - for name, info in local.account_info_map : name => format(var.profile_template, compact( - [ - module.this.namespace, - lookup(info, "tenant", ""), - var.global_environment_name, - info.stage, - (contains([ - var.root_account_account_name - ], name) ? "admin" : "cicd") - ] - )...) - } } diff --git a/modules/account-map/modules/iam-roles/main.tf b/modules/account-map/modules/iam-roles/main.tf index e9a95553f..45278172a 100644 --- a/modules/account-map/modules/iam-roles/main.tf +++ b/modules/account-map/modules/iam-roles/main.tf @@ -1,3 +1,9 @@ + +data "awsutils_caller_identity" "current" { + # Avoid conflict with caller's provider which is using this module's output to assume a role. + provider = awsutils.iam-roles +} + module "always" { source = "cloudposse/label/null" version = "0.25.0" @@ -10,18 +16,56 @@ module "always" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "account-map" privileged = var.privileged - tenant = var.global_tenant_name - environment = var.global_environment_name - stage = var.global_stage_name + tenant = var.overridable_global_tenant_name + environment = var.overridable_global_environment_name + stage = var.overridable_global_stage_name context = module.always.context } locals { - account_name = lookup(module.always.descriptors, "account_name", module.always.stage) - profiles_enabled = module.account_map.outputs.profiles_enabled + profiles_enabled = local.account_map.profiles_enabled + + account_map = module.account_map.outputs + account_name = lookup(module.always.descriptors, "account_name", module.always.stage) + account_org_role_arn = local.account_name == local.account_map.root_account_account_name ? null : format( + "arn:%s:iam::%s:role/OrganizationAccountAccessRole", local.account_map.aws_partition, + local.account_map.full_account_map[local.account_name] + ) + + dynamic_terraform_role_enabled = try(local.account_map.terraform_dynamic_role_enabled, false) + + static_terraform_role = local.account_map.terraform_roles[local.account_name] + dynamic_terraform_role = try(local.dynamic_terraform_role_map[local.dynamic_terraform_role_type], null) + + current_user_role_arn = coalesce(data.awsutils_caller_identity.current.eks_role_arn, data.awsutils_caller_identity.current.arn) + dynamic_terraform_role_type = try(local.account_map.terraform_access_map[local.current_user_role_arn][local.account_name], "none") + + current_identity_account = split(":", local.current_user_role_arn)[4] + is_root_user = local.current_identity_account == local.account_map.full_account_map[local.account_map.root_account_account_name] + is_target_user = local.current_identity_account == local.account_map.full_account_map[local.account_name] + + dynamic_terraform_role_map = local.dynamic_terraform_role_enabled ? { + apply = format(local.account_map.iam_role_arn_templates[local.account_name], local.account_map.terraform_role_name_map["apply"]) + plan = format(local.account_map.iam_role_arn_templates[local.account_name], local.account_map.terraform_role_name_map["plan"]) + # For user without explicit permissions: + # If the current user is a user in the `root` account, assume the `OrganizationAccountAccessRole` role in the target account. + # If the current user is a user in the target account, do not assume a role at all, let them do what their role allows. + # Otherwise, force them into the static Terraform role for the target account, + # to prevent users from accidentally running Terraform in the wrong account. + none = local.is_root_user ? local.account_org_role_arn : ( + # null means use current user's role + local.is_target_user ? null : local.static_terraform_role + ) + } : {} + + final_terraform_role_arn = local.profiles_enabled ? null : ( + local.dynamic_terraform_role_enabled ? local.dynamic_terraform_role : local.static_terraform_role + ) + + final_terraform_profile_name = local.profiles_enabled ? local.account_map.profiles[local.account_name] : null } diff --git a/modules/account-map/modules/iam-roles/outputs.tf b/modules/account-map/modules/iam-roles/outputs.tf index 252b6a3e2..380f4d89a 100644 --- a/modules/account-map/modules/iam-roles/outputs.tf +++ b/modules/account-map/modules/iam-roles/outputs.tf @@ -1,28 +1,25 @@ output "terraform_role_arn" { - value = local.profiles_enabled ? null : module.account_map.outputs.terraform_roles[local.account_name] + value = local.profiles_enabled ? null : local.final_terraform_role_arn description = "The AWS Role ARN for Terraform to use when provisioning resources in the account, when Role ARNs are in use" } output "terraform_role_arns" { - value = module.account_map.outputs.terraform_roles + value = local.account_map.terraform_roles description = "All of the terraform role arns" } output "terraform_profile_name" { - value = local.profiles_enabled ? module.account_map.outputs.terraform_profiles[local.account_name] : null + value = local.profiles_enabled ? local.final_terraform_profile_name : null description = "The AWS config profile name for Terraform to use when provisioning resources in the account, when profiles are in use" } output "aws_partition" { - value = module.account_map.outputs.aws_partition + value = local.account_map.aws_partition description = "The AWS \"partition\" to use when constructing resource ARNs" } output "org_role_arn" { - value = local.account_name == module.account_map.outputs.root_account_account_name ? null : format( - "arn:%s:iam::%s:role/OrganizationAccountAccessRole", module.account_map.outputs.aws_partition, - module.account_map.outputs.full_account_map[local.account_name] - ) + value = local.account_org_role_arn description = "The AWS Role ARN for Terraform to use when SuperAdmin is provisioning resources in the account" } @@ -50,50 +47,40 @@ output "current_account_account_name" { } output "dns_terraform_role_arn" { - value = local.profiles_enabled ? null : module.account_map.outputs.terraform_roles[module.account_map.outputs.dns_account_account_name] + value = local.profiles_enabled ? null : local.account_map.terraform_roles[local.account_map.dns_account_account_name] description = "The AWS Role ARN for Terraform to use to provision DNS Zone delegations, when Role ARNs are in use" } output "dns_terraform_profile_name" { - value = local.profiles_enabled ? module.account_map.outputs.terraform_profiles[module.account_map.outputs.dns_account_account_name] : null + value = local.profiles_enabled ? local.account_map.terraform_profiles[local.account_map.dns_account_account_name] : null description = "The AWS config profile name for Terraform to use to provision DNS Zone delegations, when profiles are in use" } output "audit_terraform_role_arn" { - value = local.profiles_enabled ? null : module.account_map.outputs.terraform_roles[module.account_map.outputs.audit_account_account_name] + value = local.profiles_enabled ? null : local.account_map.terraform_roles[local.account_map.audit_account_account_name] description = "The AWS Role ARN for Terraform to use to provision resources in the \"audit\" role account, when Role ARNs are in use" } output "audit_terraform_profile_name" { - value = local.profiles_enabled ? module.account_map.outputs.terraform_profiles[module.account_map.outputs.audit_account_account_name] : null + value = local.profiles_enabled ? local.account_map.terraform_profiles[local.account_map.audit_account_account_name] : null description = "The AWS config profile name for Terraform to use to provision resources in the \"audit\" role account, when profiles are in use" } output "identity_account_account_name" { - value = module.account_map.outputs.identity_account_account_name + value = local.account_map.identity_account_account_name description = "The account name (usually `-`) for the account holding primary IAM roles" } output "identity_terraform_role_arn" { - value = local.profiles_enabled ? null : module.account_map.outputs.terraform_roles[module.account_map.outputs.identity_account_account_name] + value = local.profiles_enabled ? null : local.account_map.terraform_roles[local.account_map.identity_account_account_name] description = "The AWS Role ARN for Terraform to use to provision resources in the \"identity\" role account, when Role ARNs are in use" } output "identity_terraform_profile_name" { - value = local.profiles_enabled ? module.account_map.outputs.terraform_profiles[module.account_map.outputs.identity_account_account_name] : null + value = local.profiles_enabled ? local.account_map.terraform_profiles[local.account_map.identity_account_account_name] : null description = "The AWS config profile name for Terraform to use to provision resources in the \"identity\" role account, when profiles are in use" } -output "identity_cicd_role_arn" { - value = local.profiles_enabled ? null : module.account_map.outputs.cicd_roles[module.account_map.outputs.identity_account_account_name] - description = "(Deprecated) The AWS Role ARN for CI/CD tools to assume to gain access to other accounts, when Role ARNs are in use" -} - -output "identity_cicd_profile_name" { - value = local.profiles_enabled ? module.account_map.outputs.cicd_profiles[module.account_map.outputs.identity_account_account_name] : null - description = "(Deprecated) The AWS config profile name for CI/CD tools to assume to gain access to other accounts, when profiles are in use" -} - output "profiles_enabled" { value = local.profiles_enabled description = "When true, use AWS config profiles in Terraform AWS provider configurations. When false, use Role ARNs." diff --git a/modules/account-map/modules/iam-roles/providers.tf b/modules/account-map/modules/iam-roles/providers.tf new file mode 100644 index 000000000..369f99cdd --- /dev/null +++ b/modules/account-map/modules/iam-roles/providers.tf @@ -0,0 +1,11 @@ +provider "awsutils" { + # Components may want to use awsutils, and when they do, they typically want to use it in the assumed IAM role. + # That conflicts with this module's needs, so we create a separate provider alias for this module to use. + alias = "iam-roles" + + # If the provider block is empty, Terraform will output a deprecation warning, + # because earlier versions of Terraform used empty provider blocks to decalare provider requirements, + # which is now deprecated in favor of the required_providers block. + # So we add a useless setting to the provider block to avoid the deprecation warning. + profile = null +} diff --git a/modules/account-map/modules/iam-roles/versions.tf b/modules/account-map/modules/iam-roles/versions.tf new file mode 100644 index 000000000..e0cac65a2 --- /dev/null +++ b/modules/account-map/modules/iam-roles/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.2.0" + + required_providers { + awsutils = { + source = "cloudposse/awsutils" + version = ">= 0.16.0" + } + } +} diff --git a/modules/account-map/modules/roles-to-principals/README.md b/modules/account-map/modules/roles-to-principals/README.md index 82b128d8c..64d9b1419 100644 --- a/modules/account-map/modules/roles-to-principals/README.md +++ b/modules/account-map/modules/roles-to-principals/README.md @@ -7,10 +7,11 @@ other related tasks. ## Special Configuration Needed -In order to avoid having to pass customization information through every module +As with `iam-roles`, in order to avoid having to pass customization information through every module that uses this submodule, if the default configuration does not suit your needs, -you are expected to customize `variables.tf` with the defaults you want to -use in your project. For example, if you are including the `tenant` label -in the designation of your "root" account (your Organization Management Account), -then you should modify `variables.tf` so that `global_tenant_name` defaults -to the appropriate value. +you are expected to add `variables_override.tf` to override the variables with +the defaults you want to use in your project. For example, if you are not using +"core" as the `tenant` portion of your "root" account (your Organization Management Account), +then you should include the `variable "overridable_global_tenant_name"` declaration +in your `variables_override.tf` so that `overridable_global_tenant_name` defaults +to the value you are using (or the empty string if you are not using `tenant` at all). diff --git a/modules/account-map/modules/roles-to-principals/main.tf b/modules/account-map/modules/roles-to-principals/main.tf index 9addd704a..55ab7f1e6 100644 --- a/modules/account-map/modules/roles-to-principals/main.tf +++ b/modules/account-map/modules/roles-to-principals/main.tf @@ -10,13 +10,13 @@ module "always" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "account-map" privileged = var.privileged - tenant = var.global_tenant_name - environment = var.global_environment_name - stage = var.global_stage_name + tenant = var.overridable_global_tenant_name + environment = var.overridable_global_environment_name + stage = var.overridable_global_stage_name context = module.always.context } diff --git a/modules/account-map/modules/roles-to-principals/variables.tf b/modules/account-map/modules/roles-to-principals/variables.tf index eb0a06ce6..d1a82e9fc 100644 --- a/modules/account-map/modules/roles-to-principals/variables.tf +++ b/modules/account-map/modules/roles-to-principals/variables.tf @@ -15,20 +15,24 @@ variable "privileged" { default = false } -variable "global_tenant_name" { +## The overridable_* variables in this file provide Cloud Posse defaults. +## Because this module is used in bootstrapping Terraform, we do not configure +## these inputs in the normal way. Instead, to change the values, you should +## add a `variables_override.tf` file and change the default to the value you want. +variable "overridable_global_tenant_name" { type = string description = "The tenant name used for organization-wide resources" default = "core" } -variable "global_environment_name" { +variable "overridable_global_environment_name" { type = string description = "Global environment name" default = "gbl" } -variable "global_stage_name" { +variable "overridable_global_stage_name" { type = string - description = "The stage name for the organization management account (where the `accout-map` state is stored)" + description = "The stage name for the organization management account (where the `account-map` state is stored)" default = "root" } diff --git a/modules/account-map/modules/team-assume-role-policy/README.md b/modules/account-map/modules/team-assume-role-policy/README.md index e199cf58f..b2644e981 100644 --- a/modules/account-map/modules/team-assume-role-policy/README.md +++ b/modules/account-map/modules/team-assume-role-policy/README.md @@ -47,7 +47,7 @@ No requirements. |------|--------|---------| | [allowed\_role\_map](#module\_allowed\_role\_map) | ../../../account-map/modules/roles-to-principals | n/a | | [denied\_role\_map](#module\_denied\_role\_map) | ../../../account-map/modules/roles-to-principals | n/a | -| [github\_oidc\_provider](#module\_github\_oidc\_provider) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [github\_oidc\_provider](#module\_github\_oidc\_provider) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf b/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf index 03b2be3d6..4449e5a37 100644 --- a/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf +++ b/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf @@ -60,7 +60,7 @@ module "github_oidc_provider" { count = local.github_oidc_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "github-oidc-provider" environment = var.global_environment_name diff --git a/modules/account-map/modules/team-assume-role-policy/main.tf b/modules/account-map/modules/team-assume-role-policy/main.tf index 6877cb30b..40b2195ae 100644 --- a/modules/account-map/modules/team-assume-role-policy/main.tf +++ b/modules/account-map/modules/team-assume-role-policy/main.tf @@ -1,21 +1,23 @@ locals { enabled = module.this.enabled - allowed_principals = sort(distinct(concat(var.allowed_principal_arns, module.allowed_role_map.principals, module.allowed_role_map.permission_set_arn_like))) + allowed_roles = concat(module.allowed_role_map.principals, module.allowed_role_map.permission_set_arn_like) + allowed_principals = sort(var.allowed_principal_arns) allowed_account_names = compact(concat( [for k, v in var.allowed_roles : k if length(v) > 0], [for k, v in var.allowed_permission_sets : k if length(v) > 0] )) - allowed_mapped_accounts = [for acct in local.allowed_account_names : module.allowed_role_map.full_account_map[acct]] - allowed_arn_accounts = data.aws_arn.allowed[*].account - allowed_accounts = sort(distinct(concat(local.allowed_mapped_accounts, local.allowed_arn_accounts))) + allowed_mapped_accounts = [for acct in local.allowed_account_names : module.allowed_role_map.full_account_map[acct]] + allowed_principals_accounts = data.aws_arn.allowed[*].account + # allowed_accounts = sort(distinct(concat(local.allowed_mapped_accounts, local.allowed_arn_accounts))) denied_principals = sort(distinct(concat(var.denied_principal_arns, module.denied_role_map.principals, module.denied_role_map.permission_set_arn_like))) denied_mapped_accounts = [for acct in concat(keys(var.denied_roles), keys(var.denied_permission_sets)) : module.denied_role_map.full_account_map[acct]] denied_arn_accounts = data.aws_arn.denied[*].account denied_accounts = sort(distinct(concat(local.denied_mapped_accounts, local.denied_arn_accounts))) - assume_role_enabled = (length(local.allowed_accounts) + length(local.denied_accounts)) > 0 + undenied_principals = sort(tolist(setsubtract(toset(local.allowed_principals), toset(local.denied_principals)))) + assume_role_enabled = (length(local.allowed_mapped_accounts) + length(local.allowed_principals_accounts) + length(local.denied_accounts)) > 0 aws_partition = module.allowed_role_map.aws_partition } @@ -57,7 +59,7 @@ data "aws_iam_policy_document" "assume_role" { count = local.enabled && local.assume_role_enabled ? 1 : 0 dynamic "statement" { - for_each = length(local.allowed_accounts) > 0 ? ["accounts"] : [] + for_each = length(local.allowed_mapped_accounts) > 0 && length(local.allowed_roles) > 0 ? ["accounts-roles"] : [] content { sid = "RoleAssumeRole" @@ -76,21 +78,46 @@ data "aws_iam_policy_document" "assume_role" { condition { test = "ArnLike" variable = "aws:PrincipalArn" - values = local.allowed_principals + values = local.allowed_roles } principals { type = "AWS" # Principals is a required field, so we allow any principal in any of the accounts, restricted by the assumed Role ARN in the condition clauses. # This allows us to allow non-existent (yet to be created) roles, which would not be allowed if directly specified in `principals`. - identifiers = formatlist("arn:${local.aws_partition}:iam::%s:root", local.allowed_accounts) + identifiers = formatlist("arn:${local.aws_partition}:iam::%s:root", local.allowed_mapped_accounts) } } } + dynamic "statement" { + for_each = length(local.allowed_principals_accounts) > 0 && length(local.allowed_principals) > 0 ? ["accounts-principals"] : [] + + content { + sid = "PrincipalAssumeRole" + + effect = "Allow" + actions = [ + "sts:AssumeRole", + "sts:TagSession", + ] + + condition { + test = "ArnLike" + variable = "aws:PrincipalArn" + values = local.allowed_principals + } + + principals { + type = "AWS" + # Principals is a required field, so we allow any principal in any of the accounts, restricted by the assumed Role ARN in the condition clauses. + # This allows us to allow non-existent (yet to be created) roles, which would not be allowed if directly specified in `principals`. + identifiers = formatlist("arn:${local.aws_partition}:iam::%s:root", local.allowed_principals_accounts) + } + } + } # As a safety measure, we do not allow AWS Users (not Roles) to assume the SAML Teams or Team roles - # unless `deny_all_iam_users` is explicitly set to `false`. - # In particular, this prevents SuperAdmin from running Terraform on components that should be handled by Spacelift. + # unless `deny_all_iam_users` is explicitly set to `false` or the user is explicitly allowed. statement { sid = "RoleDenyAssumeRole" @@ -103,7 +130,19 @@ data "aws_iam_policy_document" "assume_role" { condition { test = "ArnLike" variable = "aws:PrincipalArn" - values = compact(concat(local.denied_principals, var.iam_users_enabled ? [] : ["arn:${local.aws_partition}:iam::*:user/*"])) + values = compact(concat(local.denied_principals, var.iam_users_enabled ? [] : [ + "arn:${local.aws_partition}:iam::*:user/*" + ])) + } + + dynamic "condition" { + for_each = length(local.undenied_principals) > 0 ? ["exceptions"] : [] + + content { + test = "ArnNotEquals" + variable = "aws:PrincipalArn" + values = local.undenied_principals + } } principals { @@ -112,7 +151,7 @@ data "aws_iam_policy_document" "assume_role" { # Principals is a required field, so we allow any principal in any of the accounts, restricted by the assumed Role ARN in the condition clauses. # This allows us to allow non-existent (yet to be created) roles, which would not be allowed if directly specified in `principals`. # We also deny all directly logged-in users from all the enabled accounts. - identifiers = formatlist("arn:${local.aws_partition}:iam::%s:root", sort(distinct(concat(local.denied_accounts, local.allowed_accounts)))) + identifiers = formatlist("arn:${local.aws_partition}:iam::%s:root", sort(distinct(concat(local.denied_accounts, local.allowed_mapped_accounts, local.allowed_principals_accounts)))) } } } diff --git a/modules/account-map/outputs.tf b/modules/account-map/outputs.tf index 3cc8ad634..3604a2e45 100644 --- a/modules/account-map/outputs.tf +++ b/modules/account-map/outputs.tf @@ -81,29 +81,28 @@ output "terraform_profiles" { description = "A list of all SSO profiles used to run terraform updates" } -output "helm_roles" { - value = local.helm_roles - description = "A list of all IAM roles used to run helm updates" -} - -output "helm_profiles" { - value = local.helm_profiles - description = "A list of all SSO profiles used to run helm updates" +output "profiles_enabled" { + value = var.profiles_enabled + description = "Whether or not to enable profiles instead of roles for the backend" } -output "cicd_roles" { - value = local.cicd_roles - description = "A list of all IAM roles used by cicd platforms" +output "terraform_dynamic_role_enabled" { + value = local.dynamic_role_enabled + description = "True if dynamic role for Terraform is enabled" + precondition { + condition = local.dynamic_role_enabled && var.profiles_enabled ? false : true + error_message = "Dynamic role for Terraform cannot be used with profiles. One of `terraform_dynamic_role_enabled` or `profiles_enabled` must be false." + } } -output "cicd_profiles" { - value = local.cicd_profiles - description = "A list of all SSO profiles used by cicd platforms" +output "terraform_access_map" { + value = local.dynamic_role_enabled ? local.role_arn_terraform_access : null + description = "Mapping of team Role ARN to map of account name to terraform action role ARN to assume" } -output "profiles_enabled" { - value = var.profiles_enabled - description = "Whether or not to enable profiles instead of roles for the backend" +output "terraform_role_name_map" { + value = local.dynamic_role_enabled ? var.terraform_role_name_map : null + description = "Mapping of Terraform action (plan or apply) to aws-team-role name to assume for that action" } resource "local_file" "account_info" { diff --git a/modules/account-map/remote-state.tf b/modules/account-map/remote-state.tf index 1112153a5..61856c5ec 100644 --- a/modules/account-map/remote-state.tf +++ b/modules/account-map/remote-state.tf @@ -1,6 +1,6 @@ module "accounts" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "account" privileged = true diff --git a/modules/account-map/variables.tf b/modules/account-map/variables.tf index d325b3de2..7a6768cdb 100644 --- a/modules/account-map/variables.tf +++ b/modules/account-map/variables.tf @@ -83,3 +83,18 @@ variable "aws_config_identity_profile_name" { default = null description = "The AWS config profile name to use as `source_profile` for credentials." } + +variable "terraform_role_name_map" { + type = map(string) + description = "Mapping of Terraform action (plan or apply) to aws-team-role name to assume for that action" + default = { + plan = "planner" + apply = "terraform" + } +} + +variable "terraform_dynamic_role_enabled" { + type = bool + description = "If true, the IAM role Terraform will assume will depend on the identity of the user running terraform" + default = false +} diff --git a/modules/account-map/versions.tf b/modules/account-map/versions.tf index 2fdade250..cf42bf06e 100644 --- a/modules/account-map/versions.tf +++ b/modules/account-map/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.2.0" required_providers { aws = { @@ -10,5 +10,9 @@ terraform { source = "hashicorp/local" version = ">= 1.3" } + utils = { + source = "cloudposse/utils" + version = ">= 1.8.0" + } } } diff --git a/modules/account-quotas/README.md b/modules/account-quotas/README.md index 6777f07b1..59a6e7831 100644 --- a/modules/account-quotas/README.md +++ b/modules/account-quotas/README.md @@ -92,8 +92,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/account-quotas/providers.tf b/modules/account-quotas/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/account-quotas/providers.tf +++ b/modules/account-quotas/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index 4ab36a608..3825fc5b3 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -95,8 +95,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/account-settings/providers.tf b/modules/account-settings/providers.tf index c4f45ca75..54257fd20 100644 --- a/modules/account-settings/providers.tf +++ b/modules/account-settings/providers.tf @@ -1,30 +1,19 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = var.import_role_arn == null ? (module.iam_roles.org_role_arn != null ? [true] : []) : ["import"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.org_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } module "iam_roles" { - source = "../account-map/modules/iam-roles" - privileged = true - context = module.this.context -} - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" + source = "../account-map/modules/iam-roles" + context = module.this.context } diff --git a/modules/acm/README.md b/modules/acm/README.md index cff3c3e32..0c540545e 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -101,8 +101,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/acm/providers.tf b/modules/acm/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/acm/providers.tf +++ b/modules/acm/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/alb/README.md b/modules/alb/README.md index 1333bc650..18b18e84c 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -82,8 +82,6 @@ No resources. | [https\_ssl\_policy](#input\_https\_ssl\_policy) | The name of the SSL Policy for the listener | `string` | `"ELBSecurityPolicy-TLS-1-1-2017-01"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [idle\_timeout](#input\_idle\_timeout) | The time in seconds that the connection is allowed to be idle | `number` | `60` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [internal](#input\_internal) | A boolean flag to determine whether the ALB should be internal | `bool` | `false` | no | | [ip\_address\_type](#input\_ip\_address\_type) | The type of IP addresses used by the subnets for your load balancer. The possible values are `ipv4` and `dualstack`. | `string` | `"ipv4"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/alb/providers.tf b/modules/alb/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/alb/providers.tf +++ b/modules/alb/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/amplify/README.md b/modules/amplify/README.md index 874344ec0..03b7b729f 100644 --- a/modules/amplify/README.md +++ b/modules/amplify/README.md @@ -195,8 +195,6 @@ atmos terraform apply amplify/example -s | [iam\_service\_role\_arn](#input\_iam\_service\_role\_arn) | The AWS Identity and Access Management (IAM) service role for the Amplify app.
If not provided, a new role will be created if the variable `iam_service_role_enabled` is set to `true`. | `list(string)` | `[]` | no | | [iam\_service\_role\_enabled](#input\_iam\_service\_role\_enabled) | Flag to create the IAM service role for the Amplify app | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/amplify/providers.tf b/modules/amplify/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/amplify/providers.tf +++ b/modules/amplify/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index 45b7cba7d..f9b0214c8 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -142,8 +142,6 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [github\_user\_email](#input\_github\_user\_email) | Github user email | `string` | n/a | yes | | [gitignore\_entries](#input\_gitignore\_entries) | List of .gitignore entries to use when populating the .gitignore file.

For example: `[".idea/", ".vscode/"]`. | `list(string)` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/argocd-repo/provider-github.tf b/modules/argocd-repo/provider-github.tf new file mode 100644 index 000000000..81949874e --- /dev/null +++ b/modules/argocd-repo/provider-github.tf @@ -0,0 +1,5 @@ +provider "github" { + base_url = var.github_base_url + owner = var.github_organization + token = local.github_token +} diff --git a/modules/argocd-repo/providers.tf b/modules/argocd-repo/providers.tf index 9ee194cea..54257fd20 100644 --- a/modules/argocd-repo/providers.tf +++ b/modules/argocd-repo/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,21 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -provider "github" { - base_url = var.github_base_url - owner = var.github_organization - token = local.github_token -} diff --git a/modules/athena/README.md b/modules/athena/README.md index 83c1e8c54..e8e234230 100644 --- a/modules/athena/README.md +++ b/modules/athena/README.md @@ -165,8 +165,6 @@ component | [enforce\_workgroup\_configuration](#input\_enforce\_workgroup\_configuration) | Boolean whether the settings for the workgroup override client-side settings. | `bool` | `true` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/athena/providers.tf b/modules/athena/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/athena/providers.tf +++ b/modules/athena/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aurora-mysql-resources/README.md b/modules/aurora-mysql-resources/README.md index 25131baa2..ea61b3f4a 100644 --- a/modules/aurora-mysql-resources/README.md +++ b/modules/aurora-mysql-resources/README.md @@ -99,8 +99,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/aurora-mysql-resources/providers.tf b/modules/aurora-mysql-resources/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/aurora-mysql-resources/providers.tf +++ b/modules/aurora-mysql-resources/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index 8987af5a2..6ba286211 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -214,8 +214,6 @@ Reploying the component should show no changes. For example, `atmos terraform ap | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [is\_promoted\_read\_replica](#input\_is\_promoted\_read\_replica) | If `true`, do not assign a Replication Source to the Cluster. Set to `true` after manually promoting the cluster from a replica to a standalone cluster. | `bool` | `false` | no | | [is\_read\_replica](#input\_is\_read\_replica) | If `true`, create this DB cluster as a Read Replica. | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/aurora-mysql/providers.tf b/modules/aurora-mysql/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/aurora-mysql/providers.tf +++ b/modules/aurora-mysql/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aurora-postgres-resources/README.md b/modules/aurora-postgres-resources/README.md index 36eb14e82..5e265ee77 100644 --- a/modules/aurora-postgres-resources/README.md +++ b/modules/aurora-postgres-resources/README.md @@ -80,8 +80,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/aurora-postgres-resources/providers.tf b/modules/aurora-postgres-resources/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/aurora-postgres-resources/providers.tf +++ b/modules/aurora-postgres-resources/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 94fbf3c4b..c51bb620d 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -165,8 +165,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [iam\_database\_authentication\_enabled](#input\_iam\_database\_authentication\_enabled) | Specifies whether or mappings of AWS Identity and Access Management (IAM) accounts to database accounts is enabled | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_type](#input\_instance\_type) | EC2 instance type for Postgres cluster | `string` | n/a | yes | | [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/aurora-postgres/providers.tf b/modules/aurora-postgres/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/aurora-postgres/providers.tf +++ b/modules/aurora-postgres/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index b4f4161c6..c0165efd9 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -227,8 +227,6 @@ No resources. | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [iam\_role\_enabled](#input\_iam\_role\_enabled) | Whether or not to create a new IAM Role and Policy Attachment | `bool` | `true` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | The server-side encryption key that is used to protect your backups | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/aws-backup/providers.tf b/modules/aws-backup/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/aws-backup/providers.tf +++ b/modules/aws-backup/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index 588d37e93..f5578c185 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -173,8 +173,6 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} | [iam\_role\_arn](#input\_iam\_role\_arn) | The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the
AWS resources associated with the account. This is only used if create\_iam\_role is false.

If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create\_iam\_role to `false`.

See the AWS Docs for further information:
http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html | `string` | `null` | no | | [iam\_roles\_environment\_name](#input\_iam\_roles\_environment\_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/aws-config/provider-awsutils.mixin.tf b/modules/aws-config/provider-awsutils.mixin.tf new file mode 100644 index 000000000..db8bd2c1d --- /dev/null +++ b/modules/aws-config/provider-awsutils.mixin.tf @@ -0,0 +1,14 @@ +provider "awsutils" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} diff --git a/modules/aws-config/providers.tf b/modules/aws-config/providers.tf index f8513b99d..54257fd20 100644 --- a/modules/aws-config/providers.tf +++ b/modules/aws-config/providers.tf @@ -1,41 +1,19 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -provider "awsutils" { - region = var.region + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } module "iam_roles" { - source = "../account-map/modules/iam-roles" - + source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aws-inspector/README.md b/modules/aws-inspector/README.md index bfd7f22fb..835a15f14 100644 --- a/modules/aws-inspector/README.md +++ b/modules/aws-inspector/README.md @@ -80,8 +80,6 @@ By customizing the configuration with the appropriate rules, you can tailor the | [enabled\_rules](#input\_enabled\_rules) | A list of AWS Inspector rules that should run on a periodic basis.

Valid values are `cve`, `cis`, `nr`, `sbp` which map to the appropriate [Inspector rule arns by region](https://docs.aws.amazon.com/inspector/latest/userguide/inspector_rules-arns.html). | `list(string)` | `[]` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/aws-inspector/providers.tf b/modules/aws-inspector/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/aws-inspector/providers.tf +++ b/modules/aws-inspector/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aws-shield/README.md b/modules/aws-shield/README.md index f4b2231ab..d20d875c5 100644 --- a/modules/aws-shield/README.md +++ b/modules/aws-shield/README.md @@ -135,8 +135,6 @@ This leads to more simplified inter-component dependencies and minimizes the nee | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/aws-shield/providers.tf b/modules/aws-shield/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/aws-shield/providers.tf +++ b/modules/aws-shield/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index 8cefb16e9..e5936a93d 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -50,6 +50,53 @@ The `identity_roles_accessible` element provides a list of role names correspond format("Identity%sTeamAccess", replace(title(role), "-", "")) ``` +### Defining a new permission set + +1. Give the permission set a name, capitalized, in CamelCase, e.g. `AuditManager`. We will use `NAME` as a + placeholder for the name in the instructions below. In Terraform, convert the name to lowercase snake case, e.g. `audit_manager`. +2. Create a file in the `aws-sso` directory with the name `policy-NAME.tf`. +3. In that file, create a policy as follows: + + ```hcl + data "aws_iam_policy_document" "TerraformUpdateAccess" { + # Define the custom policy here + } + + locals { + NAME_permission_set = { # e.g. audit_manager_permission_set + name = "NAME", # e.g. AuditManager + description = "", + relay_state = "", + session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles + tags = {}, + inline_policy = data.aws_iam_policy_document.NAME.json, + policy_attachments = [] # ARNs of AWS managed IAM policies to attach, e.g. arn:aws:iam::aws:policy/ReadOnlyAccess + customer_managed_policy_attachments = [] # ARNs of customer managed IAM policies to attach + } + } + ``` +4. Create a file named `additional-permission-sets-list_override.tf` in the `aws-sso` directory (if it does not already exist). + This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), meaning its + contents will be merged with the main terraform file, and any locals defined in it will override locals defined in other files. + Having your code in this separate override file makes it possible for the component to provide a placeholder local variable + so that it works without customization, while allowing you to customize the component and still update it without losing your customizations. +5. In that file, redefine the local variable `overridable_additional_permission_sets` as follows: + + ```hcl + locals { + overridable_additional_permission_sets = [ + local.NAME_permission_set, + ] + } + ``` + + If you have multiple custom policies, add each one to the list. +6. With that done, the new permission set will be created when the changes are applied. + You can then use it just like the others. +7. If you want the permission set to be able to use Terraform, enable access to the + Terraform state read/write (default) role in `tfstate-backend`. + + #### Example The example snippet below shows how to use this module with various combinations (plain YAML, YAML Anchors and a combination of the two): @@ -93,14 +140,10 @@ components: - AdministratorAccess - ReadOnlyAccess aws_teams_accessible: - - "admin" - - "ops" - - "poweruser" - - "observer" - - "reader" + - "developers" + - "devops" + - "managers" - "support" - - "viewer" - ``` @@ -157,7 +200,6 @@ components: | [global\_environment\_name](#input\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no | | [global\_stage\_name](#input\_global\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/aws-sso/additional-permission-sets.tf b/modules/aws-sso/additional-permission-sets.tf new file mode 100644 index 000000000..528a089ae --- /dev/null +++ b/modules/aws-sso/additional-permission-sets.tf @@ -0,0 +1,10 @@ +locals { + # If you have custom permission sets, override this declaration by creating + # a file called `additional-permission-sets_override.tf`. + # Then add the custom permission sets to the overridable_additional_permission_sets in that file. + # See the README for more details. + overridable_additional_permission_sets = [ + # Example + # local.audit_manager_permission_set, + ] +} diff --git a/modules/aws-sso/main.tf b/modules/aws-sso/main.tf index 4c46b9e42..e2e158944 100644 --- a/modules/aws-sso/main.tf +++ b/modules/aws-sso/main.tf @@ -3,6 +3,7 @@ module "permission_sets" { version = "1.0.0" permission_sets = concat( + local.overridable_additional_permission_sets, local.administrator_access_permission_set, local.billing_administrator_access_permission_set, local.billing_read_only_access_permission_set, diff --git a/modules/aws-sso/providers.tf b/modules/aws-sso/providers.tf index 9188ea7dc..54257fd20 100644 --- a/modules/aws-sso/providers.tf +++ b/modules/aws-sso/providers.tf @@ -1,27 +1,19 @@ provider "aws" { region = var.region + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.org_role_arn != null ? [true] : [] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.org_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } -provider "aws" { - alias = "root" - region = var.region -} - module "iam_roles" { - source = "../account-map/modules/iam-roles" - privileged = true - context = module.this.context -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" + source = "../account-map/modules/iam-roles" + context = module.this.context } diff --git a/modules/aws-ssosync/README.md b/modules/aws-ssosync/README.md index e6d085ca6..4fc72c9bb 100644 --- a/modules/aws-ssosync/README.md +++ b/modules/aws-ssosync/README.md @@ -4,16 +4,16 @@ Deploys [AWS ssosync](https://github.com/awslabs/ssosync) to sync Google Groups AWS `ssosync` is a Lambda application that regularly manages Identity Store users. -This component requires SuperAdmin because it deploys a role in the identity account. +This component requires manual deployment by a privileged user because it deploys a role in the identity account. -You need to have setup AWS SSO in root account and delegated identity as your delegated adminstrator. +You need to have set up AWS SSO in root account and delegated to the identity account as your SSO administrator. ## Usage You should be able to deploy the `aws-ssosync` component to the `[core-]gbl-identity` stack with `atmos terraform deploy aws-ssosync -s gbl-identity`. **Stack Level**: Global -**Deployment**: Must be deployed by super-admin using `atmos` CLI +**Deployment**: Must be deployed by `managers` or SuperAdmin using `atmos` CLI The following is an example snippet for how to use this component: @@ -181,7 +181,6 @@ Make sure that all four of the following SSM parameters exist in the `identity` | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [ignore\_groups](#input\_ignore\_groups) | Ignore these Google Workspace groups | `string` | `""` | no | | [ignore\_users](#input\_ignore\_users) | Ignore these Google Workspace users | `string` | `""` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [include\_groups](#input\_include\_groups) | Include only these Google Workspace groups. (Only applicable for sync\_method user\_groups) | `string` | `""` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/aws-ssosync/providers.tf b/modules/aws-ssosync/providers.tf index 5736c3cb2..54257fd20 100644 --- a/modules/aws-ssosync/providers.tf +++ b/modules/aws-ssosync/providers.tf @@ -1,27 +1,19 @@ provider "aws" { region = var.region - # aws-ssosync, since it creates roles in identity, - # must be run as SuperAdmin, and cannot use "profile" instead of "role_arn" - # even if the components are generally using profiles. - # Note the role_arn is the ARN of the OrganizationAccountAccessRole, not the SAML role. + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = var.import_role_arn == null ? (module.iam_roles.org_role_arn != null ? [true] : []) : ["import"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.org_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } module "iam_roles" { - source = "../account-map/modules/iam-roles" - privileged = true - context = module.this.context -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" + source = "../account-map/modules/iam-roles" + context = module.this.context } diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index d5bb97649..789e4ced0 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -44,7 +44,7 @@ directly into the list, or you can create a custom policy as follows: } ``` - If you have multiple custom policies, add each one to the map in the form `NAME = aws_iam_policy.NAME.arn`. + If you have multiple custom policies, add each one to the map in the form `NAME = aws_iam_policy.NAME.arn`. 6. With that done, you can now attach that policy by adding the name to the `role_policy_arns` list. For example: ```yaml @@ -177,7 +177,7 @@ components: | Name | Source | Version | |------|--------|---------| | [assume\_role](#module\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | -| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aws-team-roles/additional-policy-map.tf b/modules/aws-team-roles/additional-policy-map.tf index 5ead699f8..03735ef4b 100644 --- a/modules/aws-team-roles/additional-policy-map.tf +++ b/modules/aws-team-roles/additional-policy-map.tf @@ -1,7 +1,7 @@ locals { # If you have custom policies, override this declaration by creating # a file called `additional-policy-map_override.tf`. - # Then add the custom policies to the additional_custom_policy_map in that file. + # Then add the custom policies to the overridable_additional_custom_policy_map in that file. # See the README for more details. overridable_additional_custom_policy_map = { # Example: diff --git a/modules/aws-team-roles/main.tf b/modules/aws-team-roles/main.tf index 3fd6e669c..5d4fc2ff2 100644 --- a/modules/aws-team-roles/main.tf +++ b/modules/aws-team-roles/main.tf @@ -11,10 +11,7 @@ locals { # using an aws_iam_policy resource and then map it to the name you want to use in the # YAML configuration by adding an entry in `custom_policy_map`. supplied_custom_policy_map = { - billing_read_only = try(aws_iam_policy.billing_read_only[0].arn, null) - billing_admin = try(aws_iam_policy.billing_admin[0].arn, null) - support = try(aws_iam_policy.support[0].arn, null) - eks_viewer = try(aws_iam_policy.eks_viewer[0].arn, null) + eks_viewer = try(aws_iam_policy.eks_viewer[0].arn, null) } custom_policy_map = merge(local.supplied_custom_policy_map, local.overridable_additional_custom_policy_map) diff --git a/modules/aws-team-roles/remote-state.tf b/modules/aws-team-roles/remote-state.tf index a81c9b931..5e795c6e1 100644 --- a/modules/aws-team-roles/remote-state.tf +++ b/modules/aws-team-roles/remote-state.tf @@ -1,6 +1,6 @@ module "aws_saml" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "aws-saml" privileged = true diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index c62c76070..90b360deb 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -1,14 +1,8 @@ # Component: `aws-teams` -## Purpose - -This component implements the hub of a hub-and-spoke account access hierarchy, where the hub is the `identity` account. -This allows you to assign each user to a single "team" (implemented as an IAM role) in the `identity` account, which they access -via a single authentication (login), which then allows them to access a set of roles in a set of accounts. - -This component is responsible for provisioning team roles in the centralized identity account. -This is expected to be used alongside [the `aws-team-roles` component](../aws-team-roles) -which provisions the roles in the spoke accounts and configures their privileges and which teams can access them. +This component is responsible for provisioning all primary user and system roles into the centralized identity account. +This is expected to be used alongside [the `aws-team-roles` component](../aws-team-roles) to provide +fine-grained role delegation across the account hierarchy. ### Teams Function Like Groups and are Implemented as Roles The "teams" created in the `identity` account by this module can be thought of as access control "groups": @@ -16,9 +10,6 @@ a user who is allowed access one of these teams gets access to a set of roles (a across a set of accounts. Generally, there is nothing else provisioned in the `identity` account, so the teams have limited access to resources in the `identity` account by design. -Privileges for the users within the identity account itself are generally limited, and are configured -via the `role_policy_arns` variable, just as with `aws-team-roles`. See the `aws-team-roles` README for more details. - Teams are implemented as IAM Roles in each account. Access to the "teams" in the `identity` account is controlled by the `aws-saml` and `aws-sso` components. Access to the roles in all the other accounts is controlled by the "assume role" policies of those roles, which allow the "team" @@ -157,9 +148,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | | [assume\_role](#module\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | -| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -167,15 +158,11 @@ components: | Name | Type | |------|------| -| [aws_iam_policy.support](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.team_role_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [local_file.account_info](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource | -| [aws_iam_policy.aws_support_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | | [aws_iam_policy_document.assume_role_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.support_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.support_access_trusted_advisor](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.team_role_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs diff --git a/modules/aws-teams/additional-policy-map.tf b/modules/aws-teams/additional-policy-map.tf index 9596d2daa..13ffa4c3d 100644 --- a/modules/aws-teams/additional-policy-map.tf +++ b/modules/aws-teams/additional-policy-map.tf @@ -1,7 +1,7 @@ locals { # If you have custom policies, override this declaration by creating # a file called `additional-policy-map_override.tf`. - # Then add the custom policies to the additional_custom_policy_map in that file. + # Then add the custom policies to the overridable_additional_custom_policy_map in that file. # See the README in `aws-team-roles` for more details. overridable_additional_custom_policy_map = { # Example: diff --git a/modules/aws-teams/main.tf b/modules/aws-teams/main.tf index 31ecc29cc..d3013f611 100644 --- a/modules/aws-teams/main.tf +++ b/modules/aws-teams/main.tf @@ -8,7 +8,6 @@ locals { # YAML configuration by adding an entry in `custom_policy_map`. supplied_custom_policy_map = { team_role_access = aws_iam_policy.team_role_access.arn - support = try(aws_iam_policy.support[0].arn, null) } custom_policy_map = merge(local.supplied_custom_policy_map, local.overridable_additional_custom_policy_map) diff --git a/modules/aws-teams/outputs.tf b/modules/aws-teams/outputs.tf index 7cb23df80..697a29951 100644 --- a/modules/aws-teams/outputs.tf +++ b/modules/aws-teams/outputs.tf @@ -25,5 +25,5 @@ resource "local_file" "account_info" { role_name_role_arn_map = local.role_name_role_arn_map namespace = module.this.namespace }) - filename = "${path.module}/../aws-team-roles/iam-role-info/${module.this.id}.sh" + filename = "${path.module}/../aws-team-roles/iam-role-info/${module.this.id}-teams.sh" } diff --git a/modules/aws-teams/policy-support.tf b/modules/aws-teams/policy-support.tf deleted file mode 100644 index ebd22c783..000000000 --- a/modules/aws-teams/policy-support.tf +++ /dev/null @@ -1,56 +0,0 @@ -# This Terraform configuration file which creates a customer-managed policy exists in both -# aws-teams and aws-team-roles. -# - -# The reason for this is as follows: -# -# The support role (unlike most roles in the identity account) needs specific access to -# resources in the identity account. Policies must be created per-account, so the identity -# account needs a support policy, and that has to be created in aws-teams. -# -# Most other custom roles are only needed in either aws-teams and aws-team-roles, not both. -# - -locals { - support_policy_enabled = contains(local.configured_policies, "support") -} - -data "aws_iam_policy_document" "support_access_trusted_advisor" { - count = local.support_policy_enabled ? 1 : 0 - - statement { - sid = "AllowTrustedAdvisor" - effect = "Allow" - actions = [ - "trustedadvisor:Describe*", - ] - - resources = [ - "*", - ] - } -} - -data "aws_iam_policy" "aws_support_access" { - count = local.support_policy_enabled ? 1 : 0 - - arn = "arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess" -} - -data "aws_iam_policy_document" "support_access_aggregated" { - count = local.support_policy_enabled ? 1 : 0 - - source_policy_documents = [ - data.aws_iam_policy.aws_support_access[0].policy, - data.aws_iam_policy_document.support_access_trusted_advisor[0].json - ] -} - -resource "aws_iam_policy" "support" { - count = local.support_policy_enabled ? 1 : 0 - - name = format("%s-support", module.this.id) - policy = data.aws_iam_policy_document.support_access_aggregated[0].json - - tags = module.this.tags -} diff --git a/modules/aws-teams/remote-state.tf b/modules/aws-teams/remote-state.tf index 067704fe9..ec9dc956e 100644 --- a/modules/aws-teams/remote-state.tf +++ b/modules/aws-teams/remote-state.tf @@ -1,6 +1,6 @@ module "aws_saml" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "aws-saml" privileged = true @@ -16,11 +16,12 @@ module "aws_saml" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.2" component = "account-map" - environment = var.account_map_environment_name - stage = var.account_map_stage_name + tenant = module.iam_roles.global_tenant_name + environment = module.iam_roles.global_environment_name + stage = module.iam_roles.global_stage_name privileged = true context = module.this.context diff --git a/modules/aws-waf-acl/README.md b/modules/aws-waf-acl/README.md index 9b78eea97..64e50e47e 100644 --- a/modules/aws-waf-acl/README.md +++ b/modules/aws-waf-acl/README.md @@ -83,8 +83,6 @@ components: | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [geo\_match\_statement\_rules](#input\_geo\_match\_statement\_rules) | A rule statement used to identify web requests based on country of origin.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [ip\_set\_reference\_statement\_rules](#input\_ip\_set\_reference\_statement\_rules) | A rule statement used to detect web requests coming from particular IP addresses or address ranges.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
arn:
The ARN of the IP Set that this statement references.
ip\_set\_forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
position:
The position in the header to search for the IP address.
Possible values include: `FIRST`, `LAST`, or `ANY`.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/aws-waf-acl/providers.tf b/modules/aws-waf-acl/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/aws-waf-acl/providers.tf +++ b/modules/aws-waf-acl/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/bastion/README.md b/modules/bastion/README.md index 7b75057af..e8122d103 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -109,8 +109,6 @@ components: | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [image\_container](#input\_image\_container) | The image container to use in `container.sh`. | `string` | `""` | no | | [image\_repository](#input\_image\_repository) | The image repository to use in `container.sh`. | `string` | `""` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_type](#input\_instance\_type) | Bastion instance type | `string` | `"t2.micro"` | no | | [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/bastion/providers.tf b/modules/bastion/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/bastion/providers.tf +++ b/modules/bastion/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/cloudtrail-bucket/README.md b/modules/cloudtrail-bucket/README.md index a2051a73e..d7c9a8cc8 100644 --- a/modules/cloudtrail-bucket/README.md +++ b/modules/cloudtrail-bucket/README.md @@ -63,8 +63,6 @@ No resources. | [expiration\_days](#input\_expiration\_days) | Number of days after which to expunge the objects | `number` | `90` | no | | [glacier\_transition\_days](#input\_glacier\_transition\_days) | Number of days after which to move the data to the glacier storage tier | `number` | `60` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/cloudtrail-bucket/providers.tf b/modules/cloudtrail-bucket/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/cloudtrail-bucket/providers.tf +++ b/modules/cloudtrail-bucket/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/cloudtrail/README.md b/modules/cloudtrail/README.md index 4b71b58d9..80761fee8 100644 --- a/modules/cloudtrail/README.md +++ b/modules/cloudtrail/README.md @@ -86,8 +86,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [include\_global\_service\_events](#input\_include\_global\_service\_events) | Specifies whether the trail is publishing events from global services such as IAM to the log files | `bool` | `true` | no | | [is\_multi\_region\_trail](#input\_is\_multi\_region\_trail) | Specifies whether the trail is created in the current region or in all regions | `bool` | `true` | no | | [is\_organization\_trail](#input\_is\_organization\_trail) | Specifies whether the trail is created for all accounts in an organization in AWS Organizations, or only for the current AWS account.

The default is false, and cannot be true unless the call is made on behalf of an AWS account that is the management account
for an organization in AWS Organizations. | `bool` | `false` | no | diff --git a/modules/cloudtrail/providers.tf b/modules/cloudtrail/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/cloudtrail/providers.tf +++ b/modules/cloudtrail/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/cloudwatch-logs/README.md b/modules/cloudwatch-logs/README.md index 11e816db1..99ebd591b 100644 --- a/modules/cloudwatch-logs/README.md +++ b/modules/cloudwatch-logs/README.md @@ -65,8 +65,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/cloudwatch-logs/providers.tf b/modules/cloudwatch-logs/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/cloudwatch-logs/providers.tf +++ b/modules/cloudwatch-logs/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/cognito/README.md b/modules/cognito/README.md index 09920c701..ec89b7b59 100644 --- a/modules/cognito/README.md +++ b/modules/cognito/README.md @@ -119,8 +119,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [identity\_providers](#input\_identity\_providers) | Cognito Identity Providers configuration | `list(any)` | `[]` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/cognito/providers.tf b/modules/cognito/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/cognito/providers.tf +++ b/modules/cognito/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/config-bucket/README.md b/modules/config-bucket/README.md index a4a723fe5..25d1dfaf6 100644 --- a/modules/config-bucket/README.md +++ b/modules/config-bucket/README.md @@ -72,8 +72,6 @@ No resources. | [expiration\_days](#input\_expiration\_days) | Number of days after which to expunge the objects | `number` | `90` | no | | [glacier\_transition\_days](#input\_glacier\_transition\_days) | Number of days after which to move the data to the glacier storage tier | `number` | `60` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/config-bucket/providers.tf b/modules/config-bucket/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/config-bucket/providers.tf +++ b/modules/config-bucket/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index b1ea30395..f97f16511 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -114,8 +114,6 @@ provider "datadog" { | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/datadog-configuration/provider-datadog.tf b/modules/datadog-configuration/provider-datadog.tf new file mode 100644 index 000000000..5a1f4c9ce --- /dev/null +++ b/modules/datadog-configuration/provider-datadog.tf @@ -0,0 +1,23 @@ +# module.iam_roles_datadog_secrets.terraform_profile_name +provider "aws" { + alias = "api_keys" + region = coalesce(var.datadog_secrets_source_store_account_region, var.region) + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles_datadog_secrets.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles_datadog_secrets.terraform_role_arn]) + content { + role_arn = module.iam_roles_datadog_secrets.terraform_role_arn + } + } +} + +module "iam_roles_datadog_secrets" { + source = "../account-map/modules/iam-roles" + stage = var.datadog_secrets_source_store_account_stage + tenant = var.datadog_secrets_source_store_account_tenant + context = module.this.context +} diff --git a/modules/datadog-configuration/providers.tf b/modules/datadog-configuration/providers.tf index 177c5fc6c..54257fd20 100644 --- a/modules/datadog-configuration/providers.tf +++ b/modules/datadog-configuration/providers.tf @@ -1,48 +1,19 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name -module "iam_roles" { - source = "../account-map/modules/iam-roles" - context = module.this.context -} - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -provider "aws" { - alias = "api_keys" - region = coalesce(var.datadog_secrets_source_store_account_region, var.region) - - profile = module.iam_roles_datadog_secrets.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles_datadog_secrets.terraform_profile_name) : null dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles_datadog_secrets.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } -module "iam_roles_datadog_secrets" { +module "iam_roles" { source = "../account-map/modules/iam-roles" - stage = var.datadog_secrets_source_store_account_stage - tenant = var.datadog_secrets_source_store_account_tenant context = module.this.context } diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index 3adb8917d..4b8393a0b 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -70,8 +70,6 @@ components: | [filter\_tags](#input\_filter\_tags) | An array of EC2 tags (in the form `key:value`) that defines a filter that Datadog use when collecting metrics from EC2. Wildcards, such as ? (for single characters) and * (for multiple characters) can also be used | `list(string)` | `[]` | no | | [host\_tags](#input\_host\_tags) | An array of tags (in the form `key:value`) to add to all hosts and metrics reporting through this integration | `list(string)` | `[]` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [included\_regions](#input\_included\_regions) | An array of AWS regions to include in metrics collection | `list(string)` | `[]` | no | | [integrations](#input\_integrations) | List of AWS permission names to apply for different integrations (e.g. 'all', 'core') | `list(string)` |
[
"all"
]
| no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/datadog-integration/providers.tf b/modules/datadog-integration/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/datadog-integration/providers.tf +++ b/modules/datadog-integration/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 830a4b233..220fb6891 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -113,8 +113,6 @@ components: | [forwarder\_vpc\_logs\_layers](#input\_forwarder\_vpc\_logs\_layers) | List of Lambda Layer Version ARNs (maximum of 5) to attach to Datadog VPC flow log forwarder lambda function | `list(string)` | `[]` | no | | [forwarder\_vpclogs\_filter\_pattern](#input\_forwarder\_vpclogs\_filter\_pattern) | Filter pattern for Lambda forwarder VPC Logs | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_key\_id](#input\_kms\_key\_id) | Optional KMS key ID to encrypt Datadog Lambda function logs | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/datadog-lambda-forwarder/providers.tf b/modules/datadog-lambda-forwarder/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/datadog-lambda-forwarder/providers.tf +++ b/modules/datadog-lambda-forwarder/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/datadog-logs-archive/providers.tf b/modules/datadog-logs-archive/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/datadog-logs-archive/providers.tf +++ b/modules/datadog-logs-archive/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 44b2d902a..479fccc4d 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -222,8 +222,6 @@ No resources. | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/datadog-monitor/providers.tf b/modules/datadog-monitor/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/datadog-monitor/providers.tf +++ b/modules/datadog-monitor/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index 65cf193c9..e8d9031a5 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -106,8 +106,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/datadog-private-location-ecs/providers.tf b/modules/datadog-private-location-ecs/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/datadog-private-location-ecs/providers.tf +++ b/modules/datadog-private-location-ecs/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index b642777f0..6a570ebdc 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -177,8 +177,6 @@ Environment variables: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/datadog-synthetics-private-location/provider-helm.tf b/modules/datadog-synthetics-private-location/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/datadog-synthetics-private-location/provider-helm.tf +++ b/modules/datadog-synthetics-private-location/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/datadog-synthetics-private-location/providers.tf b/modules/datadog-synthetics-private-location/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/datadog-synthetics-private-location/providers.tf +++ b/modules/datadog-synthetics-private-location/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/datadog-synthetics/README.md b/modules/datadog-synthetics/README.md index 9a08231c6..5f35eed54 100644 --- a/modules/datadog-synthetics/README.md +++ b/modules/datadog-synthetics/README.md @@ -174,8 +174,6 @@ No resources. | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/datadog-synthetics/providers.tf b/modules/datadog-synthetics/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/datadog-synthetics/providers.tf +++ b/modules/datadog-synthetics/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/dms/endpoint/README.md b/modules/dms/endpoint/README.md index 05ad66b9d..588534f49 100644 --- a/modules/dms/endpoint/README.md +++ b/modules/dms/endpoint/README.md @@ -116,8 +116,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [extra\_connection\_attributes](#input\_extra\_connection\_attributes) | Additional attributes associated with the connection to the source database | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kafka\_settings](#input\_kafka\_settings) | Configuration block for Kafka settings | `map(any)` | `null` | no | | [kinesis\_settings](#input\_kinesis\_settings) | Configuration block for Kinesis settings | `map(any)` | `null` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | (Required when engine\_name is `mongodb`, optional otherwise). ARN for the KMS key that will be used to encrypt the connection parameters. If you do not specify a value for `kms_key_arn`, then AWS DMS will use your default encryption key | `string` | `null` | no | diff --git a/modules/dms/endpoint/providers.tf b/modules/dms/endpoint/providers.tf index c2419aabb..45d458575 100644 --- a/modules/dms/endpoint/providers.tf +++ b/modules/dms/endpoint/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/dms/iam/README.md b/modules/dms/iam/README.md index 8babad2c2..5fc860077 100644 --- a/modules/dms/iam/README.md +++ b/modules/dms/iam/README.md @@ -59,8 +59,6 @@ No resources. | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/dms/iam/providers.tf b/modules/dms/iam/providers.tf index c2419aabb..45d458575 100644 --- a/modules/dms/iam/providers.tf +++ b/modules/dms/iam/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/dms/replication-instance/README.md b/modules/dms/replication-instance/README.md index cbb09f1e5..6e041774a 100644 --- a/modules/dms/replication-instance/README.md +++ b/modules/dms/replication-instance/README.md @@ -86,8 +86,6 @@ No resources. | [engine\_version](#input\_engine\_version) | The engine version number of the replication instance | `string` | `"3.4"` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/dms/replication-instance/providers.tf b/modules/dms/replication-instance/providers.tf index c2419aabb..45d458575 100644 --- a/modules/dms/replication-instance/providers.tf +++ b/modules/dms/replication-instance/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/dms/replication-task/README.md b/modules/dms/replication-task/README.md index 8f0a78e36..b67f30c36 100644 --- a/modules/dms/replication-task/README.md +++ b/modules/dms/replication-task/README.md @@ -78,8 +78,6 @@ No resources. | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/dms/replication-task/providers.tf b/modules/dms/replication-task/providers.tf index c2419aabb..45d458575 100644 --- a/modules/dms/replication-task/providers.tf +++ b/modules/dms/replication-task/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index 914a2e878..91118b2ef 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -180,8 +180,6 @@ Takeaway | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/dns-delegated/providers-dns-primary.tf b/modules/dns-delegated/providers-dns-primary.tf new file mode 100644 index 000000000..1259915b6 --- /dev/null +++ b/modules/dns-delegated/providers-dns-primary.tf @@ -0,0 +1,16 @@ +provider "aws" { + # The AWS provider to use to make changes in the DNS primary account + alias = "primary" + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.dns_terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.dns_terraform_role_arn]) + content { + role_arn = module.iam_roles.dns_terraform_role_arn + } + } +} diff --git a/modules/dns-delegated/providers.tf b/modules/dns-delegated/providers.tf index 944bbdbf0..54257fd20 100644 --- a/modules/dns-delegated/providers.tf +++ b/modules/dns-delegated/providers.tf @@ -1,28 +1,14 @@ provider "aws" { - # The AWS provider to use to make changes in the DNS primary account - alias = "primary" region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.dns_terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = var.import_role_arn == null ? (module.iam_roles.dns_terraform_role_arn != null ? [true] : []) : ["import"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.dns_terraform_role_arn) - } - } -} - -provider "aws" { - # The AWS provider to use to make changes in the target (delegated) account - region = var.region - - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -31,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index 7c94804e0..fc2df279a 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -106,8 +106,6 @@ Use the [acm](/reference-architecture/components/acm) component for more advanc | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/dns-primary/providers.tf b/modules/dns-primary/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/dns-primary/providers.tf +++ b/modules/dns-primary/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/documentdb/README.md b/modules/documentdb/README.md index 55c3a998a..1af510394 100644 --- a/modules/documentdb/README.md +++ b/modules/documentdb/README.md @@ -84,8 +84,6 @@ components: | [engine\_version](#input\_engine\_version) | The version number of the database engine to use | `string` | `"3.6.0"` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_class](#input\_instance\_class) | The instance class to use. For more details, see https://docs.aws.amazon.com/documentdb/latest/developerguide/db-instance-classes.html#db-instance-class-specs | `string` | `"db.r4.large"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/documentdb/providers.tf b/modules/documentdb/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/documentdb/providers.tf +++ b/modules/documentdb/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/dynamodb/README.md b/modules/dynamodb/README.md index fb7017a2d..e148d820b 100644 --- a/modules/dynamodb/README.md +++ b/modules/dynamodb/README.md @@ -79,8 +79,6 @@ No resources. | [hash\_key](#input\_hash\_key) | DynamoDB table Hash Key | `string` | n/a | yes | | [hash\_key\_type](#input\_hash\_key\_type) | Hash Key type, which must be a scalar type: `S`, `N`, or `B` for String, Number or Binary data, respectively. | `string` | `"S"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/dynamodb/providers.tf b/modules/dynamodb/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/dynamodb/providers.tf +++ b/modules/dynamodb/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index 8af2aba11..b56e5561a 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -128,8 +128,6 @@ No resources. | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [export\_client\_certificate](#input\_export\_client\_certificate) | Flag to determine whether to export the client certificate with the VPN configuration | `bool` | `true` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/ec2-client-vpn/provider-awsutils.mixin.tf b/modules/ec2-client-vpn/provider-awsutils.mixin.tf new file mode 100644 index 000000000..db8bd2c1d --- /dev/null +++ b/modules/ec2-client-vpn/provider-awsutils.mixin.tf @@ -0,0 +1,14 @@ +provider "awsutils" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} diff --git a/modules/ec2-client-vpn/providers.tf b/modules/ec2-client-vpn/providers.tf index e27b8de2f..54257fd20 100644 --- a/modules/ec2-client-vpn/providers.tf +++ b/modules/ec2-client-vpn/providers.tf @@ -1,25 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -provider "awsutils" { - region = var.region - - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -28,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/ecr/README.md b/modules/ecr/README.md index 9f9b95f94..155e24e36 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -100,8 +100,6 @@ components: | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [image\_tag\_mutability](#input\_image\_tag\_mutability) | The tag mutability setting for the repository. Must be one of: `MUTABLE` or `IMMUTABLE` | `string` | `"MUTABLE"` | no | | [images](#input\_images) | List of image names (ECR repo names) to create repos for | `list(string)` | n/a | yes | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/ecr/providers.tf b/modules/ecr/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/ecr/providers.tf +++ b/modules/ecr/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 051e2ad24..999dcb214 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -247,8 +247,6 @@ components: | [iam\_policy\_enabled](#input\_iam\_policy\_enabled) | If set to true will create IAM policy in AWS | `bool` | `false` | no | | [iam\_policy\_statements](#input\_iam\_policy\_statements) | Map of IAM policy statements to use in the policy. This can be used with or instead of the `var.iam_source_json_url`. | `any` | `{}` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kinesis\_enabled](#input\_kinesis\_enabled) | Enable Kinesis | `bool` | `false` | no | | [kms\_key\_alias](#input\_kms\_key\_alias) | ID of KMS key | `string` | `"default"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/ecs-service/providers.tf b/modules/ecs-service/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/ecs-service/providers.tf +++ b/modules/ecs-service/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/ecs/README.md b/modules/ecs/README.md index af36ff928..5ff2e74ae 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -101,8 +101,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [internal\_enabled](#input\_internal\_enabled) | Whether to create an internal load balancer for services in this cluster | `bool` | `false` | no | | [kms\_key\_id](#input\_kms\_key\_id) | The AWS Key Management Service key ID to encrypt the data between the local client and the container. | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/ecs/providers.tf b/modules/ecs/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/ecs/providers.tf +++ b/modules/ecs/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/efs/README.md b/modules/efs/README.md index 43118870a..afb1522c2 100644 --- a/modules/efs/README.md +++ b/modules/efs/README.md @@ -69,8 +69,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [hostname\_template](#input\_hostname\_template) | The `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`"
Typically something like `"echo.%[3]v.%[2]v.example.com"`. | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/efs/providers.tf b/modules/efs/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/efs/providers.tf +++ b/modules/efs/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks-iam/README.md b/modules/eks-iam/README.md index b2f46f7f3..bd080c708 100644 --- a/modules/eks-iam/README.md +++ b/modules/eks-iam/README.md @@ -79,8 +79,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters.
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_alias\_name](#input\_kms\_alias\_name) | AWS KMS alias used for encryption/decryption of SSM parameters default is alias used in SSM | `string` | `"alias/aws/ssm"` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | | [name](#input\_name) | Solution name, e.g. 'app' or 'jenkins' | `string` | `null` | no | diff --git a/modules/eks-iam/providers.tf b/modules/eks-iam/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/eks-iam/providers.tf +++ b/modules/eks-iam/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 4257a005f..62adec149 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -438,8 +438,6 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [github\_app\_installation\_id](#input\_github\_app\_installation\_id) | The "Installation ID" of the GitHub App to use for the runner controller. | `string` | `""` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/actions-runner-controller/provider-helm.tf b/modules/eks/actions-runner-controller/provider-helm.tf index abe6f9c56..64459d4f4 100644 --- a/modules/eks/actions-runner-controller/provider-helm.tf +++ b/modules/eks/actions-runner-controller/provider-helm.tf @@ -101,7 +101,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/modules/eks/actions-runner-controller/providers.tf b/modules/eks/actions-runner-controller/providers.tf index 74ff8e62c..45d458575 100644 --- a/modules/eks/actions-runner-controller/providers.tf +++ b/modules/eks/actions-runner-controller/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/alb-controller-ingress-class/README.md b/modules/eks/alb-controller-ingress-class/README.md index 1d9a5fc0a..b1fc888ae 100644 --- a/modules/eks/alb-controller-ingress-class/README.md +++ b/modules/eks/alb-controller-ingress-class/README.md @@ -71,8 +71,6 @@ components: | [group](#input\_group) | Group name for default ingress | `string` | `"common"` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [ip\_address\_type](#input\_ip\_address\_type) | IP address type for default ingress, one of `ipv4` or `dualstack`. | `string` | `"dualstack"` | no | | [is\_default](#input\_is\_default) | Set `true` to make this the default IngressClass. There should only be one default per cluster. | `bool` | `false` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | diff --git a/modules/eks/alb-controller-ingress-class/provider-helm.tf b/modules/eks/alb-controller-ingress-class/provider-helm.tf index abe6f9c56..64459d4f4 100644 --- a/modules/eks/alb-controller-ingress-class/provider-helm.tf +++ b/modules/eks/alb-controller-ingress-class/provider-helm.tf @@ -101,7 +101,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/modules/eks/alb-controller-ingress-class/providers.tf b/modules/eks/alb-controller-ingress-class/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/alb-controller-ingress-class/providers.tf +++ b/modules/eks/alb-controller-ingress-class/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index e0460a54d..5016390e1 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -94,8 +94,6 @@ components: | [fixed\_response\_vars](#input\_fixed\_response\_vars) | The templatefile vars to use for the fixed response template | `map(any)` |
{
"email": "hello@cloudposse.com"
}
| no | | [global\_accelerator\_enabled](#input\_global\_accelerator\_enabled) | Whether or not Global Accelerator Endpoint Group should be provisioned for the load balancer | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/alb-controller-ingress-group/provider-kubernetes.tf b/modules/eks/alb-controller-ingress-group/provider-kubernetes.tf index 3bc3e6c4c..d26650e31 100644 --- a/modules/eks/alb-controller-ingress-group/provider-kubernetes.tf +++ b/modules/eks/alb-controller-ingress-group/provider-kubernetes.tf @@ -89,7 +89,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/modules/eks/alb-controller-ingress-group/providers.tf b/modules/eks/alb-controller-ingress-group/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/alb-controller-ingress-group/providers.tf +++ b/modules/eks/alb-controller-ingress-group/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index d5a81d6f3..48bf82395 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -107,8 +107,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/alb-controller/provider-helm.tf b/modules/eks/alb-controller/provider-helm.tf index abe6f9c56..64459d4f4 100644 --- a/modules/eks/alb-controller/provider-helm.tf +++ b/modules/eks/alb-controller/provider-helm.tf @@ -101,7 +101,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/modules/eks/alb-controller/providers.tf b/modules/eks/alb-controller/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/alb-controller/providers.tf +++ b/modules/eks/alb-controller/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 6c7fd3d7a..ede4126e7 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -159,8 +159,6 @@ to use google OIDC: | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [host](#input\_host) | Host name to use for ingress and ALB | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/argocd/provider-helm.tf b/modules/eks/argocd/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/argocd/provider-helm.tf +++ b/modules/eks/argocd/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/argocd/provider-secrets.tf b/modules/eks/argocd/provider-secrets.tf new file mode 100644 index 000000000..57904b0c8 --- /dev/null +++ b/modules/eks/argocd/provider-secrets.tf @@ -0,0 +1,22 @@ +provider "aws" { + alias = "config_secrets" + region = var.ssm_store_account_region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles_config_secrets.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles_config_secrets.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles_config_secrets" { + source = "../../account-map/modules/iam-roles" + stage = var.ssm_store_account + tenant = var.ssm_store_account_tenant + context = module.this.context +} diff --git a/modules/eks/argocd/providers.tf b/modules/eks/argocd/providers.tf index 018e689a0..45d458575 100644 --- a/modules/eks/argocd/providers.tf +++ b/modules/eks/argocd/providers.tf @@ -1,41 +1,19 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } -provider "aws" { - alias = "config_secrets" - region = var.ssm_store_account_region - profile = coalesce(var.import_profile_name, module.iam_roles_config_secrets.terraform_profile_name) -} - -module "iam_roles_config_secrets" { - source = "../../account-map/modules/iam-roles" - stage = var.ssm_store_account - tenant = var.ssm_store_account_tenant - context = module.this.context -} - module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 2d142d902..26eec9263 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -93,8 +93,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/aws-node-termination-handler/provider-helm.tf b/modules/eks/aws-node-termination-handler/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/aws-node-termination-handler/provider-helm.tf +++ b/modules/eks/aws-node-termination-handler/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/aws-node-termination-handler/providers.tf b/modules/eks/aws-node-termination-handler/providers.tf index 74ff8e62c..45d458575 100644 --- a/modules/eks/aws-node-termination-handler/providers.tf +++ b/modules/eks/aws-node-termination-handler/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index 7baf95adb..63eea3b6f 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -115,8 +115,6 @@ The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/cert-manager/provider-helm.tf b/modules/eks/cert-manager/provider-helm.tf index abe6f9c56..64459d4f4 100644 --- a/modules/eks/cert-manager/provider-helm.tf +++ b/modules/eks/cert-manager/provider-helm.tf @@ -101,7 +101,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/modules/eks/cert-manager/providers.tf b/modules/eks/cert-manager/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/cert-manager/providers.tf +++ b/modules/eks/cert-manager/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 2fe3de580..543d0ac25 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -295,8 +295,8 @@ For more on upgrading these EKS Addons, see | [fargate\_profile\_iam\_role\_permissions\_boundary](#input\_fargate\_profile\_iam\_role\_permissions\_boundary) | If provided, all Fargate Profiles IAM roles will be created with this permissions boundary attached | `string` | `null` | no | | [fargate\_profiles](#input\_fargate\_profiles) | Fargate Profiles config |
map(object({
kubernetes_namespace = string
kubernetes_labels = map(string)
}))
| `{}` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [karpenter\_iam\_role\_enabled](#input\_karpenter\_iam\_role\_enabled) | Flag to enable/disable creation of IAM role for EC2 Instance Profile that is attached to the nodes launched by Karpenter | `bool` | `false` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use. Defaults to the current caller's role. | `string` | `null` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | Name of `kubeconfig` file to use to configure Kubernetes provider | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | Set true to configure Kubernetes provider with a `kubeconfig` file specified by `kubeconfig_file`.
Mainly for when the standard configuration produces a Terraform error. | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 81d871e31..e98ad654a 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -98,9 +98,9 @@ module "eks_cluster" { # exec_auth is more reliable than data_auth when the aws CLI is available # Details at https://github.com/cloudposse/terraform-aws-eks-cluster/releases/tag/0.42.0 kube_exec_auth_enabled = !var.kubeconfig_file_enabled - # If using `exec` method (recommended) for authentication, provide an explict + # If using `exec` method (recommended) for authentication, provide an explicit # IAM role ARN to exec as for authentication to EKS cluster. - kube_exec_auth_role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) kube_exec_auth_role_arn_enabled = true # Path to KUBECONFIG file to use to access the EKS cluster kubeconfig_path = var.kubeconfig_file diff --git a/modules/eks/cluster/providers.tf b/modules/eks/cluster/providers.tf index 9610c5073..195cc9718 100644 --- a/modules/eks/cluster/providers.tf +++ b/modules/eks/cluster/providers.tf @@ -2,8 +2,6 @@ provider "aws" { region = var.region assume_role { - # `terraform import` will not use data from a data source, - # so on import we have to explicitly specify the role # WARNING: # The EKS cluster is owned by the role that created it, and that # role is the only role that can access the cluster without an @@ -11,9 +9,8 @@ provider "aws" { # with the provisioned Terraform role and not an SSO role that could # be removed without notice. # - # i.e. Only NON SSO assumed roles such as spacelift assumed roles, can - # plan this terraform module. - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + # This should only be run using the target account's Terraform role. + role_arn = module.iam_roles.terraform_role_arn } } @@ -21,9 +18,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index e17e0353a..8b08bbd30 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -327,6 +327,12 @@ variable "kubeconfig_file_enabled" { EOF } +variable "kube_exec_auth_role_arn" { + type = string + default = null + description = "The role ARN for `aws eks get-token` to use. Defaults to the current caller's role." +} + variable "aws_auth_yaml_strip_quotes" { type = bool default = true diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index 690db3d29..b7102d4eb 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -227,8 +227,6 @@ https-checks: | [iam\_policy\_statements](#input\_iam\_policy\_statements) | IAM policy for the service account. Required if `var.iam_role_enabled` is `true`. This will not do variable replacements. Please see `var.iam_policy_statements_template_path`. | `any` | `{}` | no | | [iam\_role\_enabled](#input\_iam\_role\_enabled) | Whether to create an IAM role. Setting this to `true` will also replace any occurrences of `{service_account_role_arn}` in `var.values_template_path` with the ARN of the IAM role created by this module. | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/datadog-agent/provider-helm.tf b/modules/eks/datadog-agent/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/datadog-agent/provider-helm.tf +++ b/modules/eks/datadog-agent/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/datadog-agent/providers.tf b/modules/eks/datadog-agent/providers.tf index 4f6c5692d..45d458575 100644 --- a/modules/eks/datadog-agent/providers.tf +++ b/modules/eks/datadog-agent/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,17 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -provider "utils" {} diff --git a/modules/eks/ebs-controller/README.md b/modules/eks/ebs-controller/README.md index 807cb5f75..178de2cbf 100644 --- a/modules/eks/ebs-controller/README.md +++ b/modules/eks/ebs-controller/README.md @@ -64,9 +64,7 @@ components: |------|------| | [kubernetes_annotations.default_storage_class](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/annotations) | resource | | [kubernetes_storage_class.gp3_enc](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/storage_class) | resource | -| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | ## Inputs @@ -84,8 +82,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/ebs-controller/provider-helm.tf b/modules/eks/ebs-controller/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/ebs-controller/provider-helm.tf +++ b/modules/eks/ebs-controller/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/ebs-controller/providers.tf b/modules/eks/ebs-controller/providers.tf index 2775903d2..45d458575 100644 --- a/modules/eks/ebs-controller/providers.tf +++ b/modules/eks/ebs-controller/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,27 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -data "aws_eks_cluster" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} - -data "aws_eks_cluster_auth" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index ee13f6343..3d310e72d 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -95,9 +95,7 @@ components: | Name | Type | |------|------| -| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | ## Inputs @@ -121,8 +119,6 @@ components: | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [hostname\_template](#input\_hostname\_template) | The `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`"
Typically something like `"echo.%[3]v.%[2]v.example.com"`. | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [ingress\_type](#input\_ingress\_type) | Set to 'nginx' to create an ingress resource relying on an NGiNX backend for the echo-server service. Set to 'alb' to create an ingress resource relying on an AWS ALB backend for the echo-server service. Leave blank to not create any ingress for the echo-server service. | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | diff --git a/modules/eks/echo-server/provider-helm.tf b/modules/eks/echo-server/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/echo-server/provider-helm.tf +++ b/modules/eks/echo-server/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/echo-server/providers.tf b/modules/eks/echo-server/providers.tf index 2775903d2..45d458575 100644 --- a/modules/eks/echo-server/providers.tf +++ b/modules/eks/echo-server/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,27 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -data "aws_eks_cluster" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} - -data "aws_eks_cluster_auth" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} diff --git a/modules/eks/efs-controller/README.md b/modules/eks/efs-controller/README.md index 1968a429a..e92ee66c2 100644 --- a/modules/eks/efs-controller/README.md +++ b/modules/eks/efs-controller/README.md @@ -95,8 +95,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/efs-controller/provider-helm.tf b/modules/eks/efs-controller/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/efs-controller/provider-helm.tf +++ b/modules/eks/efs-controller/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/efs-controller/providers.tf b/modules/eks/efs-controller/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/efs-controller/providers.tf +++ b/modules/eks/efs-controller/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/eks-without-spotinst/README.md b/modules/eks/eks-without-spotinst/README.md index 7a67f1e2a..3cb012122 100644 --- a/modules/eks/eks-without-spotinst/README.md +++ b/modules/eks/eks-without-spotinst/README.md @@ -126,7 +126,6 @@ No resources. | [iam\_primary\_roles\_tenant\_name](#input\_iam\_primary\_roles\_tenant\_name) | The name of the tenant where the IAM primary roles are provisioned | `string` | `null` | no | | [iam\_roles\_environment\_name](#input\_iam\_roles\_environment\_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | Name of `kubeconfig` file to use to configure Kubernetes provider | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | Set true to configure Kubernetes provider with a `kubeconfig` file specified by `kubeconfig_file`.
Mainly for when the standard configuration produces a Terraform error. | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/eks/eks-without-spotinst/providers.tf b/modules/eks/eks-without-spotinst/providers.tf index 9610c5073..195cc9718 100644 --- a/modules/eks/eks-without-spotinst/providers.tf +++ b/modules/eks/eks-without-spotinst/providers.tf @@ -2,8 +2,6 @@ provider "aws" { region = var.region assume_role { - # `terraform import` will not use data from a data source, - # so on import we have to explicitly specify the role # WARNING: # The EKS cluster is owned by the role that created it, and that # role is the only role that can access the cluster without an @@ -11,9 +9,8 @@ provider "aws" { # with the provisioned Terraform role and not an SSO role that could # be removed without notice. # - # i.e. Only NON SSO assumed roles such as spacelift assumed roles, can - # plan this terraform module. - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + # This should only be run using the target account's Terraform role. + role_arn = module.iam_roles.terraform_role_arn } } @@ -21,9 +18,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index 484ced012..ae455d68b 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -104,8 +104,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [istio\_enabled](#input\_istio\_enabled) | Add istio gateways to monitored sources. | `bool` | `false` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | diff --git a/modules/eks/external-dns/provider-helm.tf b/modules/eks/external-dns/provider-helm.tf index abe6f9c56..64459d4f4 100644 --- a/modules/eks/external-dns/provider-helm.tf +++ b/modules/eks/external-dns/provider-helm.tf @@ -101,7 +101,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/modules/eks/external-dns/providers.tf b/modules/eks/external-dns/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/external-dns/providers.tf +++ b/modules/eks/external-dns/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index aa101f86f..b5b4c7e56 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -12,17 +12,21 @@ kind: ExternalSecret metadata: name: app-secrets spec: - refreshInterval: 0 + refreshInterval: 30s secretStoreRef: - name: "secret-store-parameter-store" # must match name of our store - kind: SecretStore + name: "secret-store-parameter-store" # Must match name of the Cluster Secret Store created by this component + kind: ClusterSecretStore target: - creationPolicy: 'Owner' + creationPolicy: Owner name: app-secrets dataFrom: - find: name: - regexp: "^/app" + regexp: "^/app/" # Match the path prefix of your service + rewrite: + - regexp: + source: "/app/(.*)" # Remove the path prefix of your service from the name before creating the envars + target: "$1" ``` This component assumes secrets are prefixed by "service" in parameter store (e.g. `/app/my_secret`). The `SecretStore`. The component is designed to pull secrets from a `path` prefix (defaulting to `"app"`). This should work nicely along `chamber` which uses this same path (called a "service" in Chamber). For example, developers should store keys like so. @@ -117,9 +121,7 @@ components: | Name | Type | |------|------| | [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | -| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | ## Inputs @@ -143,8 +145,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/external-secrets-operator/examples/app-secrets.yaml b/modules/eks/external-secrets-operator/examples/app-secrets.yaml index fcf509bcc..ea4928d7a 100644 --- a/modules/eks/external-secrets-operator/examples/app-secrets.yaml +++ b/modules/eks/external-secrets-operator/examples/app-secrets.yaml @@ -1,4 +1,4 @@ -# example to fetch all secrets underneath the `/app` prefix (service). +# example to fetch all secrets underneath the `/app/` prefix (service). # Keys are rewritten within the K8S Secret to be predictable and omit the # prefix. @@ -7,18 +7,18 @@ kind: ExternalSecret metadata: name: app-secrets spec: - refreshInterval: "0" + refreshInterval: 30s secretStoreRef: - name: "secret-store-parameter-store" - kind: SecretStore + name: "secret-store-parameter-store" # Must match name of the Cluster Secret Store created by this component + kind: ClusterSecretStore target: - creationPolicy: 'Owner' + creationPolicy: Owner name: app-secrets dataFrom: - - find: - name: - regexp: "^/app" - rewrite: - - regexp: - source: "/app/(.*)" - target: "$1" + - find: + name: + regexp: "^/app/" # Match the path prefix of your service + rewrite: + - regexp: + source: "/app/(.*)" # Remove the path prefix of your service from the name before creating the envars + target: "$1" diff --git a/modules/eks/external-secrets-operator/examples/external-secrets.yaml b/modules/eks/external-secrets-operator/examples/external-secrets.yaml index 8d042bb06..b88414ef2 100644 --- a/modules/eks/external-secrets-operator/examples/external-secrets.yaml +++ b/modules/eks/external-secrets-operator/examples/external-secrets.yaml @@ -5,12 +5,12 @@ kind: ExternalSecret metadata: name: single-secret spec: - refreshInterval: 0 + refreshInterval: 30s secretStoreRef: - name: "secret-store-parameter-store" - kind: SecretStore + name: "secret-store-parameter-store" # Must match name of the Cluster Secret Store created by this component + kind: ClusterSecretStore target: - creationPolicy: 'Owner' + creationPolicy: Owner name: single-secret data: - secretKey: good_secret diff --git a/modules/eks/external-secrets-operator/provider-helm.tf b/modules/eks/external-secrets-operator/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/external-secrets-operator/provider-helm.tf +++ b/modules/eks/external-secrets-operator/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/external-secrets-operator/providers.tf b/modules/eks/external-secrets-operator/providers.tf index 2775903d2..45d458575 100644 --- a/modules/eks/external-secrets-operator/providers.tf +++ b/modules/eks/external-secrets-operator/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,27 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -data "aws_eks_cluster" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} - -data "aws_eks_cluster_auth" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index cf38511d4..965b0c313 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -50,9 +50,7 @@ components: | Name | Type | |------|------| -| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | ## Inputs @@ -75,8 +73,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | @@ -84,7 +80,7 @@ components: | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | | [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | -| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1alpha1"` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | | [kubernetes\_namespace](#input\_kubernetes\_namespace) | Kubernetes namespace to install the release into | `string` | `"kube-system"` | no | diff --git a/modules/eks/idp-roles/provider-helm.tf b/modules/eks/idp-roles/provider-helm.tf index 325e24d83..64459d4f4 100644 --- a/modules/eks/idp-roles/provider-helm.tf +++ b/modules/eks/idp-roles/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -73,7 +79,7 @@ variable "kube_exec_auth_aws_profile_enabled" { variable "kubeconfig_exec_auth_api_version" { type = string - default = "client.authentication.k8s.io/v1alpha1" + default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" } @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/idp-roles/providers.tf b/modules/eks/idp-roles/providers.tf index 2775903d2..45d458575 100644 --- a/modules/eks/idp-roles/providers.tf +++ b/modules/eks/idp-roles/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,27 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -data "aws_eks_cluster" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} - -data "aws_eks_cluster_auth" "kubernetes" { - count = local.enabled ? 1 : 0 - - name = module.eks.outputs.eks_cluster_id -} diff --git a/modules/eks/karpenter-provisioner/README.md b/modules/eks/karpenter-provisioner/README.md index 51ea41783..d49c06e23 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/modules/eks/karpenter-provisioner/README.md @@ -141,8 +141,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/karpenter-provisioner/provider-helm.tf b/modules/eks/karpenter-provisioner/provider-helm.tf index abe6f9c56..64459d4f4 100644 --- a/modules/eks/karpenter-provisioner/provider-helm.tf +++ b/modules/eks/karpenter-provisioner/provider-helm.tf @@ -101,7 +101,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/modules/eks/karpenter-provisioner/providers.tf b/modules/eks/karpenter-provisioner/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/karpenter-provisioner/providers.tf +++ b/modules/eks/karpenter-provisioner/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index e18c77c69..39ccf242b 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -18,19 +18,20 @@ components: # Base component of all `karpenter` components eks/karpenter: metadata: - component: eks/karpenter type: abstract + settings: + spacelift: + workspace_enabled: true vars: enabled: true - eks_component_name: "eks/cluster" tags: Team: sre Service: karpenter + eks_component_name: eks/cluster name: "karpenter" - # https://github.com/aws/karpenter/tree/main/charts/karpenter - chart_repository: "oci://public.ecr.aws/karpenter" chart: "karpenter" - chart_version: "v0.27.5" + chart_repository: "https://charts.karpenter.sh" + chart_version: "v0.16.3" create_namespace: true kubernetes_namespace: "karpenter" resources: @@ -40,17 +41,10 @@ components: requests: cpu: "100m" memory: "512Mi" - cleanup_on_fail: false - atomic: false + cleanup_on_fail: true + atomic: true wait: true rbac_enabled: true - interruption_handler_enabled: true - chart_values: - serviceMonitor: - enabled: true - replicas: 1 - aws: - enableENILimitedPodDensity: false # Provision `karpenter` component on the blue EKS cluster eks/karpenter-blue: @@ -291,24 +285,6 @@ For more details, refer to: - https://aws.github.io/aws-eks-best-practices/karpenter - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html -## Node Interruption - -Karpenter also supports Node Interruption. If interruption-handling is enabled, Karpenter will watch for upcoming involuntary interruption events that would cause disruption to your workloads. These interruption events include: - -- Spot Interruption Warnings -- Scheduled Change Health Events (Maintenance Events) -- Instance Terminating Events -- Instance Stopping Events - -:::info - -The Node Interruption Handler is not the same as the Node Termination Handler. The latter works fine and cleanly shuts down the node in 2 minutes. The former gets advance notice, so it can have 5-10 minutes to shut down a node. - -::: - -For more details, see refer to [Karpenter docs](https://karpenter.sh/v0.27.5/concepts/deprovisioning/#interruption) - -To enable Node Interruption, set `var.interruption_handler_enabled` to `true`. This will enable a SQS queue and a set of Event Bridge rules to handle interruption with Karpenter. ## Requirements @@ -370,8 +346,6 @@ To enable Node Interruption, set `var.interruption_handler_enabled` to `true`. T | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [interruption\_handler\_enabled](#input\_interruption\_handler\_enabled) | If `true`, deploy a SQS queue and Event Bridge rules to enable interruption handling by Karpenter.

https://karpenter.sh/v0.27.5/concepts/deprovisioning/#interruption | `bool` | `false` | no | | [interruption\_queue\_message\_retention](#input\_interruption\_queue\_message\_retention) | The message retention in seconds for the interruption handler SQS queue. | `number` | `300` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | diff --git a/modules/eks/karpenter/provider-helm.tf b/modules/eks/karpenter/provider-helm.tf index abe6f9c56..64459d4f4 100644 --- a/modules/eks/karpenter/provider-helm.tf +++ b/modules/eks/karpenter/provider-helm.tf @@ -101,7 +101,7 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] diff --git a/modules/eks/karpenter/providers.tf b/modules/eks/karpenter/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/karpenter/providers.tf +++ b/modules/eks/karpenter/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index fc7d1a140..6abba6e3a 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -92,8 +92,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/metrics-server/provider-helm.tf b/modules/eks/metrics-server/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/metrics-server/provider-helm.tf +++ b/modules/eks/metrics-server/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/metrics-server/providers.tf b/modules/eks/metrics-server/providers.tf index 74ff8e62c..45d458575 100644 --- a/modules/eks/metrics-server/providers.tf +++ b/modules/eks/metrics-server/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/platform/README.md b/modules/eks/platform/README.md index 2bdea43ad..0fec581a7 100644 --- a/modules/eks/platform/README.md +++ b/modules/eks/platform/README.md @@ -93,8 +93,6 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/eks/platform/providers.tf b/modules/eks/platform/providers.tf index af67fa874..45d458575 100644 --- a/modules/eks/platform/providers.tf +++ b/modules/eks/platform/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,17 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -provider "jq" {} diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 8e4e88558..96f3ce473 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -120,8 +120,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/redis-operator/provider-helm.tf b/modules/eks/redis-operator/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/redis-operator/provider-helm.tf +++ b/modules/eks/redis-operator/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/redis-operator/providers.tf b/modules/eks/redis-operator/providers.tf index 74ff8e62c..45d458575 100644 --- a/modules/eks/redis-operator/providers.tf +++ b/modules/eks/redis-operator/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index 513763944..118d18120 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -126,8 +126,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/redis/provider-helm.tf b/modules/eks/redis/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/redis/provider-helm.tf +++ b/modules/eks/redis/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/redis/providers.tf b/modules/eks/redis/providers.tf index 74ff8e62c..45d458575 100644 --- a/modules/eks/redis/providers.tf +++ b/modules/eks/redis/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/eks/reloader/README.md b/modules/eks/reloader/README.md index 249881ff5..8eda55a17 100644 --- a/modules/eks/reloader/README.md +++ b/modules/eks/reloader/README.md @@ -83,8 +83,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/reloader/provider-helm.tf b/modules/eks/reloader/provider-helm.tf index 21cecf145..64459d4f4 100644 --- a/modules/eks/reloader/provider-helm.tf +++ b/modules/eks/reloader/provider-helm.tf @@ -2,6 +2,12 @@ # # This file is a drop-in to provide a helm provider. # +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# # All the following variables are just about configuring the Kubernetes provider # to be able to modify EKS cluster. The reason there are so many options is # because at various times, each one of them has had problems, so we give you a choice. @@ -95,14 +101,16 @@ locals { "--profile", var.kube_exec_auth_aws_profile ] : [] - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ "--role-arn", local.kube_exec_auth_role_arn ] : [] - certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data - eks_cluster_id = module.eks.outputs.eks_cluster_id - eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -114,14 +122,14 @@ provider "helm" { kubernetes { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -132,21 +140,21 @@ provider "helm" { } } experiments { - manifest = var.helm_manifest_experiment_enabled + manifest = var.helm_manifest_experiment_enabled && module.this.enabled } } provider "kubernetes" { host = local.eks_cluster_endpoint cluster_ca_certificate = base64decode(local.certificate_authority_data) - token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/reloader/providers.tf b/modules/eks/reloader/providers.tf index c2419aabb..45d458575 100644 --- a/modules/eks/reloader/providers.tf +++ b/modules/eks/reloader/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md index ba3df5929..2f591fbb9 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -112,8 +112,6 @@ No resources. | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [family](#input\_family) | Redis family | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [ingress\_cidr\_blocks](#input\_ingress\_cidr\_blocks) | CIDR blocks for permitted ingress | `list(string)` | n/a | yes | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/elasticache-redis/providers.tf b/modules/elasticache-redis/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/elasticache-redis/providers.tf +++ b/modules/elasticache-redis/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index 25b995986..a6ad798c3 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -85,8 +85,6 @@ components: | [encrypt\_at\_rest\_enabled](#input\_encrypt\_at\_rest\_enabled) | Whether to enable encryption at rest | `bool` | n/a | yes | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_type](#input\_instance\_type) | The type of the instance | `string` | n/a | yes | | [kibana\_hostname\_enabled](#input\_kibana\_hostname\_enabled) | Explicit flag to enable creating a DNS hostname for Kibana. If `true`, then `var.dns_zone_id` is required. | `bool` | n/a | yes | | [kibana\_subdomain\_name](#input\_kibana\_subdomain\_name) | The name of the subdomain for Kibana in the DNS zone (\_e.g.\_ `kibana`, `ui`, `ui-es`, `search-ui`, `kibana.elasticsearch`) | `string` | n/a | yes | diff --git a/modules/elasticsearch/providers.tf b/modules/elasticsearch/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/elasticsearch/providers.tf +++ b/modules/elasticsearch/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/github-action-token-rotator/README.md b/modules/github-action-token-rotator/README.md index 96e2725aa..9ddcffecd 100644 --- a/modules/github-action-token-rotator/README.md +++ b/modules/github-action-token-rotator/README.md @@ -66,8 +66,6 @@ No resources. | [github\_app\_installation\_id](#input\_github\_app\_installation\_id) | GitHub App Installation ID | `string` | n/a | yes | | [github\_org\_name](#input\_github\_org\_name) | SSM parameter name format | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/github-action-token-rotator/providers.tf b/modules/github-action-token-rotator/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/github-action-token-rotator/providers.tf +++ b/modules/github-action-token-rotator/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index 67f17fadf..b648fd18e 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -91,7 +91,6 @@ permissions: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/github-oidc-provider/providers.tf b/modules/github-oidc-provider/providers.tf index f1096ef8d..54257fd20 100644 --- a/modules/github-oidc-provider/providers.tf +++ b/modules/github-oidc-provider/providers.tf @@ -1,26 +1,19 @@ provider "aws" { region = var.region - # github-oidc-provider, since it authorizes SAML IdPs, should be run as SuperAdmin as a security matter, - # and therefore cannot use "profile" instead of "role_arn" even if the components are generally using profiles. - # Note the role_arn is the ARN of the OrganizationAccountAccessRole, not the SAML role. + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = var.import_role_arn == null ? (module.iam_roles.org_role_arn != null ? [true] : []) : ["import"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.org_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } module "iam_roles" { - source = "../account-map/modules/iam-roles" - privileged = true - context = module.this.context -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" + source = "../account-map/modules/iam-roles" + context = module.this.context } diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index ef0139887..821857b65 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -228,8 +228,6 @@ chamber write github token | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [github\_scope](#input\_github\_scope) | Scope of the runner (e.g. `cloudposse/example` for repo or `cloudposse` for org) | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_type](#input\_instance\_type) | Default instance type for the action runner. | `string` | `"m5.large"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/github-runners/providers.tf b/modules/github-runners/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/github-runners/providers.tf +++ b/modules/github-runners/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/global-accelerator-endpoint-group/README.md b/modules/global-accelerator-endpoint-group/README.md index d0420d9fb..c8cf2c108 100644 --- a/modules/global-accelerator-endpoint-group/README.md +++ b/modules/global-accelerator-endpoint-group/README.md @@ -60,8 +60,6 @@ No resources. | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [global\_accelerator\_environment\_name](#input\_global\_accelerator\_environment\_name) | The name of the environment where the global component `global_accelerator` is provisioned | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/global-accelerator-endpoint-group/providers.tf b/modules/global-accelerator-endpoint-group/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/global-accelerator-endpoint-group/providers.tf +++ b/modules/global-accelerator-endpoint-group/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/global-accelerator/README.md b/modules/global-accelerator/README.md index 47bc35e42..da42d6522 100644 --- a/modules/global-accelerator/README.md +++ b/modules/global-accelerator/README.md @@ -68,8 +68,6 @@ No resources. | [flow\_logs\_s3\_bucket\_tenant](#input\_flow\_logs\_s3\_bucket\_tenant) | The tenant where the S3 Bucket for the Accelerator Flow Logs exists. Required if `var.flow_logs_enabled` is set to `true`. | `string` | `null` | no | | [flow\_logs\_s3\_prefix](#input\_flow\_logs\_s3\_prefix) | The Object Prefix within the S3 Bucket for the Accelerator Flow Logs. Required if `var.flow_logs_enabled` is set to `true`. | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/global-accelerator/providers.tf b/modules/global-accelerator/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/global-accelerator/providers.tf +++ b/modules/global-accelerator/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/guardduty/common/README.md b/modules/guardduty/common/README.md index c672af016..5fd4b4b10 100644 --- a/modules/guardduty/common/README.md +++ b/modules/guardduty/common/README.md @@ -122,8 +122,6 @@ atmos terraform apply guardduty/common-uw1 -s core-uw1-security | [findings\_notification\_arn](#input\_findings\_notification\_arn) | The ARN for an SNS topic to send findings notifications to. This is only used if create\_sns\_topic is false.
If you want to send findings to an existing SNS topic, set the value of this to the ARN of the existing topic and set
create\_sns\_topic to false. | `string` | `null` | no | | [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/guardduty/common/providers.tf b/modules/guardduty/common/providers.tf index 83b416f43..45d458575 100644 --- a/modules/guardduty/common/providers.tf +++ b/modules/guardduty/common/providers.tf @@ -1,41 +1,19 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -provider "awsutils" { - region = var.region + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } module "iam_roles" { - source = "../../account-map/modules/iam-roles" - + source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/guardduty/root/README.md b/modules/guardduty/root/README.md index b18e8910b..eb9b5c914 100644 --- a/modules/guardduty/root/README.md +++ b/modules/guardduty/root/README.md @@ -46,6 +46,7 @@ Please see instructions in [guardduty/common/README](../common/README.md) for in | Name | Source | Version | |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | diff --git a/modules/guardduty/root/providers.tf b/modules/guardduty/root/providers.tf index dc58d9a25..45d458575 100644 --- a/modules/guardduty/root/providers.tf +++ b/modules/guardduty/root/providers.tf @@ -1,3 +1,19 @@ provider "aws" { region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context } diff --git a/modules/iam-role/README.md b/modules/iam-role/README.md index 050946982..91886f4a0 100644 --- a/modules/iam-role/README.md +++ b/modules/iam-role/README.md @@ -95,8 +95,6 @@ No resources. | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_profile\_enabled](#input\_instance\_profile\_enabled) | Create EC2 Instance Profile for the role | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/iam-role/providers.tf b/modules/iam-role/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/iam-role/providers.tf +++ b/modules/iam-role/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/iam-service-linked-roles/README.md b/modules/iam-service-linked-roles/README.md index 7ad3912e3..1c2302c99 100644 --- a/modules/iam-service-linked-roles/README.md +++ b/modules/iam-service-linked-roles/README.md @@ -89,8 +89,6 @@ For more details, see: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/iam-service-linked-roles/providers.tf b/modules/iam-service-linked-roles/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/iam-service-linked-roles/providers.tf +++ b/modules/iam-service-linked-roles/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/kinesis-stream/README.md b/modules/kinesis-stream/README.md index 1cdfdbbe6..5496e380a 100644 --- a/modules/kinesis-stream/README.md +++ b/modules/kinesis-stream/README.md @@ -84,8 +84,6 @@ No resources. | [enforce\_consumer\_deletion](#input\_enforce\_consumer\_deletion) | Forcefully delete stream consumers before destroying the stream. | `bool` | `true` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_key\_id](#input\_kms\_key\_id) | The name of the KMS key to use for encryption. | `string` | `"alias/aws/kinesis"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/kinesis-stream/providers.tf b/modules/kinesis-stream/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/kinesis-stream/providers.tf +++ b/modules/kinesis-stream/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/kms/providers.tf b/modules/kms/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/kms/providers.tf +++ b/modules/kms/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/lakeformation/README.md b/modules/lakeformation/README.md index bbddf27a5..2ba4b509b 100644 --- a/modules/lakeformation/README.md +++ b/modules/lakeformation/README.md @@ -99,8 +99,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/lakeformation/providers.tf b/modules/lakeformation/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/lakeformation/providers.tf +++ b/modules/lakeformation/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/lambda/README.md b/modules/lambda/README.md index 6e41472db..d212189ee 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -119,8 +119,6 @@ components: | [ignore\_external\_function\_updates](#input\_ignore\_external\_function\_updates) | Ignore updates to the Lambda Function executed externally to the Terraform lifecycle. Set this to `true` if you're
using CodeDeploy, aws CLI or other external tools to update the Lambda Function code." | `bool` | `false` | no | | [image\_config](#input\_image\_config) | The Lambda OCI [image configurations](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function#image_config)
block with three (optional) arguments:
- *entry\_point* - The ENTRYPOINT for the docker image (type `list(string)`).
- *command* - The CMD for the docker image (type `list(string)`).
- *working\_directory* - The working directory for the docker image (type `string`). | `any` | `{}` | no | | [image\_uri](#input\_image\_uri) | The ECR image URI containing the function's deployment package. Conflicts with `filename`, `s3_bucket_name`, `s3_key`, and `s3_object_version`. | `string` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | Amazon Resource Name (ARN) of the AWS Key Management Service (KMS) key that is used to encrypt environment variables.
If this configuration is not provided when environment variables are in use, AWS Lambda uses a default service key.
If this configuration is provided when environment variables are not in use, the AWS Lambda API does not save this
configuration and Terraform will show a perpetual difference of adding the key. To fix the perpetual difference,
remove this configuration. | `string` | `""` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/lambda/providers.tf b/modules/lambda/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/lambda/providers.tf +++ b/modules/lambda/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/mq-broker/README.md b/modules/mq-broker/README.md index 7a67934bd..70229992e 100644 --- a/modules/mq-broker/README.md +++ b/modules/mq-broker/README.md @@ -79,8 +79,6 @@ No resources. | [general\_log\_enabled](#input\_general\_log\_enabled) | Enables general logging via CloudWatch | `bool` | `true` | no | | [host\_instance\_type](#input\_host\_instance\_type) | The broker's instance type. e.g. mq.t2.micro or mq.m4.large | `string` | `"mq.t3.micro"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_mq\_key\_arn](#input\_kms\_mq\_key\_arn) | ARN of the AWS KMS key used for Amazon MQ encryption | `string` | `null` | no | | [kms\_ssm\_key\_arn](#input\_kms\_ssm\_key\_arn) | ARN of the AWS KMS key used for SSM encryption | `string` | `"alias/aws/ssm"` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/mq-broker/providers.tf b/modules/mq-broker/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/mq-broker/providers.tf +++ b/modules/mq-broker/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/mwaa/README.md b/modules/mwaa/README.md index 01d21278a..dd9fa0db3 100644 --- a/modules/mwaa/README.md +++ b/modules/mwaa/README.md @@ -100,8 +100,6 @@ components: | [environment\_class](#input\_environment\_class) | Environment class for the cluster. Possible options are mw1.small, mw1.medium, mw1.large. | `string` | `"mw1.small"` | no | | [execution\_role\_arn](#input\_execution\_role\_arn) | If `create_iam_role` is `false` then set this to the target MWAA execution role | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/mwaa/providers.tf b/modules/mwaa/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/mwaa/providers.tf +++ b/modules/mwaa/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/network-firewall/README.md b/modules/network-firewall/README.md index 4db5de917..d13bed98c 100644 --- a/modules/network-firewall/README.md +++ b/modules/network-firewall/README.md @@ -277,8 +277,6 @@ No resources. | [firewall\_subnet\_name](#input\_firewall\_subnet\_name) | Firewall subnet name | `string` | `"firewall"` | no | | [flow\_logs\_bucket\_component\_name](#input\_flow\_logs\_bucket\_component\_name) | Flow logs bucket component name | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/network-firewall/providers.tf b/modules/network-firewall/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/network-firewall/providers.tf +++ b/modules/network-firewall/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index c21722b5d..fa5b375f1 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -301,8 +301,6 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [escalations](#input\_escalations) | Escalations to configure and create for the team. | `map(any)` | `{}` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [integrations](#input\_integrations) | API Integrations for the team. If not specified, `datadog` is assumed. | `map(any)` | `{}` | no | | [integrations\_enabled](#input\_integrations\_enabled) | Whether to enable the integrations submodule or not | `bool` | `true` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | AWS KMS key used for writing to SSM | `string` | `"alias/aws/ssm"` | no | diff --git a/modules/opsgenie-team/provider-opsgenie.tf b/modules/opsgenie-team/provider-opsgenie.tf new file mode 100644 index 000000000..5b067ef80 --- /dev/null +++ b/modules/opsgenie-team/provider-opsgenie.tf @@ -0,0 +1,8 @@ +data "aws_ssm_parameter" "opsgenie_api_key" { + name = format(var.ssm_parameter_name_format, var.ssm_path, "opsgenie_api_key") + with_decryption = true +} + +provider "opsgenie" { + api_key = join("", data.aws_ssm_parameter.opsgenie_api_key[*].value) +} diff --git a/modules/opsgenie-team/providers.tf b/modules/opsgenie-team/providers.tf index 265107f7b..54257fd20 100644 --- a/modules/opsgenie-team/providers.tf +++ b/modules/opsgenie-team/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,24 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -data "aws_ssm_parameter" "opsgenie_api_key" { - name = format(var.ssm_parameter_name_format, var.ssm_path, "opsgenie_api_key") - with_decryption = true -} - -provider "opsgenie" { - api_key = join("", data.aws_ssm_parameter.opsgenie_api_key[*].value) -} diff --git a/modules/rds/README.md b/modules/rds/README.md index 2566c3c0f..fdee4fa61 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -172,8 +172,6 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | [host\_name](#input\_host\_name) | The DB host name created in Route53 | `string` | `"db"` | no | | [iam\_database\_authentication\_enabled](#input\_iam\_database\_authentication\_enabled) | Specifies whether or mappings of AWS Identity and Access Management (IAM) accounts to database accounts is enabled | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_class](#input\_instance\_class) | Class of RDS instance | `string` | n/a | yes | | [iops](#input\_iops) | The amount of provisioned IOPS. Setting this implies a storage\_type of 'io1'. Default is 0 if rds storage type is not 'io1' | `number` | `0` | no | | [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | diff --git a/modules/rds/providers.tf b/modules/rds/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/rds/providers.tf +++ b/modules/rds/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/redshift/README.md b/modules/redshift/README.md index c5b956515..bc440e033 100644 --- a/modules/redshift/README.md +++ b/modules/redshift/README.md @@ -98,8 +98,6 @@ components: | [engine\_version](#input\_engine\_version) | The version of the Amazon Redshift engine to use. See https://docs.aws.amazon.com/redshift/latest/mgmt/cluster-versions.html | `string` | `"1.0"` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/redshift/providers.tf b/modules/redshift/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/redshift/providers.tf +++ b/modules/redshift/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/route53-resolver-dns-firewall/README.md b/modules/route53-resolver-dns-firewall/README.md index 7f5939a20..e467d0b4d 100644 --- a/modules/route53-resolver-dns-firewall/README.md +++ b/modules/route53-resolver-dns-firewall/README.md @@ -116,8 +116,6 @@ No resources. | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [firewall\_fail\_open](#input\_firewall\_fail\_open) | Determines how Route 53 Resolver handles queries during failures, for example when all traffic that is sent to DNS Firewall fails to receive a reply.
By default, fail open is disabled, which means the failure mode is closed.
This approach favors security over availability. DNS Firewall blocks queries that it is unable to evaluate properly.
If you enable this option, the failure mode is open. This approach favors availability over security.
In this case, DNS Firewall allows queries to proceed if it is unable to properly evaluate them.
Valid values: ENABLED, DISABLED. | `string` | `"ENABLED"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/route53-resolver-dns-firewall/providers.tf b/modules/route53-resolver-dns-firewall/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/route53-resolver-dns-firewall/providers.tf +++ b/modules/route53-resolver-dns-firewall/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index 8d6474458..55270021b 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -139,8 +139,6 @@ components: | [iam\_policy\_statements](#input\_iam\_policy\_statements) | Map of IAM policy statements to use in the bucket policy. | `any` | `{}` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [ignore\_public\_acls](#input\_ignore\_public\_acls) | Set to `false` to disable the ignoring of public access lists on the bucket | `bool` | `true` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_master\_key\_arn](#input\_kms\_master\_key\_arn) | The AWS KMS master key ARN used for the `SSE-KMS` encryption. This can only be used when you set the value of `sse_algorithm` as `aws:kms`. The default aws/s3 AWS KMS master key is used if this element is absent while the `sse_algorithm` is `aws:kms` | `string` | `""` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/s3-bucket/providers.tf b/modules/s3-bucket/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/s3-bucket/providers.tf +++ b/modules/s3-bucket/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/securityhub/common/README.md b/modules/securityhub/common/README.md index 2a13b4779..21e1e3761 100644 --- a/modules/securityhub/common/README.md +++ b/modules/securityhub/common/README.md @@ -150,8 +150,6 @@ done | [finding\_aggregator\_regions](#input\_finding\_aggregator\_regions) | A list of regions to aggregate findings from.

This is only used if `finding_aggregator_enabled` is `true`. | `any` | `null` | no | | [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/securityhub/common/providers.tf b/modules/securityhub/common/providers.tf index 83b416f43..45d458575 100644 --- a/modules/securityhub/common/providers.tf +++ b/modules/securityhub/common/providers.tf @@ -1,41 +1,19 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -provider "awsutils" { - region = var.region + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } module "iam_roles" { - source = "../../account-map/modules/iam-roles" - + source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/securityhub/root/README.md b/modules/securityhub/root/README.md index 07feda735..371a8a756 100644 --- a/modules/securityhub/root/README.md +++ b/modules/securityhub/root/README.md @@ -49,6 +49,7 @@ Please see instructions in [securityhub/README](../common/README.md) for informa | Name | Source | Version | |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/securityhub/root/providers.tf b/modules/securityhub/root/providers.tf index dc58d9a25..45d458575 100644 --- a/modules/securityhub/root/providers.tf +++ b/modules/securityhub/root/providers.tf @@ -1,3 +1,19 @@ provider "aws" { region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context } diff --git a/modules/ses/README.md b/modules/ses/README.md index 05583a331..3ae8a5afe 100644 --- a/modules/ses/README.md +++ b/modules/ses/README.md @@ -73,8 +73,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/ses/provider-awsutils.mixin.tf b/modules/ses/provider-awsutils.mixin.tf new file mode 100644 index 000000000..db8bd2c1d --- /dev/null +++ b/modules/ses/provider-awsutils.mixin.tf @@ -0,0 +1,14 @@ +provider "awsutils" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} diff --git a/modules/ses/providers.tf b/modules/ses/providers.tf index aeed337fa..54257fd20 100644 --- a/modules/ses/providers.tf +++ b/modules/ses/providers.tf @@ -1,27 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -provider "awsutils" { - # TODO: remove skip_region_validation until awsutils 0.11.1 can be downloaded from the registry - skip_region_validation = true - region = var.region - - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -30,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/sftp/README.md b/modules/sftp/README.md index 9748c903f..f97eeb362 100644 --- a/modules/sftp/README.md +++ b/modules/sftp/README.md @@ -71,7 +71,6 @@ components: | [force\_destroy](#input\_force\_destroy) | Forces the AWS Transfer Server to be destroyed | `bool` | `false` | no | | [hosted\_zone\_suffix](#input\_hosted\_zone\_suffix) | The hosted zone name suffix. The stage name will be prefixed to this suffix. | `string` | n/a | yes | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/sftp/provider-awsutils.mixin.tf b/modules/sftp/provider-awsutils.mixin.tf new file mode 100644 index 000000000..db8bd2c1d --- /dev/null +++ b/modules/sftp/provider-awsutils.mixin.tf @@ -0,0 +1,14 @@ +provider "awsutils" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} diff --git a/modules/sftp/providers.tf b/modules/sftp/providers.tf index 8d0c33309..54257fd20 100644 --- a/modules/sftp/providers.tf +++ b/modules/sftp/providers.tf @@ -1,25 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -provider "awsutils" { - region = var.region - - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null - - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -28,9 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/snowflake-account/README.md b/modules/snowflake-account/README.md index 26175dd70..0fce6874f 100644 --- a/modules/snowflake-account/README.md +++ b/modules/snowflake-account/README.md @@ -116,8 +116,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [global\_environment\_name](#input\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/snowflake-account/provider-snowflake.tf b/modules/snowflake-account/provider-snowflake.tf new file mode 100644 index 000000000..46eed29fa --- /dev/null +++ b/modules/snowflake-account/provider-snowflake.tf @@ -0,0 +1,12 @@ +data "aws_ssm_parameter" "snowflake_password" { + count = local.enabled ? 1 : 0 + name = local.ssm_path_admin_user_password + with_decryption = true +} + +provider "snowflake" { + account = var.snowflake_account + region = "${var.snowflake_account_region}.aws" # required to append ".aws" to region, see https://github.com/chanzuckerberg/terraform-provider-snowflake/issues/529 + username = local.admin_username + password = data.aws_ssm_parameter.snowflake_password[0].value +} diff --git a/modules/snowflake-account/providers.tf b/modules/snowflake-account/providers.tf index e611933c5..54257fd20 100644 --- a/modules/snowflake-account/providers.tf +++ b/modules/snowflake-account/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,28 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -data "aws_ssm_parameter" "snowflake_password" { - count = local.enabled ? 1 : 0 - name = local.ssm_path_admin_user_password - with_decryption = true -} - -provider "snowflake" { - account = var.snowflake_account - region = "${var.snowflake_account_region}.aws" # required to append ".aws" to region, see https://github.com/chanzuckerberg/terraform-provider-snowflake/issues/529 - username = local.admin_username - password = data.aws_ssm_parameter.snowflake_password[0].value -} diff --git a/modules/snowflake-database/README.md b/modules/snowflake-database/README.md index dae66f2e9..277c74120 100644 --- a/modules/snowflake-database/README.md +++ b/modules/snowflake-database/README.md @@ -100,8 +100,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/snowflake-database/provider-snowflake.tf b/modules/snowflake-database/provider-snowflake.tf new file mode 100644 index 000000000..777c558b6 --- /dev/null +++ b/modules/snowflake-database/provider-snowflake.tf @@ -0,0 +1,18 @@ +data "aws_ssm_parameter" "snowflake_username" { + count = local.enabled ? 1 : 0 + name = module.snowflake_account.outputs.ssm_path_terraform_user_name +} + +data "aws_ssm_parameter" "snowflake_private_key" { + count = local.enabled ? 1 : 0 + name = module.snowflake_account.outputs.ssm_path_terraform_user_private_key + with_decryption = true +} + +provider "snowflake" { + account = local.snowflake_account + # required to append ".aws" to region, see https://github.com/chanzuckerberg/terraform-provider-snowflake/issues/529 + region = "${local.snowflake_account_region}.aws" + username = data.aws_ssm_parameter.snowflake_username[0].value + private_key = data.aws_ssm_parameter.snowflake_private_key[0].value +} diff --git a/modules/snowflake-database/providers.tf b/modules/snowflake-database/providers.tf index c0018f0a3..54257fd20 100644 --- a/modules/snowflake-database/providers.tf +++ b/modules/snowflake-database/providers.tf @@ -1,11 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -14,34 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -data "aws_ssm_parameter" "snowflake_username" { - count = local.enabled ? 1 : 0 - name = module.snowflake_account.outputs.ssm_path_terraform_user_name -} - -data "aws_ssm_parameter" "snowflake_private_key" { - count = local.enabled ? 1 : 0 - name = module.snowflake_account.outputs.ssm_path_terraform_user_private_key - with_decryption = true -} - -provider "snowflake" { - account = local.snowflake_account - # required to append ".aws" to region, see https://github.com/chanzuckerberg/terraform-provider-snowflake/issues/529 - region = "${local.snowflake_account_region}.aws" - username = data.aws_ssm_parameter.snowflake_username[0].value - private_key = data.aws_ssm_parameter.snowflake_private_key[0].value -} diff --git a/modules/sns-topic/README.md b/modules/sns-topic/README.md index bc7f61e66..3fb0a61d9 100644 --- a/modules/sns-topic/README.md +++ b/modules/sns-topic/README.md @@ -108,8 +108,6 @@ No resources. | [fifo\_queue\_enabled](#input\_fifo\_queue\_enabled) | Whether or not to create a FIFO (first-in-first-out) queue | `bool` | `false` | no | | [fifo\_topic](#input\_fifo\_topic) | Whether or not to create a FIFO (first-in-first-out) topic | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_master\_key\_id](#input\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SNS or a custom CMK. | `string` | `"alias/aws/sns"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/sns-topic/providers.tf b/modules/sns-topic/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/sns-topic/providers.tf +++ b/modules/sns-topic/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index e85d771a7..5ad68239b 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -184,8 +184,6 @@ an extensive explanation for how these preview environments work. | [github\_runners\_stage\_name](#input\_github\_runners\_stage\_name) | The stage name where the CloudTrail bucket is provisioned | `string` | `"auto"` | no | | [github\_runners\_tenant\_name](#input\_github\_runners\_tenant\_name) | The tenant name where the GitHub Runners are provisioned | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/spa-s3-cloudfront/provider-other-regions.tf b/modules/spa-s3-cloudfront/provider-other-regions.tf new file mode 100644 index 000000000..1ce639363 --- /dev/null +++ b/modules/spa-s3-cloudfront/provider-other-regions.tf @@ -0,0 +1,34 @@ +provider "aws" { + region = local.failover_region # if var.failover_s3_region is not set, this will fall back on var.region + + alias = "failover" + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +# For cloudfront, the acm has to be created in us-east-1 or it will not work +provider "aws" { + region = "us-east-1" + + alias = "us-east-1" + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} diff --git a/modules/spa-s3-cloudfront/providers.tf b/modules/spa-s3-cloudfront/providers.tf index fef70fbce..54257fd20 100644 --- a/modules/spa-s3-cloudfront/providers.tf +++ b/modules/spa-s3-cloudfront/providers.tf @@ -1,39 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -provider "aws" { - region = local.failover_region # if var.failover_s3_region is not set, this will fall back on var.region - - alias = "failover" - - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) - } - } -} - -# For cloudfront, the acm has to be created in us-east-1 or it will not work -provider "aws" { - region = "us-east-1" - - alias = "us-east-1" - - dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] - content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -42,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index 5f2d2d991..993d9e337 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -246,62 +246,199 @@ during a Terraform run. ```bash chmod +x rootfs/usr/local/bin/spacelift* ``` -This folder contains a set of components used to manage [Spacelift](https://docs.spacelift.io/) in an -[atmos-opinionated](https://atmos.tools/) way. ## Bootstrapping -### Environment Setup +After creating & linking Spacelift to this repo (see the +[docs](https://docs.spacelift.io/integrations/github)), follow these steps... -Since `spacelift` is designed to automate the plan/apply/destroy cycles of each component instance in an atmos -environment, we have a chicken-and-egg problem where spacelift needs to be configured manually before it can manage the -rest of the infrastructure. This section walks through the initial bootstrapping process. +### Deploy the [`spacelift-worker-pool`](../spacelift-worker-pool) Component -1. First, to authenticate to `spacelift` from outside of the spacelift environment, we are going to use the - [spacectl](https://github.com/spacelift-io/spacectl) command-line interface tool. If you use a - [geodesic](https://github.com/cloudposse/geodesic) shell configured by Cloud Posse, this is already installed. If you - use another method, follow the installation instructions in the [spacectl](https://github.com/spacelift-io/spacectl) - repo. +See [`spacelift-worker-pool` README](../spacelift-worker-pool/README.md) for the configuration and deployment needs. -1. Now that we have `spacectl` installed, let's configure it for our `spacelift` instance: +### Update the `spacelift` catalog - 1. Run `spacectl profile login acme` replacing `acme` with your company's name. - 1. Enter your company's spacelift URL when prompted (e.g., https://acme.app.spacelift.io) - 1. Select `for login with a web browser` as your authentication type - 1. Your browser will be opened, and you will log in to spacelift. +1. `git_repository` = Name of `infrastructure` repository +1. `git_branch` = Name of main/master branch +1. `worker_pool_name_id_map` = Map of arbitrary names to IDs Spacelift worker pools, +taken from the `worker_pool_id` output of the `spacelift-worker-pool` component. +1. Set `components.terraform.spacelift.settings.spacelift.worker_pool_name` +to the name of the worker pool you want to use for the `spacelift` component, +the name being the key you set in the `worker_pool_name_id_map` map. -1. We can now use the `spacectl` CLI to export an environment variable, allowing Terraform to manage spacelift resources. + +### Deploy the admin stacks + +Set these ENV vars: ```bash -export SPACELIFT_API_TOKEN=$(spacectl profile export-token) +export SPACELIFT_API_KEY_ENDPOINT=https://.app.spacelift.io +export SPACELIFT_API_KEY_ID=... +export SPACELIFT_API_KEY_SECRET=... +``` + +The name of the spacelift stack resource will be different depending on the name of the component and the root atmos stack. +This would be the command if the root atmos stack is `core-gbl-auto` and the spacelift component is `spacelift`. + ``` +atmos terraform apply spacelift --stack core-gbl-auto -target 'module.spacelift.module.stacks["core-gbl-auto-spacelift"]' +``` + +Note that this is the only manually operation you need to perform in `geodesic` using `atmos` to create the initial admin stack. +All other infrastructure stacks wil be created in Spacelift by this admin stack. + + +## Pull Request Workflow -4. Finally, log in to [AWS using Leapp](https://docs.cloudposse.com/howto/geodesic/authenticate-with-leapp) so that you have access to the Terraform state bucket. +1. Create a new branch & make changes +2. Create a new pull request (targeting the `main` branch) +3. View the modified resources directly in the pull request +4. View the successful Spacelift checks in the pull request +5. Merge the pull request and check the Spacelift job -### Applying Components -With our environment configured, we can now apply the base components necessary for `spacelift` to manage the rest of the environment. +## spacectl + +See docs https://github.com/spaceone-dev/spacectl + +### Install + +``` +⨠ apt install -y spacectl -qq +``` + +Setup a profile + +``` +⨠ spacectl profile login gbl-identity +Enter Spacelift endpoint (eg. https://unicorn.app.spacelift.io/): https://.app.spacelift.io +Select credentials type: 1 for API key, 2 for GitHub access token: 1 +Enter API key ID: 01FKN... +Enter API key secret: +``` -1. Create spacelift [spaces](https://docs.spacelift.io/concepts/spaces/) by configuring and applying the [spaces](./spaces/) component. +### Listing stacks ```bash -atmos terraform apply spacelift/spaces -s infra-gbl-root +spacectl stack list ``` -2. Create one or more spacelift [worker pools](https://docs.spacelift.io/concepts/worker-pools) by configuring and applying the [worker-pool](./worker-pool/) component. +Grab all the stack ids (use the JSON output to avoid bad chars) ```bash -atmos terraform apply spacelift/worker-pool -s core-ue2-auto +spacectl stack list --output json | jq -r '.[].id' > stacks.txt ``` -3. Create the root spacelift [stack](https://docs.spacelift.io/concepts/stack/) by configuring and applying the [admin-stack](./admin-stack/) component. +If the latest commit for each stack is desired, run something like this. -NOTE: Before running this command, make sure all of your code changes to configure these components have been merged to your `main` branch, and you have `main` checked out. +NOTE: remove the `echo` to remove the dry-run functionality ```bash -atmos terraform apply spacelift/admin-stack -s infra-gbl-root -var "spacelift_run_enabled=true" -var "commit_sha=$(git rev-parse HEAD)" +cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-current-commit --sha 25dd359749cfe30c76cce19f58e0a33555256afd --id $stack; done ``` -Running this command will create the `root` spacelift admin stack, which will, in turn, read your atmos stack config and create all other spacelift admin stacks defined in the config. -Each of these non-root spacelift admin stacks will then be triggered, planned, and applied by spacelift, and they, in turn, will create each of the spacelift stacks they are responsible for managing. + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [aws](#requirement\_aws) | >= 4.0 | +| [spacelift](#requirement\_spacelift) | >= 0.1.31 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.55.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ssm_parameter.spacelift_key_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.spacelift_key_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [administrative\_push\_policy\_enabled](#input\_administrative\_push\_policy\_enabled) | Flag to enable/disable the global administrative push policy | `bool` | `true` | no | +| [administrative\_stack\_drift\_detection\_enabled](#input\_administrative\_stack\_drift\_detection\_enabled) | Flag to enable/disable administrative stack drift detection | `bool` | `true` | no | +| [administrative\_stack\_drift\_detection\_reconcile](#input\_administrative\_stack\_drift\_detection\_reconcile) | Flag to enable/disable administrative stack drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | +| [administrative\_stack\_drift\_detection\_schedule](#input\_administrative\_stack\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the administrative stack | `list(string)` |
[
"0 4 * * *"
]
| no | +| [administrative\_trigger\_policy\_enabled](#input\_administrative\_trigger\_policy\_enabled) | Flag to enable/disable the global administrative trigger policy | `bool` | `true` | no | +| [attachment\_space\_id](#input\_attachment\_space\_id) | Specify the space ID for attachments (e.g. policies, contexts, etc.) | `string` | `"legacy"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [autodeploy](#input\_autodeploy) | Default autodeploy value for all stacks created by this project | `bool` | n/a | yes | +| [aws\_role\_arn](#input\_aws\_role\_arn) | ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment | `string` | `null` | no | +| [aws\_role\_enabled](#input\_aws\_role\_enabled) | Flag to enable/disable Spacelift to use AWS STS to assume the supplied IAM role and put its temporary credentials in the runtime environment | `bool` | `false` | no | +| [aws\_role\_external\_id](#input\_aws\_role\_external\_id) | Custom external ID (works only for private workers). See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html for more details | `string` | `null` | no | +| [aws\_role\_generate\_credentials\_in\_worker](#input\_aws\_role\_generate\_credentials\_in\_worker) | Flag to enable/disable generating AWS credentials in the private worker after assuming the supplied IAM role | `bool` | `false` | no | +| [before\_init](#input\_before\_init) | List of before-init scripts | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [context\_filters](#input\_context\_filters) | Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`. | `map(list(string))` | `{}` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [drift\_detection\_enabled](#input\_drift\_detection\_enabled) | Flag to enable/disable drift detection on the infrastructure stacks | `bool` | `true` | no | +| [drift\_detection\_reconcile](#input\_drift\_detection\_reconcile) | Flag to enable/disable infrastructure stacks drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | +| [drift\_detection\_schedule](#input\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the infrastructure stacks | `list(string)` |
[
"0 4 * * *"
]
| no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [external\_execution](#input\_external\_execution) | Set this to true if you're calling this module from outside of a Spacelift stack (e.g. the `complete` example) | `bool` | `false` | no | +| [git\_branch](#input\_git\_branch) | The Git branch name | `string` | `"main"` | no | +| [git\_commit\_sha](#input\_git\_commit\_sha) | The commit SHA for which to trigger a run. Requires `var.spacelift_run_enabled` to be set to `true` | `string` | `null` | no | +| [git\_repository](#input\_git\_repository) | The Git repository name | `string` | n/a | yes | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [infracost\_enabled](#input\_infracost\_enabled) | Flag to enable/disable infracost. If this is enabled, it will add infracost label to each stack. See [spacelift infracost](https://docs.spacelift.io/vendors/terraform/infracost) docs for more details. | `bool` | `false` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [policies\_available](#input\_policies\_available) | List of available default policies to create in Spacelift (these policies will not be attached to Spacelift stacks by default, use `var.policies_enabled`) | `list(string)` |
[
"git_push.proposed-run",
"git_push.tracked-run",
"plan.default",
"trigger.dependencies",
"trigger.retries"
]
| no | +| [policies\_by\_id\_enabled](#input\_policies\_by\_id\_enabled) | List of existing policy IDs to attach to all Spacelift stacks. These policies must already exist in Spacelift | `list(string)` | `[]` | no | +| [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of existing policy names to attach to all Spacelift stacks. These policies must exist at `modules/spacelift/rego-policies` OR `var.policies_by_name_path`. | `list(string)` | `[]` | no | +| [policies\_by\_name\_path](#input\_policies\_by\_name\_path) | Path to the catalog of external Rego policies. The Rego files must exist in the caller's code at the path. The module will create Spacelift policies from the external Rego definitions | `string` | `""` | no | +| [policies\_enabled](#input\_policies\_enabled) | DEPRECATED: Use `policies_by_id_enabled` instead. List of default policies created by this stack to attach to all Spacelift stacks | `list(string)` | `[]` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [runner\_image](#input\_runner\_image) | Full address & tag of the Spacelift runner image (e.g. on ECR) | `string` | n/a | yes | +| [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | +| [spacelift\_component\_path](#input\_spacelift\_component\_path) | The Spacelift Component Path | `string` | `"components/terraform"` | no | +| [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | +| [spacelift\_stack\_dependency\_enabled](#input\_spacelift\_stack\_dependency\_enabled) | If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached | `bool` | `false` | no | +| [stack\_config\_path\_template](#input\_stack\_config\_path\_template) | Stack config path template | `string` | `"stacks/%s.yaml"` | no | +| [stack\_destructor\_enabled](#input\_stack\_destructor\_enabled) | Flag to enable/disable the stack destructor to destroy the resources of a stack before deleting the stack itself | `bool` | `false` | no | +| [stacks\_space\_id](#input\_stacks\_space\_id) | Override the space ID for all stacks (unless the stack config has `dedicated_space` set to true). Otherwise, it will default to the admin stack's space. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tag\_filters](#input\_tag\_filters) | A map of tags that will filter stack creation by the matching `tags` set in a component `vars` configuration. | `map(string)` | `{}` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [terraform\_version](#input\_terraform\_version) | Default Terraform version for all stacks created by this project | `string` | n/a | yes | +| [terraform\_version\_map](#input\_terraform\_version\_map) | A map to determine which Terraform patch version to use for each minor version | `map(string)` | `{}` | no | +| [worker\_pool\_name\_id\_map](#input\_worker\_pool\_name\_id\_map) | Map of worker pool names to worker pool IDs | `map(any)` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [stacks](#output\_stacks) | Spacelift stacks | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spacelift) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index b754df85e..3f6a5b208 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -159,8 +159,6 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [health\_check\_type](#input\_health\_check\_type) | Controls how health checking is done. Valid values are `EC2` or `ELB` | `string` | `"EC2"` | no | | [iam\_attributes](#input\_iam\_attributes) | Additional attributes to add to the IDs of the IAM role and policy | `list(string)` | `[]` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [infracost\_api\_token\_ssm\_path](#input\_infracost\_api\_token\_ssm\_path) | This is the SSM path to retrieve and set the INFRACOST\_API\_TOKEN environment variable | `string` | `"/infracost/token"` | no | | [infracost\_cli\_args](#input\_infracost\_cli\_args) | These are the CLI args passed to infracost | `string` | `""` | no | | [infracost\_enabled](#input\_infracost\_enabled) | Whether to enable infracost for Spacelift stacks | `bool` | `false` | no | diff --git a/modules/spacelift/worker-pool/provider-spacelift.tf b/modules/spacelift/worker-pool/provider-spacelift.tf new file mode 100644 index 000000000..9634cde90 --- /dev/null +++ b/modules/spacelift/worker-pool/provider-spacelift.tf @@ -0,0 +1,6 @@ +# This provider always validates its credentials, so we always pass api_key_id and api_key_secret +provider "spacelift" { + api_key_endpoint = var.spacelift_api_endpoint + api_key_id = data.aws_ssm_parameter.spacelift_key_id.value + api_key_secret = data.aws_ssm_parameter.spacelift_key_secret.value +} diff --git a/modules/spacelift/worker-pool/providers.tf b/modules/spacelift/worker-pool/providers.tf index e569b8514..54257fd20 100644 --- a/modules/spacelift/worker-pool/providers.tf +++ b/modules/spacelift/worker-pool/providers.tf @@ -1,19 +1,14 @@ -# This provider always validates its credentials, so we always pass api_key_id and api_key_secret -provider "spacelift" { - api_key_endpoint = var.spacelift_api_endpoint - api_key_id = data.aws_ssm_parameter.spacelift_key_id.value - api_key_secret = data.aws_ssm_parameter.spacelift_key_secret.value -} - provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -22,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index eade031f1..6ab9c7110 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -60,8 +60,6 @@ No resources. | [fifo\_queue](#input\_fifo\_queue) | Boolean designating a FIFO queue. If not set, it defaults to false making it standard. | `bool` | `false` | no | | [fifo\_throughput\_limit](#input\_fifo\_throughput\_limit) | Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group. Valid values are perQueue and perMessageGroupId. This can be specified if fifo\_queue is true. | `list(string)` | `[]` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_data\_key\_reuse\_period\_seconds](#input\_kms\_data\_key\_reuse\_period\_seconds) | The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. An integer representing seconds, between 60 seconds (1 minute) and 86,400 seconds (24 hours). The default is 300 (5 minutes). | `number` | `300` | no | | [kms\_master\_key\_id](#input\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see Key Terms. | `list(string)` |
[
"alias/aws/sqs"
]
| no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | diff --git a/modules/sqs-queue/providers.tf b/modules/sqs-queue/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/sqs-queue/providers.tf +++ b/modules/sqs-queue/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/ssm-parameters/README.md b/modules/ssm-parameters/README.md index 7ffc2a3a6..5bfdf6ebd 100644 --- a/modules/ssm-parameters/README.md +++ b/modules/ssm-parameters/README.md @@ -67,8 +67,6 @@ components: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kms\_arn](#input\_kms\_arn) | The ARN of a KMS key used to encrypt and decrypt SecretString values | `string` | `""` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/ssm-parameters/providers.tf b/modules/ssm-parameters/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/ssm-parameters/providers.tf +++ b/modules/ssm-parameters/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/sso-saml-provider/providers.tf b/modules/sso-saml-provider/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/sso-saml-provider/providers.tf +++ b/modules/sso-saml-provider/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/sso/default.auto.tfvars b/modules/sso/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/sso/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/strongdm/README.md b/modules/strongdm/README.md index dff63ec23..29709df12 100644 --- a/modules/strongdm/README.md +++ b/modules/strongdm/README.md @@ -69,12 +69,11 @@ components: | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {}
}
| no | | [create\_roles](#input\_create\_roles) | Set `true` to create roles (should only be set in one account) | `bool` | `false` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [dns\_zone](#input\_dns\_zone) | n/a | `string` | `null` | no | +| [dns\_zone](#input\_dns\_zone) | DNS zone (e.g. example.com) into which to install the web host. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [gateway\_count](#input\_gateway\_count) | Number of gateways to provision | `number` | `2` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | | [install\_gateway](#input\_install\_gateway) | Set `true` to install a pair of gateways | `bool` | `false` | no | | [install\_relay](#input\_install\_relay) | Set `true` to install a pair of relays | `bool` | `true` | no | | [kms\_alias\_name](#input\_kms\_alias\_name) | AWS KMS alias used for encryption/decryption default is alias used in SSM | `string` | `"alias/aws/ssm"` | no | diff --git a/modules/strongdm/provider-strongdm.tf b/modules/strongdm/provider-strongdm.tf new file mode 100644 index 000000000..1b15be46f --- /dev/null +++ b/modules/strongdm/provider-strongdm.tf @@ -0,0 +1,26 @@ +provider "aws" { + alias = "api_keys" + region = var.ssm_region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles_network.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles_network.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles_network.terraform_role_arn]) + content { + role_arn = module.iam_roles_network.terraform_role_arn + } + } +} + +module "iam_roles_network" { + source = "../account-map/modules/iam-roles" + stage = var.ssm_account + context = module.this.context +} + +provider "sdm" { + api_access_key = local.enabled ? data.aws_ssm_parameter.api_access_key[0].value : null + api_secret_key = local.enabled ? data.aws_ssm_parameter.api_secret_key[0].value : null +} diff --git a/modules/strongdm/providers.tf b/modules/strongdm/providers.tf index c05628072..54257fd20 100644 --- a/modules/strongdm/providers.tf +++ b/modules/strongdm/providers.tf @@ -1,34 +1,19 @@ provider "aws" { region = var.region - profile = coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) -} + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name -module "iam_roles" { - source = "../account-map/modules/iam-roles" - context = module.this.context + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } } -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -provider "aws" { - alias = "api_keys" - region = var.ssm_region - - profile = coalesce(var.import_profile_name, module.iam_roles_network.terraform_profile_name) -} - -module "iam_roles_network" { +module "iam_roles" { source = "../account-map/modules/iam-roles" - stage = var.ssm_account context = module.this.context } - -provider "sdm" { - api_access_key = local.enabled ? data.aws_ssm_parameter.api_access_key[0].value : null - api_secret_key = local.enabled ? data.aws_ssm_parameter.api_secret_key[0].value : null -} diff --git a/modules/strongdm/variables.tf b/modules/strongdm/variables.tf index 4abcddc63..1057c5886 100644 --- a/modules/strongdm/variables.tf +++ b/modules/strongdm/variables.tf @@ -53,7 +53,7 @@ variable "kubernetes_namespace" { variable "dns_zone" { type = string - description = "" + description = "DNS zone (e.g. example.com) into which to install the web host." default = null } diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 0d6b344e5..44482e130 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -6,20 +6,50 @@ Once the initial S3 backend is configured, this component can create additional However, perhaps counter-intuitively, all Terraform users require read access to the most sensitive accounts, such as `root` and `audit`, in order to read security configuration information, so careful planning is required when architecting backend splits. :::info -Part of cold start so it has to initially be run with `SuperAdmin` +Part of cold start so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and then to move the state into it. Follow the guide **[here](/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** to get started. ::: ### Access Control -For each backend, this module will create an IAM role with read/write access and, optionally, an IAM role with read-only access. You can configure who is allowed to assume these roles. -- While read/write access is required for `terraform apply`, the created role only grants read/write access to the Terraform state, it does not grant permission to create/modify/destroy AWS resources. -- Similarly, while the read-only role prohibits making changes to the Terraform state, it does not prevent anyone from making changes to AWS resources using a different role. -- Many Cloud Posse components store information about resources they create in the Terraform state via their outputs, and many other components read this information from the Terraform state backend via the CloudPosse `remote-state` module and use it as part of their configuration. For example, the `account-map` component exists solely for the purpose of organizing information about the created AWS accounts and storing it in its Terraform state, making it available via `remote-state`. This means that you if you are going to restrict access to some backends, you need to carefully orchestrate what is stored there and ensure that you are not storing information a component needs in a backend it will not have access to. Typically, information in the most sensitive accounts, such as `root`, `audit`, and `security`, is nevertheless needed by every account, for example to know where to send audit logs, so it is not obvious and can be counter-intuitive which accounts need access to which backends. Plan carefully. -- Atmos provides separate configuration for Terraform state access via the `backend` and `remote_state_backend` settings. Always configure the `backend` setting with a role that has read/write access (and override that setting to be `null` for components deployed by SuperAdmin). If a read-only role is available (and we recommend you create one via this module), use that role in `remote_state_backend.s3.role_arn`. -- Note that the "read-only" in the "read-only role" refers solely to the S3 bucket that stores the backend data. That role still has read/write access to the DynamoDB table, which is desirable so that users restricted to the read-only role can still perform drift detection by running `terraform plan`. The DynamoDB table only stores checksums and mutual-exclusion lock information, so it is not considered sensitive. The worst a malicious user could do would be to corrupt the table and cause a denial-of-service (DoS) for Terraform, but such DoS would only affect making changes to the infrastructure, it would not affect the operation of the existing infrastructure, so it is an ineffective and therefore unlikely vector of attack. (Also note that the entire DynamoDB table is optional and can be deleted entirely; Terraform will repopulate it as new activity takes place.) - - +For each backend, this module will create an IAM role with read/write access and, optionally, an IAM role with read-only access. +You can configure who is allowed to assume these roles. + +- While read/write access is required for `terraform apply`, the created role only grants read/write access to the Terraform state, + it does not grant permission to create/modify/destroy AWS resources. + +- Similarly, while the read-only role prohibits making changes to the Terraform state, it does not prevent anyone + from making changes to AWS resources using a different role. + +- Many Cloud Posse components store information about resources they create in the Terraform state via their outputs, + and many other components read this information from the Terraform state backend via the CloudPosse `remote-state` + module and use it as part of their configuration. For example, the `account-map` component exists solely for the + purpose of organizing information about the created AWS accounts and storing it in its Terraform state, making it + available via `remote-state`. This means that you if you are going to restrict access to some backends, you need to + carefully orchestrate what is stored there and ensure that you are not storing information a component needs in a + backend it will not have access to. Typically, information in the most sensitive accounts, such as `root`, `audit`, + and `security`, is nevertheless needed by every account, for example to know where to send audit logs, so it is not + obvious and can be counter-intuitive which accounts need access to which backends. Plan carefully. + +- Atmos provides separate configuration for Terraform state access via the `backend` and `remote_state_backend` + settings. Always configure the `backend` setting with a role that has read/write access (and override that setting + to be `null` for components deployed by SuperAdmin). If a read-only role is available (only helpful if you have + more than one backend), use that role in `remote_state_backend.s3.role_arn`. Otherwise, use the read/write role in + `remote_state_backend.s3.role_arn`, to ensure that all components can read the Terraform state, even if + `backend.s3.role_arn` is set to `null`, as it is with a few critical components meant to be deployed by SuperAdmin. + +- Note that the "read-only" in the "read-only role" refers solely to the S3 bucket that stores the backend data. + That role still has read/write access to the DynamoDB table, which is desirable so that users restricted to the + read-only role can still perform drift detection by running `terraform plan`. The DynamoDB table only stores + checksums and mutual-exclusion lock information, so it is not considered sensitive. The worst a malicious user + could do would be to corrupt the table and cause a denial-of-service (DoS) for Terraform, but such DoS would only + affect making changes to the infrastructure, it would not affect the operation of the existing infrastructure, so + it is an ineffective and therefore unlikely vector of attack. (Also note that the entire DynamoDB table is + optional and can be deleted entirely; Terraform will repopulate it as new activity takes place.) + +- For convenience, the component automatically grants access to the backend to the user deploying it. This is + helpful because it allows that user, presumably SuperAdmin, to deploy the normal components that expect + the user does not have direct access to Terraform state. ## Usage @@ -49,19 +79,14 @@ Here's an example snippet for how to use this component. default: &tfstate-access-template write_enabled: true allowed_roles: - identity: ["admin", "cicd", "poweruser", "spacelift", "terraform"] + core-identity: ["devops", "developers", "managers", "spacelift"] + core-root: ["admin"] denied_roles: {} allowed_permission_sets: - identity: ["AdministratorAccess"] + core-identity: ["AdministratorAccess"] denied_permission_sets: {} allowed_principal_arns: [] denied_principal_arns: [] - ro: - <<: *tfstate-access-template - write_enabled: false - allowed_roles: - identity: ["admin", "cicd", "poweruser", "spacelift", "terraform", "reader", "observer", "support"] - ``` @@ -71,12 +96,14 @@ Here's an example snippet for how to use this component. |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.9.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | +| [awsutils](#provider\_awsutils) | >= 0.16.0 | ## Modules @@ -84,7 +111,7 @@ Here's an example snippet for how to use this component. |------|--------|---------| | [assume\_role](#module\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | | [label](#module\_label) | cloudposse/label/null | 0.25.0 | -| [tfstate\_backend](#module\_tfstate\_backend) | cloudposse/tfstate-backend/aws | 0.38.1 | +| [tfstate\_backend](#module\_tfstate\_backend) | cloudposse/tfstate-backend/aws | 1.1.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -93,6 +120,7 @@ Here's an example snippet for how to use this component. |------|------| | [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_policy_document.tfstate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [awsutils_caller_identity.current](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/data-sources/caller_identity) | data source | ## Inputs @@ -105,7 +133,7 @@ Here's an example snippet for how to use this component. | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [enable\_point\_in\_time\_recovery](#input\_enable\_point\_in\_time\_recovery) | Enable DynamoDB point-in-time recovery | `bool` | `false` | no | +| [enable\_point\_in\_time\_recovery](#input\_enable\_point\_in\_time\_recovery) | Enable DynamoDB point-in-time recovery | `bool` | `true` | no | | [enable\_server\_side\_encryption](#input\_enable\_server\_side\_encryption) | Enable DynamoDB and S3 server-side encryption | `bool` | `true` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | diff --git a/modules/tfstate-backend/iam.tf b/modules/tfstate-backend/iam.tf index caaa48a5b..4376c50fe 100644 --- a/modules/tfstate-backend/iam.tf +++ b/modules/tfstate-backend/iam.tf @@ -5,8 +5,12 @@ locals { ) => v } : {} access_roles_enabled = module.this.enabled && length(keys(local.access_roles)) > 0 + + caller_arn = coalesce(data.awsutils_caller_identity.current.eks_role_arn, data.awsutils_caller_identity.current.arn) } +data "awsutils_caller_identity" "current" {} + module "label" { for_each = var.access_roles @@ -27,9 +31,11 @@ module "assume_role" { for_each = local.access_roles source = "../account-map/modules/team-assume-role-policy" - allowed_roles = each.value.allowed_roles - denied_roles = each.value.denied_roles - allowed_principal_arns = each.value.allowed_principal_arns + allowed_roles = each.value.allowed_roles + denied_roles = each.value.denied_roles + + # Allow whatever user or role is running Terraform to manage the backend to assume any backend access role + allowed_principal_arns = concat(each.value.allowed_principal_arns, [local.caller_arn]) denied_principal_arns = each.value.denied_principal_arns # Permission sets are for AWS SSO, which is optional allowed_permission_sets = try(each.value.allowed_permission_sets, {}) diff --git a/modules/tfstate-backend/main.tf b/modules/tfstate-backend/main.tf index 74b989e96..74f370ef2 100644 --- a/modules/tfstate-backend/main.tf +++ b/modules/tfstate-backend/main.tf @@ -4,12 +4,13 @@ locals { module "tfstate_backend" { source = "cloudposse/tfstate-backend/aws" - version = "0.38.1" + version = "1.1.0" - force_destroy = var.force_destroy - prevent_unencrypted_uploads = var.prevent_unencrypted_uploads - enable_server_side_encryption = var.enable_server_side_encryption - enable_point_in_time_recovery = var.enable_point_in_time_recovery + force_destroy = var.force_destroy + prevent_unencrypted_uploads = var.prevent_unencrypted_uploads + // enable_server_side_encryption = var.enable_server_side_encryption + enable_point_in_time_recovery = var.enable_point_in_time_recovery + bucket_ownership_enforced_enabled = false context = module.this.context } diff --git a/modules/tfstate-backend/variables.tf b/modules/tfstate-backend/variables.tf index 3ee90c1ac..541cbf855 100644 --- a/modules/tfstate-backend/variables.tf +++ b/modules/tfstate-backend/variables.tf @@ -24,7 +24,7 @@ variable "enable_server_side_encryption" { variable "enable_point_in_time_recovery" { type = bool description = "Enable DynamoDB point-in-time recovery" - default = false + default = true } variable "access_roles" { diff --git a/modules/tfstate-backend/versions.tf b/modules/tfstate-backend/versions.tf index cc73ffd35..1130c8c07 100644 --- a/modules/tfstate-backend/versions.tf +++ b/modules/tfstate-backend/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = ">= 4.9.0" } + awsutils = { + source = "cloudposse/awsutils" + version = ">= 0.16.0" + } } } diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 867a34c45..4f745d524 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -120,8 +120,6 @@ No resources. | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [expose\_eks\_sg](#input\_expose\_eks\_sg) | Set true to allow EKS clusters to accept traffic from source accounts | `bool` | `true` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/tgw/hub/providers.tf b/modules/tgw/hub/providers.tf index c2419aabb..45d458575 100644 --- a/modules/tgw/hub/providers.tf +++ b/modules/tgw/hub/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index afeafd088..663ef7d5c 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -129,8 +129,6 @@ No resources. | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [expose\_eks\_sg](#input\_expose\_eks\_sg) | Set true to allow EKS clusters to accept traffic from source accounts | `bool` | `true` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/tgw/spoke/provider-hub.tf b/modules/tgw/spoke/provider-hub.tf new file mode 100644 index 000000000..14974efa7 --- /dev/null +++ b/modules/tgw/spoke/provider-hub.tf @@ -0,0 +1,47 @@ +provider "aws" { + alias = "tgw-hub" + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.tgw_hub_role.terraform_profile_name + + dynamic "assume_role" { + # module.tgw_hub_role.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.tgw_hub_role.terraform_role_arn]) + content { + role_arn = module.tgw_hub_role.terraform_role_arn + } + } +} + +variable "tgw_hub_environment_name" { + type = string + description = "The name of the environment where `tgw/gateway` is provisioned" + default = "ue2" +} + +variable "tgw_hub_stage_name" { + type = string + description = "The name of the stage where `tgw/gateway` is provisioned" + default = "network" +} + +variable "tgw_hub_tenant_name" { + type = string + description = <<-EOT + The name of the tenant where `tgw/hub` is provisioned. + + If the `tenant` label is not used, leave this as `null`. + EOT + default = null +} + +module "tgw_hub_role" { + source = "../../account-map/modules/iam-roles" + + stage = var.tgw_hub_stage_name + environment = var.tgw_hub_environment_name + tenant = var.tgw_hub_tenant_name + + context = module.this.context +} diff --git a/modules/tgw/spoke/providers.tf b/modules/tgw/spoke/providers.tf index bfa49d241..45d458575 100644 --- a/modules/tgw/spoke/providers.tf +++ b/modules/tgw/spoke/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,56 +17,3 @@ module "iam_roles" { source = "../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} - -provider "aws" { - alias = "tgw-hub" - region = var.region - - assume_role { - role_arn = coalesce(var.import_role_arn, module.tgw_hub_role.terraform_role_arn) - } -} - -variable "tgw_hub_environment_name" { - type = string - description = "The name of the environment where `tgw/gateway` is provisioned" - default = "ue2" -} - -variable "tgw_hub_stage_name" { - type = string - description = "The name of the stage where `tgw/gateway` is provisioned" - default = "network" -} - -variable "tgw_hub_tenant_name" { - type = string - description = <<-EOT - The name of the tenant where `tgw/hub` is provisioned. - - If the `tenant` label is not used, leave this as `null`. - EOT - default = null -} - -module "tgw_hub_role" { - source = "../../account-map/modules/iam-roles" - - stage = var.tgw_hub_stage_name - environment = var.tgw_hub_environment_name - tenant = var.tgw_hub_tenant_name - - context = module.this.context -} diff --git a/modules/vpc-flow-logs-bucket/README.md b/modules/vpc-flow-logs-bucket/README.md index c9170ee2b..750e1e5db 100644 --- a/modules/vpc-flow-logs-bucket/README.md +++ b/modules/vpc-flow-logs-bucket/README.md @@ -62,8 +62,6 @@ No resources. | [force\_destroy](#input\_force\_destroy) | A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable | `bool` | `false` | no | | [glacier\_transition\_days](#input\_glacier\_transition\_days) | Number of days after which to move the data to the glacier storage tier | `number` | `60` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/vpc-flow-logs-bucket/providers.tf b/modules/vpc-flow-logs-bucket/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/vpc-flow-logs-bucket/providers.tf +++ b/modules/vpc-flow-logs-bucket/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/vpc-peering/README.md b/modules/vpc-peering/README.md index d9d38e192..fa5f39764 100644 --- a/modules/vpc-peering/README.md +++ b/modules/vpc-peering/README.md @@ -229,8 +229,6 @@ atmos terraform apply vpc-peering -s ue1-prod | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/vpc-peering/provider-accepter.tf b/modules/vpc-peering/provider-accepter.tf new file mode 100644 index 000000000..281e7ed2e --- /dev/null +++ b/modules/vpc-peering/provider-accepter.tf @@ -0,0 +1,9 @@ +provider "aws" { + alias = "accepter" + + region = var.accepter_region + + assume_role { + role_arn = local.accepter_aws_assume_role_arn + } +} diff --git a/modules/vpc-peering/providers.tf b/modules/vpc-peering/providers.tf index 6c306aeb1..54257fd20 100644 --- a/modules/vpc-peering/providers.tf +++ b/modules/vpc-peering/providers.tf @@ -1,38 +1,19 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } -provider "aws" { - alias = "accepter" - - region = var.accepter_region - - assume_role { - role_arn = local.accepter_aws_assume_role_arn - } -} - module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 5bcfcd350..dd986a97e 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -104,8 +104,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [gateway\_vpc\_endpoints](#input\_gateway\_vpc\_endpoints) | A list of Gateway VPC Endpoints to provision into the VPC. Only valid values are "dynamodb" and "s3". | `set(string)` | `[]` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [interface\_vpc\_endpoints](#input\_interface\_vpc\_endpoints) | A list of Interface VPC Endpoints to provision into the VPC. | `set(string)` | `[]` | no | | [ipv4\_additional\_cidr\_block\_associations](#input\_ipv4\_additional\_cidr\_block\_associations) | IPv4 CIDR blocks to assign to the VPC.
`ipv4_cidr_block` can be set explicitly, or set to `null` with the CIDR block derived from `ipv4_ipam_pool_id` using `ipv4_netmask_length`.
Map keys must be known at `plan` time, and are only used to track changes. |
map(object({
ipv4_cidr_block = string
ipv4_ipam_pool_id = string
ipv4_netmask_length = number
}))
| `{}` | no | | [ipv4\_cidr\_block\_association\_timeouts](#input\_ipv4\_cidr\_block\_association\_timeouts) | Timeouts (in `go` duration format) for creating and destroying IPv4 CIDR block associations |
object({
create = string
delete = string
})
| `null` | no | diff --git a/modules/vpc/providers.tf b/modules/vpc/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/vpc/providers.tf +++ b/modules/vpc/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/waf/README.md b/modules/waf/README.md index 160dbd63b..025487901 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -81,8 +81,6 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [geo\_match\_statement\_rules](#input\_geo\_match\_statement\_rules) | A rule statement used to identify web requests based on country of origin.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [ip\_set\_reference\_statement\_rules](#input\_ip\_set\_reference\_statement\_rules) | A rule statement used to detect web requests coming from particular IP addresses or address ranges.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
arn:
The ARN of the IP Set that this statement references.
ip\_set\_forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
position:
The position in the header to search for the IP address.
Possible values include: `FIRST`, `LAST`, or `ANY`.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/waf/providers.tf b/modules/waf/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/waf/providers.tf +++ b/modules/waf/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index 0adefc619..9dd623537 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -84,8 +84,6 @@ import: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [instance\_type](#input\_instance\_type) | The instance family to use for the ZScaler EC2 instances. | `string` | `"r5n.medium"` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/zscaler/providers.tf b/modules/zscaler/providers.tf index 08ee01b2a..54257fd20 100644 --- a/modules/zscaler/providers.tf +++ b/modules/zscaler/providers.tf @@ -1,12 +1,14 @@ provider "aws" { region = var.region - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -15,15 +17,3 @@ module "iam_roles" { source = "../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/rootfs/usr/local/bin/aws-config b/rootfs/usr/local/bin/aws-config index 6d081843e..beb7e304b 100755 --- a/rootfs/usr/local/bin/aws-config +++ b/rootfs/usr/local/bin/aws-config @@ -163,7 +163,7 @@ function spacelift() { echo _no_source_profile=skip - saml admin terraform + saml admin terraform planner } functions+=(accounts) @@ -187,17 +187,18 @@ case $1 in shift fi ;; - account*) + *) if [[ -n $NAMESPACE ]]; then target_namespace=($NAMESPACE) - elif [[ ${#namespaces[@]} == 1 ]]; then - target_namespace=("${namespaces[0]}") else - echo "ERROR: NAMESPACE not set. Please set NAMESPACE or use -n to specify the namespace to use." - exit 1 + target_namespace=("${namespaces[@]}") fi ;; - *) +esac + +case $1 in + # These commands automatically use all namespaces, so we don't need to loop over them. + spacelift|switch-roles|teams|saml) target_namespace=("${namespaces[0]}") ;; esac From a6f972d58fb70ad32bb6e7774070527fd50c2d42 Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Thu, 15 Jun 2023 15:57:25 -0500 Subject: [PATCH 154/501] alb: use the https_ssl_policy (#722) --- modules/alb/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/alb/main.tf b/modules/alb/main.tf index 81af7c303..46d97718d 100644 --- a/modules/alb/main.tf +++ b/modules/alb/main.tf @@ -11,6 +11,7 @@ module "alb" { https_enabled = var.https_enabled http2_enabled = var.http2_enabled http_redirect = var.http_redirect + https_ssl_policy = var.https_ssl_policy access_logs_enabled = var.access_logs_enabled alb_access_logs_s3_bucket_force_destroy = var.alb_access_logs_s3_bucket_force_destroy cross_zone_load_balancing_enabled = var.cross_zone_load_balancing_enabled From 034a19bab7b58fcd2e90dcf8efc410e83d58e0bf Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 20 Jun 2023 12:37:14 -0700 Subject: [PATCH 155/501] upstream `github-action-runners` dockerhub authentication (#726) --- .../eks/actions-runner-controller/README.md | 18 ++++ .../templates/runnerdeployment.yaml | 94 ++++++++++++++----- modules/eks/actions-runner-controller/main.tf | 16 +++- .../actions-runner-controller/variables.tf | 12 +++ 4 files changed, 114 insertions(+), 26 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 62adec149..020254e63 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -37,6 +37,21 @@ components: github_app_id: "REPLACE_ME_GH_APP_ID" github_app_installation_id: "REPLACE_ME_GH_INSTALLATION_ID" + # use to enable docker config json secret, which can login to dockerhub for your GHA Runners + docker_config_json_enabled: true + # The content of this param should look like: + # { + # "auths": { + # "https://index.docker.io/v1/": { + # "username": "your_username", + # "password": "your_password + # "email": "your_email", + # "auth": "$(echo "your_username:your_password" | base64)" + # } + # } + # } | base64 + ssm_docker_config_json_path: "/github_runners/docker/config-json" + # ssm_github_webhook_secret_token_path: "/github_runners/github_webhook_secret_token" # The webhook based autoscaler is much more efficient than the polling based autoscaler webhook: @@ -410,6 +425,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | Name | Type | |------|------| | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_ssm_parameter.docker_config_json](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.github_token](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.github_webhook_secret_token](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | @@ -430,6 +446,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [docker\_config\_json\_enabled](#input\_docker\_config\_json\_enabled) | Whether the Docker config JSON is enabled | `bool` | `false` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | @@ -461,6 +478,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | | [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(bool, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | +| [ssm\_docker\_config\_json\_path](#input\_ssm\_docker\_config\_json\_path) | SSM path to the Docker config JSON | `string` | `null` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | | [ssm\_github\_webhook\_secret\_token\_path](#input\_ssm\_github\_webhook\_secret\_token\_path) | The path in SSM to the GitHub Webhook Secret token. | `string` | `""` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index 5a63e96f7..6ecffda81 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -18,6 +18,16 @@ spec: # save space. storage: 100Gi {{- end }} +{{- if .Values.docker_config_json_enabled }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: regcred +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ .Values.docker_config_json }} +{{- end }} --- apiVersion: actions.summerwind.dev/v1alpha1 kind: RunnerDeployment @@ -34,16 +44,32 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} spec: + {{- if .Values.docker_config_json_enabled }} + # secrets volumeMount are always mounted readOnly so config.json has to be copied to the correct directory + # https://github.com/kubernetes/kubernetes/issues/62099 + # https://github.com/actions/actions-runner-controller/issues/2123#issuecomment-1527077517 + + initContainers: + - name: docker-config-writer + image: {{ .Values.image | quote }} + command: [ "sh", "-c", "cat /home/.docker/config.json > /home/runner/.docker/config.json" ] + volumeMounts: + - mountPath: /home/.docker/ + name: docker-secret + - mountPath: /home/runner/.docker + name: docker-config-volume + {{- end }} + # As of 2023-03-31 # Recommended by https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md terminationGracePeriodSeconds: 100 env: - # RUNNER_GRACEFUL_STOP_TIMEOUT is the time the runner will give itself to try to finish - # a job before it gracefully cancels itself in response to a pod termination signal. - # It should be less than the terminationGracePeriodSeconds above so that it has time - # to report its status and deregister itself from the runner pool. - - name: RUNNER_GRACEFUL_STOP_TIMEOUT - value: "90" + # RUNNER_GRACEFUL_STOP_TIMEOUT is the time the runner will give itself to try to finish + # a job before it gracefully cancels itself in response to a pod termination signal. + # It should be less than the terminationGracePeriodSeconds above so that it has time + # to report its status and deregister itself from the runner pool. + - name: RUNNER_GRACEFUL_STOP_TIMEOUT + value: "90" # You could reserve nodes for runners by labeling and tainting nodes with # node-role.kubernetes.io/actions-runner @@ -89,6 +115,10 @@ spec: dockerdWithinRunnerContainer: {{ .Values.dind_enabled }} image: {{ .Values.image | quote }} imagePullPolicy: IfNotPresent + {{- if .Values.docker_config_json_enabled }} + imagePullSecrets: + - name: regcred + {{- end }} serviceAccountName: {{ .Values.service_account_name }} resources: limits: @@ -105,29 +135,47 @@ spec: {{- end }} {{- if and .Values.dind_enabled .Values.storage }} dockerVolumeMounts: - - mountPath: /var/lib/docker - name: docker-volume + - mountPath: /var/lib/docker + name: docker-volume {{- end }} - {{- if .Values.pvc_enabled }} + {{- if or (.Values.pvc_enabled) (.Values.docker_config_json_enabled) }} volumeMounts: - - mountPath: /home/runner/work/shared - name: shared-volume + {{- if .Values.pvc_enabled }} + - mountPath: /home/runner/work/shared + name: shared-volume + {{- end }} + {{- if .Values.docker_config_json_enabled }} + - mountPath: /home/.docker/ + name: docker-secret + - mountPath: /home/runner/.docker + name: docker-config-volume + {{- end }} {{- end }} - {{- if or (and .Values.dind_enabled .Values.storage) (.Values.pvc_enabled) }} + {{- if or (and .Values.dind_enabled .Values.storage) (.Values.pvc_enabled) (.Values.docker_config_json_enabled) }} volumes: {{- if and .Values.dind_enabled .Values.storage }} - - name: docker-volume - ephemeral: - volumeClaimTemplate: - spec: - accessModes: [ "ReadWriteOnce" ] # Only 1 pod can connect at a time - resources: - requests: - storage: {{ .Values.storage }} + - name: docker-volume + ephemeral: + volumeClaimTemplate: + spec: + accessModes: [ "ReadWriteOnce" ] # Only 1 pod can connect at a time + resources: + requests: + storage: {{ .Values.storage }} {{- end }} {{- if .Values.pvc_enabled }} - - name: shared-volume - persistentVolumeClaim: - claimName: {{ .Values.release_name }} + - name: shared-volume + persistentVolumeClaim: + claimName: {{ .Values.release_name }} {{- end }} + {{- if .Values.docker_config_json_enabled }} + - name: docker-secret + secret: + secretName: regcred + items: + - key: .dockerconfigjson + path: config.json + - name: docker-config-volume + emptyDir: + {{- end }} {{- end }} diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index 70597ce6c..30d05f596 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -1,9 +1,11 @@ locals { enabled = module.this.enabled - webhook_enabled = local.enabled ? try(var.webhook.enabled, false) : false - webhook_host = local.webhook_enabled ? format(var.webhook.hostname_template, var.tenant, var.stage, var.environment) : "example.com" - runner_groups_enabled = length(compact(values(var.runners)[*].group)) > 0 + webhook_enabled = local.enabled ? try(var.webhook.enabled, false) : false + webhook_host = local.webhook_enabled ? format(var.webhook.hostname_template, var.tenant, var.stage, var.environment) : "example.com" + runner_groups_enabled = length(compact(values(var.runners)[*].group)) > 0 + docker_config_json_enabled = local.enabled && var.docker_config_json_enabled + docker_config_json = one(data.aws_ssm_parameter.docker_config_json[*].value) github_app_enabled = length(var.github_app_id) > 0 && length(var.github_app_installation_id) > 0 create_secret = local.enabled && length(var.existing_kubernetes_secret_name) == 0 @@ -100,6 +102,12 @@ data "aws_ssm_parameter" "github_webhook_secret_token" { with_decryption = true } +data "aws_ssm_parameter" "docker_config_json" { + count = local.docker_config_json_enabled ? 1 : 0 + name = var.ssm_docker_config_json_path + with_decryption = true +} + module "actions_runner_controller" { source = "cloudposse/helm-release/aws" version = "0.7.0" @@ -225,6 +233,8 @@ module "actions_runner" { pvc_enabled = each.value.pvc_enabled node_selector = each.value.node_selector tolerations = each.value.tolerations + docker_config_json_enabled = local.docker_config_json_enabled + docker_config_json = local.docker_config_json }), each.value.group == null ? "" : yamlencode({ group = each.value.group }), local.busy_metrics_filtered[each.key] == null ? "" : yamlencode(local.busy_metrics_filtered[each.key]), diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index af8c1c414..ccdae7d68 100644 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -131,6 +131,18 @@ variable "s3_bucket_arns" { default = [] } +variable "docker_config_json_enabled" { + type = bool + description = "Whether the Docker config JSON is enabled" + default = false +} + +variable "ssm_docker_config_json_path" { + type = string + description = "SSM path to the Docker config JSON" + default = null +} + variable "runners" { description = <<-EOT Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in From 28eb0d1eea770cf01e3ec030e8e003321d49ae5a Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Tue, 20 Jun 2023 21:49:31 -0400 Subject: [PATCH 156/501] refactor guardduty module (#725) Co-authored-by: cloudpossebot Co-authored-by: Andriy Knysh --- deprecated/guardduty/common/README.md | 117 +++++++++++ .../guardduty/common/main.tf | 0 .../guardduty/common/outputs.tf | 0 .../guardduty/common/providers.tf | 0 .../guardduty/common/remote-state.tf | 0 .../guardduty/root/README.md | 0 .../guardduty/root}/context.tf | 0 .../guardduty/root/main.tf | 0 .../guardduty/root/providers.tf | 0 .../guardduty/root/remote-state.tf | 0 .../guardduty/root/variables.tf | 0 .../guardduty/root}/versions.tf | 0 modules/guardduty/{common => }/README.md | 191 +++++++++++++----- modules/guardduty/{root => }/context.tf | 0 modules/guardduty/main.tf | 82 ++++++++ modules/guardduty/outputs.tf | 24 +++ modules/guardduty/providers.tf | 37 ++++ modules/guardduty/remote-state.tf | 27 +++ modules/guardduty/{common => }/variables.tf | 177 ++++++++++------ modules/guardduty/{root => }/versions.tf | 2 +- 20 files changed, 543 insertions(+), 114 deletions(-) create mode 100644 deprecated/guardduty/common/README.md rename {modules => deprecated}/guardduty/common/main.tf (100%) rename {modules => deprecated}/guardduty/common/outputs.tf (100%) rename {modules => deprecated}/guardduty/common/providers.tf (100%) rename {modules => deprecated}/guardduty/common/remote-state.tf (100%) rename {modules => deprecated}/guardduty/root/README.md (100%) rename {modules/guardduty/common => deprecated/guardduty/root}/context.tf (100%) rename {modules => deprecated}/guardduty/root/main.tf (100%) rename {modules => deprecated}/guardduty/root/providers.tf (100%) rename {modules => deprecated}/guardduty/root/remote-state.tf (100%) rename {modules => deprecated}/guardduty/root/variables.tf (100%) rename {modules/guardduty/common => deprecated/guardduty/root}/versions.tf (100%) rename modules/guardduty/{common => }/README.md (54%) rename modules/guardduty/{root => }/context.tf (100%) create mode 100644 modules/guardduty/main.tf create mode 100644 modules/guardduty/outputs.tf create mode 100644 modules/guardduty/providers.tf create mode 100644 modules/guardduty/remote-state.tf rename modules/guardduty/{common => }/variables.tf (51%) rename modules/guardduty/{root => }/versions.tf (89%) diff --git a/deprecated/guardduty/common/README.md b/deprecated/guardduty/common/README.md new file mode 100644 index 000000000..3135006e8 --- /dev/null +++ b/deprecated/guardduty/common/README.md @@ -0,0 +1,117 @@ +# Component: `guardduty/common` + +This component is responsible for configuring GuardDuty and it should be used in tandem with the [guardduty/root](../root) component. + +AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by continuously monitoring for malicious activities and unauthorized behaviors. GuardDuty analyzes various data sources within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS logs, to detect potential security threats. + +Key features and components of AWS GuardDuty include: + +- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event logs and network traffic data to detect patterns, anomalies, and known attack techniques. + +- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, domains, and other indicators of compromise. + +- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS Lambda for immediate action or custom response workflows. + +- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security policies and practices. + +- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of security incidents and reduces the need for manual intervention. + +- Security findings and reports: GuardDuty provides detailed security findings and reports that include information about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed through the AWS Management Console or retrieved via APIs for further analysis and reporting. + +GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations with an additional layer of security to proactively identify and respond to potential security risks. + +## Usage + +**Stack Level**: Regional + +The example snippet below shows how to use this component: + +```yaml +components: + terraform: + guardduty/common: + metadata: + component: guardduty/common + vars: + enabled: true + account_map_tenant: core + central_resource_collector_account: core-security + admin_delegated: true +``` + +## Deployment + +This set of steps assumes that `var.central_resource_collector_account = "core-security"`. + +1. Apply `guardduty/common` to `core-security` with `var.admin_delegated = false` +2. Apply `guardduty/root` to `core-root` +3. Apply `guardduty/common` to `core-security` with `var.admin_delegated = true` + +Example: + +``` +# Apply guardduty/common to all regions in core-security +atmos terraform apply guardduty/common-ue2 -s core-ue2-security -var=admin_delegated=false +atmos terraform apply guardduty/common-ue1 -s core-ue1-security -var=admin_delegated=false +atmos terraform apply guardduty/common-uw1 -s core-uw1-security -var=admin_delegated=false +# ... other regions + +# Apply guardduty/root to all regions in core-root +atmos terraform apply guardduty/root-ue2 -s core-ue2-root +atmos terraform apply guardduty/root-ue1 -s core-ue1-root +atmos terraform apply guardduty/root-uw1 -s core-uw1-root +# ... other regions + +# Apply guardduty/common to all regions in core-security but with default values for admin_delegated +atmos terraform apply guardduty/common-ue2 -s core-ue2-security +atmos terraform apply guardduty/common-ue1 -s core-ue1-security +atmos terraform apply guardduty/common-uw1 -s core-uw1-security +# ... other regions +``` + + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | +| [awsutils](#provider\_awsutils) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [guardduty](#module\_guardduty) | cloudposse/guardduty/aws | 0.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | + +## Resources + +| Name | Type | +|------|------| +| [awsutils_guardduty_organization_settings.this](https://registry.terraform.io/providers/hashicorp/awsutils/latest/docs/resources/guardduty_organization_settings) | resource | +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [guardduty\_detector\_arn](#output\_guardduty\_detector\_arn) | GuardDuty detector ARN | +| [guardduty\_detector\_id](#output\_guardduty\_detector\_id) | GuardDuty detector ID | +| [sns\_topic\_name](#output\_sns\_topic\_name) | SNS topic name | +| [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | SNS topic subscriptions | + + +## References +* [AWS GuardDuty Documentation](https://aws.amazon.com/guardduty/) +* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/guardduty/common/) + +[](https://cpco.io/component) diff --git a/modules/guardduty/common/main.tf b/deprecated/guardduty/common/main.tf similarity index 100% rename from modules/guardduty/common/main.tf rename to deprecated/guardduty/common/main.tf diff --git a/modules/guardduty/common/outputs.tf b/deprecated/guardduty/common/outputs.tf similarity index 100% rename from modules/guardduty/common/outputs.tf rename to deprecated/guardduty/common/outputs.tf diff --git a/modules/guardduty/common/providers.tf b/deprecated/guardduty/common/providers.tf similarity index 100% rename from modules/guardduty/common/providers.tf rename to deprecated/guardduty/common/providers.tf diff --git a/modules/guardduty/common/remote-state.tf b/deprecated/guardduty/common/remote-state.tf similarity index 100% rename from modules/guardduty/common/remote-state.tf rename to deprecated/guardduty/common/remote-state.tf diff --git a/modules/guardduty/root/README.md b/deprecated/guardduty/root/README.md similarity index 100% rename from modules/guardduty/root/README.md rename to deprecated/guardduty/root/README.md diff --git a/modules/guardduty/common/context.tf b/deprecated/guardduty/root/context.tf similarity index 100% rename from modules/guardduty/common/context.tf rename to deprecated/guardduty/root/context.tf diff --git a/modules/guardduty/root/main.tf b/deprecated/guardduty/root/main.tf similarity index 100% rename from modules/guardduty/root/main.tf rename to deprecated/guardduty/root/main.tf diff --git a/modules/guardduty/root/providers.tf b/deprecated/guardduty/root/providers.tf similarity index 100% rename from modules/guardduty/root/providers.tf rename to deprecated/guardduty/root/providers.tf diff --git a/modules/guardduty/root/remote-state.tf b/deprecated/guardduty/root/remote-state.tf similarity index 100% rename from modules/guardduty/root/remote-state.tf rename to deprecated/guardduty/root/remote-state.tf diff --git a/modules/guardduty/root/variables.tf b/deprecated/guardduty/root/variables.tf similarity index 100% rename from modules/guardduty/root/variables.tf rename to deprecated/guardduty/root/variables.tf diff --git a/modules/guardduty/common/versions.tf b/deprecated/guardduty/root/versions.tf similarity index 100% rename from modules/guardduty/common/versions.tf rename to deprecated/guardduty/root/versions.tf diff --git a/modules/guardduty/common/README.md b/modules/guardduty/README.md similarity index 54% rename from modules/guardduty/common/README.md rename to modules/guardduty/README.md index 5fd4b4b10..378483051 100644 --- a/modules/guardduty/common/README.md +++ b/modules/guardduty/README.md @@ -1,71 +1,143 @@ -# Component: `guardduty/common` +# Component: `guardduty` -This component is responsible for configuring GuardDuty and it should be used in tandem with the [guardduty/root](../root) component. +This component is responsible for configuring GuardDuty within an AWS Organization. -AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by continuously monitoring for malicious activities and unauthorized behaviors. GuardDuty analyzes various data sources within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS logs, to detect potential security threats. +AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by +continuously monitoring for malicious activities and unauthorized behaviors. To detect potential security threats, +GuardDuty analyzes various data sources within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS +logs. Key features and components of AWS GuardDuty include: -- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event logs and network traffic data to detect patterns, anomalies, and known attack techniques. +- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence + to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event + logs and network traffic data to detect patterns, anomalies, and known attack techniques. -- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, domains, and other indicators of compromise. +- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global + community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, + domains, and other indicators of compromise. -- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS Lambda for immediate action or custom response workflows. +- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be + delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS + Lambda for immediate action or custom response workflows. -- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security policies and practices. +- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and + monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security + policies and practices. -- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of security incidents and reduces the need for manual intervention. +- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS + Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of + security incidents and reduces the need for manual intervention. -- Security findings and reports: GuardDuty provides detailed security findings and reports that include information about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed through the AWS Management Console or retrieved via APIs for further analysis and reporting. +- Security findings and reports: GuardDuty provides detailed security findings and reports that include information + about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed + through the AWS Management Console or retrieved via APIs for further analysis and reporting. -GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations with an additional layer of security to proactively identify and respond to potential security risks. +GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations +with an additional layer of security to proactively identify and respond to potential security risks. ## Usage **Stack Level**: Regional -The example snippet below shows how to use this component: +## Deployment Overview + +This component is complex in that it must be deployed multiple times with different variables set to configure the AWS +Organization successfully. + +It is further complicated by the fact that you must deploy each of the the component instances described below to +every region that existed before March 2019 and to any regions that have been opted-in as described in the [AWS +Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions). + +In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization +Delegated Administrator account is `security`, both in the `core` tenant. + +### Deploy to Delegated Admininstrator Account + +First, the component is deployed to the [Delegated +Admininstrator](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) account in each region in +order to configure the central GuardDuty detector that each account will send its findings to. ```yaml +# core-ue1-security components: terraform: - guardduty/common: + guardduty/delegated-administrator/ue1: metadata: - component: guardduty/common + component: guardduty vars: enabled: true - account_map_tenant: core - central_resource_collector_account: core-security - admin_delegated: true + delegated_administrator_account_name: core-security + environment: ue1 + region: us-east-1 ``` -## Deployment +```bash +atmos terraform apply guardduty/delegated-administrator/ue1 -s core-ue1-security +atmos terraform apply guardduty/delegated-administrator/ue2 -s core-ue2-security +atmos terraform apply guardduty/delegated-administrator/uw1 -s core-uw1-security +# ... other regions +``` -This set of steps assumes that `var.central_resource_collector_account = "core-security"`. +### Deploy to Organization Management (root) Account -1. Apply `guardduty/common` to `core-security` with `var.admin_delegated = false` -2. Apply `guardduty/root` to `core-root` -3. Apply `guardduty/common` to `core-security` with `var.admin_delegated = true` +Next, the component is deployed to the AWS Organization Management, a/k/a `root`, Account in order to set the AWS +Organization Designated Admininstrator account. -Example: +Note that you must use the `SuperAdmin` permissions as we are deploying to the AWS Organization Managment account. Since +we are using the `SuperAdmin` user, it will already have access to the state bucket, so we set the `role_arn` of the +backend config to null and set `var.privileged` to `true`. +```yaml +# core-ue1-root +components: + terraform: + guardduty/root/ue1: + metadata: + component: guardduty + backend: + s3: + role_arn: null + vars: + enabled: true + delegated_administrator_account_name: core-security + environment: ue1 + region: us-east-1 + privileged: true ``` -# Apply guardduty/common to all regions in core-security -atmos terraform apply guardduty/common-ue2 -s core-ue2-security -var=admin_delegated=false -atmos terraform apply guardduty/common-ue1 -s core-ue1-security -var=admin_delegated=false -atmos terraform apply guardduty/common-uw1 -s core-uw1-security -var=admin_delegated=false -# ... other regions -# Apply guardduty/root to all regions in core-root -atmos terraform apply guardduty/root-ue2 -s core-ue2-root -atmos terraform apply guardduty/root-ue1 -s core-ue1-root -atmos terraform apply guardduty/root-uw1 -s core-uw1-root +```bash +atmos terraform apply guardduty/root/ue1 -s core-ue1-root +atmos terraform apply guardduty/root/ue2 -s core-ue2-root +atmos terraform apply guardduty/root/uw1 -s core-uw1-root # ... other regions +``` + +### Deploy Organization Settings in Delegated Administrator Account + +Finally, the component is deployed to the Delegated Administrator Account again in order to create the +organization-wide configuration for the AWS Organization, but with `var.admin_delegated` set to `true` to indicate that +the delegation has already been performed from the Organization Management account. + +```yaml +# core-ue1-security +components: + terraform: + guardduty/org-settings/ue1: + metadata: + component: guardduty + vars: + enabled: true + delegated_administrator_account_name: core-security + environment: use1 + region: us-east-1 + admin_delegated: true +``` -# Apply guardduty/common to all regions in core-security but with default values for admin_delegated -atmos terraform apply guardduty/common-ue2 -s core-ue2-security -atmos terraform apply guardduty/common-ue1 -s core-ue1-security -atmos terraform apply guardduty/common-uw1 -s core-uw1-security +```bash +atmos terraform apply guardduty/org-settings/ue1 -s core-ue1-security +atmos terraform apply guardduty/org-settings/ue2 -s core-ue2-security +atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security # ... other regions ``` @@ -75,29 +147,32 @@ atmos terraform apply guardduty/common-uw1 -s core-uw1-security | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 4.0 | +| [aws](#requirement\_aws) | >= 5.0 | | [awsutils](#requirement\_awsutils) | >= 0.16.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.0 | +| [aws](#provider\_aws) | >= 5.0 | | [awsutils](#provider\_awsutils) | >= 0.16.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [guardduty](#module\_guardduty) | cloudposse/guardduty/aws | 0.5.0 | -| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [guardduty\_delegated\_detector](#module\_guardduty\_delegated\_detector) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources | Name | Type | |------|------| +| [aws_guardduty_organization_admin_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_admin_account) | resource | +| [aws_guardduty_organization_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_configuration) | resource | | [awsutils_guardduty_organization_settings.this](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/resources/guardduty_organization_settings) | resource | | [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | @@ -105,35 +180,41 @@ atmos terraform apply guardduty/common-uw1 -s core-uw1-security | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `""` | no | +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `"core"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the GuardDuty Admininstrator account has been designated from the root account.

This component should be applied with this variable set to false, then the guardduty-root component should be applied
to designate the administrator account, then this component should be applied again with this variable set to `true`. | `bool` | `true` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Admininstrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [central\_resource\_collector\_account](#input\_central\_resource\_collector\_account) | The name of the account that is the centralized aggregation account | `string` | n/a | yes | +| [auto\_enable\_organization\_members](#input\_auto\_enable\_organization\_members) | Indicates the auto-enablement configuration of GuardDuty for the member accounts in the organization. Valid values are `ALL`, `NEW`, `NONE`.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_configuration#auto_enable_organization_members | `string` | `"NEW"` | no | +| [cloudwatch\_enabled](#input\_cloudwatch\_enabled) | Flag to indicate whether CloudWatch logging should be enabled for GuardDuty | `bool` | `false` | no | | [cloudwatch\_event\_rule\_pattern\_detail\_type](#input\_cloudwatch\_event\_rule\_pattern\_detail\_type) | The detail-type pattern used to match events that will be sent to SNS.

For more information, see:
https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html
https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html
https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings_cloudwatch.html | `string` | `"GuardDuty Finding"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create\_sns\_topic](#input\_create\_sns\_topic) | Flag to indicate whether an SNS topic should be created for notifications.
If you want to send findings to a new SNS topic, set this to true and provide a valid configuration for subscribers. | `bool` | `false` | no | +| [create\_sns\_topic](#input\_create\_sns\_topic) | Flag to indicate whether an SNS topic should be created for notifications. If you want to send findings to a new SNS
topic, set this to true and provide a valid configuration for subscribers. | `bool` | `false` | no | +| [delegated\_admininstrator\_component\_name](#input\_delegated\_admininstrator\_component\_name) | The name of the component that created the GuardDuty detector. | `string` | `"guardduty/delegated-administrator"` | no | +| [delegated\_administrator\_account\_name](#input\_delegated\_administrator\_account\_name) | The name of the account that is the AWS Organization Delegated Administrator account | `string` | `"core-security"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [enable\_cloudwatch](#input\_enable\_cloudwatch) | Flag to indicate whether an CloudWatch logging should be enabled for GuardDuty | `bool` | `false` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [finding\_publishing\_frequency](#input\_finding\_publishing\_frequency) | The frequency of notifications sent for finding occurrences. If the detector is a GuardDuty member account, the value
is determined by the GuardDuty master account and cannot be modified, otherwise it defaults to SIX\_HOURS.

For standalone and GuardDuty master accounts, it must be configured in Terraform to enable drift detection.
Valid values for standalone and master accounts: FIFTEEN\_MINUTES, ONE\_HOUR, SIX\_HOURS."

For more information, see:
https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings_cloudwatch.html#guardduty_findings_cloudwatch_notification_frequency | `string` | `null` | no | -| [findings\_notification\_arn](#input\_findings\_notification\_arn) | The ARN for an SNS topic to send findings notifications to. This is only used if create\_sns\_topic is false.
If you want to send findings to an existing SNS topic, set the value of this to the ARN of the existing topic and set
create\_sns\_topic to false. | `string` | `null` | no | +| [findings\_notification\_arn](#input\_findings\_notification\_arn) | The ARN for an SNS topic to send findings notifications to. This is only used if create\_sns\_topic is false.
If you want to send findings to an existing SNS topic, set this to the ARN of the existing topic and set
create\_sns\_topic to false. | `string` | `null` | no | | [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [kubernetes\_audit\_logs\_enabled](#input\_kubernetes\_audit\_logs\_enabled) | If `true`, enables Kubernetes audit logs as a data source for Kubernetes protection.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector#audit_logs | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [malware\_protection\_scan\_ec2\_ebs\_volumes\_enabled](#input\_malware\_protection\_scan\_ec2\_ebs\_volumes\_enabled) | Configure whether Malware Protection is enabled as data source for EC2 instances EBS Volumes in GuardDuty.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector#malware-protection | `bool` | `false` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | +| [organization\_management\_account\_name](#input\_organization\_management\_account\_name) | The name of the AWS Organization management account | `string` | `null` | no | +| [privileged](#input\_privileged) | true if the default provider already has access to the backend | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account | `string` | `"root"` | no | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account. This is used to lookup account IDs from account names
using the `account-map` component. | `string` | `"root"` | no | +| [s3\_protection\_enabled](#input\_s3\_protection\_enabled) | If `true`, enables S3 protection.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector#s3-logs | `bool` | `true` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [subscribers](#input\_subscribers) | A map of subscription configurations for SNS topics

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription#argument-reference

protocol:
The protocol to use. The possible values for this are: sqs, sms, lambda, application. (http or https are partially
supported, see link) (email is an option but is unsupported in terraform, see link).
endpoint:
The endpoint to send data to, the contents will vary with the protocol. (see link for more information)
endpoint\_auto\_confirms:
Boolean indicating whether the end point is capable of auto confirming subscription e.g., PagerDuty. Default is
false
raw\_message\_delivery:
Boolean indicating whether or not to enable raw message delivery (the original message is directly passed, not wrapped in JSON with the original message in the message property).
Default is false |
map(object({
protocol = string
endpoint = string
endpoint_auto_confirms = bool
raw_message_delivery = bool
}))
| `{}` | no | +| [subscribers](#input\_subscribers) | A map of subscription configurations for SNS topics

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription#argument-reference

protocol:
The protocol to use. The possible values for this are: sqs, sms, lambda, application. (http or https are partially
supported, see link) (email is an option but is unsupported in terraform, see link).
endpoint:
The endpoint to send data to, the contents will vary with the protocol. (see link for more information)
endpoint\_auto\_confirms:
Boolean indicating whether the end point is capable of auto confirming subscription e.g., PagerDuty. Default is
false.
raw\_message\_delivery:
Boolean indicating whether or not to enable raw message delivery (the original message is directly passed, not
wrapped in JSON with the original message in the message property). Default is false. |
map(object({
protocol = string
endpoint = string
endpoint_auto_confirms = bool
raw_message_delivery = bool
}))
| `{}` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | @@ -141,14 +222,16 @@ atmos terraform apply guardduty/common-uw1 -s core-uw1-security | Name | Description | |------|-------------| -| [guardduty\_detector\_arn](#output\_guardduty\_detector\_arn) | GuardDuty detector ARN | -| [guardduty\_detector\_id](#output\_guardduty\_detector\_id) | GuardDuty detector ID | -| [sns\_topic\_name](#output\_sns\_topic\_name) | SNS topic name | -| [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | SNS topic subscriptions | +| [delegated\_administrator\_account\_id](#output\_delegated\_administrator\_account\_id) | The AWS Account ID of the AWS Organization delegated administrator account | +| [guardduty\_detector\_arn](#output\_guardduty\_detector\_arn) | The ARN of the GuardDuty detector created by the component | +| [guardduty\_detector\_id](#output\_guardduty\_detector\_id) | The ID of the GuardDuty detector created by the component | +| [sns\_topic\_name](#output\_sns\_topic\_name) | The name of the SNS topic created by the component | +| [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | The SNS topic subscriptions created by the component | ## References -* [AWS GuardDuty Documentation](https://aws.amazon.com/guardduty/) -* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/guardduty/common/) + +- [AWS GuardDuty Documentation](https://aws.amazon.com/guardduty/) +- [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/guardduty/common/) [](https://cpco.io/component) diff --git a/modules/guardduty/root/context.tf b/modules/guardduty/context.tf similarity index 100% rename from modules/guardduty/root/context.tf rename to modules/guardduty/context.tf diff --git a/modules/guardduty/main.tf b/modules/guardduty/main.tf new file mode 100644 index 000000000..0d966067e --- /dev/null +++ b/modules/guardduty/main.tf @@ -0,0 +1,82 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map + + current_account_id = one(data.aws_caller_identity.this[*].account_id) + member_account_id_list = [for a in keys(local.account_map) : (local.account_map[a]) if local.account_map[a] != local.current_account_id] + org_delegated_administrator_account_id = local.account_map[var.delegated_administrator_account_name] + org_management_account_id = var.organization_management_account_name == null ? local.account_map[module.account_map.outputs.root_account_account_name] : local.account_map[var.organization_management_account_name] + is_org_delegated_administrator_account = local.current_account_id == local.org_delegated_administrator_account_id + is_org_management_account = local.current_account_id == local.org_management_account_id + + create_sns_topic = local.enabled && var.create_sns_topic + create_guardduty_collector = local.enabled && local.is_org_delegated_administrator_account && !var.admin_delegated + create_org_delegation = local.enabled && local.is_org_management_account + create_org_configuration = local.enabled && local.is_org_delegated_administrator_account && var.admin_delegated +} + +data "aws_caller_identity" "this" { + count = local.enabled ? 1 : 0 +} + +# If we are are in the AWS Org management account, delegate GuardDuty to the org administrator account +# (usually the security account) +resource "aws_guardduty_organization_admin_account" "this" { + count = local.create_org_delegation ? 1 : 0 + + admin_account_id = local.org_delegated_administrator_account_id +} + +# If we are are in the AWS Org designated administrator account, enable the GuardDuty detector and optionally create an +# SNS topic for notifications and CloudWatch event rules for findings +module "guardduty" { + count = local.create_guardduty_collector ? 1 : 0 + source = "cloudposse/guardduty/aws" + version = "0.5.0" + + finding_publishing_frequency = var.finding_publishing_frequency + create_sns_topic = var.create_sns_topic + findings_notification_arn = var.findings_notification_arn + subscribers = var.subscribers + enable_cloudwatch = var.cloudwatch_enabled + cloudwatch_event_rule_pattern_detail_type = var.cloudwatch_event_rule_pattern_detail_type + s3_protection_enabled = var.s3_protection_enabled + + context = module.this.context +} + +# If we are are in the AWS Org designated administrator account, set the AWS Org-wide GuardDuty configuration by +# configuring all other accounts to send their GuardDuty findings to the detector in this account. +# +# This also configures the various Data Sources. +resource "awsutils_guardduty_organization_settings" "this" { + count = local.create_org_configuration ? 1 : 0 + + member_accounts = local.member_account_id_list + detector_id = module.guardduty_delegated_detector[0].outputs.guardduty_detector_id +} + +resource "aws_guardduty_organization_configuration" "this" { + count = local.create_org_configuration ? 1 : 0 + + auto_enable_organization_members = var.auto_enable_organization_members + detector_id = module.guardduty_delegated_detector[0].outputs.guardduty_detector_id + + datasources { + s3_logs { + auto_enable = var.s3_protection_enabled + } + kubernetes { + audit_logs { + enable = var.kubernetes_audit_logs_enabled + } + } + malware_protection { + scan_ec2_instance_with_findings { + ebs_volumes { + auto_enable = var.malware_protection_scan_ec2_ebs_volumes_enabled + } + } + } + } +} diff --git a/modules/guardduty/outputs.tf b/modules/guardduty/outputs.tf new file mode 100644 index 000000000..bfffac9a0 --- /dev/null +++ b/modules/guardduty/outputs.tf @@ -0,0 +1,24 @@ +output "delegated_administrator_account_id" { + value = local.org_delegated_administrator_account_id + description = "The AWS Account ID of the AWS Organization delegated administrator account" +} + +output "guardduty_detector_arn" { + value = local.create_guardduty_collector ? try(module.guardduty[0].guardduty_detector.arn, null) : null + description = "The ARN of the GuardDuty detector created by the component" +} + +output "guardduty_detector_id" { + value = local.create_guardduty_collector ? try(module.guardduty[0].guardduty_detector.id, null) : null + description = "The ID of the GuardDuty detector created by the component" +} + +output "sns_topic_name" { + value = local.create_guardduty_collector ? try(module.guardduty[0].sns_topic.name, null) : null + description = "The name of the SNS topic created by the component" +} + +output "sns_topic_subscriptions" { + value = local.create_guardduty_collector ? try(module.guardduty[0].sns_topic_subscriptions, null) : null + description = "The SNS topic subscriptions created by the component" +} diff --git a/modules/guardduty/providers.tf b/modules/guardduty/providers.tf new file mode 100644 index 000000000..2ad1813eb --- /dev/null +++ b/modules/guardduty/providers.tf @@ -0,0 +1,37 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + + +provider "awsutils" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/guardduty/remote-state.tf b/modules/guardduty/remote-state.tf new file mode 100644 index 000000000..5da185e0f --- /dev/null +++ b/modules/guardduty/remote-state.tf @@ -0,0 +1,27 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + component = "account-map" + tenant = var.account_map_tenant != "" ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} + +module "guardduty_delegated_detector" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + # If we are creating the delegated detector (because we are in the delegated admin account), then don't try to lookup + # the delegated detector ID from remote state + count = local.create_guardduty_collector ? 0 : 1 + + component = "${var.delegated_admininstrator_component_name}/${module.this.environment}" + stage = replace(var.delegated_administrator_account_name, "${module.this.tenant}-", "") + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/guardduty/common/variables.tf b/modules/guardduty/variables.tf similarity index 51% rename from modules/guardduty/common/variables.tf rename to modules/guardduty/variables.tf index 9304a7205..2e4c9eb4d 100644 --- a/modules/guardduty/common/variables.tf +++ b/modules/guardduty/variables.tf @@ -1,49 +1,76 @@ -variable "region" { - type = string - description = "AWS Region" -} - variable "account_map_tenant" { type = string - default = "" + default = "core" description = "The tenant where the `account_map` component required by remote-state is deployed" } -variable "root_account_stage" { +variable "admin_delegated" { + type = bool + default = false + description = < Date: Wed, 21 Jun 2023 10:54:29 -0400 Subject: [PATCH 157/501] roll guard duty back to previous providers logic (#727) Co-authored-by: cloudpossebot --- modules/guardduty/README.md | 2 ++ modules/guardduty/providers.tf | 31 ++++++++++++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/modules/guardduty/README.md b/modules/guardduty/README.md index 378483051..6730e0c5d 100644 --- a/modules/guardduty/README.md +++ b/modules/guardduty/README.md @@ -199,6 +199,8 @@ atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security | [findings\_notification\_arn](#input\_findings\_notification\_arn) | The ARN for an SNS topic to send findings notifications to. This is only used if create\_sns\_topic is false.
If you want to send findings to an existing SNS topic, set this to the ARN of the existing topic and set
create\_sns\_topic to false. | `string` | `null` | no | | [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kubernetes\_audit\_logs\_enabled](#input\_kubernetes\_audit\_logs\_enabled) | If `true`, enables Kubernetes audit logs as a data source for Kubernetes protection.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector#audit_logs | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/guardduty/providers.tf b/modules/guardduty/providers.tf index 2ad1813eb..b06180f50 100644 --- a/modules/guardduty/providers.tf +++ b/modules/guardduty/providers.tf @@ -1,30 +1,23 @@ provider "aws" { region = var.region - # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. - profile = module.iam_roles.terraform_profile_name - + profile = !var.privileged && module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null dynamic "assume_role" { - # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. - for_each = compact([module.iam_roles.terraform_role_arn]) + for_each = var.privileged || module.iam_roles.profiles_enabled ? [] : ["role"] content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) } } } - provider "awsutils" { region = var.region - # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. - profile = module.iam_roles.terraform_profile_name - + profile = !var.privileged && module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null dynamic "assume_role" { - # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. - for_each = compact([module.iam_roles.terraform_role_arn]) + for_each = var.privileged || module.iam_roles.profiles_enabled ? [] : ["role"] content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) } } } @@ -35,3 +28,15 @@ module "iam_roles" { context = module.this.context } + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} From 8a91732848ca2fed9029ab5657289e5f887e91db Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Wed, 21 Jun 2023 11:48:45 -0400 Subject: [PATCH 158/501] refactor securityhub component (#728) Co-authored-by: cloudpossebot --- .../securityhub}/securityhub/common/README.md | 0 .../securityhub/common/context.tf | 0 .../securityhub}/securityhub/common/main.tf | 0 .../securityhub/common/outputs.tf | 0 .../securityhub/common/providers.tf | 0 .../securityhub/common/remote-state.tf | 0 .../securityhub/common/variables.tf | 0 .../securityhub/common/versions.tf | 0 .../securityhub}/securityhub/root/README.md | 0 .../securityhub}/securityhub/root/context.tf | 0 .../securityhub}/securityhub/root/main.tf | 0 .../securityhub/root/providers.tf | 0 .../securityhub/root/remote-state.tf | 0 .../securityhub/root/variables.tf | 0 .../securityhub}/securityhub/root/versions.tf | 0 modules/security-hub/README.md | 245 +++++++++++++++ modules/security-hub/context.tf | 279 ++++++++++++++++++ modules/security-hub/main.tf | 77 +++++ modules/security-hub/outputs.tf | 14 + modules/security-hub/providers.tf | 43 +++ modules/security-hub/remote-state.tf | 12 + modules/security-hub/variables.tf | 187 ++++++++++++ modules/security-hub/versions.tf | 15 + 23 files changed, 872 insertions(+) rename {modules => deprecated/securityhub}/securityhub/common/README.md (100%) rename {modules => deprecated/securityhub}/securityhub/common/context.tf (100%) rename {modules => deprecated/securityhub}/securityhub/common/main.tf (100%) rename {modules => deprecated/securityhub}/securityhub/common/outputs.tf (100%) rename {modules => deprecated/securityhub}/securityhub/common/providers.tf (100%) rename {modules => deprecated/securityhub}/securityhub/common/remote-state.tf (100%) rename {modules => deprecated/securityhub}/securityhub/common/variables.tf (100%) rename {modules => deprecated/securityhub}/securityhub/common/versions.tf (100%) rename {modules => deprecated/securityhub}/securityhub/root/README.md (100%) rename {modules => deprecated/securityhub}/securityhub/root/context.tf (100%) rename {modules => deprecated/securityhub}/securityhub/root/main.tf (100%) rename {modules => deprecated/securityhub}/securityhub/root/providers.tf (100%) rename {modules => deprecated/securityhub}/securityhub/root/remote-state.tf (100%) rename {modules => deprecated/securityhub}/securityhub/root/variables.tf (100%) rename {modules => deprecated/securityhub}/securityhub/root/versions.tf (100%) create mode 100644 modules/security-hub/README.md create mode 100644 modules/security-hub/context.tf create mode 100644 modules/security-hub/main.tf create mode 100644 modules/security-hub/outputs.tf create mode 100644 modules/security-hub/providers.tf create mode 100644 modules/security-hub/remote-state.tf create mode 100644 modules/security-hub/variables.tf create mode 100644 modules/security-hub/versions.tf diff --git a/modules/securityhub/common/README.md b/deprecated/securityhub/securityhub/common/README.md similarity index 100% rename from modules/securityhub/common/README.md rename to deprecated/securityhub/securityhub/common/README.md diff --git a/modules/securityhub/common/context.tf b/deprecated/securityhub/securityhub/common/context.tf similarity index 100% rename from modules/securityhub/common/context.tf rename to deprecated/securityhub/securityhub/common/context.tf diff --git a/modules/securityhub/common/main.tf b/deprecated/securityhub/securityhub/common/main.tf similarity index 100% rename from modules/securityhub/common/main.tf rename to deprecated/securityhub/securityhub/common/main.tf diff --git a/modules/securityhub/common/outputs.tf b/deprecated/securityhub/securityhub/common/outputs.tf similarity index 100% rename from modules/securityhub/common/outputs.tf rename to deprecated/securityhub/securityhub/common/outputs.tf diff --git a/modules/securityhub/common/providers.tf b/deprecated/securityhub/securityhub/common/providers.tf similarity index 100% rename from modules/securityhub/common/providers.tf rename to deprecated/securityhub/securityhub/common/providers.tf diff --git a/modules/securityhub/common/remote-state.tf b/deprecated/securityhub/securityhub/common/remote-state.tf similarity index 100% rename from modules/securityhub/common/remote-state.tf rename to deprecated/securityhub/securityhub/common/remote-state.tf diff --git a/modules/securityhub/common/variables.tf b/deprecated/securityhub/securityhub/common/variables.tf similarity index 100% rename from modules/securityhub/common/variables.tf rename to deprecated/securityhub/securityhub/common/variables.tf diff --git a/modules/securityhub/common/versions.tf b/deprecated/securityhub/securityhub/common/versions.tf similarity index 100% rename from modules/securityhub/common/versions.tf rename to deprecated/securityhub/securityhub/common/versions.tf diff --git a/modules/securityhub/root/README.md b/deprecated/securityhub/securityhub/root/README.md similarity index 100% rename from modules/securityhub/root/README.md rename to deprecated/securityhub/securityhub/root/README.md diff --git a/modules/securityhub/root/context.tf b/deprecated/securityhub/securityhub/root/context.tf similarity index 100% rename from modules/securityhub/root/context.tf rename to deprecated/securityhub/securityhub/root/context.tf diff --git a/modules/securityhub/root/main.tf b/deprecated/securityhub/securityhub/root/main.tf similarity index 100% rename from modules/securityhub/root/main.tf rename to deprecated/securityhub/securityhub/root/main.tf diff --git a/modules/securityhub/root/providers.tf b/deprecated/securityhub/securityhub/root/providers.tf similarity index 100% rename from modules/securityhub/root/providers.tf rename to deprecated/securityhub/securityhub/root/providers.tf diff --git a/modules/securityhub/root/remote-state.tf b/deprecated/securityhub/securityhub/root/remote-state.tf similarity index 100% rename from modules/securityhub/root/remote-state.tf rename to deprecated/securityhub/securityhub/root/remote-state.tf diff --git a/modules/securityhub/root/variables.tf b/deprecated/securityhub/securityhub/root/variables.tf similarity index 100% rename from modules/securityhub/root/variables.tf rename to deprecated/securityhub/securityhub/root/variables.tf diff --git a/modules/securityhub/root/versions.tf b/deprecated/securityhub/securityhub/root/versions.tf similarity index 100% rename from modules/securityhub/root/versions.tf rename to deprecated/securityhub/securityhub/root/versions.tf diff --git a/modules/security-hub/README.md b/modules/security-hub/README.md new file mode 100644 index 000000000..7cb155b0f --- /dev/null +++ b/modules/security-hub/README.md @@ -0,0 +1,245 @@ +# Component: `security-hub` + +This component is responsible for configuring Security Hub within an AWS Organization. + +Amazon Security Hub enables users to centrally manage and monitor the security and compliance of their AWS accounts and +resources. It aggregates, organizes, and prioritizes security findings from various AWS services, third-party tools, and +integrated partner solutions. + +Here are the key features and capabilities of Amazon Security Hub: + +- Centralized security management: Security Hub provides a centralized dashboard where users can view and manage + security findings from multiple AWS accounts and regions. This allows for a unified view of the security posture + across the entire AWS environment. + +- Automated security checks: Security Hub automatically performs continuous security checks on AWS resources, + configurations, and security best practices. It leverages industry standards and compliance frameworks, such as AWS + CIS Foundations Benchmark, to identify potential security issues. + +- Integrated partner solutions: Security Hub integrates with a wide range of AWS native services, as well as third-party + security products and solutions. This integration enables the ingestion and analysis of security findings from diverse + sources, offering a comprehensive security view. + +- Security standards and compliance: Security Hub provides compliance checks against industry standards and regulatory + frameworks, such as PCI DSS, HIPAA, and GDPR. It identifies non-compliant resources and provides guidance on + remediation actions to ensure adherence to security best practices. + +- Prioritized security findings: Security Hub analyzes and prioritizes security findings based on severity, enabling + users to focus on the most critical issues. It assigns severity levels and generates a consolidated view of security + alerts, allowing for efficient threat response and remediation. + +- Custom insights and event aggregation: Security Hub supports custom insights, allowing users to create their own rules + and filters to focus on specific security criteria or requirements. It also provides event aggregation and correlation + capabilities to identify related security findings and potential attack patterns. + +- Integration with other AWS services: Security Hub seamlessly integrates with other AWS services, such as AWS + CloudTrail, Amazon GuardDuty, AWS Config, and AWS IAM Access Analyzer. This integration allows for enhanced + visibility, automated remediation, and streamlined security operations. + +- Alert notifications and automation: Security Hub supports alert notifications through Amazon SNS, enabling users to + receive real-time notifications of security findings. It also facilitates automation and response through integration + with AWS Lambda, allowing for automated remediation actions. + +By utilizing Amazon Security Hub, organizations can improve their security posture, gain insights into security risks, +and effectively manage security compliance across their AWS accounts and resources. + +## Usage + +**Stack Level**: Regional + +## Deployment Overview + +This component is complex in that it must be deployed multiple times with different variables set to configure the AWS +Organization successfully. + +It is further complicated by the fact that you must deploy each of the component instances described below to +every region that existed before March 2019 and to any regions that have been opted-in as described in the [AWS +Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions). + +In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization +Delegated Administrator account is `security`, both in the `core` tenant. + +### Deploy to Delegated Administrator Account + +First, the component is deployed to the [Delegated +Administrator](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) account in each region to +configure the Security Hub instance to which each account will send its findings. + +```yaml +# core-ue1-security +components: + terraform: + security-hub/delegated-administrator/ue1: + metadata: + component: security-hub + vars: + enabled: true + delegated_administrator_account_name: core-security + environment: ue1 + region: us-east-1 +``` + +```bash +atmos terraform apply security-hub/delegated-administrator/ue1 -s core-ue1-security +atmos terraform apply security-hub/delegated-administrator/ue2 -s core-ue2-security +atmos terraform apply security-hub/delegated-administrator/uw1 -s core-uw1-security +# ... other regions +``` + +### Deploy to Organization Management (root) Account + +Next, the component is deployed to the AWS Organization Management (a/k/a `root`) Account in order to set the AWS +Organization Designated Administrator account. + +Note that `SuperAdmin` permissions must be used as we are deploying to the AWS Organization Management account. Since we +are using the `SuperAdmin` user, it will already have access to the state bucket, so we set the `role_arn` of the +backend config to null and set `var.privileged` to `true`. + +```yaml +# core-ue1-root +components: + terraform: + security-hub/root/ue1: + metadata: + component: security-hub + backend: + s3: + role_arn: null + vars: + enabled: true + delegated_administrator_account_name: core-security + environment: ue1 + region: us-east-1 + privileged: true +``` + +```bash +atmos terraform apply security-hub/root/ue1 -s core-ue1-root +atmos terraform apply security-hub/root/ue2 -s core-ue2-root +atmos terraform apply security-hub/root/uw1 -s core-uw1-root +# ... other regions +``` + +### Deploy Organization Settings in Delegated Administrator Account + +Finally, the component is deployed to the Delegated Administrator Account again in order to create the organization-wide +Security Hub configuration for the AWS Organization, but with `var.admin_delegated` set to `true` this time to indicate +that the delegation from the Organization Management account has already been performed. + +```yaml +# core-ue1-security +components: + terraform: + security-hub/org-settings/ue1: + metadata: + component: security-hub + vars: + enabled: true + delegated_administrator_account_name: core-security + environment: use1 + region: us-east-1 + admin_delegated: true +``` + +```bash +atmos terraform apply security-hub/org-settings/ue1 -s core-ue1-security +atmos terraform apply security-hub/org-settings/ue2 -s core-ue2-security +atmos terraform apply security-hub/org-settings/uw1 -s core-uw1-security +# ... other regions +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 5.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.0 | +| [awsutils](#provider\_awsutils) | >= 0.16.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [security\_hub](#module\_security\_hub) | cloudposse/security-hub/aws | 0.10.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_securityhub_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_account) | resource | +| [aws_securityhub_organization_admin_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_organization_admin_account) | resource | +| [aws_securityhub_organization_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_organization_configuration) | resource | +| [awsutils_security_hub_organization_settings.this](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/resources/security_hub_organization_settings) | resource | +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `"core"` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the Security
Hub Admininstrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [auto\_enable\_organization\_members](#input\_auto\_enable\_organization\_members) | Flag to toggle auto-enablement of Security Hub for new member accounts in the organization.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_organization_configuration#auto_enable | `bool` | `true` | no | +| [cloudwatch\_event\_rule\_pattern\_detail\_type](#input\_cloudwatch\_event\_rule\_pattern\_detail\_type) | The detail-type pattern used to match events that will be sent to SNS.

For more information, see:
https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html
https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html | `string` | `"ecurity Hub Findings - Imported"` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_sns\_topic](#input\_create\_sns\_topic) | Flag to indicate whether an SNS topic should be created for notifications. If you want to send findings to a new SNS
topic, set this to true and provide a valid configuration for subscribers. | `bool` | `false` | no | +| [default\_standards\_enabled](#input\_default\_standards\_enabled) | Flag to indicate whether default standards should be enabled | `bool` | `true` | no | +| [delegated\_administrator\_account\_name](#input\_delegated\_administrator\_account\_name) | The name of the account that is the AWS Organization Delegated Administrator account | `string` | `"core-security"` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [enabled\_standards](#input\_enabled\_standards) | A list of standards to enable in the account.

For example:
- standards/aws-foundational-security-best-practices/v/1.0.0
- ruleset/cis-aws-foundations-benchmark/v/1.2.0
- standards/pci-dss/v/3.2.1
- standards/cis-aws-foundations-benchmark/v/1.4.0 | `set(string)` | `[]` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [finding\_aggregation\_region](#input\_finding\_aggregation\_region) | If finding aggreation is enabled, the region that collects findings | `string` | `null` | no | +| [finding\_aggregator\_enabled](#input\_finding\_aggregator\_enabled) | Flag to indicate whether a finding aggregator should be created

If you want to aggregate findings from one region, set this to `true`.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_finding_aggregator | `bool` | `false` | no | +| [finding\_aggregator\_linking\_mode](#input\_finding\_aggregator\_linking\_mode) | Linking mode to use for the finding aggregator.

The possible values are:
- `ALL_REGIONS` - Aggregate from all regions
- `ALL_REGIONS_EXCEPT_SPECIFIED` - Aggregate from all regions except those specified in `var.finding_aggregator_regions`
- `SPECIFIED_REGIONS` - Aggregate from regions specified in `var.finding_aggregator_regions` | `string` | `"ALL_REGIONS"` | no | +| [finding\_aggregator\_regions](#input\_finding\_aggregator\_regions) | A list of regions to aggregate findings from.

This is only used if `finding_aggregator_enabled` is `true`. | `any` | `null` | no | +| [findings\_notification\_arn](#input\_findings\_notification\_arn) | The ARN for an SNS topic to send findings notifications to. This is only used if create\_sns\_topic is false.
If you want to send findings to an existing SNS topic, set this to the ARN of the existing topic and set
create\_sns\_topic to false. | `string` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [organization\_management\_account\_name](#input\_organization\_management\_account\_name) | The name of the AWS Organization management account | `string` | `null` | no | +| [privileged](#input\_privileged) | true if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account. This is used to lookup account IDs from account names
using the `account-map` component. | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [subscribers](#input\_subscribers) | A map of subscription configurations for SNS topics

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription#argument-reference

protocol:
The protocol to use. The possible values for this are: sqs, sms, lambda, application. (http or https are partially
supported, see link) (email is an option but is unsupported in terraform, see link).
endpoint:
The endpoint to send data to, the contents will vary with the protocol. (see link for more information)
endpoint\_auto\_confirms:
Boolean indicating whether the end point is capable of auto confirming subscription e.g., PagerDuty. Default is
false.
raw\_message\_delivery:
Boolean indicating whether or not to enable raw message delivery (the original message is directly passed, not
wrapped in JSON with the original message in the message property). Default is false. |
map(object({
protocol = string
endpoint = string
endpoint_auto_confirms = bool
raw_message_delivery = bool
}))
| `{}` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [delegated\_administrator\_account\_id](#output\_delegated\_administrator\_account\_id) | The AWS Account ID of the AWS Organization delegated administrator account | +| [sns\_topic\_name](#output\_sns\_topic\_name) | The name of the SNS topic created by the component | +| [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | The SNS topic subscriptions created by the component | + + +## References + +- [AWS Security Hub Documentation](https://aws.amazon.com/security-hub/) +- [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/security-hub) + +[](https://cpco.io/component) diff --git a/modules/security-hub/context.tf b/modules/security-hub/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/security-hub/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/security-hub/main.tf b/modules/security-hub/main.tf new file mode 100644 index 000000000..9ff80f673 --- /dev/null +++ b/modules/security-hub/main.tf @@ -0,0 +1,77 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map + + current_account_id = one(data.aws_caller_identity.this[*].account_id) + member_account_id_list = [for a in keys(local.account_map) : (local.account_map[a]) if local.account_map[a] != local.current_account_id] + org_delegated_administrator_account_id = local.account_map[var.delegated_administrator_account_name] + org_management_account_id = var.organization_management_account_name == null ? local.account_map[module.account_map.outputs.root_account_account_name] : local.account_map[var.organization_management_account_name] + is_org_delegated_administrator_account = local.current_account_id == local.org_delegated_administrator_account_id + is_org_management_account = local.current_account_id == local.org_management_account_id + is_finding_aggregation_region = local.enabled && var.finding_aggregator_enabled && var.finding_aggregation_region == data.aws_region.this[0].name + + create_sns_topic = local.enabled && var.create_sns_topic + create_securityhub = local.enabled && local.is_org_delegated_administrator_account && !var.admin_delegated + create_org_delegation = local.enabled && local.is_org_management_account + create_org_configuration = local.enabled && local.is_org_delegated_administrator_account && var.admin_delegated +} + +data "aws_caller_identity" "this" { + count = local.enabled ? 1 : 0 +} + +data "aws_region" "this" { + count = local.enabled ? 1 : 0 +} + +# If we are running in the AWS Org Management account, delegate Security Hub to the Delegated Admininstrator account +# (usually the security account). We also need to turn on Security Hub in the Management account so that it can +# aggregate findings and be managed by the Delegated Admininstrator account. +resource "aws_securityhub_organization_admin_account" "this" { + count = local.create_org_delegation ? 1 : 0 + + admin_account_id = local.org_delegated_administrator_account_id +} + +resource "aws_securityhub_account" "this" { + count = local.create_org_delegation ? 1 : 0 + + enable_default_standards = var.default_standards_enabled +} + +# If we are running in the AWS Org designated administrator account, enable Security Hub and optionally enable standards +# and finding aggregation +module "security_hub" { + count = local.create_securityhub ? 1 : 0 + source = "cloudposse/security-hub/aws" + version = "0.10.0" + + + cloudwatch_event_rule_pattern_detail_type = var.cloudwatch_event_rule_pattern_detail_type + create_sns_topic = local.create_sns_topic + enable_default_standards = var.default_standards_enabled + enabled_standards = var.enabled_standards + finding_aggregator_enabled = local.is_finding_aggregation_region + finding_aggregator_linking_mode = var.finding_aggregator_linking_mode + finding_aggregator_regions = var.finding_aggregator_regions + imported_findings_notification_arn = var.findings_notification_arn + subscribers = var.subscribers + + context = module.this.context +} + +# If we are running in the AWS Org designated administrator account with admin_delegated set to tru, set the AWS +# Organization-wide Security Hub configuration by configuring all other accounts to send their Security Hub findings to +# this account. +resource "awsutils_security_hub_organization_settings" "this" { + count = local.create_org_configuration ? 1 : 0 + + member_accounts = local.member_account_id_list +} + +resource "aws_securityhub_organization_configuration" "this" { + count = local.create_org_configuration ? 1 : 0 + + auto_enable = var.auto_enable_organization_members + auto_enable_standards = var.default_standards_enabled ? "DEFAULT" : "NONE" +} diff --git a/modules/security-hub/outputs.tf b/modules/security-hub/outputs.tf new file mode 100644 index 000000000..542055299 --- /dev/null +++ b/modules/security-hub/outputs.tf @@ -0,0 +1,14 @@ +output "delegated_administrator_account_id" { + value = local.org_delegated_administrator_account_id + description = "The AWS Account ID of the AWS Organization delegated administrator account" +} + +output "sns_topic_name" { + value = local.create_securityhub ? try(module.security_hub[0].sns_topic.name, null) : null + description = "The name of the SNS topic created by the component" +} + +output "sns_topic_subscriptions" { + value = local.create_securityhub ? try(module.security_hub[0].sns_topic_subscriptions, null) : null + description = "The SNS topic subscriptions created by the component" +} diff --git a/modules/security-hub/providers.tf b/modules/security-hub/providers.tf new file mode 100644 index 000000000..eed361d44 --- /dev/null +++ b/modules/security-hub/providers.tf @@ -0,0 +1,43 @@ + +provider "aws" { + region = var.region + + profile = !var.privileged && module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = var.privileged || module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +provider "awsutils" { + region = var.region + + profile = !var.privileged && module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + dynamic "assume_role" { + for_each = var.privileged || module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + privileged = var.privileged + + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/security-hub/remote-state.tf b/modules/security-hub/remote-state.tf new file mode 100644 index 000000000..da115834a --- /dev/null +++ b/modules/security-hub/remote-state.tf @@ -0,0 +1,12 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + component = "account-map" + tenant = var.account_map_tenant != "" ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/security-hub/variables.tf b/modules/security-hub/variables.tf new file mode 100644 index 000000000..6cdc54e79 --- /dev/null +++ b/modules/security-hub/variables.tf @@ -0,0 +1,187 @@ +variable "account_map_tenant" { + type = string + default = "core" + description = "The tenant where the `account_map` component required by remote-state is deployed" +} + +variable "admin_delegated" { + type = bool + default = false + description = < Date: Wed, 21 Jun 2023 23:03:15 +0300 Subject: [PATCH 159/501] [lambda] feat: allows to use YAML instead of JSON for IAM policy (#692) --- modules/lambda/README.md | 35 +++++++++++++++++++++++++++-------- modules/lambda/main.tf | 26 ++++++++++++-------------- modules/lambda/variables.tf | 35 +++++++++++++++++++++++++++++++++-- modules/lambda/versions.tf | 2 +- 4 files changed, 73 insertions(+), 25 deletions(-) diff --git a/modules/lambda/README.md b/modules/lambda/README.md index d212189ee..570fb0ee6 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -41,8 +41,27 @@ components: # https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html runtime: python3.9 package_type: Zip # `Zip` or `Image` - policy_json: null - + policy_json: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ListAllBuckets", + "Effect": "Allow", + "Action": "s3:ListAllMyBuckets", + "Resource": "*" + } + ] + } + iam_policy: + statements: + - sid: AllowSQSWorkerWriteAccess + effect: Allow + actions: + - sqs:SendMessage + - sqs:SendMessageBatch + resources: + - arn:aws:sqs:*:111111111111:worker-queue # Filename example filename: lambdas/hello-world-python/output.zip # generated by zip variable. zip: @@ -53,7 +72,6 @@ components: # S3 Source Example # s3_bucket_name: lambda-source # lambda main.tf calculates the rest of the bucket_name # s3_key: hello-world-go.zip - ``` @@ -62,7 +80,7 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | | [archive](#requirement\_archive) | >= 2.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | @@ -77,6 +95,7 @@ components: | Name | Source | Version | |------|--------|---------| +| [iam\_policy](#module\_iam\_policy) | cloudposse/iam-policy/aws | 1.0.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [label](#module\_label) | cloudposse/label/null | 0.25.0 | | [lambda](#module\_lambda) | cloudposse/lambda-function/aws | 0.4.1 | @@ -86,7 +105,6 @@ components: | Name | Type | |------|------| -| [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [archive_file.lambdazip](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | @@ -103,7 +121,7 @@ components: | [cloudwatch\_logs\_kms\_key\_arn](#input\_cloudwatch\_logs\_kms\_key\_arn) | The ARN of the KMS Key to use when encrypting log data. | `string` | `null` | no | | [cloudwatch\_logs\_retention\_in\_days](#input\_cloudwatch\_logs\_retention\_in\_days) | Specifies the number of days you want to retain log events in the specified log group. Possible values are:
1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653, and 0. If you select 0, the events in the
log group are always retained and never expire. | `number` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [custom\_iam\_policy\_arns](#input\_custom\_iam\_policy\_arns) | ARNs of custom policies to be attached to the lambda role | `set(string)` | `[]` | no | +| [custom\_iam\_policy\_arns](#input\_custom\_iam\_policy\_arns) | ARNs of IAM policies to be attached to the Lambda role | `set(string)` | `[]` | no | | [dead\_letter\_config\_target\_arn](#input\_dead\_letter\_config\_target\_arn) | ARN of an SNS topic or SQS queue to notify when an invocation fails. If this option is used, the function's IAM role
must be granted suitable access to write to the target object, which means allowing either the sns:Publish or
sqs:SendMessage action on this ARN, depending on which service is targeted." | `string` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | Description of what the Lambda Function does. | `string` | `null` | no | @@ -112,8 +130,9 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [event\_source\_mappings](#input\_event\_source\_mappings) | Creates event source mappings to allow the Lambda function to get events from Kinesis, DynamoDB and SQS. The IAM role
of this Lambda function will be enhanced with necessary minimum permissions to get those events. | `any` | `{}` | no | | [filename](#input\_filename) | The path to the function's deployment package within the local filesystem. If defined, The s3\_-prefixed options and image\_uri cannot be used. | `string` | `null` | no | -| [function\_name](#input\_function\_name) | Unique name for the Lambda Function. | `string` | n/a | yes | +| [function\_name](#input\_function\_name) | Unique name for the Lambda Function. | `string` | `null` | no | | [handler](#input\_handler) | The function entrypoint in your code. | `string` | `null` | no | +| [iam\_policy](#input\_iam\_policy) | IAM policy to attach to the Lambda role, specified as a Terraform object. This can be used with or instead of `var.policy_json`. |
object({
policy_id = optional(string, null)
version = optional(string, null)
statements = list(object({
sid = optional(string, null)
effect = optional(string, null)
actions = optional(list(string), null)
not_actions = optional(list(string), null)
resources = optional(list(string), null)
not_resources = optional(list(string), null)
conditions = optional(list(object({
test = string
variable = string
values = list(string)
})), [])
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
not_principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
}))
})
| `null` | no | | [iam\_policy\_description](#input\_iam\_policy\_description) | Description of the IAM policy for the Lambda IAM role | `string` | `"Minimum SSM read permissions for Lambda IAM Role"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [ignore\_external\_function\_updates](#input\_ignore\_external\_function\_updates) | Ignore updates to the Lambda Function executed externally to the Terraform lifecycle. Set this to `true` if you're
using CodeDeploy, aws CLI or other external tools to update the Lambda Function code." | `bool` | `false` | no | @@ -132,7 +151,7 @@ components: | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [package\_type](#input\_package\_type) | The Lambda deployment package type. Valid values are `Zip` and `Image`. | `string` | `"Zip"` | no | | [permissions\_boundary](#input\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the role | `string` | `""` | no | -| [policy\_json](#input\_policy\_json) | IAM policy to attach to the Lambda IAM role | `string` | `null` | no | +| [policy\_json](#input\_policy\_json) | IAM policy to attach to the Lambda role, specified as JSON. This can be used with or instead of `var.iam_policy`. | `string` | `null` | no | | [publish](#input\_publish) | Whether to publish creation/change as new Lambda Function Version. | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | diff --git a/modules/lambda/main.tf b/modules/lambda/main.tf index d2944c185..29d808b00 100644 --- a/modules/lambda/main.tf +++ b/modules/lambda/main.tf @@ -1,11 +1,9 @@ locals { enabled = module.this.enabled - iam_policy_enabled = local.enabled && var.policy_json != null + iam_policy_enabled = local.enabled && (try(length(var.iam_policy), 0) > 0 || var.policy_json != null) s3_bucket_full_name = var.s3_bucket_name != null ? format("%s-%s-%s-%s-%s", module.this.namespace, module.this.tenant, module.this.environment, module.this.stage, var.s3_bucket_name) : null } - - module "label" { source = "cloudposse/label/null" version = "0.25.0" @@ -15,37 +13,37 @@ module "label" { context = module.this.context } -resource "aws_iam_policy" "default" { - count = local.iam_policy_enabled ? 1 : 0 +module "iam_policy" { + count = local.iam_policy_enabled ? 1 : 0 + source = "cloudposse/iam-policy/aws" + version = "1.0.1" - name = module.label.id - path = "/" - description = format("%s Lambda policy", module.label.id) - policy = var.policy_json + iam_policy_enabled = true + iam_policy = var.iam_policy + iam_source_policy_documents = var.policy_json != null ? [var.policy_json] : [] - tags = module.this.tags + context = module.this.context } resource "aws_iam_role_policy_attachment" "default" { count = local.iam_policy_enabled ? 1 : 0 role = module.lambda.role_name - policy_arn = aws_iam_policy.default[0].arn + policy_arn = module.iam_policy[0].policy_arn } data "archive_file" "lambdazip" { count = var.zip.enabled ? 1 : 0 type = "zip" output_path = "${path.module}/lambdas/${var.zip.output}" - - source_dir = "${path.module}/lambdas/${var.zip.input_dir}" + source_dir = "${path.module}/lambdas/${var.zip.input_dir}" } module "lambda" { source = "cloudposse/lambda-function/aws" version = "0.4.1" - function_name = module.label.id + function_name = coalesce(var.function_name, module.label.id) description = var.description handler = var.handler lambda_environment = var.lambda_environment diff --git a/modules/lambda/variables.tf b/modules/lambda/variables.tf index 88f59b33e..2dbcd85cb 100644 --- a/modules/lambda/variables.tf +++ b/modules/lambda/variables.tf @@ -6,6 +6,7 @@ variable "region" { variable "function_name" { type = string description = "Unique name for the Lambda Function." + default = null } variable "architectures" { @@ -246,7 +247,7 @@ variable "vpc_config" { variable "custom_iam_policy_arns" { type = set(string) - description = "ARNs of custom policies to be attached to the lambda role" + description = "ARNs of IAM policies to be attached to the Lambda role" default = [] } @@ -268,7 +269,37 @@ variable "iam_policy_description" { variable "policy_json" { type = string - description = "IAM policy to attach to the Lambda IAM role" + description = "IAM policy to attach to the Lambda role, specified as JSON. This can be used with or instead of `var.iam_policy`." + default = null +} + +variable "iam_policy" { + type = object({ + policy_id = optional(string, null) + version = optional(string, null) + statements = list(object({ + sid = optional(string, null) + effect = optional(string, null) + actions = optional(list(string), null) + not_actions = optional(list(string), null) + resources = optional(list(string), null) + not_resources = optional(list(string), null) + conditions = optional(list(object({ + test = string + variable = string + values = list(string) + })), []) + principals = optional(list(object({ + type = string + identifiers = list(string) + })), []) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + })), []) + })) + }) + description = "IAM policy to attach to the Lambda role, specified as a Terraform object. This can be used with or instead of `var.policy_json`." default = null } diff --git a/modules/lambda/versions.tf b/modules/lambda/versions.tf index 37fe7029f..b3dce983a 100644 --- a/modules/lambda/versions.tf +++ b/modules/lambda/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.3.0" required_providers { aws = { From 81eca55c8faa63e21a4819c2f11890b3a2d3e6c5 Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 21 Jun 2023 15:44:34 -0700 Subject: [PATCH 160/501] [account-map] Feature flag to enable legacy Terraform role mapping (#730) --- modules/account-map/README.md | 1 + modules/account-map/main.tf | 23 ++++++++++++++++------- modules/account-map/variables.tf | 10 ++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index 26ddf9d91..e91970a4e 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -106,6 +106,7 @@ components: | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [legacy\_terraform\_uses\_admin](#input\_legacy\_terraform\_uses\_admin) | If `true`, the legacy behavior of using the `admin` role rather than the `terraform` role in the
`root` and identity accounts will be preserved.
The default is to use the negations of the value of `terraform_dynamic_role_enabled`. | `bool` | `null` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [profile\_template](#input\_profile\_template) | The template used to render AWS Profile names.
Default is appropriate when using `tenant` and default label order with `null-label`.
Use `"%s-%s-%s-%s"` when not using `tenant`.

Note that if the `null-label` variable `label_order` is truncated or extended with additional labels, this template will
need to be updated to reflect the new number of labels. | `string` | `"%s-%s-%s-%s-%s"` | no | diff --git a/modules/account-map/main.tf b/modules/account-map/main.tf index e465a3e88..8f48f13b5 100644 --- a/modules/account-map/main.tf +++ b/modules/account-map/main.tf @@ -3,7 +3,8 @@ data "aws_organizations_organization" "organization" {} data "aws_partition" "current" {} locals { - aws_partition = data.aws_partition.current.partition + aws_partition = data.aws_partition.current.partition + legacy_terraform_uses_admin = coalesce(var.legacy_terraform_uses_admin, !var.terraform_dynamic_role_enabled) full_account_map = { for acct in data.aws_organizations_organization.organization.accounts @@ -57,10 +58,16 @@ locals { terraform_roles = { - for name, info in local.account_info_map : name => format(local.iam_role_arn_templates[name], "terraform") + for name, info in local.account_info_map : name => format(local.iam_role_arn_templates[name], + (local.legacy_terraform_uses_admin && + contains([ + var.root_account_account_name, + var.identity_account_account_name + ], name) + ) ? "admin" : "terraform") } - // legacy support for `aws` config profiles, where root and identity accounts' terraform profiles are not for Terraforming + // legacy support for `aws` config profiles terraform_profiles = { for name, info in local.account_info_map : name => format(var.profile_template, compact( [ @@ -68,10 +75,12 @@ locals { lookup(info, "tenant", ""), module.this.environment, info.stage, - (contains([ - var.root_account_account_name, - var.identity_account_account_name - ], name) ? "admin" : "terraform") + ((local.legacy_terraform_uses_admin && + contains([ + var.root_account_account_name, + var.identity_account_account_name + ], name) + ) ? "admin" : "terraform"), ] )...) } diff --git a/modules/account-map/variables.tf b/modules/account-map/variables.tf index 7a6768cdb..826f52853 100644 --- a/modules/account-map/variables.tf +++ b/modules/account-map/variables.tf @@ -93,6 +93,16 @@ variable "terraform_role_name_map" { } } +variable "legacy_terraform_uses_admin" { + type = bool + description = <<-EOT + If `true`, the legacy behavior of using the `admin` role rather than the `terraform` role in the + `root` and identity accounts will be preserved. + The default is to use the negations of the value of `terraform_dynamic_role_enabled`. + EOT + default = null +} + variable "terraform_dynamic_role_enabled" { type = bool description = "If true, the IAM role Terraform will assume will depend on the identity of the user running terraform" From 8d61dbec17987dafa3f12f1cd9fe14b4105eaeba Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 22 Jun 2023 14:05:58 -0700 Subject: [PATCH 161/501] [account-map] Backwards compatibility for terraform profile users and eks/cluster (#731) --- modules/account-map/modules/iam-roles/main.tf | 2 +- .../modules/iam-roles/variables.tf | 9 ++++++- modules/eks/cluster/providers.tf | 5 +++- modules/guardduty/README.md | 2 -- modules/guardduty/providers.tf | 24 +++++-------------- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/modules/account-map/modules/iam-roles/main.tf b/modules/account-map/modules/iam-roles/main.tf index 45278172a..fdd0db25e 100644 --- a/modules/account-map/modules/iam-roles/main.tf +++ b/modules/account-map/modules/iam-roles/main.tf @@ -28,7 +28,7 @@ module "account_map" { } locals { - profiles_enabled = local.account_map.profiles_enabled + profiles_enabled = coalesce(var.profiles_enabled, local.account_map.profiles_enabled) account_map = module.account_map.outputs account_name = lookup(module.always.descriptors, "account_name", module.always.stage) diff --git a/modules/account-map/modules/iam-roles/variables.tf b/modules/account-map/modules/iam-roles/variables.tf index 554da715f..247a08c38 100644 --- a/modules/account-map/modules/iam-roles/variables.tf +++ b/modules/account-map/modules/iam-roles/variables.tf @@ -1,9 +1,16 @@ variable "privileged" { type = bool - description = "True if the default provider already has access to the backend" + description = "True if the Terraform user already has access to the backend" default = false } +variable "profiles_enabled" { + type = bool + description = "Whether or not to use profiles instead of roles for Terraform. Default (null) means to use global settings." + default = null +} + + ## The overridable_* variables in this file provide Cloud Posse defaults. ## Because this module is used in bootstrapping Terraform, we do not configure ## these inputs in the normal way. Instead, to change the values, you should diff --git a/modules/eks/cluster/providers.tf b/modules/eks/cluster/providers.tf index 195cc9718..8ad77541f 100644 --- a/modules/eks/cluster/providers.tf +++ b/modules/eks/cluster/providers.tf @@ -15,6 +15,9 @@ provider "aws" { } module "iam_roles" { - source = "../../account-map/modules/iam-roles" + source = "../../account-map/modules/iam-roles" + + profiles_enabled = false + context = module.this.context } diff --git a/modules/guardduty/README.md b/modules/guardduty/README.md index 6730e0c5d..378483051 100644 --- a/modules/guardduty/README.md +++ b/modules/guardduty/README.md @@ -199,8 +199,6 @@ atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security | [findings\_notification\_arn](#input\_findings\_notification\_arn) | The ARN for an SNS topic to send findings notifications to. This is only used if create\_sns\_topic is false.
If you want to send findings to an existing SNS topic, set this to the ARN of the existing topic and set
create\_sns\_topic to false. | `string` | `null` | no | | [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [kubernetes\_audit\_logs\_enabled](#input\_kubernetes\_audit\_logs\_enabled) | If `true`, enables Kubernetes audit logs as a data source for Kubernetes protection.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector#audit_logs | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/guardduty/providers.tf b/modules/guardduty/providers.tf index b06180f50..e4a566a8a 100644 --- a/modules/guardduty/providers.tf +++ b/modules/guardduty/providers.tf @@ -1,11 +1,11 @@ provider "aws" { region = var.region - profile = !var.privileged && module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + profile = !var.privileged && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null dynamic "assume_role" { - for_each = var.privileged || module.iam_roles.profiles_enabled ? [] : ["role"] + for_each = var.privileged || module.iam_roles.profiles_enabled || (module.iam_roles.terraform_role_arn == null) ? [] : ["role"] content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -13,11 +13,11 @@ provider "aws" { provider "awsutils" { region = var.region - profile = !var.privileged && module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + profile = !var.privileged && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null dynamic "assume_role" { - for_each = var.privileged || module.iam_roles.profiles_enabled ? [] : ["role"] + for_each = var.privileged || module.iam_roles.profiles_enabled || (module.iam_roles.terraform_role_arn == null) ? [] : ["role"] content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = module.iam_roles.terraform_role_arn } } } @@ -28,15 +28,3 @@ module "iam_roles" { context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} From 9f81bc120bc7cb87bb7f93dbc9e8e84ce6c4db8c Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Mon, 26 Jun 2023 14:14:08 -0400 Subject: [PATCH 162/501] Update `eks/echo-server` and `eks/alb-controller-ingress-group` components (#733) Co-authored-by: cloudpossebot --- .../alb-controller-ingress-group/README.md | 19 ++++++++++++------- .../eks/alb-controller-ingress-group/main.tf | 5 ++++- .../alb-controller-ingress-group/outputs.tf | 12 ++++++++++++ .../remote-state.tf | 8 ++++---- modules/eks/argocd/provider-secrets.tf | 2 +- modules/eks/echo-server/README.md | 13 ++++++++----- .../echo-server/charts/echo-server/Chart.yaml | 4 ++-- .../charts/echo-server/templates/ingress.yaml | 2 +- modules/eks/echo-server/main.tf | 8 ++++++-- modules/eks/echo-server/remote-state.tf | 4 ++-- 10 files changed, 52 insertions(+), 25 deletions(-) diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index 5016390e1..d197b1056 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -1,6 +1,6 @@ -# alb-controller-ingress-group +# Component `eks/alb-controller-ingress-group` -This component creates a Kubernetes Service that creates an ALB for a specific [IngressGroup]. +This component provisions a Kubernetes Service that creates an ALB for a specific [IngressGroup]. An [IngressGroup] is a feature of the [alb-controller] which allows multiple Kubernetes Ingresses to share the same Application Load Balancer. @@ -15,12 +15,15 @@ import: - catalog/eks/alb-controller-ingress-group ``` -The default catalog values `e.g. stacks/catalog/eks/alb-controller-ingress-group.yaml` will create a Kubernetes Service in the `default` namespace with an [IngressGroup] named `alb-controller-ingress-group`. +The default catalog values `e.g. stacks/catalog/eks/alb-controller-ingress-group.yaml` +will create a Kubernetes Service in the `default` namespace with an [IngressGroup] named `alb-controller-ingress-group`. ```yaml components: terraform: eks/alb-controller-ingress-group: + metadata: + component: eks/alb-controller-ingress-group settings: spacelift: workspace_enabled: true @@ -50,13 +53,13 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [load\_balancer\_name](#module\_load\_balancer\_name) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | ## Resources @@ -128,7 +131,9 @@ components: | [annotations](#output\_annotations) | The annotations of the Ingress | | [group\_name](#output\_group\_name) | The value of `alb.ingress.kubernetes.io/group.name` of the Ingress | | [host](#output\_host) | The name of the host used by the Ingress | +| [ingress\_class](#output\_ingress\_class) | The value of the `kubernetes.io/ingress.class` annotation of the Kubernetes Ingress | | [load\_balancer\_name](#output\_load\_balancer\_name) | The name of the load balancer created by the Ingress | +| [load\_balancer\_scheme](#output\_load\_balancer\_scheme) | The value of the `alb.ingress.kubernetes.io/scheme` annotation of the Kubernetes Ingress | | [message\_body\_length](#output\_message\_body\_length) | The length of the message body to ensure it's lower than the maximum limit | diff --git a/modules/eks/alb-controller-ingress-group/main.tf b/modules/eks/alb-controller-ingress-group/main.tf index bf482cd09..205c0e8f8 100644 --- a/modules/eks/alb-controller-ingress-group/main.tf +++ b/modules/eks/alb-controller-ingress-group/main.tf @@ -33,7 +33,9 @@ locals { # for outputs annotations = try(kubernetes_ingress_v1.default[0].metadata.0.annotations, null) group_name_annotation = try(lookup(kubernetes_ingress_v1.default[0].metadata.0.annotations, "alb.ingress.kubernetes.io/group.name", null), null) - load_balancer_name = join("", data.aws_lb.default[*].name) + scheme_annotation = try(lookup(kubernetes_ingress_v1.default[0].metadata.0.annotations, "alb.ingress.kubernetes.io/scheme", null), null) + class_annotation = try(lookup(kubernetes_ingress_v1.default[0].metadata.0.annotations, "kubernetes.io/ingress.class", null), null) + load_balancer_name = one(data.aws_lb.default[*].name) host = join(".", [module.this.environment, module.dns_delegated.outputs.default_domain_name]) } @@ -89,6 +91,7 @@ resource "kubernetes_ingress_v1" "default" { labels = {} name = module.this.id namespace = local.kubernetes_namespace + annotations = merge( local.waf_acl_arn, local.alb_logging_annotation, diff --git a/modules/eks/alb-controller-ingress-group/outputs.tf b/modules/eks/alb-controller-ingress-group/outputs.tf index 8926b3a80..5a27ced83 100644 --- a/modules/eks/alb-controller-ingress-group/outputs.tf +++ b/modules/eks/alb-controller-ingress-group/outputs.tf @@ -22,3 +22,15 @@ output "message_body_length" { description = "The length of the message body to ensure it's lower than the maximum limit" value = length(local.message_body) } + +# https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/ +output "load_balancer_scheme" { + description = "The value of the `alb.ingress.kubernetes.io/scheme` annotation of the Kubernetes Ingress" + value = local.scheme_annotation +} + +# https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/ +output "ingress_class" { + description = "The value of the `kubernetes.io/ingress.class` annotation of the Kubernetes Ingress" + value = local.class_annotation +} diff --git a/modules/eks/alb-controller-ingress-group/remote-state.tf b/modules/eks/alb-controller-ingress-group/remote-state.tf index 738022a1d..1898c564c 100644 --- a/modules/eks/alb-controller-ingress-group/remote-state.tf +++ b/modules/eks/alb-controller-ingress-group/remote-state.tf @@ -1,6 +1,6 @@ module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = "dns-delegated" environment = var.dns_delegated_environment_name @@ -10,7 +10,7 @@ module "dns_delegated" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = var.eks_component_name @@ -19,7 +19,7 @@ module "eks" { module "global_accelerator" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" for_each = local.global_accelerator_enabled ? toset(["true"]) : [] @@ -31,7 +31,7 @@ module "global_accelerator" { module "waf" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" for_each = local.waf_enabled ? toset(["true"]) : [] diff --git a/modules/eks/argocd/provider-secrets.tf b/modules/eks/argocd/provider-secrets.tf index 57904b0c8..277d04a10 100644 --- a/modules/eks/argocd/provider-secrets.tf +++ b/modules/eks/argocd/provider-secrets.tf @@ -9,7 +9,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles_config_secrets.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index 3d310e72d..3cc6be112 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -5,12 +5,13 @@ This is copied from [cloudposse/terraform-aws-components](https://github.com/clo This component installs the [Ealenn/Echo-Server](https://github.com/Ealenn/Echo-Server) to EKS clusters. The echo server is a server that sends it back to the client a JSON representation of all the data the server received, which is a combination of information sent by the client and information sent -by the web server infrastructure. For further details, please consult the [Echo-Server documentation](https://ealenn.github.io/Echo-Server/). +by the web server infrastructure. For further details, please see [Echo-Server documentation](https://ealenn.github.io/Echo-Server/). ## Prerequisites Echo server is intended to provide end-to-end testing of everything needed to deploy an application or service with a public HTTPS endpoint. Therefore, it requires several other components. + At the moment, it supports 2 configurations: 1. ALB with ACM Certificate @@ -45,7 +46,9 @@ Use this in the catalog or use these variables to overwrite the catalog values. ```yaml components: terraform: - echo-server: + eks/echo-server: + metadata: + component: eks/echo-server settings: spacelift: workspace_enabled: true @@ -85,9 +88,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.7.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.9.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/echo-server/charts/echo-server/Chart.yaml b/modules/eks/echo-server/charts/echo-server/Chart.yaml index 03519d53b..24c16905b 100644 --- a/modules/eks/echo-server/charts/echo-server/Chart.yaml +++ b/modules/eks/echo-server/charts/echo-server/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.0 +version: 0.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.2.0" +appVersion: "0.3.0" diff --git a/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml b/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml index f76922fae..86c8bb577 100644 --- a/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml +++ b/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml @@ -19,7 +19,7 @@ metadata: alb.ingress.kubernetes.io/load-balancer-name: {{ index .Values.ingress.alb "load_balancer_name" | default "k8s-common" }} {{- end }} alb.ingress.kubernetes.io/group.name: {{ index .Values.ingress.alb "group_name" | default "common" }} - alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/scheme: {{ index .Values.ingress.alb "scheme" | default "internet-facing" }} {{- if .Values.ingress.alb.access_logs.enabled }} alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket={{.Values.ingress.alb.access_logs.s3_bucket_name}},access_logs.s3.prefix={{.Values.ingress.alb.access_logs.s3_bucket_prefix}} {{- end }} diff --git a/modules/eks/echo-server/main.tf b/modules/eks/echo-server/main.tf index ede55ddb1..44158a076 100644 --- a/modules/eks/echo-server/main.tf +++ b/modules/eks/echo-server/main.tf @@ -1,12 +1,11 @@ locals { - enabled = module.this.enabled ingress_nginx_enabled = var.ingress_type == "nginx" ? true : false ingress_alb_enabled = var.ingress_type == "alb" ? true : false } module "echo_server" { source = "cloudposse/helm-release/aws" - version = "0.7.0" + version = "0.9.1" name = module.this.name chart = "${path.module}/charts/echo-server" @@ -48,6 +47,11 @@ module "echo_server" { value = local.ingress_alb_enabled type = "auto" }, + { + name = "ingress.alb.scheme" + value = module.alb.outputs.load_balancer_scheme + type = "auto" + }, ] values = compact([ diff --git a/modules/eks/echo-server/remote-state.tf b/modules/eks/echo-server/remote-state.tf index a712431cf..f990c3bcb 100644 --- a/modules/eks/echo-server/remote-state.tf +++ b/modules/eks/echo-server/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "alb" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = var.alb_controller_ingress_group_component_name From 3ca3045677659b0a5b11d0275ff9e6683f106832 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 27 Jun 2023 15:27:26 -0700 Subject: [PATCH 163/501] Add Missing `github-oidc-provider` Thumbprint (#736) --- modules/github-oidc-provider/README.md | 9 ++++++++- .../scripts/get_github_oidc_thumbprint.sh | 7 +++++++ modules/github-oidc-provider/variables.tf | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index b648fd18e..921e679d5 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -33,6 +33,13 @@ the provider's thumbprint may change, at which point you can use [scripts/get_github_oidc_thumbprint.sh](./scripts/get_github_oidc_thumbprint.sh) to get the new thumbprint and add it to the list in `var.thumbprint_list`. +This script will pull one of two thumbprints. There are two possible intermediary certificates for the Actions SSL +certificate and either can be returned by the GitHub servers, requiring customers to trust both. This is a known +behavior when the intermediary certificates are cross-signed by the CA. Therefore, run this script until both values +are retrieved. Add both to `var.thumbprint_list`. + +For more, see https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/ + ## FAQ ### I cannot assume the role from GitHub Actions after deploying @@ -102,7 +109,7 @@ permissions: | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [thumbprint\_list](#input\_thumbprint\_list) | List of OIDC provider certificate thumbprints | `list(string)` |
[
"6938fd4d98bab03faadb97b34396831e3780aea1"
]
| no | +| [thumbprint\_list](#input\_thumbprint\_list) | List of OIDC provider certificate thumbprints | `list(string)` |
[
"6938fd4d98bab03faadb97b34396831e3780aea1",
"1c58a3a8518e8759bf075b76b750d4f2df264fcd"
]
| no | ## Outputs diff --git a/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh b/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh index 90d630d02..5667f8f13 100755 --- a/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh +++ b/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh @@ -4,6 +4,13 @@ # This script downloads the certificate information from $GITHUB_OIDC_HOST, extracts the certificate material, then uses # the openssl command to calculate the thumbprint. It is meant to be called manually and the output used to populate # the `thumbprint_list` variable in the terraform configuration for this module. +# +# This script will pull one of two thumbprints. There are two possible intermediary certificates for the Actions SSL +# certificate and either can be returned by the GitHub servers, requiring customers to trust both. This is a known +# behavior when the intermediary certificates are cross-signed by the CA. Therefore, run this script until both values +# are retrieved. +# +# For more, see https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/ ######################################################################################################################## GITHUB_OIDC_HOST="token.actions.githubusercontent.com" THUMBPRINT=$(echo \ diff --git a/modules/github-oidc-provider/variables.tf b/modules/github-oidc-provider/variables.tf index ee40228a2..0eb4879d1 100644 --- a/modules/github-oidc-provider/variables.tf +++ b/modules/github-oidc-provider/variables.tf @@ -6,5 +6,5 @@ variable "region" { variable "thumbprint_list" { type = list(string) description = "List of OIDC provider certificate thumbprints" - default = ["6938fd4d98bab03faadb97b34396831e3780aea1"] + default = ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8518e8759bf075b76b750d4f2df264fcd"] } From d9169a55b231a6046d698e6523f4f413ff3f203d Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 29 Jun 2023 12:38:51 -0700 Subject: [PATCH 164/501] IAM upgrades: SSO Permission Sets as Teams, SourceIdentity support, region independence (#738) --- mixins/providers.depth-1.tf | 2 +- mixins/providers.depth-2.tf | 2 +- modules/account-map/modules/iam-roles/main.tf | 5 +-- .../modules/roles-to-principals/main.tf | 17 ++++++++-- .../modules/roles-to-principals/outputs.tf | 5 +++ .../modules/roles-to-principals/variables.tf | 24 ++++++++++++++ .../modules/team-assume-role-policy/main.tf | 3 ++ modules/account-quotas/providers.tf | 2 +- modules/account-settings/providers.tf | 2 +- modules/acm/providers.tf | 2 +- modules/alb/providers.tf | 2 +- modules/amplify/providers.tf | 2 +- modules/argocd-repo/providers.tf | 2 +- modules/athena/providers.tf | 2 +- modules/aurora-mysql-resources/providers.tf | 2 +- modules/aurora-mysql/providers.tf | 2 +- .../aurora-postgres-resources/providers.tf | 2 +- modules/aurora-postgres/providers.tf | 2 +- modules/aws-backup/providers.tf | 2 +- modules/aws-config/provider-awsutils.mixin.tf | 2 +- modules/aws-config/providers.tf | 2 +- modules/aws-inspector/providers.tf | 2 +- modules/aws-saml/main.tf | 16 ++++++++-- modules/aws-shield/providers.tf | 2 +- modules/aws-sso/README.md | 13 +++----- modules/aws-sso/main.tf | 2 +- .../policy-Identity-role-TeamAccess.tf | 28 +++++++---------- .../aws-sso/policy-TerraformUpdateAccess.tf | 31 +++++++++++++++++-- modules/aws-sso/providers.tf | 2 +- modules/aws-sso/remote-state.tf | 18 +++++------ modules/aws-sso/variables.tf | 23 -------------- modules/aws-ssosync/providers.tf | 2 +- modules/aws-teams/policy-team-role-access.tf | 1 + modules/aws-waf-acl/providers.tf | 2 +- modules/bastion/providers.tf | 2 +- modules/cloudtrail-bucket/providers.tf | 2 +- modules/cloudtrail/providers.tf | 2 +- modules/cloudwatch-logs/providers.tf | 2 +- modules/cognito/providers.tf | 2 +- modules/config-bucket/providers.tf | 2 +- .../datadog-configuration/provider-datadog.tf | 4 +-- modules/datadog-configuration/providers.tf | 2 +- modules/datadog-integration/providers.tf | 2 +- modules/datadog-lambda-forwarder/providers.tf | 2 +- modules/datadog-logs-archive/providers.tf | 2 +- modules/datadog-monitor/providers.tf | 2 +- .../datadog-private-location-ecs/providers.tf | 2 +- .../providers.tf | 2 +- modules/datadog-synthetics/providers.tf | 2 +- modules/dms/endpoint/providers.tf | 2 +- modules/dms/iam/providers.tf | 2 +- modules/dms/replication-instance/providers.tf | 2 +- modules/dms/replication-task/providers.tf | 2 +- .../dns-delegated/providers-dns-primary.tf | 4 +-- modules/dns-delegated/providers.tf | 2 +- modules/dns-primary/providers.tf | 2 +- modules/documentdb/providers.tf | 2 +- modules/dynamodb/providers.tf | 2 +- .../ec2-client-vpn/provider-awsutils.mixin.tf | 2 +- modules/ec2-client-vpn/providers.tf | 2 +- modules/ecr/providers.tf | 2 +- modules/ecs-service/providers.tf | 2 +- modules/ecs/providers.tf | 2 +- modules/efs/providers.tf | 2 +- modules/eks-iam/providers.tf | 2 +- .../actions-runner-controller/providers.tf | 2 +- .../alb-controller-ingress-class/providers.tf | 2 +- .../alb-controller-ingress-group/providers.tf | 2 +- modules/eks/alb-controller/providers.tf | 2 +- modules/eks/argocd/providers.tf | 2 +- .../aws-node-termination-handler/providers.tf | 2 +- modules/eks/cert-manager/providers.tf | 2 +- modules/eks/datadog-agent/providers.tf | 2 +- modules/eks/ebs-controller/providers.tf | 2 +- modules/eks/echo-server/providers.tf | 2 +- modules/eks/efs-controller/providers.tf | 2 +- modules/eks/external-dns/providers.tf | 2 +- .../external-secrets-operator/providers.tf | 2 +- modules/eks/idp-roles/providers.tf | 2 +- .../eks/karpenter-provisioner/providers.tf | 2 +- modules/eks/karpenter/providers.tf | 2 +- modules/eks/metrics-server/providers.tf | 2 +- modules/eks/platform/providers.tf | 2 +- modules/eks/redis-operator/providers.tf | 2 +- modules/eks/redis/providers.tf | 2 +- modules/eks/reloader/providers.tf | 2 +- modules/elasticache-redis/providers.tf | 2 +- modules/elasticsearch/providers.tf | 2 +- .../github-action-token-rotator/providers.tf | 2 +- modules/github-oidc-provider/providers.tf | 2 +- modules/github-runners/providers.tf | 2 +- .../providers.tf | 2 +- modules/global-accelerator/providers.tf | 2 +- modules/iam-role/README.md | 2 +- modules/iam-role/providers.tf | 2 +- modules/iam-role/variables.tf | 2 +- modules/iam-service-linked-roles/providers.tf | 2 +- modules/kinesis-stream/providers.tf | 2 +- modules/kms/providers.tf | 2 +- modules/lakeformation/providers.tf | 2 +- modules/lambda/providers.tf | 2 +- modules/mq-broker/providers.tf | 2 +- modules/mwaa/providers.tf | 2 +- modules/network-firewall/providers.tf | 2 +- modules/opsgenie-team/providers.tf | 2 +- modules/rds/providers.tf | 2 +- modules/redshift/providers.tf | 2 +- .../providers.tf | 2 +- modules/s3-bucket/providers.tf | 2 +- modules/ses/provider-awsutils.mixin.tf | 2 +- modules/ses/providers.tf | 2 +- modules/sftp/provider-awsutils.mixin.tf | 2 +- modules/sftp/providers.tf | 2 +- modules/snowflake-account/providers.tf | 2 +- modules/snowflake-database/providers.tf | 2 +- modules/sns-topic/providers.tf | 2 +- .../provider-other-regions.tf | 4 +-- modules/spa-s3-cloudfront/providers.tf | 2 +- modules/spacelift/worker-pool/iam.tf | 2 ++ modules/spacelift/worker-pool/providers.tf | 2 +- modules/sqs-queue/providers.tf | 2 +- modules/ssm-parameters/providers.tf | 2 +- modules/sso-saml-provider/providers.tf | 2 +- modules/strongdm/provider-strongdm.tf | 2 +- modules/strongdm/providers.tf | 2 +- modules/tgw/hub/providers.tf | 2 +- modules/tgw/spoke/provider-hub.tf | 2 +- modules/tgw/spoke/providers.tf | 2 +- modules/vpc-flow-logs-bucket/providers.tf | 2 +- modules/vpc-peering/providers.tf | 2 +- modules/vpc/providers.tf | 2 +- modules/waf/providers.tf | 2 +- modules/zscaler/providers.tf | 2 +- 133 files changed, 240 insertions(+), 192 deletions(-) diff --git a/mixins/providers.depth-1.tf b/mixins/providers.depth-1.tf index 54257fd20..ef923e10a 100644 --- a/mixins/providers.depth-1.tf +++ b/mixins/providers.depth-1.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/mixins/providers.depth-2.tf b/mixins/providers.depth-2.tf index 45d458575..89ed50a98 100644 --- a/mixins/providers.depth-2.tf +++ b/mixins/providers.depth-2.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/account-map/modules/iam-roles/main.tf b/modules/account-map/modules/iam-roles/main.tf index fdd0db25e..fbf93b3d7 100644 --- a/modules/account-map/modules/iam-roles/main.tf +++ b/modules/account-map/modules/iam-roles/main.tf @@ -1,5 +1,6 @@ data "awsutils_caller_identity" "current" { + count = local.dynamic_terraform_role_enabled ? 1 : 0 # Avoid conflict with caller's provider which is using this module's output to assume a role. provider = awsutils.iam-roles } @@ -42,10 +43,10 @@ locals { static_terraform_role = local.account_map.terraform_roles[local.account_name] dynamic_terraform_role = try(local.dynamic_terraform_role_map[local.dynamic_terraform_role_type], null) - current_user_role_arn = coalesce(data.awsutils_caller_identity.current.eks_role_arn, data.awsutils_caller_identity.current.arn) + current_user_role_arn = coalesce(one(data.awsutils_caller_identity.current[*].eks_role_arn), one(data.awsutils_caller_identity.current[*].arn), "disabled") dynamic_terraform_role_type = try(local.account_map.terraform_access_map[local.current_user_role_arn][local.account_name], "none") - current_identity_account = split(":", local.current_user_role_arn)[4] + current_identity_account = local.dynamic_terraform_role_enabled ? split(":", local.current_user_role_arn)[4] : "" is_root_user = local.current_identity_account == local.account_map.full_account_map[local.account_map.root_account_account_name] is_target_user = local.current_identity_account == local.account_map.full_account_map[local.account_name] diff --git a/modules/account-map/modules/roles-to-principals/main.tf b/modules/account-map/modules/roles-to-principals/main.tf index 55ab7f1e6..51386707d 100644 --- a/modules/account-map/modules/roles-to-principals/main.tf +++ b/modules/account-map/modules/roles-to-principals/main.tf @@ -22,7 +22,15 @@ module "account_map" { } locals { - aws_partition = module.account_map.outputs.aws_partition + aws_partition = module.account_map.outputs.aws_partition + team_ps_pattern = var.overridable_team_permission_set_name_pattern + identity_account_name = module.account_map.outputs.identity_account_account_name + teams_from_role_map = var.overridable_team_permission_sets_enabled ? try(var.role_map[local.identity_account_name], []) : [] + + team_permission_set_name_map = { + for team in distinct(concat(var.teams, local.teams_from_role_map)) : team => format(local.team_ps_pattern, replace(title(replace(team, "_", "-")), "-", "")) + } + permission_sets_from_team_roles = [for team in local.teams_from_role_map : local.team_permission_set_name_map[team]] principals_map = { for acct, v in var.role_map : acct => ( contains(v, "*") ? { @@ -37,11 +45,14 @@ locals { principals = distinct(compact(flatten([for acct, v in var.role_map : values(local.principals_map[acct])]))) # Support for AWS SSO Permission Sets - permission_set_arn_like = distinct(compact(flatten([for acct, v in var.permission_set_map : formatlist( + # We ensure that the identity account is included in the map so that we can add the permission sets from team roles to it. + permission_set_arn_like = distinct(compact(flatten([for acct, v in merge({ (local.identity_account_name) = [] }, var.permission_set_map) : formatlist( # Usually like: # arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc # But sometimes AWS SSO ARN includes `/region/`, like: # arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-southeast-1/AWSReservedSSO_IdentityAdminRoleAccess_b68e107e9495e2fc + # If trust polices get too large, some space can be saved by using `*` instead of `aws-reserved/sso.amazonaws.com*` format("arn:%s:iam::%s:role/aws-reserved/sso.amazonaws.com*/AWSReservedSSO_%%s_*", local.aws_partition, module.account_map.outputs.full_account_map[acct]), - v)]))) + acct == local.identity_account_name ? distinct(concat(v, local.permission_sets_from_team_roles)) : v + )]))) } diff --git a/modules/account-map/modules/roles-to-principals/outputs.tf b/modules/account-map/modules/roles-to-principals/outputs.tf index 434b4dd0b..461530a4d 100644 --- a/modules/account-map/modules/roles-to-principals/outputs.tf +++ b/modules/account-map/modules/roles-to-principals/outputs.tf @@ -13,6 +13,11 @@ output "permission_set_arn_like" { description = "List of Role ARN regexes suitable for IAM Condition `ArnLike` corresponding to given input `permission_set_map`" } +output "team_permission_set_name_map" { + value = local.team_permission_set_name_map + description = "Map of team names (from `var.teams` and `role_map[\"identity\"]) to permission set names" +} + output "full_account_map" { value = module.account_map.outputs.full_account_map description = "Map of account names to account IDs" diff --git a/modules/account-map/modules/roles-to-principals/variables.tf b/modules/account-map/modules/roles-to-principals/variables.tf index d1a82e9fc..f942b2418 100644 --- a/modules/account-map/modules/roles-to-principals/variables.tf +++ b/modules/account-map/modules/roles-to-principals/variables.tf @@ -1,6 +1,7 @@ variable "role_map" { type = map(list(string)) description = "Map of account:[role, role...]. Use `*` as role for entire account" + default = {} } variable "permission_set_map" { @@ -9,6 +10,12 @@ variable "permission_set_map" { default = {} } +variable "teams" { + type = list(string) + description = "List of team names to translate to AWS SSO PermissionSet names" + default = [] +} + variable "privileged" { type = bool description = "True if the default provider already has access to the backend" @@ -36,3 +43,20 @@ variable "overridable_global_stage_name" { description = "The stage name for the organization management account (where the `account-map` state is stored)" default = "root" } + +variable "overridable_team_permission_set_name_pattern" { + type = string + description = "The pattern used to generate the AWS SSO PermissionSet name for each team" + default = "Identity%sTeamAccess" +} + +variable "overridable_team_permission_sets_enabled" { + type = bool + description = <<-EOT + When true, any roles (teams or team-roles) in the identity account references in `role_map` + will cause corresponding AWS SSO PermissionSets to be included in the `permission_set_arn_like` output. + This has the effect of treating those PermissionSets as if they were teams. + The main reason to set this `false` is if IAM trust policies are exceeding size limits and you are not using AWS SSO. + EOT + default = true +} diff --git a/modules/account-map/modules/team-assume-role-policy/main.tf b/modules/account-map/modules/team-assume-role-policy/main.tf index 40b2195ae..edf2ddbe4 100644 --- a/modules/account-map/modules/team-assume-role-policy/main.tf +++ b/modules/account-map/modules/team-assume-role-policy/main.tf @@ -67,6 +67,7 @@ data "aws_iam_policy_document" "assume_role" { effect = "Allow" actions = [ "sts:AssumeRole", + "sts:SetSourceIdentity", "sts:TagSession", ] @@ -99,6 +100,7 @@ data "aws_iam_policy_document" "assume_role" { effect = "Allow" actions = [ "sts:AssumeRole", + "sts:SetSourceIdentity", "sts:TagSession", ] @@ -124,6 +126,7 @@ data "aws_iam_policy_document" "assume_role" { effect = "Deny" actions = [ "sts:AssumeRole", + "sts:SetSourceIdentity", "sts:TagSession", ] diff --git a/modules/account-quotas/providers.tf b/modules/account-quotas/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/account-quotas/providers.tf +++ b/modules/account-quotas/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/account-settings/providers.tf b/modules/account-settings/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/account-settings/providers.tf +++ b/modules/account-settings/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/acm/providers.tf b/modules/acm/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/acm/providers.tf +++ b/modules/acm/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/alb/providers.tf b/modules/alb/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/alb/providers.tf +++ b/modules/alb/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/amplify/providers.tf b/modules/amplify/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/amplify/providers.tf +++ b/modules/amplify/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/argocd-repo/providers.tf b/modules/argocd-repo/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/argocd-repo/providers.tf +++ b/modules/argocd-repo/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/athena/providers.tf b/modules/athena/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/athena/providers.tf +++ b/modules/athena/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aurora-mysql-resources/providers.tf b/modules/aurora-mysql-resources/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aurora-mysql-resources/providers.tf +++ b/modules/aurora-mysql-resources/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aurora-mysql/providers.tf b/modules/aurora-mysql/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aurora-mysql/providers.tf +++ b/modules/aurora-mysql/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aurora-postgres-resources/providers.tf b/modules/aurora-postgres-resources/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aurora-postgres-resources/providers.tf +++ b/modules/aurora-postgres-resources/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aurora-postgres/providers.tf b/modules/aurora-postgres/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aurora-postgres/providers.tf +++ b/modules/aurora-postgres/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aws-backup/providers.tf b/modules/aws-backup/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aws-backup/providers.tf +++ b/modules/aws-backup/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aws-config/provider-awsutils.mixin.tf b/modules/aws-config/provider-awsutils.mixin.tf index db8bd2c1d..70fa8d095 100644 --- a/modules/aws-config/provider-awsutils.mixin.tf +++ b/modules/aws-config/provider-awsutils.mixin.tf @@ -8,7 +8,7 @@ provider "awsutils" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aws-config/providers.tf b/modules/aws-config/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aws-config/providers.tf +++ b/modules/aws-config/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aws-inspector/providers.tf b/modules/aws-inspector/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aws-inspector/providers.tf +++ b/modules/aws-inspector/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aws-saml/main.tf b/modules/aws-saml/main.tf index 1aca411d9..959ffefe1 100644 --- a/modules/aws-saml/main.tf +++ b/modules/aws-saml/main.tf @@ -25,6 +25,7 @@ data "aws_iam_policy_document" "saml_provider_assume" { sid = "SamlProviderAssume" actions = [ "sts:AssumeRoleWithSAML", + "sts:SetSourceIdentity", "sts:TagSession", ] @@ -35,9 +36,20 @@ data "aws_iam_policy_document" "saml_provider_assume" { } condition { - test = "StringEquals" + # Use StringLike rather than StringEquals to avoid having to list every region's endpoint + test = "StringLike" variable = "SAML:aud" - values = ["https://signin.aws.amazon.com/saml"] + # Allow sign in from any valid AWS SAML endpoint + # See https://docs.aws.amazon.com/general/latest/gr/signin-service.html + # and https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-saml + # Note: The value for this key comes from the SAML Recipient field in the assertion, not the Audience field, + # and is thus not the actual SAML:aud in the SAML assertion. + values = [ + "https://signin.aws.amazon.com/saml", + "https://*.signin.aws.amazon.com/saml", + "https://signin.amazonaws-us-gov.com/saml", + "https://us-gov-east-1.signin.amazonaws-us-gov.com/saml", + ] } } } diff --git a/modules/aws-shield/providers.tf b/modules/aws-shield/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aws-shield/providers.tf +++ b/modules/aws-shield/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index e5936a93d..dea250090 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -164,13 +164,13 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 1.0.0 | -| [role\_prefix](#module\_role\_prefix) | cloudposse/label/null | 0.25.0 | -| [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 1.0.0 | +| [role\_map](#module\_role\_map) | ../account-map/modules/roles-to-principals | n/a | +| [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 1.1.0 | | [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 1.0.0 | -| [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -191,14 +191,11 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [aws\_teams\_accessible](#input\_aws\_teams\_accessible) | List of IAM roles (e.g. ["admin", "terraform"]) for which to create permission
sets that allow the user to assume that role. Named like
admin -> IdentityAdminTeamAccess | `set(string)` | `[]` | no | -| [aws\_teams\_stage\_name](#input\_aws\_teams\_stage\_name) | The name of the stage where the IAM primary roles are provisioned | `string` | `"identity"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [global\_environment\_name](#input\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no | -| [global\_stage\_name](#input\_global\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | @@ -212,7 +209,7 @@ components: | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [tfstate\_environment\_name](#input\_tfstate\_environment\_name) | The name of the environment where `tfstate-backend` is provisioned | `string` | n/a | yes | +| [tfstate\_environment\_name](#input\_tfstate\_environment\_name) | The name of the environment where `tfstate-backend` is provisioned. If not set, the TerraformUpdateAccess permission set will not be created. | `string` | `null` | no | ## Outputs diff --git a/modules/aws-sso/main.tf b/modules/aws-sso/main.tf index e2e158944..80e062557 100644 --- a/modules/aws-sso/main.tf +++ b/modules/aws-sso/main.tf @@ -19,7 +19,7 @@ module "permission_sets" { module "sso_account_assignments" { source = "cloudposse/sso/aws//modules/account-assignments" - version = "1.0.0" + version = "1.1.0" account_assignments = local.account_assignments context = module.this.context diff --git a/modules/aws-sso/policy-Identity-role-TeamAccess.tf b/modules/aws-sso/policy-Identity-role-TeamAccess.tf index c1df61856..4e9ddc2e8 100644 --- a/modules/aws-sso/policy-Identity-role-TeamAccess.tf +++ b/modules/aws-sso/policy-Identity-role-TeamAccess.tf @@ -3,19 +3,6 @@ # which is named "IdentityTeamAccess" and grants access to only that role, # plus ViewOnly access because it is difficult to navigate without any access at all. -locals { - identity_account = module.account_map.outputs.full_account_map[module.account_map.outputs.identity_account_account_name] -} - -module "role_prefix" { - source = "cloudposse/label/null" - version = "0.25.0" - - stage = var.aws_teams_stage_name - - context = module.this.context -} - data "aws_iam_policy_document" "assume_aws_team" { for_each = local.enabled ? var.aws_teams_accessible : [] @@ -25,12 +12,11 @@ data "aws_iam_policy_document" "assume_aws_team" { effect = "Allow" actions = [ "sts:AssumeRole", + "sts:SetSourceIdentity", "sts:TagSession", ] - resources = [ - format("arn:${local.aws_partition}:iam::%s:role/%s-%s", local.identity_account, module.role_prefix.id, each.value) - ] + resources = ["*"] /* For future reference, this tag-based restriction also works, based on the fact that we always tag our IAM roles with the "Name" tag. @@ -52,9 +38,17 @@ data "aws_iam_policy_document" "assume_aws_team" { } } +module "role_map" { + source = "../account-map/modules/roles-to-principals" + + teams = var.aws_teams_accessible + + context = module.this.context +} + locals { identity_access_permission_sets = [for role in var.aws_teams_accessible : { - name = format("Identity%sTeamAccess", replace(title(role), "-", "")), + name = module.role_map.team_permission_set_name_map[role], description = format("Allow user to assume the %s Team role in the Identity account, which allows access to other accounts", replace(title(role), "-", "")) relay_state = "", session_duration = "", diff --git a/modules/aws-sso/policy-TerraformUpdateAccess.tf b/modules/aws-sso/policy-TerraformUpdateAccess.tf index 3cbb73123..2bccb22be 100644 --- a/modules/aws-sso/policy-TerraformUpdateAccess.tf +++ b/modules/aws-sso/policy-TerraformUpdateAccess.tf @@ -1,5 +1,30 @@ +variable "tfstate_environment_name" { + type = string + description = "The name of the environment where `tfstate-backend` is provisioned. If not set, the TerraformUpdateAccess permission set will not be created." + default = null +} + +locals { + tf_update_access_enabled = var.tfstate_environment_name != null && module.this.enabled +} + +module "tfstate" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + bypass = !local.tf_update_access_enabled + + component = "tfstate-backend" + environment = var.tfstate_environment_name + stage = module.iam_roles.global_stage_name + privileged = var.privileged + + context = module.this.context +} data "aws_iam_policy_document" "TerraformUpdateAccess" { + count = local.tf_update_access_enabled ? 1 : 0 + statement { sid = "TerraformStateBackendS3Bucket" effect = "Allow" @@ -18,14 +43,14 @@ data "aws_iam_policy_document" "TerraformUpdateAccess" { } locals { - terraform_update_access_permission_set = [{ + terraform_update_access_permission_set = local.tf_update_access_enabled ? [{ name = "TerraformUpdateAccess", description = "Allow access to Terraform state sufficient to make changes", relay_state = "", session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles tags = {}, - inline_policy = data.aws_iam_policy_document.TerraformUpdateAccess.json, + inline_policy = one(data.aws_iam_policy_document.TerraformUpdateAccess[*].json), policy_attachments = [] customer_managed_policy_attachments = [] - }] + }] : [] } diff --git a/modules/aws-sso/providers.tf b/modules/aws-sso/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aws-sso/providers.tf +++ b/modules/aws-sso/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aws-sso/remote-state.tf b/modules/aws-sso/remote-state.tf index 838324082..52726e842 100644 --- a/modules/aws-sso/remote-state.tf +++ b/modules/aws-sso/remote-state.tf @@ -1,23 +1,19 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = "account-map" - environment = var.global_environment_name - stage = var.global_stage_name + environment = module.iam_roles.global_environment_name + stage = module.iam_roles.global_stage_name + tenant = module.iam_roles.global_tenant_name privileged = var.privileged context = module.this.context } -module "tfstate" { - source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" - - component = "tfstate-backend" - environment = var.tfstate_environment_name - stage = var.global_stage_name - privileged = var.privileged +# Module "iam_roles" is usually in providers.tf, but not so for this component +module "iam_roles" { + source = "../account-map/modules/iam-roles" context = module.this.context } diff --git a/modules/aws-sso/variables.tf b/modules/aws-sso/variables.tf index cc82a245e..dc6e4cf1a 100644 --- a/modules/aws-sso/variables.tf +++ b/modules/aws-sso/variables.tf @@ -3,23 +3,6 @@ variable "region" { description = "AWS Region" } -variable "tfstate_environment_name" { - type = string - description = "The name of the environment where `tfstate-backend` is provisioned" -} - -variable "global_environment_name" { - type = string - description = "Global environment name" - default = "gbl" -} - -variable "global_stage_name" { - type = string - description = "The name of the stage where `account_map` is provisioned" - default = "root" -} - variable "privileged" { type = bool description = "True if the default provider already has access to the backend" @@ -50,12 +33,6 @@ variable "account_assignments" { default = {} } -variable "aws_teams_stage_name" { - type = string - description = "The name of the stage where the IAM primary roles are provisioned" - default = "identity" -} - variable "aws_teams_accessible" { type = set(string) description = <<-EOT diff --git a/modules/aws-ssosync/providers.tf b/modules/aws-ssosync/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aws-ssosync/providers.tf +++ b/modules/aws-ssosync/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/aws-teams/policy-team-role-access.tf b/modules/aws-teams/policy-team-role-access.tf index f35f242a5..d207bf335 100644 --- a/modules/aws-teams/policy-team-role-access.tf +++ b/modules/aws-teams/policy-team-role-access.tf @@ -8,6 +8,7 @@ data "aws_iam_policy_document" "team_role_access" { effect = "Allow" actions = [ "sts:AssumeRole", + "sts:SetSourceIdentity", "sts:TagSession", ] resources = [ diff --git a/modules/aws-waf-acl/providers.tf b/modules/aws-waf-acl/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/aws-waf-acl/providers.tf +++ b/modules/aws-waf-acl/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/bastion/providers.tf b/modules/bastion/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/bastion/providers.tf +++ b/modules/bastion/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/cloudtrail-bucket/providers.tf b/modules/cloudtrail-bucket/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/cloudtrail-bucket/providers.tf +++ b/modules/cloudtrail-bucket/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/cloudtrail/providers.tf b/modules/cloudtrail/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/cloudtrail/providers.tf +++ b/modules/cloudtrail/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/cloudwatch-logs/providers.tf b/modules/cloudwatch-logs/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/cloudwatch-logs/providers.tf +++ b/modules/cloudwatch-logs/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/cognito/providers.tf b/modules/cognito/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/cognito/providers.tf +++ b/modules/cognito/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/config-bucket/providers.tf b/modules/config-bucket/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/config-bucket/providers.tf +++ b/modules/config-bucket/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-configuration/provider-datadog.tf b/modules/datadog-configuration/provider-datadog.tf index 5a1f4c9ce..852b643f2 100644 --- a/modules/datadog-configuration/provider-datadog.tf +++ b/modules/datadog-configuration/provider-datadog.tf @@ -7,10 +7,10 @@ provider "aws" { profile = module.iam_roles_datadog_secrets.terraform_profile_name dynamic "assume_role" { - # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + # module.iam_roles_datadog_secrets.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles_datadog_secrets.terraform_role_arn]) content { - role_arn = module.iam_roles_datadog_secrets.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-configuration/providers.tf b/modules/datadog-configuration/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/datadog-configuration/providers.tf +++ b/modules/datadog-configuration/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-integration/providers.tf b/modules/datadog-integration/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/datadog-integration/providers.tf +++ b/modules/datadog-integration/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-lambda-forwarder/providers.tf b/modules/datadog-lambda-forwarder/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/datadog-lambda-forwarder/providers.tf +++ b/modules/datadog-lambda-forwarder/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-logs-archive/providers.tf b/modules/datadog-logs-archive/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/datadog-logs-archive/providers.tf +++ b/modules/datadog-logs-archive/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-monitor/providers.tf b/modules/datadog-monitor/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/datadog-monitor/providers.tf +++ b/modules/datadog-monitor/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-private-location-ecs/providers.tf b/modules/datadog-private-location-ecs/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/datadog-private-location-ecs/providers.tf +++ b/modules/datadog-private-location-ecs/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-synthetics-private-location/providers.tf b/modules/datadog-synthetics-private-location/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/datadog-synthetics-private-location/providers.tf +++ b/modules/datadog-synthetics-private-location/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/datadog-synthetics/providers.tf b/modules/datadog-synthetics/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/datadog-synthetics/providers.tf +++ b/modules/datadog-synthetics/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/dms/endpoint/providers.tf b/modules/dms/endpoint/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/dms/endpoint/providers.tf +++ b/modules/dms/endpoint/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/dms/iam/providers.tf b/modules/dms/iam/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/dms/iam/providers.tf +++ b/modules/dms/iam/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/dms/replication-instance/providers.tf b/modules/dms/replication-instance/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/dms/replication-instance/providers.tf +++ b/modules/dms/replication-instance/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/dms/replication-task/providers.tf b/modules/dms/replication-task/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/dms/replication-task/providers.tf +++ b/modules/dms/replication-task/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/dns-delegated/providers-dns-primary.tf b/modules/dns-delegated/providers-dns-primary.tf index 1259915b6..76fee705b 100644 --- a/modules/dns-delegated/providers-dns-primary.tf +++ b/modules/dns-delegated/providers-dns-primary.tf @@ -7,10 +7,10 @@ provider "aws" { profile = module.iam_roles.dns_terraform_profile_name dynamic "assume_role" { - # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + # module.iam_roles.dns_terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.dns_terraform_role_arn]) content { - role_arn = module.iam_roles.dns_terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/dns-delegated/providers.tf b/modules/dns-delegated/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/dns-delegated/providers.tf +++ b/modules/dns-delegated/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/dns-primary/providers.tf b/modules/dns-primary/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/dns-primary/providers.tf +++ b/modules/dns-primary/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/documentdb/providers.tf b/modules/documentdb/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/documentdb/providers.tf +++ b/modules/documentdb/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/dynamodb/providers.tf b/modules/dynamodb/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/dynamodb/providers.tf +++ b/modules/dynamodb/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/ec2-client-vpn/provider-awsutils.mixin.tf b/modules/ec2-client-vpn/provider-awsutils.mixin.tf index db8bd2c1d..70fa8d095 100644 --- a/modules/ec2-client-vpn/provider-awsutils.mixin.tf +++ b/modules/ec2-client-vpn/provider-awsutils.mixin.tf @@ -8,7 +8,7 @@ provider "awsutils" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/ec2-client-vpn/providers.tf b/modules/ec2-client-vpn/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/ec2-client-vpn/providers.tf +++ b/modules/ec2-client-vpn/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/ecr/providers.tf b/modules/ecr/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/ecr/providers.tf +++ b/modules/ecr/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/ecs-service/providers.tf b/modules/ecs-service/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/ecs-service/providers.tf +++ b/modules/ecs-service/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/ecs/providers.tf b/modules/ecs/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/ecs/providers.tf +++ b/modules/ecs/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/efs/providers.tf b/modules/efs/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/efs/providers.tf +++ b/modules/efs/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks-iam/providers.tf b/modules/eks-iam/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/eks-iam/providers.tf +++ b/modules/eks-iam/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/actions-runner-controller/providers.tf b/modules/eks/actions-runner-controller/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/actions-runner-controller/providers.tf +++ b/modules/eks/actions-runner-controller/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/alb-controller-ingress-class/providers.tf b/modules/eks/alb-controller-ingress-class/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/alb-controller-ingress-class/providers.tf +++ b/modules/eks/alb-controller-ingress-class/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/alb-controller-ingress-group/providers.tf b/modules/eks/alb-controller-ingress-group/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/alb-controller-ingress-group/providers.tf +++ b/modules/eks/alb-controller-ingress-group/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/alb-controller/providers.tf b/modules/eks/alb-controller/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/alb-controller/providers.tf +++ b/modules/eks/alb-controller/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/argocd/providers.tf b/modules/eks/argocd/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/argocd/providers.tf +++ b/modules/eks/argocd/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/aws-node-termination-handler/providers.tf b/modules/eks/aws-node-termination-handler/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/aws-node-termination-handler/providers.tf +++ b/modules/eks/aws-node-termination-handler/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/cert-manager/providers.tf b/modules/eks/cert-manager/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/cert-manager/providers.tf +++ b/modules/eks/cert-manager/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/datadog-agent/providers.tf b/modules/eks/datadog-agent/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/datadog-agent/providers.tf +++ b/modules/eks/datadog-agent/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/ebs-controller/providers.tf b/modules/eks/ebs-controller/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/ebs-controller/providers.tf +++ b/modules/eks/ebs-controller/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/echo-server/providers.tf b/modules/eks/echo-server/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/echo-server/providers.tf +++ b/modules/eks/echo-server/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/efs-controller/providers.tf b/modules/eks/efs-controller/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/efs-controller/providers.tf +++ b/modules/eks/efs-controller/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/external-dns/providers.tf b/modules/eks/external-dns/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/external-dns/providers.tf +++ b/modules/eks/external-dns/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/external-secrets-operator/providers.tf b/modules/eks/external-secrets-operator/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/external-secrets-operator/providers.tf +++ b/modules/eks/external-secrets-operator/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/idp-roles/providers.tf b/modules/eks/idp-roles/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/idp-roles/providers.tf +++ b/modules/eks/idp-roles/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/karpenter-provisioner/providers.tf b/modules/eks/karpenter-provisioner/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/karpenter-provisioner/providers.tf +++ b/modules/eks/karpenter-provisioner/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/karpenter/providers.tf b/modules/eks/karpenter/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/karpenter/providers.tf +++ b/modules/eks/karpenter/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/metrics-server/providers.tf b/modules/eks/metrics-server/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/metrics-server/providers.tf +++ b/modules/eks/metrics-server/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/platform/providers.tf b/modules/eks/platform/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/platform/providers.tf +++ b/modules/eks/platform/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/redis-operator/providers.tf b/modules/eks/redis-operator/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/redis-operator/providers.tf +++ b/modules/eks/redis-operator/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/redis/providers.tf b/modules/eks/redis/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/redis/providers.tf +++ b/modules/eks/redis/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/reloader/providers.tf b/modules/eks/reloader/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/reloader/providers.tf +++ b/modules/eks/reloader/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/elasticache-redis/providers.tf b/modules/elasticache-redis/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/elasticache-redis/providers.tf +++ b/modules/elasticache-redis/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/elasticsearch/providers.tf b/modules/elasticsearch/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/elasticsearch/providers.tf +++ b/modules/elasticsearch/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/github-action-token-rotator/providers.tf b/modules/github-action-token-rotator/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/github-action-token-rotator/providers.tf +++ b/modules/github-action-token-rotator/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/github-oidc-provider/providers.tf b/modules/github-oidc-provider/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/github-oidc-provider/providers.tf +++ b/modules/github-oidc-provider/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/github-runners/providers.tf b/modules/github-runners/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/github-runners/providers.tf +++ b/modules/github-runners/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/global-accelerator-endpoint-group/providers.tf b/modules/global-accelerator-endpoint-group/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/global-accelerator-endpoint-group/providers.tf +++ b/modules/global-accelerator-endpoint-group/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/global-accelerator/providers.tf b/modules/global-accelerator/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/global-accelerator/providers.tf +++ b/modules/global-accelerator/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/iam-role/README.md b/modules/iam-role/README.md index 91886f4a0..2b75d8ff0 100644 --- a/modules/iam-role/README.md +++ b/modules/iam-role/README.md @@ -86,7 +86,7 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [assume\_role\_actions](#input\_assume\_role\_actions) | The IAM action to be granted by the AssumeRole policy | `list(string)` |
[
"sts:AssumeRole",
"sts:TagSession"
]
| no | +| [assume\_role\_actions](#input\_assume\_role\_actions) | The IAM action to be granted by the AssumeRole policy | `list(string)` |
[
"sts:AssumeRole",
"sts:SetSourceIdentity",
"sts:TagSession"
]
| no | | [assume\_role\_conditions](#input\_assume\_role\_conditions) | List of conditions for the assume role policy |
list(object({
test = string
variable = string
values = list(string)
}))
| `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | diff --git a/modules/iam-role/providers.tf b/modules/iam-role/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/iam-role/providers.tf +++ b/modules/iam-role/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/iam-role/variables.tf b/modules/iam-role/variables.tf index 0ef3b1f9c..3d80279eb 100644 --- a/modules/iam-role/variables.tf +++ b/modules/iam-role/variables.tf @@ -67,7 +67,7 @@ variable "policy_description" { variable "assume_role_actions" { type = list(string) - default = ["sts:AssumeRole", "sts:TagSession"] + default = ["sts:AssumeRole", "sts:SetSourceIdentity", "sts:TagSession"] description = "The IAM action to be granted by the AssumeRole policy" } diff --git a/modules/iam-service-linked-roles/providers.tf b/modules/iam-service-linked-roles/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/iam-service-linked-roles/providers.tf +++ b/modules/iam-service-linked-roles/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/kinesis-stream/providers.tf b/modules/kinesis-stream/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/kinesis-stream/providers.tf +++ b/modules/kinesis-stream/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/kms/providers.tf b/modules/kms/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/kms/providers.tf +++ b/modules/kms/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/lakeformation/providers.tf b/modules/lakeformation/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/lakeformation/providers.tf +++ b/modules/lakeformation/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/lambda/providers.tf b/modules/lambda/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/lambda/providers.tf +++ b/modules/lambda/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/mq-broker/providers.tf b/modules/mq-broker/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/mq-broker/providers.tf +++ b/modules/mq-broker/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/mwaa/providers.tf b/modules/mwaa/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/mwaa/providers.tf +++ b/modules/mwaa/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/network-firewall/providers.tf b/modules/network-firewall/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/network-firewall/providers.tf +++ b/modules/network-firewall/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/opsgenie-team/providers.tf b/modules/opsgenie-team/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/opsgenie-team/providers.tf +++ b/modules/opsgenie-team/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/rds/providers.tf b/modules/rds/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/rds/providers.tf +++ b/modules/rds/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/redshift/providers.tf b/modules/redshift/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/redshift/providers.tf +++ b/modules/redshift/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/route53-resolver-dns-firewall/providers.tf b/modules/route53-resolver-dns-firewall/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/route53-resolver-dns-firewall/providers.tf +++ b/modules/route53-resolver-dns-firewall/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/s3-bucket/providers.tf b/modules/s3-bucket/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/s3-bucket/providers.tf +++ b/modules/s3-bucket/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/ses/provider-awsutils.mixin.tf b/modules/ses/provider-awsutils.mixin.tf index db8bd2c1d..70fa8d095 100644 --- a/modules/ses/provider-awsutils.mixin.tf +++ b/modules/ses/provider-awsutils.mixin.tf @@ -8,7 +8,7 @@ provider "awsutils" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/ses/providers.tf b/modules/ses/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/ses/providers.tf +++ b/modules/ses/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/sftp/provider-awsutils.mixin.tf b/modules/sftp/provider-awsutils.mixin.tf index db8bd2c1d..70fa8d095 100644 --- a/modules/sftp/provider-awsutils.mixin.tf +++ b/modules/sftp/provider-awsutils.mixin.tf @@ -8,7 +8,7 @@ provider "awsutils" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/sftp/providers.tf b/modules/sftp/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/sftp/providers.tf +++ b/modules/sftp/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/snowflake-account/providers.tf b/modules/snowflake-account/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/snowflake-account/providers.tf +++ b/modules/snowflake-account/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/snowflake-database/providers.tf b/modules/snowflake-database/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/snowflake-database/providers.tf +++ b/modules/snowflake-database/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/sns-topic/providers.tf b/modules/sns-topic/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/sns-topic/providers.tf +++ b/modules/sns-topic/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/spa-s3-cloudfront/provider-other-regions.tf b/modules/spa-s3-cloudfront/provider-other-regions.tf index 1ce639363..62441d58d 100644 --- a/modules/spa-s3-cloudfront/provider-other-regions.tf +++ b/modules/spa-s3-cloudfront/provider-other-regions.tf @@ -10,7 +10,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } @@ -28,7 +28,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/spa-s3-cloudfront/providers.tf b/modules/spa-s3-cloudfront/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/spa-s3-cloudfront/providers.tf +++ b/modules/spa-s3-cloudfront/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/spacelift/worker-pool/iam.tf b/modules/spacelift/worker-pool/iam.tf index f12e59f51..3ce1756ba 100644 --- a/modules/spacelift/worker-pool/iam.tf +++ b/modules/spacelift/worker-pool/iam.tf @@ -13,6 +13,7 @@ data "aws_iam_policy_document" "assume_role_policy" { statement { actions = [ "sts:AssumeRole", + "sts:SetSourceIdentity", "sts:TagSession", ] @@ -33,6 +34,7 @@ data "aws_iam_policy_document" "default" { statement { actions = [ "sts:AssumeRole", + "sts:SetSourceIdentity", "sts:TagSession", ] resources = formatlist(local.role_arn_template, ["spacelift"]) diff --git a/modules/spacelift/worker-pool/providers.tf b/modules/spacelift/worker-pool/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/spacelift/worker-pool/providers.tf +++ b/modules/spacelift/worker-pool/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/sqs-queue/providers.tf b/modules/sqs-queue/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/sqs-queue/providers.tf +++ b/modules/sqs-queue/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/ssm-parameters/providers.tf b/modules/ssm-parameters/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/ssm-parameters/providers.tf +++ b/modules/ssm-parameters/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/sso-saml-provider/providers.tf b/modules/sso-saml-provider/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/sso-saml-provider/providers.tf +++ b/modules/sso-saml-provider/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/strongdm/provider-strongdm.tf b/modules/strongdm/provider-strongdm.tf index 1b15be46f..c704f951c 100644 --- a/modules/strongdm/provider-strongdm.tf +++ b/modules/strongdm/provider-strongdm.tf @@ -9,7 +9,7 @@ provider "aws" { # module.iam_roles_network.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles_network.terraform_role_arn]) content { - role_arn = module.iam_roles_network.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/strongdm/providers.tf b/modules/strongdm/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/strongdm/providers.tf +++ b/modules/strongdm/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/tgw/hub/providers.tf b/modules/tgw/hub/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/tgw/hub/providers.tf +++ b/modules/tgw/hub/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/tgw/spoke/provider-hub.tf b/modules/tgw/spoke/provider-hub.tf index 14974efa7..e969db12e 100644 --- a/modules/tgw/spoke/provider-hub.tf +++ b/modules/tgw/spoke/provider-hub.tf @@ -9,7 +9,7 @@ provider "aws" { # module.tgw_hub_role.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.tgw_hub_role.terraform_role_arn]) content { - role_arn = module.tgw_hub_role.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/tgw/spoke/providers.tf b/modules/tgw/spoke/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/tgw/spoke/providers.tf +++ b/modules/tgw/spoke/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/vpc-flow-logs-bucket/providers.tf b/modules/vpc-flow-logs-bucket/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/vpc-flow-logs-bucket/providers.tf +++ b/modules/vpc-flow-logs-bucket/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/vpc-peering/providers.tf b/modules/vpc-peering/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/vpc-peering/providers.tf +++ b/modules/vpc-peering/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/vpc/providers.tf b/modules/vpc/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/vpc/providers.tf +++ b/modules/vpc/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/waf/providers.tf b/modules/waf/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/waf/providers.tf +++ b/modules/waf/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/zscaler/providers.tf b/modules/zscaler/providers.tf index 54257fd20..ef923e10a 100644 --- a/modules/zscaler/providers.tf +++ b/modules/zscaler/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } From ae244d341bbe92861afe1aa19d4400bdca16b2b0 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 29 Jun 2023 14:15:29 -0700 Subject: [PATCH 165/501] [github-oidc-provider] extra-compatible provider (#742) --- modules/github-oidc-provider/README.md | 1 + modules/github-oidc-provider/providers.tf | 51 +++++++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index 921e679d5..8cb60d553 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -107,6 +107,7 @@ permissions: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [superadmin](#input\_superadmin) | Set `true` if running as the SuperAdmin user | `bool` | `false` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [thumbprint\_list](#input\_thumbprint\_list) | List of OIDC provider certificate thumbprints | `list(string)` |
[
"6938fd4d98bab03faadb97b34396831e3780aea1",
"1c58a3a8518e8759bf075b76b750d4f2df264fcd"
]
| no | diff --git a/modules/github-oidc-provider/providers.tf b/modules/github-oidc-provider/providers.tf index ef923e10a..b22bd5ed8 100644 --- a/modules/github-oidc-provider/providers.tf +++ b/modules/github-oidc-provider/providers.tf @@ -1,19 +1,56 @@ +# This is a special provider configuration that allows us to use many different +# versions of the Cloud Posse reference architecture to deploy this component +# in any account, including the identity and root accounts. + +# If you have dynamic Terraform roles enabled and an `aws-team` (such as `managers`) +# empowered to make changes in the identity and root accounts. Then you can +# use those roles to deploy this component in the identity and root accounts, +# just like almost any other component. +# +# If you are restricted to using the SuperAdmin role to deploy this component +# in the identity and root accounts, then modify the stack configuration for +# this component for the identity and/or root accounts to set `superadmin: true` +# and backend `role_arn` to `null`. +# +# components: +# terraform: +# github-oidc-provider: +# backend: +# s3: +# role_arn: null +# vars: +# superadmin: true + provider "aws" { region = var.region - # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. - profile = module.iam_roles.terraform_profile_name - + profile = !var.superadmin && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null dynamic "assume_role" { - # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. - for_each = compact([module.iam_roles.terraform_role_arn]) + for_each = [ + !var.superadmin && module.iam_roles.profiles_enabled ? null : ( + var.superadmin ? { + role_arn = module.iam_roles.org_role_arn + } : { + role_arn = module.iam_roles.terraform_role_arn + } + ) + ] content { - role_arn = assume_role.value + role_arn = module.iam_roles.terraform_role_arn } } } + module "iam_roles" { - source = "../account-map/modules/iam-roles" + source = "../account-map/modules/iam-roles" + privileged = var.superadmin + context = module.this.context } + +variable "superadmin" { + type = bool + default = false + description = "Set `true` if running as the SuperAdmin user" +} From a63341e8fb54178294c4f42be78a9b2e0c205870 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 29 Jun 2023 16:34:31 -0700 Subject: [PATCH 166/501] Bump `cloudposse/ec2-autoscale-group/aws` to `0.35.0` (#734) --- modules/bastion/README.md | 2 +- modules/bastion/main.tf | 2 +- modules/github-runners/README.md | 2 +- modules/github-runners/main.tf | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/bastion/README.md b/modules/bastion/README.md index e8122d103..488208d6f 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -71,7 +71,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [bastion\_autoscale\_group](#module\_bastion\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.30.1 | +| [bastion\_autoscale\_group](#module\_bastion\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [sg](#module\_sg) | cloudposse/security-group/aws | 2.0.0 | | [ssm\_tls\_ssh\_key\_pair](#module\_ssm\_tls\_ssh\_key\_pair) | cloudposse/ssm-tls-ssh-key-pair/aws | 0.10.2 | diff --git a/modules/bastion/main.tf b/modules/bastion/main.tf index a34b27cb4..34f736dce 100644 --- a/modules/bastion/main.tf +++ b/modules/bastion/main.tf @@ -91,7 +91,7 @@ data "aws_ami" "bastion_image" { module "bastion_autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" - version = "0.30.1" + version = "0.35.0" image_id = join("", data.aws_ami.bastion_image.*.id) instance_type = var.instance_type diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 821857b65..26b593f05 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -180,7 +180,7 @@ chamber write github token | Name | Source | Version | |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.30.1 | +| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.0 | | [graceful\_scale\_in](#module\_graceful\_scale\_in) | ./modules/graceful_scale_in | n/a | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [sg](#module\_sg) | cloudposse/security-group/aws | 1.0.1 | diff --git a/modules/github-runners/main.tf b/modules/github-runners/main.tf index 2a4131c5b..3bd1de0fd 100644 --- a/modules/github-runners/main.tf +++ b/modules/github-runners/main.tf @@ -107,7 +107,7 @@ module "sg" { module "autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" - version = "0.30.1" + version = "0.35.0" image_id = join("", data.aws_ami.runner.*.id) instance_type = var.instance_type From 6338228a99ed150af550160342d30652c6f8709f Mon Sep 17 00:00:00 2001 From: Maksym Vlasov Date: Mon, 3 Jul 2023 21:13:51 +0300 Subject: [PATCH 167/501] Fix TFLint violations in account-map (#745) Co-authored-by: cloudpossebot --- modules/account-map/README.md | 1 - modules/account-map/main.tf | 2 +- .../account-map/modules/team-assume-role-policy/outputs.tf | 2 +- modules/account-map/variables.tf | 6 ------ 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index e91970a4e..eb748bc56 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -98,7 +98,6 @@ components: | [dns\_account\_account\_name](#input\_dns\_account\_account\_name) | The stage name for the primary DNS account | `string` | `"dns"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [global\_environment\_name](#input\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no | | [iam\_role\_arn\_template\_template](#input\_iam\_role\_arn\_template\_template) | The template for the template used to render Role ARNs.
The template is first used to render a template for the account that takes only the role name.
Then that rendered template is used to create the final Role ARN for the account.
Default is appropriate when using `tenant` and default label order with `null-label`.
Use `"arn:%s:iam::%s:role/%s-%s-%s-%%s"` when not using `tenant`.

Note that if the `null-label` variable `label_order` is truncated or extended with additional labels, this template will
need to be updated to reflect the new number of labels. | `string` | `"arn:%s:iam::%s:role/%s-%s-%s-%s-%%s"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [identity\_account\_account\_name](#input\_identity\_account\_account\_name) | The stage name for the account holding primary IAM roles | `string` | `"identity"` | no | diff --git a/modules/account-map/main.tf b/modules/account-map/main.tf index 8f48f13b5..850e23cf0 100644 --- a/modules/account-map/main.tf +++ b/modules/account-map/main.tf @@ -67,7 +67,7 @@ locals { ) ? "admin" : "terraform") } - // legacy support for `aws` config profiles + # legacy support for `aws` config profiles terraform_profiles = { for name, info in local.account_info_map : name => format(var.profile_template, compact( [ diff --git a/modules/account-map/modules/team-assume-role-policy/outputs.tf b/modules/account-map/modules/team-assume-role-policy/outputs.tf index e9efb142a..2f4d7cd1b 100644 --- a/modules/account-map/modules/team-assume-role-policy/outputs.tf +++ b/modules/account-map/modules/team-assume-role-policy/outputs.tf @@ -1,4 +1,4 @@ output "policy_document" { description = "JSON encoded string representing the \"Assume Role\" policy configured by the inputs" - value = join("", data.aws_iam_policy_document.assume_role.*.json) + value = join("", data.aws_iam_policy_document.assume_role[*].json) } diff --git a/modules/account-map/variables.tf b/modules/account-map/variables.tf index 826f52853..c944d15bd 100644 --- a/modules/account-map/variables.tf +++ b/modules/account-map/variables.tf @@ -66,12 +66,6 @@ variable "profile_template" { EOT } -variable "global_environment_name" { - type = string - default = "gbl" - description = "Global environment name" -} - variable "profiles_enabled" { type = bool default = false From 483b0953aff020c4cb282f54d972e4b2de8220ef Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 4 Jul 2023 00:54:06 -0400 Subject: [PATCH 168/501] bugfix `aws-sso`, `github-oidc-provider` (#740) --- modules/aws-sso/CHANGELOG.md | 45 ++++++++++++ modules/aws-sso/README.md | 11 +-- modules/aws-sso/main.tf | 6 +- .../policy-Identity-role-TeamAccess.tf | 4 +- .../aws-sso/policy-TerraformUpdateAccess.tf | 4 +- modules/aws-sso/providers.tf | 68 +++++++++++++++++-- modules/aws-sso/remote-state.tf | 7 -- modules/aws-sso/variables.tf | 4 +- modules/github-oidc-provider/providers.tf | 14 ++-- 9 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 modules/aws-sso/CHANGELOG.md diff --git a/modules/aws-sso/CHANGELOG.md b/modules/aws-sso/CHANGELOG.md new file mode 100644 index 000000000..6815a2f01 --- /dev/null +++ b/modules/aws-sso/CHANGELOG.md @@ -0,0 +1,45 @@ +# Change log for aws-sso component + +***NOTE***: This file is manually generated and is a work-in-progress. + +### PR 740 + +This PR restores compatibility with `account-map` prior to version 1.227.0 +and fixes bugs that made versions 1.227.0 up to this release unusable. + +Access control configuration (`aws-teams`, `iam-primary-roles`, `aws-sso`, etc.) +has undergone several transformations over the evolution of Cloud Posse's reference +architecture. This update resolves a number of compatibility issues with some of them. + +If the roles you are using to deploy this component are allowed to assume +the `tfstate-backend` access roles (typically `...-gbl-root-tfstate`, possibly +`...-gbl-root-tfstate-ro` or `...-gbl-root-terraform`), then you can use the +defaults. This configuration was introduced in `terraform-aws-components` v1.227.0 +and is the default for all new deployments. + +If the roles you are using to deploy this component are not allowed to assume +the `tfstate-backend` access roles, then you will need to configure this component +to include the following: + +```yaml +components: + terraform: + aws-sso: + backend: + s3: + role_arn: null + vars: + privileged: true +``` + +If you are deploying this component to the `identity` account, then this +restriction will require you to deploy it via the SuperAdmin user. If you are +deploying this component to the `root` account, then any user or role +in the `root` account with the `AdministratorAccess` policy attached will be +able to deploy this component. + + +## v1.227.0 + +This component was broken by changes made in v1.227.0. Either use a version +before v1.227.0 or use the version released by PR 740 or later. diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index dea250090..51351f100 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -166,10 +166,11 @@ components: |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 1.0.0 | +| [iam\_roles\_root](#module\_iam\_roles\_root) | ../account-map/modules/iam-roles | n/a | +| [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 1.1.1 | | [role\_map](#module\_role\_map) | ../account-map/modules/roles-to-principals | n/a | -| [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 1.1.0 | -| [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 1.0.0 | +| [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 1.1.1 | +| [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 1.1.1 | | [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -177,10 +178,10 @@ components: | Name | Type | |------|------| -| [aws_iam_policy_document.TerraformUpdateAccess](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.assume_aws_team](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.dns_administrator_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.eks_read_only](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.terraform_update_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | ## Inputs @@ -203,7 +204,7 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `true` | no | +| [privileged](#input\_privileged) | True if the user running the Terraform command already has access to the Terraform backend | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/aws-sso/main.tf b/modules/aws-sso/main.tf index 80e062557..7f71b703b 100644 --- a/modules/aws-sso/main.tf +++ b/modules/aws-sso/main.tf @@ -1,6 +1,6 @@ module "permission_sets" { source = "cloudposse/sso/aws//modules/permission-sets" - version = "1.0.0" + version = "1.1.1" permission_sets = concat( local.overridable_additional_permission_sets, @@ -19,7 +19,7 @@ module "permission_sets" { module "sso_account_assignments" { source = "cloudposse/sso/aws//modules/account-assignments" - version = "1.1.0" + version = "1.1.1" account_assignments = local.account_assignments context = module.this.context @@ -27,7 +27,7 @@ module "sso_account_assignments" { module "sso_account_assignments_root" { source = "cloudposse/sso/aws//modules/account-assignments" - version = "1.0.0" + version = "1.1.1" providers = { aws = aws.root diff --git a/modules/aws-sso/policy-Identity-role-TeamAccess.tf b/modules/aws-sso/policy-Identity-role-TeamAccess.tf index 4e9ddc2e8..7c02b2ffd 100644 --- a/modules/aws-sso/policy-Identity-role-TeamAccess.tf +++ b/modules/aws-sso/policy-Identity-role-TeamAccess.tf @@ -12,7 +12,6 @@ data "aws_iam_policy_document" "assume_aws_team" { effect = "Allow" actions = [ "sts:AssumeRole", - "sts:SetSourceIdentity", "sts:TagSession", ] @@ -41,7 +40,8 @@ data "aws_iam_policy_document" "assume_aws_team" { module "role_map" { source = "../account-map/modules/roles-to-principals" - teams = var.aws_teams_accessible + teams = var.aws_teams_accessible + privileged = var.privileged context = module.this.context } diff --git a/modules/aws-sso/policy-TerraformUpdateAccess.tf b/modules/aws-sso/policy-TerraformUpdateAccess.tf index 2bccb22be..c9ad00d9f 100644 --- a/modules/aws-sso/policy-TerraformUpdateAccess.tf +++ b/modules/aws-sso/policy-TerraformUpdateAccess.tf @@ -22,7 +22,7 @@ module "tfstate" { context = module.this.context } -data "aws_iam_policy_document" "TerraformUpdateAccess" { +data "aws_iam_policy_document" "terraform_update_access" { count = local.tf_update_access_enabled ? 1 : 0 statement { @@ -49,7 +49,7 @@ locals { relay_state = "", session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles tags = {}, - inline_policy = one(data.aws_iam_policy_document.TerraformUpdateAccess[*].json), + inline_policy = one(data.aws_iam_policy_document.terraform_update_access[*].json), policy_attachments = [] customer_managed_policy_attachments = [] }] : [] diff --git a/modules/aws-sso/providers.tf b/modules/aws-sso/providers.tf index ef923e10a..09347de4a 100644 --- a/modules/aws-sso/providers.tf +++ b/modules/aws-sso/providers.tf @@ -1,19 +1,75 @@ +# This is a special provider configuration that allows us to use many different +# versions of the Cloud Posse reference architecture to deploy this component +# in any account, including the identity and root accounts. + +# If you have dynamic Terraform roles enabled and an `aws-team` (such as `managers`) +# empowered to make changes in the identity and root accounts. Then you can +# use those roles to deploy this component in the identity and root accounts, +# just like almost any other component. Leave `privileged: false` and leave the +# backend `role_arn` at its default value. +# +# For those not using dynamic Terraform roles: +# +# If you are deploying this to the "identity" account and are restricted to using +# the SuperAdmin role to deploy components to "identity", then you will need to +# set the stack configuration for this component to set `privileged: true` +# and backend `role_arn` to `null`. +# +# If you are deploying this to the "identity" account and have a team empowered +# to deploy components to "identity", then you will need to set the stack +# configuration for this component to set `privileged: false` and leave the +# backend `role_arn` at its default value. +# +# If you are deploying this to the "root" account, then you will need to +# set the stack configuration for this component to set `privileged: true` +# and backend `role_arn` to `null`, and deploy it using either the SuperAdmin +# role or any other role in the `root` account with Admin access. + provider "aws" { region = var.region - # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. - profile = module.iam_roles.terraform_profile_name - + profile = !var.privileged && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null dynamic "assume_role" { - # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. - for_each = compact([module.iam_roles.terraform_role_arn]) + for_each = !var.privileged && module.iam_roles.profiles_enabled ? [] : ( + var.privileged ? compact([module.iam_roles.org_role_arn]) : compact([module.iam_roles.terraform_role_arn]) + ) content { role_arn = assume_role.value } } } + module "iam_roles" { - source = "../account-map/modules/iam-roles" + source = "../account-map/modules/iam-roles" + privileged = var.privileged + + context = module.this.context +} + +provider "aws" { + alias = "root" + region = var.region + + profile = !var.privileged && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null + dynamic "assume_role" { + for_each = !var.privileged && module.iam_roles.profiles_enabled ? [] : ( + var.privileged ? compact([module.iam_roles.org_role_arn]) : compact([module.iam_roles.terraform_role_arn]) + ) + content { + role_arn = assume_role.value + } + } +} + + +module "iam_roles_root" { + source = "../account-map/modules/iam-roles" + + privileged = var.privileged + tenant = module.iam_roles.global_tenant_name + stage = module.iam_roles.global_stage_name + environment = module.iam_roles.global_environment_name + context = module.this.context } diff --git a/modules/aws-sso/remote-state.tf b/modules/aws-sso/remote-state.tf index 52726e842..b6edc1678 100644 --- a/modules/aws-sso/remote-state.tf +++ b/modules/aws-sso/remote-state.tf @@ -10,10 +10,3 @@ module "account_map" { context = module.this.context } - -# Module "iam_roles" is usually in providers.tf, but not so for this component - -module "iam_roles" { - source = "../account-map/modules/iam-roles" - context = module.this.context -} diff --git a/modules/aws-sso/variables.tf b/modules/aws-sso/variables.tf index dc6e4cf1a..70c5993c2 100644 --- a/modules/aws-sso/variables.tf +++ b/modules/aws-sso/variables.tf @@ -5,8 +5,8 @@ variable "region" { variable "privileged" { type = bool - description = "True if the default provider already has access to the backend" - default = true + description = "True if the user running the Terraform command already has access to the Terraform backend" + default = false } variable "account_assignments" { diff --git a/modules/github-oidc-provider/providers.tf b/modules/github-oidc-provider/providers.tf index b22bd5ed8..13a1a5b6a 100644 --- a/modules/github-oidc-provider/providers.tf +++ b/modules/github-oidc-provider/providers.tf @@ -26,17 +26,11 @@ provider "aws" { profile = !var.superadmin && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null dynamic "assume_role" { - for_each = [ - !var.superadmin && module.iam_roles.profiles_enabled ? null : ( - var.superadmin ? { - role_arn = module.iam_roles.org_role_arn - } : { - role_arn = module.iam_roles.terraform_role_arn - } - ) - ] + for_each = !var.superadmin && module.iam_roles.profiles_enabled ? [] : ( + var.superadmin ? compact([module.iam_roles.org_role_arn]) : compact([module.iam_roles.terraform_role_arn]) + ) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } From d043b5ac0aea09ba75d7ed53d31bddf05ab7536f Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Wed, 5 Jul 2023 19:52:33 +0300 Subject: [PATCH 169/501] Fixed broken links in READMEs (#749) --- modules/account-map/README.md | 2 +- modules/acm/README.md | 2 +- modules/datadog-monitor/README.md | 2 +- modules/datadog-synthetics/README.md | 2 +- modules/dns-primary/README.md | 2 +- modules/eks/eks-without-spotinst/README.md | 6 +++--- modules/eks/external-secrets-operator/README.md | 2 +- modules/github-oidc-provider/README.md | 2 +- modules/opsgenie-team/README.md | 2 +- modules/opsgenie-team/modules/escalation/README.md | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index eb748bc56..58bcc4904 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -4,7 +4,7 @@ This component is responsible for provisioning information only: it simply popul ## Pre-requisites -- [account](/reference-architecture/components/account) must be provisioned before [account-map](/reference-architecture/components/account-map) component +- [account](/components/library/aws/account) must be provisioned before [account-map](/components/library/aws/account-map) component ## Usage diff --git a/modules/acm/README.md b/modules/acm/README.md index 0c540545e..82d3527fb 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -2,7 +2,7 @@ This component is responsible for requesting an ACM certificate for a domain and adding a CNAME record to the DNS zone to complete certificate validation. -The ACM component is to manage an unlimited number of certificates, predominantly for vanity domains. While the [dns-primary](/reference-architecture/components/dns-primary) component has the ability to generate ACM certificates, it is very opinionated and can only manage one zone. In reality, companies have many branded domains associated with a load balancer, so we need to be able to generate more complicated certificates. +The ACM component is to manage an unlimited number of certificates, predominantly for vanity domains. While the [dns-primary](/components/library/aws/dns-primary) component has the ability to generate ACM certificates, it is very opinionated and can only manage one zone. In reality, companies have many branded domains associated with a load balancer, so we need to be able to generate more complicated certificates. We have, as a convenience, the ability to create an ACM certificate as part of creating a DNS zone, whether primary or delegated. That convenience is limited to creating `example.com` and `*.example.com` when creating a zone for `example.com`. For example, Acme has delegated `acct.acme.com` and in addition to `*.acct.acme.com` needed an ACM certificate for `*.usw2.acct.acme.com`, so we use the ACM component to provision that, rather than extend the DNS primary or delegated components to take a list of additional certificates. Both are different views on the Single Responsibility Principle. diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 479fccc4d..f853bb04e 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -36,7 +36,7 @@ components: ``` ## Conventions -- Treat datadog like a separate cloud provider with integrations ([datadog-integration](/reference-architecture/components/datadog-integration)) into your accounts. +- Treat datadog like a separate cloud provider with integrations ([datadog-integration](/components/library/aws/datadog-integration)) into your accounts. - Use the `catalog` convention to define a step of alerts. You can use ours or define your own. [https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors](https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors) diff --git a/modules/datadog-synthetics/README.md b/modules/datadog-synthetics/README.md index 5f35eed54..534d2a5c4 100644 --- a/modules/datadog-synthetics/README.md +++ b/modules/datadog-synthetics/README.md @@ -109,7 +109,7 @@ my-api-test: jsonpath: foo.bar ``` -These configuration examples are defined in the YAML files in the [catalog/synthetics/examples](catalog/synthetics/examples) folder. +These configuration examples are defined in the YAML files in the [catalog/synthetics/examples](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-synthetics/catalog/synthetics/examples) folder. You can use different subfolders for your use-case. For example, you can have `dev` and `prod` subfolders to define different synthetic tests for the `dev` and `prod` environments. diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index fc2df279a..a041aef77 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -56,7 +56,7 @@ components: ``` :::info -Use the [acm](/reference-architecture/components/acm) component for more advanced certificate requirements. +Use the [acm](/components/library/aws/acm) component for more advanced certificate requirements. ::: diff --git a/modules/eks/eks-without-spotinst/README.md b/modules/eks/eks-without-spotinst/README.md index 3cb012122..1303afb4c 100644 --- a/modules/eks/eks-without-spotinst/README.md +++ b/modules/eks/eks-without-spotinst/README.md @@ -6,10 +6,10 @@ NOTE: This component can only be deployed after logging in to AWS via Federated If Spotinst is going to be used, the following course of action needs to be followed: 1. Create Spotinst account and subscribe to a Business Plan. -1. Provision [spotinst-integration](../spotinst-integration), as documented in the component. +1. Provision [spotinst-integration](https://spot.io/), as documented in the component. 1. Provision EKS with Spotinst Ocean pool only. -1. Deploy core K8s components, including [metrics-server](../metrics-server), [external-dns](../external-dns), etc. -1. Deploy Spotinst [ocean-controller](../ocean-controller). +1. Deploy core K8s components, including [metrics-server](/components/library/aws/eks/metrics-server), [external-dns](/components/library/aws/eks/external-dns), etc. +1. Deploy Spotinst [ocean-controller](https://docs.spot.io/ocean/tutorials/spot-kubernetes-controller/). ## Usage diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index b5b4c7e56..bc79aad1f 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -182,6 +182,6 @@ components: ## References -* [ADR-0067](../../../../docs/adr/0067-secrets-manager-strategy.md) +* [Secrets Management Strategy](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-secrets-management-strategy-for-terraform/) * https://external-secrets.io/v0.5.9/ * https://external-secrets.io/v0.5.9/provider-aws-parameter-store/ diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index 8cb60d553..c0a5908d5 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -30,7 +30,7 @@ This component was created to add the Github OIDC provider so that Github Action without the need to store static credentials in the environment. The details of the GitHub OIDC provider are hard coded in the component, however at some point the provider's thumbprint may change, at which point you can use -[scripts/get_github_oidc_thumbprint.sh](./scripts/get_github_oidc_thumbprint.sh) +[get_github_oidc_thumbprint.sh](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh) to get the new thumbprint and add it to the list in `var.thumbprint_list`. This script will pull one of two thumbprints. There are two possible intermediary certificates for the Actions SSL diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index fa5b375f1..e6b28b70c 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -345,7 +345,7 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ - [How to Create Escalation Rules in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-escalation-rules-in-opsgenie) - [How to Setup Rotations in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-setup-rotations-in-opsgenie) - [How to Create New Teams in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-new-teams-in-opsgenie) -- [How to Sign Up for OpsGenie?](/reference-architecture/how-to-guides/tutorials/how-to-sign-up-for-opsgenie) +- [How to Sign Up for OpsGenie?](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-sign-up-for-opsgenie/) - [How to Implement Incident Management with OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie) ## References diff --git a/modules/opsgenie-team/modules/escalation/README.md b/modules/opsgenie-team/modules/escalation/README.md index 1d64e1962..e5261df7c 100644 --- a/modules/opsgenie-team/modules/escalation/README.md +++ b/modules/opsgenie-team/modules/escalation/README.md @@ -5,7 +5,7 @@ Terraform module to configure [Opsgenie Escalation](https://registry.terraform.i ## Usage -[Create Opsgenie Escalation example](../../examples/escalation) +[Create Opsgenie Escalation example](https://github.com/cloudposse/terraform-opsgenie-incident-management/tree/main/examples/escalation) ```hcl module "escalation" { From 7eb39ab6169726928fcb250e38423df8b5b3183c Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 5 Jul 2023 10:05:37 -0700 Subject: [PATCH 170/501] Restore backwards compatibility of account-map output (#748) --- modules/account-map/README.md | 4 ++++ modules/account-map/main.tf | 5 +++++ modules/account-map/outputs.tf | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index 58bcc4904..bc2ccc764 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -129,9 +129,13 @@ components: | [artifacts\_account\_account\_name](#output\_artifacts\_account\_account\_name) | The short name for the artifacts account | | [audit\_account\_account\_name](#output\_audit\_account\_account\_name) | The short name for the audit account | | [aws\_partition](#output\_aws\_partition) | The AWS "partition" to use when constructing resource ARNs | +| [cicd\_profiles](#output\_cicd\_profiles) | OBSOLETE: dummy results returned to avoid breaking code that depends on this output | +| [cicd\_roles](#output\_cicd\_roles) | OBSOLETE: dummy results returned to avoid breaking code that depends on this output | | [dns\_account\_account\_name](#output\_dns\_account\_account\_name) | The short name for the primary DNS account | | [eks\_accounts](#output\_eks\_accounts) | A list of all accounts in the AWS Organization that contain EKS clusters | | [full\_account\_map](#output\_full\_account\_map) | The map of account name to account ID (number). | +| [helm\_profiles](#output\_helm\_profiles) | OBSOLETE: dummy results returned to avoid breaking code that depends on this output | +| [helm\_roles](#output\_helm\_roles) | OBSOLETE: dummy results returned to avoid breaking code that depends on this output | | [iam\_role\_arn\_templates](#output\_iam\_role\_arn\_templates) | Map of accounts to corresponding IAM Role ARN templates | | [identity\_account\_account\_name](#output\_identity\_account\_account\_name) | The short name for the account holding primary IAM roles | | [non\_eks\_accounts](#output\_non\_eks\_accounts) | A list of all accounts in the AWS Organization that do not contain EKS clusters | diff --git a/modules/account-map/main.tf b/modules/account-map/main.tf index 850e23cf0..25785a93b 100644 --- a/modules/account-map/main.tf +++ b/modules/account-map/main.tf @@ -30,6 +30,11 @@ locals { all_accounts = concat(local.eks_accounts, local.non_eks_accounts) account_info_map = module.accounts.outputs.account_info_map + # Provide empty lists for deprecated outputs, to avoid breaking old code + # before it can be replaced. + empty_account_map = merge({ for name, info in local.account_info_map : name => "" }, { _OBSOLETE = "DUMMY RESULTS for backwards compatibility" }) + + # We should move this to be specified by tags on the accounts, # like we do with EKS, but for now.... account_role_map = { diff --git a/modules/account-map/outputs.tf b/modules/account-map/outputs.tf index 3604a2e45..1354e55f9 100644 --- a/modules/account-map/outputs.tf +++ b/modules/account-map/outputs.tf @@ -115,3 +115,37 @@ resource "local_file" "account_info" { }) filename = "${path.module}/account-info/${module.this.id}.sh" } + + +###################### +## Deprecated outputs +## These outputs are deprecated and will be removed in a future release +## As of this release, they return empty lists so as not to break old +## versions of account-map/modules/iam-roles and imposing an order +## on deploying new code vs applying the updated account-map +###################### + +output "helm_roles" { + value = local.empty_account_map + description = "OBSOLETE: dummy results returned to avoid breaking code that depends on this output" +} + +output "helm_profiles" { + value = local.empty_account_map + description = "OBSOLETE: dummy results returned to avoid breaking code that depends on this output" +} + +output "cicd_roles" { + value = local.empty_account_map + description = "OBSOLETE: dummy results returned to avoid breaking code that depends on this output" +} + +output "cicd_profiles" { + value = local.empty_account_map + description = "OBSOLETE: dummy results returned to avoid breaking code that depends on this output" +} + +###################### +## End of Deprecated outputs +## Please add new outputs above this section +###################### From 07ebfbddb77066c2202cd0fbad737fdd6faed398 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Wed, 5 Jul 2023 22:45:46 +0300 Subject: [PATCH 171/501] Use the new subnets data source (#737) --- .../modules/node_group_by_az/main.tf | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_az/main.tf b/modules/eks/eks-without-spotinst/modules/node_group_by_az/main.tf index cc965d431..939007d37 100644 --- a/modules/eks/eks-without-spotinst/modules/node_group_by_az/main.tf +++ b/modules/eks/eks-without-spotinst/modules/node_group_by_az/main.tf @@ -1,16 +1,19 @@ -data "aws_subnet_ids" "private" { +data "aws_subnets" "private" { count = local.enabled ? 1 : 0 - vpc_id = var.cluster_context.vpc_id - - tags = { - (var.cluster_context.subnet_type_tag_key) = "private" + filter { + name = "vpc-id" + values = [var.cluster_context.vpc_id] } filter { name = "availability-zone" values = [var.availability_zone] } + + tags = { + (var.cluster_context.subnet_type_tag_key) = "private" + } } module "az_abbreviation" { @@ -21,7 +24,7 @@ module "az_abbreviation" { locals { enabled = module.this.enabled && length(var.availability_zone) > 0 sentinel = "~~" - subnet_ids_test = coalescelist(flatten(data.aws_subnet_ids.private[*].ids), [local.sentinel]) + subnet_ids_test = coalescelist(flatten(data.aws_subnets.private[*].ids), [local.sentinel]) subnet_ids = local.subnet_ids_test[0] == local.sentinel ? null : local.subnet_ids_test az_map = var.cluster_context.az_abbreviation_type == "short" ? module.az_abbreviation.region_az_alt_code_maps.to_short : module.az_abbreviation.region_az_alt_code_maps.to_fixed az_attribute = local.az_map[var.availability_zone] From 2ea9d7b35a1a40b8c22a1ea90f08cdad0c625d37 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 6 Jul 2023 13:03:42 -0700 Subject: [PATCH 172/501] Upstream `gitops` (#735) --- modules/gitops/README.md | 130 ++++++++ modules/gitops/context.tf | 279 ++++++++++++++++++ modules/gitops/github-actions-iam-policy.tf | 68 +++++ .../gitops/github-actions-iam-role.mixin.tf | 72 +++++ modules/gitops/providers.tf | 19 ++ modules/gitops/remote-state.tf | 19 ++ modules/gitops/variables.tf | 28 ++ modules/gitops/versions.tf | 10 + 8 files changed, 625 insertions(+) create mode 100644 modules/gitops/README.md create mode 100644 modules/gitops/context.tf create mode 100644 modules/gitops/github-actions-iam-policy.tf create mode 100644 modules/gitops/github-actions-iam-role.mixin.tf create mode 100644 modules/gitops/providers.tf create mode 100644 modules/gitops/remote-state.tf create mode 100644 modules/gitops/variables.tf create mode 100644 modules/gitops/versions.tf diff --git a/modules/gitops/README.md b/modules/gitops/README.md new file mode 100644 index 000000000..749d288df --- /dev/null +++ b/modules/gitops/README.md @@ -0,0 +1,130 @@ +# Component: `gitops` + +This component is used to deploy GitHub OIDC roles for accessing the `gitops` Team. We use this team to run Terraform from GitHub Actions. + +Examples: + +* [cloudposse/github-action-terraform-plan-storage](https://github.com/cloudposse/github-action-terraform-plan-storage/blob/main/.github/workflows/build-and-test.yml) + +## Usage + +**Stack Level**: Regional + +Here are some example snippets for how to use this component: + +```yaml +import: + - catalog/s3-bucket/defaults + - catalog/dynamodb/defaults + +components: + terraform: + # S3 Bucket for storing Terraform Plans + gitops/s3-bucket: + metadata: + component: s3-bucket + inherits: + - s3-bucket/defaults + vars: + name: gitops-plan-storage + allow_encrypted_uploads_only: false + + # DynamoDB table used to store metadata for Terraform Plans + gitops/dynamodb: + metadata: + component: dynamodb + inherits: + - dynamodb/defaults + vars: + name: gitops-plan-storage + # These keys (case-sensitive) are required for the cloudposse/github-action-terraform-plan-storage action + hash_key: id + range_key: createdAt + + gitops: + vars: + enabled: true + github_actions_iam_role_enabled: true + github_actions_iam_role_attributes: [ "gitops" ] + github_actions_allowed_repos: + - "acmeOrg/infra" + s3_bucket_component_name: gitops/s3-bucket + dynamodb_component_name: gitops/dynamodb +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [dynamodb](#module\_dynamodb) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | +| [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [s3\_bucket](#module\_s3\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dynamodb\_component\_name](#input\_dynamodb\_component\_name) | The name of the dynamodb component used to store Terraform state | `string` | `"gitops/dynamodb"` | no | +| [dynamodb\_environment\_name](#input\_dynamodb\_environment\_name) | The name of the dynamodb environment used to store Terraform state | `string` | `null` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github\_actions\_allowed\_repos](#input\_github\_actions\_allowed\_repos) | A list of the GitHub repositories that are allowed to assume this role from GitHub Actions. For example,
["cloudposse/infra-live"]. Can contain "*" as wildcard.
If org part of repo name is omitted, "cloudposse" will be assumed. | `list(string)` | `[]` | no | +| [github\_actions\_iam\_role\_attributes](#input\_github\_actions\_iam\_role\_attributes) | Additional attributes to add to the role name | `list(string)` | `[]` | no | +| [github\_actions\_iam\_role\_enabled](#input\_github\_actions\_iam\_role\_enabled) | Flag to toggle creation of an IAM Role that GitHub Actions can assume to access AWS resources | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [s3\_bucket\_component\_name](#input\_s3\_bucket\_component\_name) | The name of the s3\_bucket component used to store Terraform state | `string` | `"gitops/s3-bucket"` | no | +| [s3\_bucket\_environment\_name](#input\_s3\_bucket\_environment\_name) | The name of the s3\_bucket environment used to store Terraform state | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [github\_actions\_iam\_role\_arn](#output\_github\_actions\_iam\_role\_arn) | ARN of IAM role for GitHub Actions | +| [github\_actions\_iam\_role\_name](#output\_github\_actions\_iam\_role\_name) | Name of IAM role for GitHub Actions | + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/gitops) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/gitops/context.tf b/modules/gitops/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/gitops/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/gitops/github-actions-iam-policy.tf b/modules/gitops/github-actions-iam-policy.tf new file mode 100644 index 000000000..52ef73671 --- /dev/null +++ b/modules/gitops/github-actions-iam-policy.tf @@ -0,0 +1,68 @@ +locals { + enabled = module.this.enabled + github_actions_iam_policy = data.aws_iam_policy_document.github_actions_iam_policy.json + + s3_bucket_arn = module.s3_bucket.outputs.bucket_arn + dynamodb_table_arn = module.dynamodb.outputs.table_arn +} + +data "aws_iam_policy_document" "github_actions_iam_policy" { + # Allow access to the Dynamodb table used to store TF Plans + # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_dynamodb_specific-table.html + statement { + sid = "AllowDynamodbAccess" + effect = "Allow" + actions = [ + "dynamodb:List*", + "dynamodb:DescribeReservedCapacity*", + "dynamodb:DescribeLimits", + "dynamodb:DescribeTimeToLive" + ] + resources = [ + "*" + ] + } + statement { + sid = "AllowDynamodbTableAccess" + effect = "Allow" + actions = [ + "dynamodb:BatchGet*", + "dynamodb:DescribeStream", + "dynamodb:DescribeTable", + "dynamodb:Get*", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWrite*", + "dynamodb:CreateTable", + "dynamodb:Delete*", + "dynamodb:Update*", + "dynamodb:PutItem" + ] + resources = [ + local.dynamodb_table_arn + ] + } + + # Allow access to the S3 Bucket used to store TF Plans + # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html + statement { + sid = "AllowS3Actions" + effect = "Allow" + actions = [ + "s3:ListBucket" + ] + resources = [ + local.s3_bucket_arn + ] + } + statement { + sid = "AllowS3ObjectActions" + effect = "Allow" + actions = [ + "s3:*Object" + ] + resources = [ + "${local.s3_bucket_arn}/*" + ] + } +} diff --git a/modules/gitops/github-actions-iam-role.mixin.tf b/modules/gitops/github-actions-iam-role.mixin.tf new file mode 100644 index 000000000..de68c6602 --- /dev/null +++ b/modules/gitops/github-actions-iam-role.mixin.tf @@ -0,0 +1,72 @@ +# This mixin requires that a local variable named `github_actions_iam_policy` be defined +# and its value to be a JSON IAM Policy Document defining the permissions for the role. +# It also requires that the `github-oidc-provider` has been previously installed and the +# `github-assume-role-policy.mixin.tf` has been added to `account-map/modules/team-assume-role-policy`. + +variable "github_actions_iam_role_enabled" { + type = bool + description = <<-EOF + Flag to toggle creation of an IAM Role that GitHub Actions can assume to access AWS resources + EOF + default = false +} + +variable "github_actions_allowed_repos" { + type = list(string) + description = < 0 +} + +module "gha_role_name" { + source = "cloudposse/label/null" + version = "0.25.0" + + enabled = local.github_actions_iam_role_enabled + attributes = compact(concat(var.github_actions_iam_role_attributes, ["gha"])) + + context = module.this.context +} + +module "gha_assume_role" { + source = "../account-map/modules/team-assume-role-policy" + + trusted_github_repos = var.github_actions_allowed_repos + + context = module.gha_role_name.context +} + +resource "aws_iam_role" "github_actions" { + count = local.github_actions_iam_role_enabled ? 1 : 0 + name = module.gha_role_name.id + assume_role_policy = module.gha_assume_role.github_assume_role_policy + + inline_policy { + name = module.gha_role_name.id + policy = local.github_actions_iam_policy + } +} + +output "github_actions_iam_role_arn" { + value = one(aws_iam_role.github_actions[*].arn) + description = "ARN of IAM role for GitHub Actions" +} + +output "github_actions_iam_role_name" { + value = one(aws_iam_role.github_actions[*].name) + description = "Name of IAM role for GitHub Actions" +} diff --git a/modules/gitops/providers.tf b/modules/gitops/providers.tf new file mode 100644 index 000000000..54257fd20 --- /dev/null +++ b/modules/gitops/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/gitops/remote-state.tf b/modules/gitops/remote-state.tf new file mode 100644 index 000000000..948097924 --- /dev/null +++ b/modules/gitops/remote-state.tf @@ -0,0 +1,19 @@ +module "s3_bucket" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + component = var.s3_bucket_component_name + environment = try(var.s3_bucket_environment_name, module.this.environment) + + context = module.this.context +} + +module "dynamodb" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + component = var.dynamodb_component_name + environment = try(var.dynamodb_environment_name, module.this.environment) + + context = module.this.context +} diff --git a/modules/gitops/variables.tf b/modules/gitops/variables.tf new file mode 100644 index 000000000..64bb993d4 --- /dev/null +++ b/modules/gitops/variables.tf @@ -0,0 +1,28 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "s3_bucket_component_name" { + type = string + description = "The name of the s3_bucket component used to store Terraform state" + default = "gitops/s3-bucket" +} + +variable "s3_bucket_environment_name" { + type = string + description = "The name of the s3_bucket environment used to store Terraform state" + default = null +} + +variable "dynamodb_component_name" { + type = string + description = "The name of the dynamodb component used to store Terraform state" + default = "gitops/dynamodb" +} + +variable "dynamodb_environment_name" { + type = string + description = "The name of the dynamodb environment used to store Terraform state" + default = null +} diff --git a/modules/gitops/versions.tf b/modules/gitops/versions.tf new file mode 100644 index 000000000..cc73ffd35 --- /dev/null +++ b/modules/gitops/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From cbf6c0adddb9787f8ee7ad1a00edbe8d32e93952 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 11 Jul 2023 10:49:53 -0700 Subject: [PATCH 173/501] Upstream Spacelift and Documentation (#732) Co-authored-by: Andriy Knysh Co-authored-by: cloudpossebot --- modules/spacelift/README.md | 681 ++++++++---------- modules/spacelift/admin-stack/README.md | 135 +++- modules/spacelift/admin-stack/outputs.tf | 12 +- modules/spacelift/admin-stack/versions.tf | 4 + modules/spacelift/bin/spacelift-configure | 42 -- modules/spacelift/bin/spacelift-git-use-https | 14 - modules/spacelift/bin/spacelift-tf-workspace | 31 - modules/spacelift/bin/spacelift-write-vars | 28 - modules/spacelift/spaces/README.md | 21 +- modules/spacelift/spaces/main.tf | 2 +- modules/spacelift/spaces/outputs.tf | 6 +- modules/spacelift/spaces/variables.tf | 1 + modules/spacelift/worker-pool/README.md | 59 +- modules/spacelift/worker-pool/main.tf | 6 +- modules/spacelift/worker-pool/remote-state.tf | 18 +- modules/spacelift/worker-pool/variables.tf | 30 + 16 files changed, 523 insertions(+), 567 deletions(-) delete mode 100644 modules/spacelift/bin/spacelift-configure delete mode 100644 modules/spacelift/bin/spacelift-git-use-https delete mode 100644 modules/spacelift/bin/spacelift-tf-workspace delete mode 100644 modules/spacelift/bin/spacelift-write-vars diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index 993d9e337..a18a0fe99 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -1,444 +1,371 @@ -# Component: `spacelift` +# Spacelift -This component is responsible for provisioning Spacelift stacks. +These components are responsible for setting up Spacelift and include three components: `spacelift/admin-stack`, `spacelift/spaces`, and `spacelift/worker-pool`. -Spacelift is a specialized, Terraform-compatible continuous integration and deployment (CI/CD) platform for -infrastructure-as-code. It's designed and implemented by long-time DevOps practitioners based on previous experience with -large-scale installations - dozens of teams, hundreds of engineers and tens of thousands of cloud resources. +Spacelift is a specialized, Terraform-compatible continuous integration and deployment (CI/CD) platform for infrastructure-as-code. It's designed and implemented by long-time DevOps practitioners based on previous experience with large-scale installations - dozens of teams, hundreds of engineers and tens of thousands of cloud resources. -## Usage +## Stack Configuration -**Stack Level**: Regional +Spacelift exists outside of the AWS ecosystem, so we define these components as unique to our standard stack organization. Spacelift Spaces are required before tenant-specific stacks are created in Spacelift, and the root administrator stack, referred to as `root-gbl-spacelift-admin-stack`, also does not belong to a specific tenant. Therefore, we define both outside of the standard `core` or `plat` stacks directories. That root administrator stack is responsible for creating the tenant-specific administrator stacks, `core-gbl-spacelift-admin-stack` and `plat-gbl-spacelift-admin-stack`. -This component provisions an administrative Spacelift stack and assigns it to a worker pool. Although -the stack can manage stacks in any region, it should be provisioned in the same region as the worker pool. +Our solution is to define a spacelift-specific configuration file per Spacelift Space. Typically our Spaces would be `root`, `core`, and `plat`, so we add three files: +```diff ++ stacks/orgs/NAMESPACE/spacelift.yaml ++ stacks/orgs/NAMESPACE/core/spacelift.yaml ++ stacks/orgs/NAMESPACE/plat/spacelift.yaml +``` + +### Global Configuration + +In order to apply common Spacelift configuration to all stacks, we need to set a few global Spacelift settings. The `pr-comment-triggered` label will be required to trigger stacks with GitHub comments but is not required otherwise. More on triggering Spacelift stacks to follow. + +Add the following to `stacks/orgs/NAMESPACE/_defaults.yaml`: +```yaml + settings: + spacelift: + workspace_enabled: true # enable spacelift by default + before_apply: + - spacelift-configure-paths + before_init: + - spacelift-configure-paths + - spacelift-write-vars + - spacelift-tf-workspace + before_plan: + - spacelift-configure-paths + labels: + - pr-comment-triggered +``` + +Furthermore, specify additional tenant-specific Space configuration for both `core` and `plat` tenants. + +For example, for `core` add the following to `stacks/orgs/NAMESPACE/core/_defaults.yaml`: +```yaml +terraform: + settings: + spacelift: + space_name: core +``` + +And for `plat` add the following to `stacks/orgs/NAMESPACE/plat/_defaults.yaml`: +```yaml +terraform: + settings: + spacelift: + space_name: plat +``` + + +### Spacelift `root` Space + +The `root` Space in Spacelift is responsible for deploying the root adminstrator stack, `admin-stack`, and the Spaces component, `spaces`. This Spaces component also includes Spacelift policies. Since the root adminstrator stack is unique to tenants, we modify the stack context to create a unique stack slug, `root-gbl-spacelift`. + +`stacks/orgs/NAMESPACE/spacelift.yaml`: ```yaml +import: + - mixins/region/global-region + - orgs/NAMESPACE/_defaults + - catalog/terraform/spacelift/admin-stack + - catalog/terraform/spacelift/spaces + +# These intentionally overwrite the default values +vars: + tenant: root + environment: gbl + stage: spacelift + components: terraform: - spacelift/defaults: - metadata: - type: abstract - component: spacelift - settings: - spacelift: - workspace_enabled: true - administrative: true - autodeploy: true - before_init: - - spacelift-configure - - spacelift-write-vars - - spacelift-tf-workspace - before_plan: - - spacelift-configure - before_apply: - - spacelift-configure - component_root: components/terraform/spacelift - description: Spacelift Administrative stack - stack_destructor_enabled: false - # TODO: replace with the name of the worker pool - worker_pool_name: WORKER_POOL_NAME - repository: infra - branch: main - labels: - - folder:admin - # Do not add normal set of child policies to admin stacks - policies_enabled: [] - policies_by_id_enabled: [] - vars: - enabled: true - spacelift_api_endpoint: https://TODO.app.spacelift.io - administrative_stack_drift_detection_enabled: true - administrative_stack_drift_detection_reconcile: true - administrative_stack_drift_detection_schedule: ["0 4 * * *"] - administrative_trigger_policy_enabled: false - autodeploy: false - aws_role_enabled: false - drift_detection_enabled: true - drift_detection_reconcile: true - drift_detection_schedule: ["0 4 * * *"] - external_execution: true - git_repository: infra # TODO: replace with your repository name - git_branch: main - - # List of available default Rego policies to create in Spacelift. - # These policies are defined in the catalog https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies - # These policies will not be attached to Spacelift stacks by default (but will be created in Spacelift, and could be attached to a stack manually). - # For specify policies to attach to each Spacelift stack, use `var.policies_enabled`. - policies_available: - - "git_push.proposed-run" - - "git_push.tracked-run" - - "plan.default" - - "trigger.dependencies" - - "trigger.retries" - - # List of default Rego policies to attach to all Spacelift stacks. - # These policies are defined in the catalog https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies - policies_enabled: - - "git_push.proposed-run" - - "git_push.tracked-run" - - "plan.default" - - "trigger.dependencies" - - # List of custom policy names to attach to all Spacelift stacks - # These policies must exist in `components/terraform/spacelift/rego-policies` - policies_by_name_enabled: [] - - runner_image: 000000000000.dkr.ecr.us-west-2.amazonaws.com/infra #TODO: replace with your ECR repository - spacelift_component_path: components/terraform - stack_config_path_template: stacks/%s.yaml - stack_destructor_enabled: false - worker_pool_name_id_map: - -spacelift-worker-pool: SOMEWORKERPOOLID #TODO: replace with your worker pool ID - infracost_enabled: false # TODO: decide on infracost - terraform_version: "1.3.6" - terraform_version_map: - "1": "1.3.6" - - # These could be moved to $PROJECT_ROOT/.spacelift/config.yml - before_init: - - spacelift-configure - - spacelift-write-vars - - spacelift-tf-workspace - before_plan: - - spacelift-configure - before_apply: - - spacelift-configure - - # Manages policies, admin stacks, and core OU accounts - spacelift: + # This admin stack creates other "admin" stacks + admin-stack: metadata: - component: spacelift + component: spacelift/admin-stack inherits: - - spacelift/defaults + - admin-stack/default settings: spacelift: - policies_by_id_enabled: - # This component also creates this policy so this is omitted prior to the first apply - # then added so it's consistent with all admin stacks. - - trigger-administrative-policy + root_administrative: true + labels: + - root-admin + - admin vars: enabled: true - # Use context_filters to split up admin stack management - # context_filters: - # stages: - # - artifacts - # - audit - # - auto - # - corp - # - dns - # - identity - # - marketplace - # - network - # - public - # - security - # These are the policies created from https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/master/catalog/policies - # Make sure to remove the .rego suffix - policies_available: - - git_push.proposed-run - - git_push.tracked-run - - plan.default - - trigger.dependencies - - trigger.retries - # This is to auto deploy launch template image id changes - - plan.warn-on-resource-changes-except-image-id - # This is the global admin policy - - trigger.administrative - # These are the policies added to each spacelift stack created by this admin stack - policies_enabled: - - git_push.proposed-run - - git_push.tracked-run - - plan.default - - trigger.dependencies - # Keep these empty - policies_by_id_enabled: [] + root_admin_stack: true # This stack will be created in the root space and will create all the other admin stacks as children. + context_filters: # context_filters determine which child stacks to manage with this admin stack + administrative: true # This stack is managing all the other admin stacks + root_administrative: false # We don't want this stack to also find itself in the config and add itself a second time + labels: + - admin + # attachments only on the root stack + root_stack_policy_attachments: + - TRIGGER Global administrator + # this creates policies for the children (admin) stacks + child_policy_attachments: + - TRIGGER Global administrator ``` -## Prerequisites +#### Deployment -### GitHub Integration +:::info -1. The GitHub owner will need to sign up for a [free trial of Spacelift](https://spacelift.io/free-trial.html) -1. Once an account is created take note of the URL - usually its `https://.app.spacelift.io/` -1. Create a Login Policy +The following steps assume that you've already authenticated with Spacelift locally. - - Click on Policies then Add Policy - - Use the following policy and replace `GITHUBORG` with the GitHub Organization slug and DEV with the GitHub id for the Dev setting up the Spacelift module. +::: - ```rego - package spacelift - - # See https://docs.spacelift.io/concepts/policy/login-policy for implementation details. - # Note: Login policies don't affect GitHub organization or SSO admins. - # Note 2: Enabling SSO requires that all users have an IdP (G Suite) account, so we'll just use - # GitHub authentication in the meantime while working with external collaborators. - # Map session input data to human friendly variables to use in policy evaluation +First deploy Spaces and policies with the `spaces` component: +```bash +atmos terraform apply spaces -s root-gbl-spacelift +``` - username := input.session.login - member_of := input.session.teams # Input is friendly name, e.g. "SRE" not "sre" or "@GITHUBORG/sre" - GITHUBORG := input.session.member # Is this user a member of the CUSTOMER GitHub org? +In the Spacelift UI, you should see each Space and each policy. - # Define GitHub usernames of non org external collaborators with admin vs. user access - admin_collaborators := { "DEV" } - user_collaborators := { "GITHUBORG" } # Using GITHUBORG as a placeholder to avoid empty set +Next, deploy the `root` `admin-stack` with the following: +```bash +atmos terraform apply admin-stack -s root-gbl-spacelift +``` - # Grant admin access to GITHUBORG org members in the CloudPosse group - admin { - GITHUBORG - member_of[_] == "CloudPosse" - } +Now in the Spacelift UI, you should see the administrator stacks created. Typically these should look similiar to the following: - # Grant admin access to non-GITHUBORG org accounts in the admin_collaborators set - admin { - # not GITHUBORG - admin_collaborators[username] - } +```diff ++ root-gbl-spacelift-admin-stack ++ root-gbl-spacelift-spaces ++ core-gbl-spacelift-admin-stack ++ plat-gbl-spacelift-admin-stack ++ core-ue1-auto-spacelift-worker-pool +``` - # Grant user access to GITHUBORG org members in the Developers group - # allow { - # GITHUBORG - # member_of[_] == "Developers" - # } +:::info - # Grant user access to non-GITHUBORG org accounts in the user_collaborators set - allow { - not GITHUBORG - user_collaborators[username] - } +The `spacelift/worker-pool` component is deployed to a specific tenant, stage, and region but is still deployed by the root administrator stack. Verify the administrator stack by checking the `managed-by:` label. - # Deny access to any non-GITHUBORG org accounts who aren't defined in external collaborators sets - deny { - not GITHUBORG - not user_collaborators[username] - not admin_collaborators[username] - } +::: - # Grant spaces read only user access to all members - space_read[space.id] { - space := input.spaces[_] - GITHUBORG - } +Finally, deploy the Spacelift Worker Pool (change the stack-slug to match your configuration): +```bash +atmos terraform apply spacelift/worker-pool -s core-ue1-auto +``` - # Grant spaces write access to GITHUBORG org members in the Developers group - # space_write[space.id] { - # space := input.spaces[_] - # member_of[_] == "Developers" - # } - ``` +### Spacelift Tenant-Specific Spaces -## Spacelift Layout +A tenant-specific Space in Spacelift, such as `core` or `plat`, includes the administrator stack for that specific Space and _all_ components in the given tenant. This administrator stack uses `var.context_filters` to select all components in the given tenant and create Spacelift stacks for each. Similar to the root adminstrator stack, we again create a unique stack slug for each tenant. For example `core-gbl-spacelift` or `plat-gbl-spacelift`. -[Runtime configuration](https://docs.spacelift.io/concepts/configuration/runtime-configuration) is a piece of setup -that is applied to individual runs instead of being global to the stack. -It's defined in `.spacelift/config.yml` YAML file at the root of your repository. -It is required for Spacelift to work with `atmos`. +For example, configure a `core` administrator stack with `stacks/orgs/NAMESPACE/core/spacelift.yaml`. -### Create Spacelift helper scripts +```yaml +import: + - mixins/region/global-region + - orgs/NAMESPACE/core/_defaults + - catalog/terraform/spacelift/admin-stack -[/rootfs/usr/local/bin/spacelift-tf-workspace](/rootfs/usr/local/bin/spacelift-tf-workspace) manages selecting or creating a Terraform workspace; similar to how `atmos` manages workspaces -during a Terraform run. +vars: + tenant: core + environment: gbl + stage: spacelift -[/rootfs/usr/local/bin/spacelift-write-vars](/rootfs/usr/local/bin/spacelift-write-vars) writes the component config using `atmos` to the `spacelift.auto.tfvars.json` file. +components: + terraform: + admin-stack: + metadata: + component: spacelift/admin-stack + inherits: + - admin-stack/default + settings: + spacelift: + labels: # Additional labels for this stack + - admin-stack-name:core + vars: + enabled: true + context_filters: + tenants: ["core"] + labels: # Additional labels added to all children + - admin-stack-name:core # will be used to automatically create the `managed-by:stack-name` label + child_policy_attachments: + - TRIGGER Dependencies +``` -**NOTE**: make sure they are all executable: +Deploy the `core` `admin-stack` with the following: +```bash +atmos terraform apply admin-stack -s core-gbl-spacelift +``` +Create the same for the `plat` tenant in `stacks/orgs/NAMESPACE/plat/spacelift.yaml`, update the tenant and configuration as necessary, and deploy with the following: ```bash -chmod +x rootfs/usr/local/bin/spacelift* +atmos terraform apply admin-stack -s plat-gbl-spacelift ``` -## Bootstrapping +Now all stacks for all components should be created in the Spacelift UI. -After creating & linking Spacelift to this repo (see the -[docs](https://docs.spacelift.io/integrations/github)), follow these steps... +## Triggering Spacelift Runs -### Deploy the [`spacelift-worker-pool`](../spacelift-worker-pool) Component +Cloud Posse recommends two options to trigger Spacelift stacks. -See [`spacelift-worker-pool` README](../spacelift-worker-pool/README.md) for the configuration and deployment needs. +### Triggering with Policy Attachments -### Update the `spacelift` catalog +Historically, all stacks were triggered with three `GIT_PUSH` policies: -1. `git_repository` = Name of `infrastructure` repository -1. `git_branch` = Name of main/master branch -1. `worker_pool_name_id_map` = Map of arbitrary names to IDs Spacelift worker pools, -taken from the `worker_pool_id` output of the `spacelift-worker-pool` component. -1. Set `components.terraform.spacelift.settings.spacelift.worker_pool_name` -to the name of the worker pool you want to use for the `spacelift` component, -the name being the key you set in the `worker_pool_name_id_map` map. + 1. [GIT_PUSH Global Administrator](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.administrative.rego) triggers admin stacks + 2. [GIT_PUSH Proposed Run](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.proposed-run.rego) triggers Proposed runs (typically Terraform Plan) for all non-admin stacks on Pull Requests + 3. [GIT_PUSH Tracked Run](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.tracked-run.rego) triggers Tracked runs (typically Terraform Apply) for all non-admin stacks on merges into `main` +Attach these policies to stacks and Spacelift will trigger them on the respective git push. -### Deploy the admin stacks -Set these ENV vars: +### Triggering with GitHub Comments (Preferred) -```bash -export SPACELIFT_API_KEY_ENDPOINT=https://.app.spacelift.io -export SPACELIFT_API_KEY_ID=... -export SPACELIFT_API_KEY_SECRET=... -``` +Atmos support for `atmos describe affected` made it possible to greatly improve Spacelift's triggering workflow. Now we can add a GitHub Action to collect all affected components for a given Pull Request and add a GitHub comment to the given PR with a formatted list of the affected stacks. Then Spacelift can watch for a GitHub comment event and then trigger stacks based on that comment. -The name of the spacelift stack resource will be different depending on the name of the component and the root atmos stack. -This would be the command if the root atmos stack is `core-gbl-auto` and the spacelift component is `spacelift`. +In order to set up GitHub Comment triggers, first add the following `GIT_PUSH Plan Affected` policy to the `spaces` component. +For example, `stacks/catalog/spacelift/spaces.yaml` +```yaml +components: + terraform: + spaces: + metadata: + component: spacelift/spaces + settings: + spacelift: + administrative: true + space_name: root + vars: + spaces: + root: + policies: +... + # This policy will automatically assign itself to stacks and is used to trigger stacks directly from the `cloudposse/github-action-atmos-affected-trigger-spacelift` GitHub action + # This is only used if said GitHub action is set to trigger on "comments" + "GIT_PUSH Plan Affected": + type: GIT_PUSH + labels: + - autoattach:pr-comment-triggered + body: | + package spacelift + + # This policy runs whenever a comment is added to a pull request. It looks for the comment body to contain either: + # /spacelift preview input.stack.id + # /spacelift deploy input.stack.id + # + # If the comment matches those patterns it will queue a tracked run (deploy) or a proposed run (preview). In the case of + # a proposed run, it will also cancel all of the other pending runs for the same branch. + # + # This is being used on conjunction with the GitHub actions `atmos-trigger-spacelift-feature-branch.yaml` and + # `atmos-trigger-spacelift-main-branch.yaml` in .github/workflows to automatically trigger a preview or deploy run based + # on the `atmos describe affected` output. + + track { + commented + contains(input.pull_request.comment, concat(" ", ["/spacelift", "deploy", input.stack.id])) + } + + propose { + commented + contains(input.pull_request.comment, concat(" ", ["/spacelift", "preview", input.stack.id])) + } + + # Ignore if the event is not a comment + ignore { + not commented + } + + # Ignore if the PR has a `spacelift-no-trigger` label + ignore { + input.pull_request.labels[_] = "spacelift-no-trigger" + } + + # Ignore if the PR is a draft and deesnt have a `spacelift-trigger` label + ignore { + input.pull_request.draft + not has_spacelift_trigger_label + } + + has_spacelift_trigger_label { + input.pull_request.labels[_] == "spacelift-trigger" + } + + commented { + input.pull_request.action == "commented" + } + + cancel[run.id] { + run := input.in_progress[_] + run.type == "PROPOSED" + run.state == "QUEUED" + run.branch == input.pull_request.head.branch + } + + # This is a random sample of 10% of the runs + sample { + millis := round(input.request.timestamp_ns / 1e6) + millis % 100 <= 10 + } ``` -atmos terraform apply spacelift --stack core-gbl-auto -target 'module.spacelift.module.stacks["core-gbl-auto-spacelift"]' -``` - -Note that this is the only manually operation you need to perform in `geodesic` using `atmos` to create the initial admin stack. -All other infrastructure stacks wil be created in Spacelift by this admin stack. - - -## Pull Request Workflow - -1. Create a new branch & make changes -2. Create a new pull request (targeting the `main` branch) -3. View the modified resources directly in the pull request -4. View the successful Spacelift checks in the pull request -5. Merge the pull request and check the Spacelift job +This policy will automatically attach itself to _all_ components that have the `pr-comment-triggered` label, already defined in `stacks/orgs/NAMESPACE/_defaults.yaml` under `settings.spacelift.labels`. -## spacectl +Next, create two new GitHub Action workflows: -See docs https://github.com/spaceone-dev/spacectl +```diff ++ .github/workflows/atmos-trigger-spacelift-feature-branch.yaml ++ .github/workflows/atmos-trigger-spacelift-main-branch.yaml +``` -### Install +The feature branch workflow will create a comment event in Spacelift to run a Proposed run for a given stack. Whereas the main branch workflow will create a comment event in Spacelift to run a Deploy run for those same stacks. -``` -⨠ apt install -y spacectl -qq +#### Feature Branch +```yaml +name: "Plan Affected Spacelift Stacks" + +on: + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - main + +jobs: + context: + runs-on: ["self-hosted"] + steps: + - name: Atmos Affected Stacks Trigger Spacelift + uses: cloudposse/github-action-atmos-affected-trigger-spacelift@v1 + with: + atmos-config-path: ./rootfs/usr/local/etc/atmos + github-token: ${{ secrets.GITHUB_TOKEN }} ``` -Setup a profile - +This will add a GitHub comment such as: ``` -⨠ spacectl profile login gbl-identity -Enter Spacelift endpoint (eg. https://unicorn.app.spacelift.io/): https://.app.spacelift.io -Select credentials type: 1 for API key, 2 for GitHub access token: 1 -Enter API key ID: 01FKN... -Enter API key secret: +/spacelift preview plat-ue1-sandbox-foobar ``` -### Listing stacks - -```bash -spacectl stack list +#### Main Branch +```yaml +name: "Deploy Affected Spacelift Stacks" + +on: + pull_request: + types: [closed] + branches: + - main + +jobs: + run: + if: github.event.pull_request.merged == true + runs-on: ["self-hosted"] + steps: + - name: Atmos Affected Stacks Trigger Spacelift + uses: cloudposse/github-action-atmos-affected-trigger-spacelift@v1 + with: + atmos-config-path: ./rootfs/usr/local/etc/atmos + deploy: true + github-token: ${{ secrets.GITHUB_TOKEN }} + head-ref: ${{ github.sha }}~1 ``` -Grab all the stack ids (use the JSON output to avoid bad chars) - -```bash -spacectl stack list --output json | jq -r '.[].id' > stacks.txt +This will add a GitHub comment such as: ``` - -If the latest commit for each stack is desired, run something like this. - -NOTE: remove the `echo` to remove the dry-run functionality - -```bash -cat stacks.txt | while read stack; do echo $stack && echo spacectl stack set-current-commit --sha 25dd359749cfe30c76cce19f58e0a33555256afd --id $stack; done +/spacelift deploy plat-ue1-sandbox-foobar ``` - - - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.3 | -| [aws](#requirement\_aws) | >= 4.0 | -| [spacelift](#requirement\_spacelift) | >= 0.1.31 | - -## Providers - -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | >= 4.0 | - -## Modules - -| Name | Source | Version | -|------|--------|---------| -| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [spacelift](#module\_spacelift) | cloudposse/cloud-infrastructure-automation/spacelift | 0.55.0 | -| [this](#module\_this) | cloudposse/label/null | 0.25.0 | - -## Resources - -| Name | Type | -|------|------| -| [aws_ssm_parameter.spacelift_key_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [aws_ssm_parameter.spacelift_key_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [administrative\_push\_policy\_enabled](#input\_administrative\_push\_policy\_enabled) | Flag to enable/disable the global administrative push policy | `bool` | `true` | no | -| [administrative\_stack\_drift\_detection\_enabled](#input\_administrative\_stack\_drift\_detection\_enabled) | Flag to enable/disable administrative stack drift detection | `bool` | `true` | no | -| [administrative\_stack\_drift\_detection\_reconcile](#input\_administrative\_stack\_drift\_detection\_reconcile) | Flag to enable/disable administrative stack drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | -| [administrative\_stack\_drift\_detection\_schedule](#input\_administrative\_stack\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the administrative stack | `list(string)` |
[
"0 4 * * *"
]
| no | -| [administrative\_trigger\_policy\_enabled](#input\_administrative\_trigger\_policy\_enabled) | Flag to enable/disable the global administrative trigger policy | `bool` | `true` | no | -| [attachment\_space\_id](#input\_attachment\_space\_id) | Specify the space ID for attachments (e.g. policies, contexts, etc.) | `string` | `"legacy"` | no | -| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [autodeploy](#input\_autodeploy) | Default autodeploy value for all stacks created by this project | `bool` | n/a | yes | -| [aws\_role\_arn](#input\_aws\_role\_arn) | ARN of the AWS IAM role to assume and put its temporary credentials in the runtime environment | `string` | `null` | no | -| [aws\_role\_enabled](#input\_aws\_role\_enabled) | Flag to enable/disable Spacelift to use AWS STS to assume the supplied IAM role and put its temporary credentials in the runtime environment | `bool` | `false` | no | -| [aws\_role\_external\_id](#input\_aws\_role\_external\_id) | Custom external ID (works only for private workers). See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html for more details | `string` | `null` | no | -| [aws\_role\_generate\_credentials\_in\_worker](#input\_aws\_role\_generate\_credentials\_in\_worker) | Flag to enable/disable generating AWS credentials in the private worker after assuming the supplied IAM role | `bool` | `false` | no | -| [before\_init](#input\_before\_init) | List of before-init scripts | `list(string)` | `[]` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [context\_filters](#input\_context\_filters) | Context filters to create stacks for specific context information. Valid lists are `namespaces`, `environments`, `tenants`, `stages`. | `map(list(string))` | `{}` | no | -| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [drift\_detection\_enabled](#input\_drift\_detection\_enabled) | Flag to enable/disable drift detection on the infrastructure stacks | `bool` | `true` | no | -| [drift\_detection\_reconcile](#input\_drift\_detection\_reconcile) | Flag to enable/disable infrastructure stacks drift automatic reconciliation. If drift is detected and `reconcile` is turned on, Spacelift will create a tracked run to correct the drift | `bool` | `true` | no | -| [drift\_detection\_schedule](#input\_drift\_detection\_schedule) | List of cron expressions to schedule drift detection for the infrastructure stacks | `list(string)` |
[
"0 4 * * *"
]
| no | -| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [external\_execution](#input\_external\_execution) | Set this to true if you're calling this module from outside of a Spacelift stack (e.g. the `complete` example) | `bool` | `false` | no | -| [git\_branch](#input\_git\_branch) | The Git branch name | `string` | `"main"` | no | -| [git\_commit\_sha](#input\_git\_commit\_sha) | The commit SHA for which to trigger a run. Requires `var.spacelift_run_enabled` to be set to `true` | `string` | `null` | no | -| [git\_repository](#input\_git\_repository) | The Git repository name | `string` | n/a | yes | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [infracost\_enabled](#input\_infracost\_enabled) | Flag to enable/disable infracost. If this is enabled, it will add infracost label to each stack. See [spacelift infracost](https://docs.spacelift.io/vendors/terraform/infracost) docs for more details. | `bool` | `false` | no | -| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | -| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | -| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [policies\_available](#input\_policies\_available) | List of available default policies to create in Spacelift (these policies will not be attached to Spacelift stacks by default, use `var.policies_enabled`) | `list(string)` |
[
"git_push.proposed-run",
"git_push.tracked-run",
"plan.default",
"trigger.dependencies",
"trigger.retries"
]
| no | -| [policies\_by\_id\_enabled](#input\_policies\_by\_id\_enabled) | List of existing policy IDs to attach to all Spacelift stacks. These policies must already exist in Spacelift | `list(string)` | `[]` | no | -| [policies\_by\_name\_enabled](#input\_policies\_by\_name\_enabled) | List of existing policy names to attach to all Spacelift stacks. These policies must exist at `modules/spacelift/rego-policies` OR `var.policies_by_name_path`. | `list(string)` | `[]` | no | -| [policies\_by\_name\_path](#input\_policies\_by\_name\_path) | Path to the catalog of external Rego policies. The Rego files must exist in the caller's code at the path. The module will create Spacelift policies from the external Rego definitions | `string` | `""` | no | -| [policies\_enabled](#input\_policies\_enabled) | DEPRECATED: Use `policies_by_id_enabled` instead. List of default policies created by this stack to attach to all Spacelift stacks | `list(string)` | `[]` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [runner\_image](#input\_runner\_image) | Full address & tag of the Spacelift runner image (e.g. on ECR) | `string` | n/a | yes | -| [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | -| [spacelift\_component\_path](#input\_spacelift\_component\_path) | The Spacelift Component Path | `string` | `"components/terraform"` | no | -| [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | -| [spacelift\_stack\_dependency\_enabled](#input\_spacelift\_stack\_dependency\_enabled) | If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached | `bool` | `false` | no | -| [stack\_config\_path\_template](#input\_stack\_config\_path\_template) | Stack config path template | `string` | `"stacks/%s.yaml"` | no | -| [stack\_destructor\_enabled](#input\_stack\_destructor\_enabled) | Flag to enable/disable the stack destructor to destroy the resources of a stack before deleting the stack itself | `bool` | `false` | no | -| [stacks\_space\_id](#input\_stacks\_space\_id) | Override the space ID for all stacks (unless the stack config has `dedicated_space` set to true). Otherwise, it will default to the admin stack's space. | `string` | `null` | no | -| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tag\_filters](#input\_tag\_filters) | A map of tags that will filter stack creation by the matching `tags` set in a component `vars` configuration. | `map(string)` | `{}` | no | -| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [terraform\_version](#input\_terraform\_version) | Default Terraform version for all stacks created by this project | `string` | n/a | yes | -| [terraform\_version\_map](#input\_terraform\_version\_map) | A map to determine which Terraform patch version to use for each minor version | `map(string)` | `{}` | no | -| [worker\_pool\_name\_id\_map](#input\_worker\_pool\_name\_id\_map) | Map of worker pool names to worker pool IDs | `map(any)` | `{}` | no | - -## Outputs - -| Name | Description | -|------|-------------| -| [stacks](#output\_stacks) | Spacelift stacks | - - -## References - -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spacelift) - Cloud Posse's upstream component - -[](https://cpco.io/component) diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index 46c43a781..5efb16bb5 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -9,23 +9,17 @@ The component uses a series of `context_fiters` to select atmos component instan **Stack Level**: Global -The following are example snippets of how to use this component: +The following are example snippets of how to use this component. For more on Spacelift admin stack usage, see the [Spacelift README](https://docs.cloudposse.com/components/library/aws/spacelift/) +First define the default configuration for any admin stack: ```yaml -# stacks/orgs/acme/spacelift.yaml -import: - - mixins/region/global-region - - orgs/acme/_defaults - -vars: - tenant: infra - environment: gbl - stage: root - +# stacks/catalog/spacelift/admin-stack.yaml components: terraform: - spacelift/admin-stack: + admin-stack/default: metadata: + type: abstract + component: spacelift/admin-stack settings: spacelift: administrative: true @@ -42,39 +36,101 @@ components: drift_detection_reconcile: true drift_detection_schedule: - 0 4 * * * - labels: - - admin - - folder:admin manage_state: false + policies: {} + vars: + # Organization specific configuration + branch: main + repository: infrastructure + worker_pool_name: "acme-core-ue1-auto-spacelift-worker-pool" + runner_image: 111111111111.dkr.ecr.us-east-1.amazonaws.com/infrastructure:latest + spacelift_spaces_stage_name: "root" + # These values need to be manually updated as external configuration changes + # This should match the version set in the Dockerfile and be updated when the version changes. + terraform_version: "1.3.6" + # Common configuration + administrative: true # Whether this stack can manage other stacks + component_root: components/terraform +``` + +Then define the root-admin stack: +```yaml +# stacks/orgs/acme/spacelift.yaml +import: + - mixins/region/global-region + - orgs/acme/_defaults + - catalog/terraform/spacelift/admin-stack + - catalog/terraform/spacelift/spaces + +# These intentionally overwrite the default values +vars: + tenant: root + environment: gbl + stage: spacelift + +components: + terraform: + # This admin stack creates other "admin" stacks + admin-stack: + metadata: + component: spacelift/admin-stack + inherits: + - admin-stack/default + settings: + spacelift: root_administrative: true labels: - root-admin - admin - policies: {} vars: - administrative: true - branch: main - child_policy_attachments: - - GIT_PUSH Global Administrator - - TRIGGER Global Administrator - context_filters: - administrative: true # This stack is managing all the other admin stacks - root_administrative: false # We don't want this stack to also find itself in the config and add itself a second time - component_root: components/terraform enabled: true + root_admin_stack: true # This stack will be created in the root space and will create all the other admin stacks as children. + context_filters: # context_filters determine which child stacks to manage with this admin stack + administrative: true # This stack is managing all the other admin stacks + root_administrative: false # We don't want this stack to also find itself in the config and add itself a second time labels: - - admin-stack-name:root - repository: infra-live - root_admin_stack: true + - admin + # attachments only on the root stack root_stack_policy_attachments: - - GIT_PUSH Global Administrator - - TRIGGER Global Administrator - runner_image: 000000000000.dkr.ecr.us-east-2.amazonaws.com/acme/infra-live:latest - spacelift_spaces_environment_name: gbl - spacelift_spaces_stage_name: root - spacelift_spaces_tenant_name: infra - terraform_version: "1.3.9" - worker_pool_name: acme-core-ue2-auto-spacelift-default-worker-pool + - TRIGGER Global administrator + # this creates policies for the children (admin) stacks + child_policy_attachments: + - TRIGGER Global administrator + +``` + +Finally define any tenant-specific stacks: +```yaml +# stacks/orgs/acme/core/spacelift.yaml +import: + - mixins/region/global-region + - orgs/acme/core/_defaults + - catalog/terraform/spacelift/admin-stack + +vars: + tenant: core + environment: gbl + stage: spacelift + +components: + terraform: + admin-stack: + metadata: + component: spacelift/admin-stack + inherits: + - admin-stack/default + settings: + spacelift: + labels: # Additional labels for this stack + - admin-stack-name:core + vars: + enabled: true + context_filters: + tenants: ["core"] + labels: # Additional labels added to all children + - admin-stack-name:core # will be used to automatically create the `managed-by:stack-name` label + child_policy_attachments: + - TRIGGER Dependencies ``` @@ -84,13 +140,14 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.3 | | [aws](#requirement\_aws) | >= 4.0 | +| [null](#requirement\_null) | >= 3.0 | | [spacelift](#requirement\_spacelift) | >= 0.1.31 | ## Providers | Name | Version | |------|---------| -| [null](#provider\_null) | n/a | +| [null](#provider\_null) | >= 3.0 | | [spacelift](#provider\_spacelift) | >= 0.1.31 | ## Modules @@ -202,7 +259,7 @@ components: | Name | Description | |------|-------------| -| [child\_stacks](#output\_child\_stacks) | n/a | -| [root\_stack](#output\_root\_stack) | n/a | +| [child\_stacks](#output\_child\_stacks) | All children stacks managed by this component | +| [root\_stack](#output\_root\_stack) | The root stack, if enabled and created by this component | | [root\_stack\_id](#output\_root\_stack\_id) | The stack id | diff --git a/modules/spacelift/admin-stack/outputs.tf b/modules/spacelift/admin-stack/outputs.tf index 1c5425ba8..16b4a09d7 100644 --- a/modules/spacelift/admin-stack/outputs.tf +++ b/modules/spacelift/admin-stack/outputs.tf @@ -1,14 +1,16 @@ output "root_stack_id" { description = "The stack id" - value = local.enabled && local.create_root_admin_stack ? module.root_admin_stack.id : null + value = local.enabled && local.create_root_admin_stack ? module.root_admin_stack.id : "" } output "root_stack" { - value = local.enabled && local.create_root_admin_stack ? module.root_admin_stack : null - sensitive = true + description = "The root stack, if enabled and created by this component" + value = local.enabled && local.create_root_admin_stack ? module.root_admin_stack : "" + sensitive = true } output "child_stacks" { - value = local.enabled ? values(module.child_stack)[*] : null - sensitive = true + description = "All children stacks managed by this component" + value = local.enabled ? values(module.child_stack)[*] : [] + sensitive = true } diff --git a/modules/spacelift/admin-stack/versions.tf b/modules/spacelift/admin-stack/versions.tf index 1174cd191..89c50bdbe 100644 --- a/modules/spacelift/admin-stack/versions.tf +++ b/modules/spacelift/admin-stack/versions.tf @@ -10,5 +10,9 @@ terraform { source = "hashicorp/aws" version = ">= 4.0" } + null = { + source = "hashicorp/null" + version = ">= 3.0" + } } } diff --git a/modules/spacelift/bin/spacelift-configure b/modules/spacelift/bin/spacelift-configure deleted file mode 100644 index 43a3bade2..000000000 --- a/modules/spacelift/bin/spacelift-configure +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -set -ex - -# Link the default terraform binary to Spacelift's Terraform installation path of `/bin/terraform`. -# Because the Terraform commands are executed as just `terraform` by `atmos` (unless otherwise specified) -# and also in scripts, and the default PATH has `/usr/bin` before `/bin`, -# plain 'terraform' would otherwise resolve to the Docker container's -# chosen version of Terraform, not Spacelift's configured version. - -ln -sfTv /bin/terraform /usr/bin/terraform -echo "Using Terraform: " -which terraform -terraform version - -# Remove -x for security and cleaner output -set +x - -# Log the AWS authentication settings -identity=$(unset AWS_PROFILE && aws sts get-caller-identity --query Arn --output text) - -printf "\nIAM Role without profile is %s\n\n" "$identity" - -# If you want to have dynamic AWS config file or profile selection, do it here. -# For example: -# if (printf "%s" "$identity" | grep -q -- -prod-); then -# printf "Detected production\n\n" -# ln -sfTv /etc/aws-config/aws-config-spacelift-production /etc/aws-config/aws-config-spacelift -# else -# printf "Configuring for non-production environment\n\n" -# ln -sfTv /etc/aws-config/aws-config-spacelift-non-production /etc/aws-config/aws-config-spacelift -# fi - -printf "\nAWS_CONFIG_FILE set to %s\n" "$AWS_CONFIG_FILE" -printf "AWS_PROFILE set to %s\n\n" "$AWS_PROFILE" - -echo "+ crudini --get --format=ini $AWS_CONFIG_FILE \"profile $AWS_PROFILE\"" -crudini --get --format=ini $AWS_CONFIG_FILE "profile $AWS_PROFILE" - -effective_arn="$(aws sts get-caller-identity --query Arn --output text)" - -printf "\nEffective AWS Role Arn is %s\n\n\n" "$effective_arn" diff --git a/modules/spacelift/bin/spacelift-git-use-https b/modules/spacelift/bin/spacelift-git-use-https deleted file mode 100644 index 809ee358b..000000000 --- a/modules/spacelift/bin/spacelift-git-use-https +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -set -ex - -# Spacelift can use a PAT via a .netrc file, in which case any -# git@github.com: urls need to be converted to HTTPS urls or Spacelift will fail. -# This allows us to use SSH paths throughout the codebase so local plans work -# while maintaining compatibility with Spacelift. - -# The URL "git@github.com:" is used by `git` (e.g. `git clone`) -git config --global url."https://github.com/".insteadOf "git@github.com:" -# The URL "ssh://git@github.com/" is used by Terraform (e.g. `terraform init --from-module=...`) -# NOTE: we use `--add` to append the second URL to the config file -git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" --add diff --git a/modules/spacelift/bin/spacelift-tf-workspace b/modules/spacelift/bin/spacelift-tf-workspace deleted file mode 100644 index 70fef3f64..000000000 --- a/modules/spacelift/bin/spacelift-tf-workspace +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# This goes before set -ex because we do not need to echo the commands -# and we do not really care if they fail, they are just informational. -backend_profile=$(jq -r '.terraform.backend.s3.profile' backend.tf.json) - -if [[ -n $backend_profile ]] && [[ $backend_profile != null ]]; then - printf "\nBackend configured to use profile %s\n" "$backend_profile" - printf "Which maps to Role ARN %s\n\n" $(crudini --get --format=sh $AWS_CONFIG_FILE "profile $backend_profile" role_arn | cut -f2 -d=) -fi - -# Add -x for troubleshooting -set -ex -o pipefail - -terraform init -reconfigure - -printf "\n\nSelecting Terraform workspace...\n" - -# We have explicitly set up `backend.tf.json` in the same step where we set up the varfile, so to avoid surprises, do not regenerate it now -atmos terraform workspace "$ATMOS_COMPONENT" --stack="$ATMOS_STACK" --auto-generate-backend-file=false || { - printf "%s\n" "$?" - set +x - printf "\n\nUnable to select workspace\n" - echo "+ crudini --get --format=ini $AWS_CONFIG_FILE \"profile $AWS_PROFILE\"" - crudini --get --format=ini $AWS_CONFIG_FILE "profile $AWS_PROFILE" - printf "\n\n" - false -} - -# Remove -x for security -set +x diff --git a/modules/spacelift/bin/spacelift-write-vars b/modules/spacelift/bin/spacelift-write-vars deleted file mode 100644 index 775983044..000000000 --- a/modules/spacelift/bin/spacelift-write-vars +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -# Add -x for troubleshooting -set -ex -o pipefail - -function main() { - if [[ -z $ATMOS_STACK ]] || [[ -z $ATMOS_COMPONENT ]]; then - echo "Missing required environment variable" >&2 - echo " ATMOS_STACK=$ATMOS_STACK" >&2 - echo " ATMOS_COMPONENT=$ATMOS_COMPONENT" >&2 - return 3 - fi - - echo "Writing Stack variables to spacelift.auto.tfvars.json for Spacelift..." - - atmos terraform generate varfile "$ATMOS_COMPONENT" --stack="$ATMOS_STACK" -f spacelift.auto.tfvars.json >/dev/null - jq . [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [spaces](#input\_spaces) | n/a |
map(object({
parent_space_id = string,
description = optional(string),
inherit_entities = optional(bool, false),
labels = optional(set(string), []),
policies = optional(map(object({
body = optional(string),
body_url = optional(string),
body_url_version = optional(string, "master"),
type = optional(string),
labels = optional(set(string), []),
})), {}),
}))
| n/a | yes | +| [spaces](#input\_spaces) | A map of all Spaces to create in Spacelift |
map(object({
parent_space_id = string,
description = optional(string),
inherit_entities = optional(bool, false),
labels = optional(set(string), []),
policies = optional(map(object({
body = optional(string),
body_url = optional(string),
body_url_version = optional(string, "master"),
type = optional(string),
labels = optional(set(string), []),
})), {}),
}))
| n/a | yes | | [ssm\_params\_enabled](#input\_ssm\_params\_enabled) | Whether to write the IDs of the created spaces to SSM parameters | `bool` | `true` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | @@ -125,6 +118,6 @@ No resources. | Name | Description | |------|-------------| -| [policies](#output\_policies) | n/a | -| [spaces](#output\_spaces) | n/a | +| [policies](#output\_policies) | The policies created by this component | +| [spaces](#output\_spaces) | The spaces created by this component | diff --git a/modules/spacelift/spaces/main.tf b/modules/spacelift/spaces/main.tf index c26e0e50d..e4885f318 100644 --- a/modules/spacelift/spaces/main.tf +++ b/modules/spacelift/spaces/main.tf @@ -24,7 +24,7 @@ locals { body = p.body body_url = p.body_url body_url_version = p.body_url_version - labels = toset(v.labels) + labels = setunion(toset(v.labels), toset(p.labels)) name = pn space_id = k == "root" ? "root" : module.space[k].space_id type = p.type diff --git a/modules/spacelift/spaces/outputs.tf b/modules/spacelift/spaces/outputs.tf index 196858586..eca2e02c7 100644 --- a/modules/spacelift/spaces/outputs.tf +++ b/modules/spacelift/spaces/outputs.tf @@ -1,7 +1,9 @@ output "spaces" { - value = local.enabled ? local.spaces : {} + description = "The spaces created by this component" + value = local.enabled ? local.spaces : {} } output "policies" { - value = local.enabled ? local.policies : {} + description = "The policies created by this component" + value = local.enabled ? local.policies : {} } diff --git a/modules/spacelift/spaces/variables.tf b/modules/spacelift/spaces/variables.tf index d4f90a898..4db9772f9 100644 --- a/modules/spacelift/spaces/variables.tf +++ b/modules/spacelift/spaces/variables.tf @@ -12,6 +12,7 @@ variable "spaces" { labels = optional(set(string), []), })), {}), })) + description = "A map of all Spaces to create in Spacelift" } variable "ssm_params_enabled" { diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index 3f6a5b208..61a5517cd 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -25,19 +25,52 @@ aws ssm start-session --target Here's an example snippet for how to use this component. ```yaml +# stacks/catalog/spacelift/worker-pool.yaml components: terraform: - spacelift-worker-pool: + spacelift/worker-pool: settings: spacelift: - workspace_enabled: true + administrative: true + space_name: root vars: enabled: true - name: "spacelift-worker-pool" - ec2_instance_type: m6i.large - ecr_account_name: corp - ecr_repo_name: infrastructure spacelift_api_endpoint: https://.app.spacelift.io + spacelift_spaces_tenant_name: "acme" + spacelift_spaces_environment_name: "gbl" + spacelift_spaces_stage_name: "root" + account_map_tenant_name: core + ecr_environment_name: ue1 + ecr_repo_name: infrastructure + ecr_stage_name: artifacts + ecr_tenant_name: core + # Set a low scaling threshold to ensure new workers are launched as soon as the current one(s) are busy + cpu_utilization_high_threshold_percent: 10 + cpu_utilization_low_threshold_percent: 5 + default_cooldown: 300 + desired_capacity: null + health_check_grace_period: 300 + health_check_type: EC2 + infracost_enabled: true + instance_type: m6i.large + max_size: 3 + min_size: 1 + name: spacelift-worker-pool + scale_down_cooldown_seconds: 2700 + spacelift_agents_per_node: 3 + wait_for_capacity_timeout: 5m + block_device_mappings: + - device_name: "/dev/xvda" + no_device: null + virtual_name: null + ebs: + delete_on_termination: null + encrypted: false + iops: null + kms_key_id: null + snapshot_id: null + volume_size: 100 + volume_type: "gp2" ``` ## Configuration @@ -99,14 +132,15 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.30.1 | -| [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.34.2 | +| [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_label](#module\_iam\_label) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.0.0-rc1 | +| [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | ## Resources @@ -177,12 +211,17 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [scale\_down\_cooldown\_seconds](#input\_scale\_down\_cooldown\_seconds) | The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start | `number` | `300` | no | +| [space\_name](#input\_space\_name) | The name of the Space to create the worker pool in | `string` | `"root"` | no | | [spacelift\_agents\_per\_node](#input\_spacelift\_agents\_per\_node) | Number of Spacelift agents to run on one worker node | `number` | `1` | no | | [spacelift\_ami\_id](#input\_spacelift\_ami\_id) | AMI ID of Spacelift worker pool image | `string` | `null` | no | | [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | | [spacelift\_aws\_account\_id](#input\_spacelift\_aws\_account\_id) | AWS Account ID owned by Spacelift | `string` | `"643313122712"` | no | | [spacelift\_domain\_name](#input\_spacelift\_domain\_name) | Top-level domain name to use for pulling the launcher binary | `string` | `"spacelift.io"` | no | | [spacelift\_runner\_image](#input\_spacelift\_runner\_image) | URL of ECR image to use for Spacelift | `string` | `""` | no | +| [spacelift\_spaces\_component\_name](#input\_spacelift\_spaces\_component\_name) | The name of the spacelift spaces component | `string` | `"spacelift/spaces"` | no | +| [spacelift\_spaces\_environment\_name](#input\_spacelift\_spaces\_environment\_name) | The environment name of the spacelift spaces component | `string` | `null` | no | +| [spacelift\_spaces\_stage\_name](#input\_spacelift\_spaces\_stage\_name) | The stage name of the spacelift spaces component | `string` | `null` | no | +| [spacelift\_spaces\_tenant\_name](#input\_spacelift\_spaces\_tenant\_name) | The tenant name of the spacelift spaces component | `string` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/spacelift/worker-pool/main.tf b/modules/spacelift/worker-pool/main.tf index 5138fa74f..d523799ea 100644 --- a/modules/spacelift/worker-pool/main.tf +++ b/modules/spacelift/worker-pool/main.tf @@ -26,6 +26,8 @@ locals { ) })} END + +space_id = lookup(module.spaces.outputs, var.space_name, "root") } resource "spacelift_worker_pool" "primary" { @@ -33,6 +35,8 @@ resource "spacelift_worker_pool" "primary" { name = module.this.id description = "Deployed to ${var.region} within '${join("-", compact([module.this.tenant, module.this.stage]))}' AWS account" + + space_id = local.space_id } data "cloudinit_config" "config" { @@ -86,7 +90,7 @@ module "security_group" { module "autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" - version = "0.30.1" + version = "0.34.2" image_id = var.spacelift_ami_id == null ? join("", data.aws_ami.spacelift.*.image_id) : var.spacelift_ami_id instance_type = var.instance_type diff --git a/modules/spacelift/worker-pool/remote-state.tf b/modules/spacelift/worker-pool/remote-state.tf index 5d78adb0d..f11ff88f7 100644 --- a/modules/spacelift/worker-pool/remote-state.tf +++ b/modules/spacelift/worker-pool/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = "account-map" environment = coalesce(var.account_map_environment_name, module.this.environment) @@ -12,7 +12,7 @@ module "account_map" { module "ecr" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = "ecr" environment = coalesce(var.ecr_environment_name, module.this.environment) @@ -24,9 +24,21 @@ module "ecr" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = "vpc" context = module.this.context } + +module "spaces" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + component = var.spacelift_spaces_component_name + environment = try(var.spacelift_spaces_environment_name, module.this.environment) + stage = try(var.spacelift_spaces_stage_name, module.this.stage) + tenant = try(var.spacelift_spaces_tenant_name, module.this.tenant) + + context = module.this.context +} diff --git a/modules/spacelift/worker-pool/variables.tf b/modules/spacelift/worker-pool/variables.tf index 385b759ab..d0d22c8ed 100644 --- a/modules/spacelift/worker-pool/variables.tf +++ b/modules/spacelift/worker-pool/variables.tf @@ -290,3 +290,33 @@ variable "spacelift_aws_account_id" { description = "AWS Account ID owned by Spacelift" default = "643313122712" } + +variable "space_name" { + type = string + description = "The name of the Space to create the worker pool in" + default = "root" +} + +variable "spacelift_spaces_component_name" { + type = string + description = "The name of the spacelift spaces component" + default = "spacelift/spaces" +} + +variable "spacelift_spaces_environment_name" { + type = string + description = "The environment name of the spacelift spaces component" + default = null +} + +variable "spacelift_spaces_stage_name" { + type = string + description = "The stage name of the spacelift spaces component" + default = null +} + +variable "spacelift_spaces_tenant_name" { + type = string + description = "The tenant name of the spacelift spaces component" + default = null +} From e1ca3f2fc9e72df87fed1f81cdd6a359a00984be Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 11 Jul 2023 12:35:49 -0700 Subject: [PATCH 174/501] Bump `spaces` module versions (#754) --- modules/spacelift/spaces/README.md | 4 ++-- modules/spacelift/spaces/main.tf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index 12ab39505..670a07e9b 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -81,8 +81,8 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.0.0 | -| [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.0.0 | +| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.1.0 | +| [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.1.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/spacelift/spaces/main.tf b/modules/spacelift/spaces/main.tf index e4885f318..bc9f433dd 100644 --- a/modules/spacelift/spaces/main.tf +++ b/modules/spacelift/spaces/main.tf @@ -36,7 +36,7 @@ locals { module "space" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space" - version = "1.0.0" + version = "1.1.0" # Create a space for each entry in the `spaces` variable, except for the root space which already exists by default # and cannot be deleted. @@ -51,7 +51,7 @@ module "space" { module "policy" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy" - version = "1.0.0" + version = "1.1.0" for_each = local.all_policies_inputs From 46be73e80d31fb510d294574249ff2dfd896ba87 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 12 Jul 2023 11:57:34 -0700 Subject: [PATCH 175/501] `acm` Upstream (#756) Co-authored-by: Dan Miller Co-authored-by: cloudpossebot --- modules/acm/README.md | 12 ++++++++---- modules/acm/main.tf | 17 ++++++++++++----- modules/acm/outputs.tf | 2 +- modules/acm/remote-state.tf | 13 ++++++++++++- modules/acm/variables.tf | 33 +++++++++++++++++++++++++++++---- 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/modules/acm/README.md b/modules/acm/README.md index 82d3527fb..88c72972e 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -6,7 +6,6 @@ The ACM component is to manage an unlimited number of certificates, predominantl We have, as a convenience, the ability to create an ACM certificate as part of creating a DNS zone, whether primary or delegated. That convenience is limited to creating `example.com` and `*.example.com` when creating a zone for `example.com`. For example, Acme has delegated `acct.acme.com` and in addition to `*.acct.acme.com` needed an ACM certificate for `*.usw2.acct.acme.com`, so we use the ACM component to provision that, rather than extend the DNS primary or delegated components to take a list of additional certificates. Both are different views on the Single Responsibility Principle. - ## Usage **Stack Level**: Global or Regional @@ -70,8 +69,9 @@ components: | Name | Source | Version | |------|--------|---------| | [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.16.0 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -95,9 +95,12 @@ components: | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | Use this component name to read from the remote state to get the dns\_delegated zone ID | `string` | `"dns-delegated"` | no | +| [dns\_delegated\_environment\_name](#input\_dns\_delegated\_environment\_name) | Use this environment name to read from the remote state to get the dns\_delegated zone ID | `string` | `"gbl"` | no | +| [dns\_delegated\_stage\_name](#input\_dns\_delegated\_stage\_name) | Use this stage name to read from the remote state to get the dns\_delegated zone ID | `string` | `null` | no | | [dns\_private\_zone\_enabled](#input\_dns\_private\_zone\_enabled) | Whether to set the zone to public or private | `bool` | `false` | no | -| [domain\_name](#input\_domain\_name) | Root domain name | `string` | n/a | yes | -| [enable\_asterisk\_subject\_alternative\_name](#input\_enable\_asterisk\_subject\_alternative\_name) | Enable or disable the use of a wildcard domain in the subject alternative names | `bool` | `true` | no | +| [domain\_name](#input\_domain\_name) | Root domain name | `string` | `""` | no | +| [domain\_name\_prefix](#input\_domain\_name\_prefix) | Root domain name prefix to use with DNS delegated remote state | `string` | `""` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | @@ -112,6 +115,7 @@ components: | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [subject\_alternative\_names](#input\_subject\_alternative\_names) | A list of domains that should be SANs in the issued certificate | `list(string)` | `[]` | no | +| [subject\_alternative\_names\_prefixes](#input\_subject\_alternative\_names\_prefixes) | A list of domain prefixes to use with DNS delegated remote state that should be SANs in the issued certificate | `list(string)` | `[]` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [validation\_method](#input\_validation\_method) | Method to use for validation, DNS or EMAIL | `string` | `"DNS"` | no | diff --git a/modules/acm/main.tf b/modules/acm/main.tf index f7d57b2b8..7683e90a8 100644 --- a/modules/acm/main.tf +++ b/modules/acm/main.tf @@ -1,6 +1,13 @@ locals { enabled = module.this.enabled + domain_suffix = format("%s.%s", var.environment, module.dns_delegated.outputs.default_domain_name) + + domain_name = length(var.domain_name) > 0 ? var.domain_name : format("%s.%s", var.domain_name_prefix, local.domain_suffix) + + subject_alternative_names = concat(var.subject_alternative_names, formatlist("%s.${local.domain_suffix}", var.subject_alternative_names_prefixes)) + all_sans = distinct(concat([format("*.%s", local.domain_name)], local.subject_alternative_names)) + private_enabled = local.enabled && var.dns_private_zone_enabled private_ca_enabled = local.private_enabled && var.certificate_authority_enabled @@ -8,7 +15,7 @@ locals { data "aws_route53_zone" "default" { count = local.enabled ? 1 : 0 - name = var.zone_name + name = length(var.zone_name) > 0 ? var.zone_name : module.dns_delegated.outputs.default_domain_name private_zone = local.private_enabled } @@ -19,10 +26,10 @@ module "acm" { certificate_authority_arn = local.private_ca_enabled ? module.private_ca[0].outputs.private_ca[var.certificate_authority_component_key].certificate_authority.arn : null validation_method = local.private_ca_enabled ? null : var.validation_method - domain_name = var.domain_name + domain_name = local.domain_name process_domain_validation_options = var.process_domain_validation_options ttl = 300 - subject_alternative_names = concat(var.enable_asterisk_subject_alternative_name ? [format("*.%s", var.domain_name)] : [], var.subject_alternative_names) + subject_alternative_names = local.all_sans zone_id = join("", data.aws_route53_zone.default.*.zone_id) context = module.this.context @@ -31,9 +38,9 @@ module "acm" { resource "aws_ssm_parameter" "acm_arn" { count = local.enabled ? 1 : 0 - name = "/acm/${var.domain_name}" + name = "/acm/${local.domain_name}" value = module.acm.arn - description = format("ACM certificate ARN for '%s' domain", var.domain_name) + description = format("ACM certificate ARN for '%s' domain", local.domain_name) type = "String" overwrite = true diff --git a/modules/acm/outputs.tf b/modules/acm/outputs.tf index 875f2357f..3a6457527 100644 --- a/modules/acm/outputs.tf +++ b/modules/acm/outputs.tf @@ -14,6 +14,6 @@ output "domain_validation_options" { } output "domain_name" { - value = local.enabled ? var.domain_name : null + value = local.enabled ? local.domain_name : null description = "Certificate domain name" } diff --git a/modules/acm/remote-state.tf b/modules/acm/remote-state.tf index f6e7dc3de..888007f58 100644 --- a/modules/acm/remote-state.tf +++ b/modules/acm/remote-state.tf @@ -1,6 +1,6 @@ module "private_ca" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" count = local.private_ca_enabled ? 1 : 0 @@ -10,3 +10,14 @@ module "private_ca" { context = module.this.context } + +module "dns_delegated" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + component = var.dns_delegated_component_name + stage = var.dns_delegated_stage_name + environment = var.dns_delegated_environment_name + + context = module.this.context +} diff --git a/modules/acm/variables.tf b/modules/acm/variables.tf index cb082d7a6..9a37ae76f 100644 --- a/modules/acm/variables.tf +++ b/modules/acm/variables.tf @@ -6,6 +6,13 @@ variable "region" { variable "domain_name" { type = string description = "Root domain name" + default = "" +} + +variable "domain_name_prefix" { + type = string + description = "Root domain name prefix to use with DNS delegated remote state" + default = "" } variable "zone_name" { @@ -35,6 +42,12 @@ variable "subject_alternative_names" { description = "A list of domains that should be SANs in the issued certificate" } +variable "subject_alternative_names_prefixes" { + type = list(string) + default = [] + description = "A list of domain prefixes to use with DNS delegated remote state that should be SANs in the issued certificate" +} + variable "dns_private_zone_enabled" { type = bool description = "Whether to set the zone to public or private" @@ -71,8 +84,20 @@ variable "certificate_authority_component_key" { description = "Use this component key e.g. `root` or `mgmt` to read from the remote state to get the certificate_authority_arn if using an authority type of SUBORDINATE" } -variable "enable_asterisk_subject_alternative_name" { - type = bool - default = true - description = "Enable or disable the use of a wildcard domain in the subject alternative names" +variable "dns_delegated_stage_name" { + type = string + default = null + description = "Use this stage name to read from the remote state to get the dns_delegated zone ID" +} + +variable "dns_delegated_environment_name" { + type = string + default = "gbl" + description = "Use this environment name to read from the remote state to get the dns_delegated zone ID" +} + +variable "dns_delegated_component_name" { + type = string + default = "dns-delegated" + description = "Use this component name to read from the remote state to get the dns_delegated zone ID" } From 4b0ccfa76259b9bcb5ab5d9a366542cbb33be083 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Wed, 12 Jul 2023 15:32:10 -0400 Subject: [PATCH 176/501] Update `waf` and `alb` components (#755) --- {modules => deprecated}/aws-waf-acl/README.md | 0 .../aws-waf-acl/context.tf | 0 {modules => deprecated}/aws-waf-acl/main.tf | 0 .../aws-waf-acl/outputs.tf | 0 .../aws-waf-acl/providers.tf | 0 .../aws-waf-acl/variables.tf | 0 .../aws-waf-acl/versions.tf | 0 modules/alb/README.md | 8 +- modules/alb/main.tf | 2 +- modules/alb/remote-state.tf | 8 +- modules/alb/variables.tf | 12 + modules/aws-waf-acl/default.auto.tfvars | 7 - .../alb-controller-ingress-class/README.md | 2 +- .../remote-state.tf | 2 +- .../alb-controller-ingress-group/README.md | 5 +- .../remote-state.tf | 6 +- .../alb-controller-ingress-group/variables.tf | 30 +- modules/eks/alb-controller/README.md | 2 +- modules/eks/alb-controller/remote-state.tf | 2 +- modules/waf/README.md | 48 +- modules/waf/default.auto.tfvars | 3 - modules/waf/main.tf | 34 +- modules/waf/outputs.tf | 21 +- modules/waf/remote-state.tf | 14 + modules/waf/variables.tf | 418 ++++++++++++++++-- modules/waf/versions.tf | 4 +- 26 files changed, 516 insertions(+), 112 deletions(-) rename {modules => deprecated}/aws-waf-acl/README.md (100%) rename {modules => deprecated}/aws-waf-acl/context.tf (100%) rename {modules => deprecated}/aws-waf-acl/main.tf (100%) rename {modules => deprecated}/aws-waf-acl/outputs.tf (100%) rename {modules => deprecated}/aws-waf-acl/providers.tf (100%) rename {modules => deprecated}/aws-waf-acl/variables.tf (100%) rename {modules => deprecated}/aws-waf-acl/versions.tf (100%) delete mode 100644 modules/aws-waf-acl/default.auto.tfvars delete mode 100644 modules/waf/default.auto.tfvars create mode 100644 modules/waf/remote-state.tf diff --git a/modules/aws-waf-acl/README.md b/deprecated/aws-waf-acl/README.md similarity index 100% rename from modules/aws-waf-acl/README.md rename to deprecated/aws-waf-acl/README.md diff --git a/modules/aws-waf-acl/context.tf b/deprecated/aws-waf-acl/context.tf similarity index 100% rename from modules/aws-waf-acl/context.tf rename to deprecated/aws-waf-acl/context.tf diff --git a/modules/aws-waf-acl/main.tf b/deprecated/aws-waf-acl/main.tf similarity index 100% rename from modules/aws-waf-acl/main.tf rename to deprecated/aws-waf-acl/main.tf diff --git a/modules/aws-waf-acl/outputs.tf b/deprecated/aws-waf-acl/outputs.tf similarity index 100% rename from modules/aws-waf-acl/outputs.tf rename to deprecated/aws-waf-acl/outputs.tf diff --git a/modules/aws-waf-acl/providers.tf b/deprecated/aws-waf-acl/providers.tf similarity index 100% rename from modules/aws-waf-acl/providers.tf rename to deprecated/aws-waf-acl/providers.tf diff --git a/modules/aws-waf-acl/variables.tf b/deprecated/aws-waf-acl/variables.tf similarity index 100% rename from modules/aws-waf-acl/variables.tf rename to deprecated/aws-waf-acl/variables.tf diff --git a/modules/aws-waf-acl/versions.tf b/deprecated/aws-waf-acl/versions.tf similarity index 100% rename from modules/aws-waf-acl/versions.tf rename to deprecated/aws-waf-acl/versions.tf diff --git a/modules/alb/README.md b/modules/alb/README.md index 18b18e84c..2d134f3ae 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -34,10 +34,10 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [alb](#module\_alb) | cloudposse/alb/aws | 1.4.0 | +| [alb](#module\_alb) | cloudposse/alb/aws | 1.10.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [remote\_dns](#module\_remote\_dns) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [remote\_vpc](#module\_remote\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [remote\_dns](#module\_remote\_dns) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [remote\_vpc](#module\_remote\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -60,6 +60,7 @@ No resources. | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [deregistration\_delay](#input\_deregistration\_delay) | The amount of time to wait in seconds before changing the state of a deregistering target to unused | `number` | `15` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | Atmos `dns-delegated` component name | `string` | `"dns-delegated"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [health\_check\_healthy\_threshold](#input\_health\_check\_healthy\_threshold) | The number of consecutive health checks successes required before considering an unhealthy target healthy | `number` | `2` | no | @@ -101,6 +102,7 @@ No resources. | [target\_group\_protocol](#input\_target\_group\_protocol) | The protocol for the default target group HTTP or HTTPS | `string` | `"HTTP"` | no | | [target\_group\_target\_type](#input\_target\_group\_target\_type) | The type (`instance`, `ip` or `lambda`) of targets that can be registered with the target group | `string` | `"ip"` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | Atmos `vpc` component name | `string` | `"vpc"` | no | ## Outputs diff --git a/modules/alb/main.tf b/modules/alb/main.tf index 46d97718d..426ac9df4 100644 --- a/modules/alb/main.tf +++ b/modules/alb/main.tf @@ -1,6 +1,6 @@ module "alb" { source = "cloudposse/alb/aws" - version = "1.4.0" + version = "1.10.0" vpc_id = module.remote_vpc.outputs.vpc_id subnet_ids = module.remote_vpc.outputs.public_subnet_ids diff --git a/modules/alb/remote-state.tf b/modules/alb/remote-state.tf index fb2c54aa4..8414dda9d 100644 --- a/modules/alb/remote-state.tf +++ b/modules/alb/remote-state.tf @@ -1,17 +1,17 @@ module "remote_vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" - component = "vpc" + component = var.vpc_component_name context = module.this.context } module "remote_dns" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" - component = "dns-delegated" + component = var.dns_delegated_component_name context = module.this.context } diff --git a/modules/alb/variables.tf b/modules/alb/variables.tf index 920443889..71a6016b9 100644 --- a/modules/alb/variables.tf +++ b/modules/alb/variables.tf @@ -209,3 +209,15 @@ variable "stickiness" { description = "Target group sticky configuration" default = null } + +variable "vpc_component_name" { + type = string + default = "vpc" + description = "Atmos `vpc` component name" +} + +variable "dns_delegated_component_name" { + type = string + default = "dns-delegated" + description = "Atmos `dns-delegated` component name" +} diff --git a/modules/aws-waf-acl/default.auto.tfvars b/modules/aws-waf-acl/default.auto.tfvars deleted file mode 100644 index 6950c9724..000000000 --- a/modules/aws-waf-acl/default.auto.tfvars +++ /dev/null @@ -1,7 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - -name = "waf" - -default_action = "allow" diff --git a/modules/eks/alb-controller-ingress-class/README.md b/modules/eks/alb-controller-ingress-class/README.md index b1fc888ae..955d1794a 100644 --- a/modules/eks/alb-controller-ingress-class/README.md +++ b/modules/eks/alb-controller-ingress-class/README.md @@ -42,7 +42,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/alb-controller-ingress-class/remote-state.tf b/modules/eks/alb-controller-ingress-class/remote-state.tf index ac55ba94c..fdf68ab92 100644 --- a/modules/eks/alb-controller-ingress-class/remote-state.tf +++ b/modules/eks/alb-controller-ingress-class/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = var.eks_component_name diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index d197b1056..05c3dd919 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -88,13 +88,15 @@ components: | [default\_annotations](#input\_default\_annotations) | Default annotations to add to the Kubernetes ingress | `map(any)` |
{
"alb.ingress.kubernetes.io/listen-ports": "[{\"HTTP\": 80}, {\"HTTPS\": 443}]",
"alb.ingress.kubernetes.io/scheme": "internet-facing",
"alb.ingress.kubernetes.io/target-type": "ip",
"kubernetes.io/ingress.class": "alb"
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | The name of the `dns_delegated` component | `string` | `"dns-delegated"` | no | | [dns\_delegated\_environment\_name](#input\_dns\_delegated\_environment\_name) | Global environment name | `string` | `"gbl"` | no | -| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the `eks` component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [fixed\_response\_config](#input\_fixed\_response\_config) | Configuration to overwrite the defaults such as `contentType`, `statusCode`, and `messageBody` | `map(any)` | `{}` | no | | [fixed\_response\_template](#input\_fixed\_response\_template) | Fixed response template to service as a default backend | `string` | `"resources/default-backend.html.tpl"` | no | | [fixed\_response\_vars](#input\_fixed\_response\_vars) | The templatefile vars to use for the fixed response template | `map(any)` |
{
"email": "hello@cloudposse.com"
}
| no | +| [global\_accelerator\_component\_name](#input\_global\_accelerator\_component\_name) | The name of the `global_accelerator` component | `string` | `"global-accelerator"` | no | | [global\_accelerator\_enabled](#input\_global\_accelerator\_enabled) | Whether or not Global Accelerator Endpoint Group should be provisioned for the load balancer | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | @@ -122,6 +124,7 @@ components: | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [waf\_component\_name](#input\_waf\_component\_name) | The name of the `waf` component | `string` | `"waf"` | no | | [waf\_enabled](#input\_waf\_enabled) | Whether or not WAF ACL annotation should be provisioned for the load balancer | `bool` | `false` | no | ## Outputs diff --git a/modules/eks/alb-controller-ingress-group/remote-state.tf b/modules/eks/alb-controller-ingress-group/remote-state.tf index 1898c564c..4cf760134 100644 --- a/modules/eks/alb-controller-ingress-group/remote-state.tf +++ b/modules/eks/alb-controller-ingress-group/remote-state.tf @@ -2,7 +2,7 @@ module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.3" - component = "dns-delegated" + component = var.dns_delegated_component_name environment = var.dns_delegated_environment_name context = module.this.context @@ -23,7 +23,7 @@ module "global_accelerator" { for_each = local.global_accelerator_enabled ? toset(["true"]) : [] - component = "global-accelerator" + component = var.global_accelerator_component_name environment = "gbl" context = module.this.context @@ -35,7 +35,7 @@ module "waf" { for_each = local.waf_enabled ? toset(["true"]) : [] - component = "waf" + component = var.waf_component_name context = module.this.context } diff --git a/modules/eks/alb-controller-ingress-group/variables.tf b/modules/eks/alb-controller-ingress-group/variables.tf index 6c272d9c0..fe4b64bf5 100644 --- a/modules/eks/alb-controller-ingress-group/variables.tf +++ b/modules/eks/alb-controller-ingress-group/variables.tf @@ -32,16 +32,34 @@ variable "default_annotations" { } } -variable "dns_delegated_environment_name" { +variable "eks_component_name" { type = string - description = "Global environment name" - default = "gbl" + description = "The name of the `eks` component" + default = "eks/cluster" } -variable "eks_component_name" { +variable "global_accelerator_component_name" { type = string - description = "The name of the eks component" - default = "eks/cluster" + description = "The name of the `global_accelerator` component" + default = "global-accelerator" +} + +variable "dns_delegated_component_name" { + type = string + description = "The name of the `dns_delegated` component" + default = "dns-delegated" +} + +variable "waf_component_name" { + type = string + description = "The name of the `waf` component" + default = "waf" +} + +variable "dns_delegated_environment_name" { + type = string + description = "Global environment name" + default = "gbl" } variable "global_accelerator_enabled" { diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 48bf82395..ae2bc99f8 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -68,7 +68,7 @@ components: | Name | Source | Version | |------|--------|---------| | [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.7.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/alb-controller/remote-state.tf b/modules/eks/alb-controller/remote-state.tf index ac55ba94c..fdf68ab92 100644 --- a/modules/eks/alb-controller/remote-state.tf +++ b/modules/eks/alb-controller/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = var.eks_component_name diff --git a/modules/waf/README.md b/modules/waf/README.md index 025487901..a9f5da3d2 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -40,20 +40,21 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 4.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 5.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.0 | +| [aws](#provider\_aws) | >= 5.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 0.2.0 | +| [association\_resource\_components](#module\_association\_resource\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 1.0.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -69,48 +70,57 @@ components: |------|-------------|------|---------|:--------:| | [acl\_name](#input\_acl\_name) | Friendly name of the ACL. The ACL ARN will be stored in SSM under {ssm\_path\_prefix}/{acl\_name}/arn | `string` | n/a | yes | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [association\_resource\_arns](#input\_association\_resource\_arns) | A list of ARNs of the resources to associate with the web ACL.
This must be an ARN of an Application Load Balancer or an Amazon API Gateway stage. | `list(string)` | `[]` | no | +| [association\_resource\_arns](#input\_association\_resource\_arns) | A list of ARNs of the resources to associate with the web ACL.
This must be an ARN of an Application Load Balancer, Amazon API Gateway stage, or AWS AppSync.

Do not use this variable to associate a Cloudfront Distribution.
Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource.
For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html | `list(string)` | `[]` | no | +| [association\_resource\_component\_selectors](#input\_association\_resource\_component\_selectors) | A list of Atmos component selectors to get from the remote state and associate their ARNs with the web ACL.
The components must be Application Load Balancers, Amazon API Gateway stages, or AWS AppSync.

component:
Atmos component name
component\_arn\_output:
The component output that defines the component ARN

Do not use this variable to select a Cloudfront Distribution component.
Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource.
For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html |
list(object({
component = string
namespace = optional(string, null)
tenant = optional(string, null)
environment = optional(string, null)
stage = optional(string, null)
component_arn_output = string
}))
| `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [byte\_match\_statement\_rules](#input\_byte\_match\_statement\_rules) | A rule statement that defines a string match search for AWS WAF to apply to web requests.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [byte\_match\_statement\_rules](#input\_byte\_match\_statement\_rules) | A rule statement that defines a string match search for AWS WAF to apply to web requests.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [custom\_response\_body](#input\_custom\_response\_body) | Defines custom response bodies that can be referenced by custom\_response actions.
The map keys are used as the `key` attribute which is a unique key identifying the custom response body.
content:
Payload of the custom response.
The response body can be plain text, HTML or JSON and cannot exceed 4KB in size.
content\_type:
Content Type of Response Body.
Valid values are `TEXT_PLAIN`, `TEXT_HTML`, or `APPLICATION_JSON`. |
map(object({
content = string
content_type = string
}))
| `{}` | no | | [default\_action](#input\_default\_action) | Specifies that AWS WAF should allow requests by default. Possible values: `allow`, `block`. | `string` | `"block"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | A friendly description of the WebACL. | `string` | `"Managed by Terraform"` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [geo\_match\_statement\_rules](#input\_geo\_match\_statement\_rules) | A rule statement used to identify web requests based on country of origin.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [geo\_allowlist\_statement\_rules](#input\_geo\_allowlist\_statement\_rules) | A rule statement used to identify a list of allowed countries which should not be blocked by the WAF.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [geo\_match\_statement\_rules](#input\_geo\_match\_statement\_rules) | A rule statement used to identify web requests based on country of origin.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [ip\_set\_reference\_statement\_rules](#input\_ip\_set\_reference\_statement\_rules) | A rule statement used to detect web requests coming from particular IP addresses or address ranges.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
arn:
The ARN of the IP Set that this statement references.
ip\_set\_forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
position:
The position in the header to search for the IP address.
Possible values include: `FIRST`, `LAST`, or `ANY`.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [ip\_set\_reference\_statement\_rules](#input\_ip\_set\_reference\_statement\_rules) | A rule statement used to detect web requests coming from particular IP addresses or address ranges.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The ARN of the IP Set that this statement references.
ip\_set\_forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
position:
The position in the header to search for the IP address.
Possible values include: `FIRST`, `LAST`, or `ANY`.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [log\_destination\_configs](#input\_log\_destination\_configs) | The Amazon Kinesis Data Firehose ARNs. | `list(string)` | `[]` | no | -| [managed\_rule\_group\_statement\_rules](#input\_managed\_rule\_group\_statement\_rules) | A rule statement used to run the rules that are defined in a managed rule group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

statement:
name:
The name of the managed rule group.
vendor\_name:
The name of the managed rule group vendor.
excluded\_rule:
The list of names of the rules to exclude.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [log\_destination\_configs](#input\_log\_destination\_configs) | The Amazon Kinesis Data Firehose, CloudWatch Log log group, or S3 bucket Amazon Resource Names (ARNs) that you want to associate with the web ACL | `list(string)` | `[]` | no | +| [logging\_filter](#input\_logging\_filter) | A configuration block that specifies which web requests are kept in the logs and which are dropped.
You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation. |
object({
default_behavior = string
filter = list(object({
behavior = string
requirement = string
condition = list(object({
action_condition = optional(object({
action = string
}), null)
label_name_condition = optional(object({
label_name = string
}), null)
}))
}))
})
| `null` | no | +| [managed\_rule\_group\_statement\_rules](#input\_managed\_rule\_group\_statement\_rules) | A rule statement used to run the rules that are defined in a managed rule group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
name:
The name of the managed rule group.
vendor\_name:
The name of the managed rule group vendor.
version:
The version of the managed rule group.
You can set `Version_1.0` or `Version_1.1` etc. If you want to use the default version, do not set anything.
rule\_action\_override:
Action settings to use in the place of the rule actions that are configured inside the rule group.
You specify one override for each rule whose action you want to change.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [rate\_based\_statement\_rules](#input\_rate\_based\_statement\_rules) | A rate-based rule tracks the rate of requests for each originating IP address,
and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
aggregate\_key\_type:
Setting that indicates how to aggregate the request counts.
Possible values include: `FORWARDED_IP` or `IP`
limit:
The limit on requests per 5-minute period for a single originating IP address.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | -| [redacted\_fields](#input\_redacted\_fields) | The parts of the request that you want to keep out of the logs.
method\_enabled:
Whether to enable redaction of the HTTP method.
The method indicates the type of operation that the request is asking the origin to perform.
uri\_path\_enabled:
Whether to enable redaction of the URI path.
This is the part of a web request that identifies a resource.
query\_string\_enabled:
Whether to enable redaction of the query string.
This is the part of a URL that appears after a `?` character, if any.
single\_header:
The list of names of the query headers to redact. | `map(any)` | `{}` | no | -| [regex\_pattern\_set\_reference\_statement\_rules](#input\_regex\_pattern\_set\_reference\_statement\_rules) | A rule statement used to search web request components for matches with regular expressions.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
arn:
The Amazon Resource Name (ARN) of the Regex Pattern Set that this statement references.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [rate\_based\_statement\_rules](#input\_rate\_based\_statement\_rules) | A rate-based rule tracks the rate of requests for each originating IP address,
and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
aggregate\_key\_type:
Setting that indicates how to aggregate the request counts.
Possible values include: `FORWARDED_IP` or `IP`
limit:
The limit on requests per 5-minute period for a single originating IP address.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [redacted\_fields](#input\_redacted\_fields) | The parts of the request that you want to keep out of the logs.
You can only specify one of the following: `method`, `query_string`, `single_header`, or `uri_path`

method:
Whether to enable redaction of the HTTP method.
The method indicates the type of operation that the request is asking the origin to perform.
uri\_path:
Whether to enable redaction of the URI path.
This is the part of a web request that identifies a resource.
query\_string:
Whether to enable redaction of the query string.
This is the part of a URL that appears after a `?` character, if any.
single\_header:
The list of names of the query headers to redact. |
map(object({
method = optional(bool, false)
uri_path = optional(bool, false)
query_string = optional(bool, false)
single_header = optional(list(string), null)
}))
| `{}` | no | +| [regex\_match\_statement\_rules](#input\_regex\_match\_statement\_rules) | A rule statement used to search web request components for a match against a single regular expression.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
regex\_string:
String representing the regular expression. Minimum of 1 and maximum of 512 characters.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl.html#field_to_match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection. At least one required.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [regex\_pattern\_set\_reference\_statement\_rules](#input\_regex\_pattern\_set\_reference\_statement\_rules) | A rule statement used to search web request components for matches with regular expressions.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The Amazon Resource Name (ARN) of the Regex Pattern Set that this statement references.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [rule\_group\_reference\_statement\_rules](#input\_rule\_group\_reference\_statement\_rules) | A rule statement used to run the rules that are defined in an WAFv2 Rule Group.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

statement:
arn:
The ARN of the `aws_wafv2_rule_group` resource.
excluded\_rule:
The list of names of the rules to exclude.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [rule\_group\_reference\_statement\_rules](#input\_rule\_group\_reference\_statement\_rules) | A rule statement used to run the rules that are defined in an WAFv2 Rule Group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The ARN of the `aws_wafv2_rule_group` resource.
rule\_action\_override:
Action settings to use in the place of the rule actions that are configured inside the rule group.
You specify one override for each rule whose action you want to change.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [scope](#input\_scope) | Specifies whether this is for an AWS CloudFront distribution or for a regional application.
Possible values are `CLOUDFRONT` or `REGIONAL`.
To work with CloudFront, you must also specify the region us-east-1 (N. Virginia) on the AWS provider. | `string` | `"REGIONAL"` | no | -| [size\_constraint\_statement\_rules](#input\_size\_constraint\_statement\_rules) | A rule statement that uses a comparison operator to compare a number of bytes against the size of a request component.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
comparison\_operator:
The operator to use to compare the request part to the size setting.
Possible values: `EQ`, `NE`, `LE`, `LT`, `GE`, or `GT`.
size:
The size, in bytes, to compare to the request part, after any transformations.
Valid values are integers between `0` and `21474836480`, inclusive.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | -| [sqli\_match\_statement\_rules](#input\_sqli\_match\_statement\_rules) | An SQL injection match condition identifies the part of web requests,
such as the URI or the query string, that you want AWS WAF to inspect.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [size\_constraint\_statement\_rules](#input\_size\_constraint\_statement\_rules) | A rule statement that uses a comparison operator to compare a number of bytes against the size of a request component.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
comparison\_operator:
The operator to use to compare the request part to the size setting.
Possible values: `EQ`, `NE`, `LE`, `LT`, `GE`, or `GT`.
size:
The size, in bytes, to compare to the request part, after any transformations.
Valid values are integers between `0` and `21474836480`, inclusive.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [sqli\_match\_statement\_rules](#input\_sqli\_match\_statement\_rules) | An SQL injection match condition identifies the part of web requests,
such as the URI or the query string, that you want AWS WAF to inspect.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | | [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | SSM path prefix (with leading but not trailing slash) under which to store all WAF info | `string` | `"/waf"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [visibility\_config](#input\_visibility\_config) | Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `map(string)` | `{}` | no | -| [xss\_match\_statement\_rules](#input\_xss\_match\_statement\_rules) | A rule statement that defines a cross-site scripting (XSS) match search for AWS WAF to apply to web requests.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

xss\_match\_statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [token\_domains](#input\_token\_domains) | Specifies the domains that AWS WAF should accept in a web request token.
This enables the use of tokens across multiple protected websites.
When AWS WAF provides a token, it uses the domain of the AWS resource that the web ACL is protecting.
If you don't specify a list of token domains, AWS WAF accepts tokens only for the domain of the protected resource.
With a token domain list, AWS WAF accepts the resource's host domain plus all domains in the token domain list,
including their prefixed subdomains. | `list(string)` | `null` | no | +| [visibility\_config](#input\_visibility\_config) | Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
object({
cloudwatch_metrics_enabled = bool
metric_name = string
sampled_requests_enabled = bool
})
| n/a | yes | +| [xss\_match\_statement\_rules](#input\_xss\_match\_statement\_rules) | A rule statement that defines a cross-site scripting (XSS) match search for AWS WAF to apply to web requests.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | ## Outputs | Name | Description | |------|-------------| -| [waf](#output\_waf) | Information about the created WAF ACL | +| [arn](#output\_arn) | The ARN of the WAF WebACL. | +| [capacity](#output\_capacity) | The web ACL capacity units (WCUs) currently being used by this web ACL. | +| [id](#output\_id) | The ID of the WAF WebACL. | +| [logging\_config\_id](#output\_logging\_config\_id) | The ARN of the WAFv2 Web ACL logging configuration. | diff --git a/modules/waf/default.auto.tfvars b/modules/waf/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/waf/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/waf/main.tf b/modules/waf/main.tf index fdbc4ea5c..3d2b9a7a5 100644 --- a/modules/waf/main.tf +++ b/modules/waf/main.tf @@ -1,34 +1,50 @@ locals { enabled = module.this.enabled + + association_resource_component_selectors_arns = [ + for i, v in var.association_resource_component_selectors : module.association_resource_components[i].outputs[v.component_arn_output] + if local.enabled + ] + + association_resource_arns = concat(var.association_resource_arns, local.association_resource_component_selectors_arns) } module "aws_waf" { source = "cloudposse/waf/aws" - version = "0.2.0" + version = "1.0.0" + + description = var.description + default_action = var.default_action + custom_response_body = var.custom_response_body + scope = var.scope + visibility_config = var.visibility_config + token_domains = var.token_domains + log_destination_configs = var.log_destination_configs + redacted_fields = var.redacted_fields + logging_filter = var.logging_filter - association_resource_arns = var.association_resource_arns + association_resource_arns = local.association_resource_arns + + # Rules byte_match_statement_rules = var.byte_match_statement_rules - default_action = var.default_action - description = var.description + geo_allowlist_statement_rules = var.geo_allowlist_statement_rules geo_match_statement_rules = var.geo_match_statement_rules ip_set_reference_statement_rules = var.ip_set_reference_statement_rules - log_destination_configs = var.log_destination_configs managed_rule_group_statement_rules = var.managed_rule_group_statement_rules rate_based_statement_rules = var.rate_based_statement_rules - redacted_fields = var.redacted_fields regex_pattern_set_reference_statement_rules = var.regex_pattern_set_reference_statement_rules + regex_match_statement_rules = var.regex_match_statement_rules rule_group_reference_statement_rules = var.rule_group_reference_statement_rules - scope = var.scope size_constraint_statement_rules = var.size_constraint_statement_rules sqli_match_statement_rules = var.sqli_match_statement_rules - visibility_config = var.visibility_config xss_match_statement_rules = var.xss_match_statement_rules context = module.this.context } resource "aws_ssm_parameter" "acl_arn" { - count = local.enabled ? 1 : 0 + count = local.enabled ? 1 : 0 + name = "${var.ssm_path_prefix}/${var.acl_name}/arn" value = module.aws_waf.arn description = "ARN for WAF web ACL ${var.acl_name}" diff --git a/modules/waf/outputs.tf b/modules/waf/outputs.tf index 6a422df7e..3a6f3adcb 100644 --- a/modules/waf/outputs.tf +++ b/modules/waf/outputs.tf @@ -1,4 +1,19 @@ -output "waf" { - value = module.aws_waf - description = "Information about the created WAF ACL" +output "id" { + description = "The ID of the WAF WebACL." + value = module.aws_waf.id +} + +output "arn" { + description = "The ARN of the WAF WebACL." + value = module.aws_waf.arn +} + +output "capacity" { + description = "The web ACL capacity units (WCUs) currently being used by this web ACL." + value = module.aws_waf.capacity +} + +output "logging_config_id" { + description = "The ARN of the WAFv2 Web ACL logging configuration." + value = module.aws_waf.logging_config_id } diff --git a/modules/waf/remote-state.tf b/modules/waf/remote-state.tf new file mode 100644 index 000000000..4e6dba912 --- /dev/null +++ b/modules/waf/remote-state.tf @@ -0,0 +1,14 @@ +module "association_resource_components" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + count = local.enabled ? length(var.association_resource_component_selectors) : 0 + + component = var.association_resource_component_selectors[count.index].component + namespace = coalesce(lookup(var.association_resource_component_selectors[count.index], "namespace", null), module.this.namespace) + tenant = coalesce(lookup(var.association_resource_component_selectors[count.index], "tenant", null), module.this.tenant) + environment = coalesce(lookup(var.association_resource_component_selectors[count.index], "environment", null), module.this.environment) + stage = coalesce(lookup(var.association_resource_component_selectors[count.index], "stage", null), module.this.stage) + + context = module.this.context +} diff --git a/modules/waf/variables.tf b/modules/waf/variables.tf index 11f415515..3d9b489ab 100644 --- a/modules/waf/variables.tf +++ b/modules/waf/variables.tf @@ -14,20 +14,41 @@ variable "acl_name" { description = "Friendly name of the ACL. The ACL ARN will be stored in SSM under {ssm_path_prefix}/{acl_name}/arn" } +variable "description" { + type = string + default = "Managed by Terraform" + description = "A friendly description of the WebACL." +} + variable "default_action" { type = string default = "block" description = "Specifies that AWS WAF should allow requests by default. Possible values: `allow`, `block`." + nullable = false validation { condition = contains(["allow", "block"], var.default_action) error_message = "Allowed values: `allow`, `block`." } } -variable "description" { - type = string - default = "Managed by Terraform" - description = "A friendly description of the WebACL." +variable "custom_response_body" { + type = map(object({ + content = string + content_type = string + })) + + description = <<-DOC + Defines custom response bodies that can be referenced by custom_response actions. + The map keys are used as the `key` attribute which is a unique key identifying the custom response body. + content: + Payload of the custom response. + The response body can be plain text, HTML or JSON and cannot exceed 4KB in size. + content_type: + Content Type of Response Body. + Valid values are `TEXT_PLAIN`, `TEXT_HTML`, or `APPLICATION_JSON`. + DOC + default = {} + nullable = false } variable "scope" { @@ -38,6 +59,7 @@ variable "scope" { Possible values are `CLOUDFRONT` or `REGIONAL`. To work with CloudFront, you must also specify the region us-east-1 (N. Virginia) on the AWS provider. DOC + nullable = false validation { condition = contains(["CLOUDFRONT", "REGIONAL"], var.scope) error_message = "Allowed values: `CLOUDFRONT`, `REGIONAL`." @@ -45,8 +67,11 @@ variable "scope" { } variable "visibility_config" { - type = map(string) - default = {} + type = object({ + cloudwatch_metrics_enabled = bool + metric_name = string + sampled_requests_enabled = bool + }) description = <<-DOC Defines and enables Amazon CloudWatch metrics and web request sample collection. @@ -57,8 +82,122 @@ variable "visibility_config" { sampled_requests_enabled: Whether AWS WAF should store a sampling of the web requests that match the rules. DOC + nullable = false +} + +variable "token_domains" { + type = list(string) + description = <<-DOC + Specifies the domains that AWS WAF should accept in a web request token. + This enables the use of tokens across multiple protected websites. + When AWS WAF provides a token, it uses the domain of the AWS resource that the web ACL is protecting. + If you don't specify a list of token domains, AWS WAF accepts tokens only for the domain of the protected resource. + With a token domain list, AWS WAF accepts the resource's host domain plus all domains in the token domain list, + including their prefixed subdomains. + DOC + default = null +} + +# Logging configuration +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration.html +variable "log_destination_configs" { + type = list(string) + default = [] + description = "The Amazon Kinesis Data Firehose, CloudWatch Log log group, or S3 bucket Amazon Resource Names (ARNs) that you want to associate with the web ACL" +} + +variable "redacted_fields" { + type = map(object({ + method = optional(bool, false) + uri_path = optional(bool, false) + query_string = optional(bool, false) + single_header = optional(list(string), null) + })) + default = {} + description = <<-DOC + The parts of the request that you want to keep out of the logs. + You can only specify one of the following: `method`, `query_string`, `single_header`, or `uri_path` + + method: + Whether to enable redaction of the HTTP method. + The method indicates the type of operation that the request is asking the origin to perform. + uri_path: + Whether to enable redaction of the URI path. + This is the part of a web request that identifies a resource. + query_string: + Whether to enable redaction of the query string. + This is the part of a URL that appears after a `?` character, if any. + single_header: + The list of names of the query headers to redact. + DOC + nullable = false } +variable "logging_filter" { + type = object({ + default_behavior = string + filter = list(object({ + behavior = string + requirement = string + condition = list(object({ + action_condition = optional(object({ + action = string + }), null) + label_name_condition = optional(object({ + label_name = string + }), null) + })) + })) + }) + default = null + description = <<-DOC + A configuration block that specifies which web requests are kept in the logs and which are dropped. + You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation. + DOC +} + +# Association resources +variable "association_resource_arns" { + type = list(string) + default = [] + description = <<-DOC + A list of ARNs of the resources to associate with the web ACL. + This must be an ARN of an Application Load Balancer, Amazon API Gateway stage, or AWS AppSync. + + Do not use this variable to associate a Cloudfront Distribution. + Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource. + For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html + DOC + nullable = false +} + +variable "association_resource_component_selectors" { + type = list(object({ + component = string + namespace = optional(string, null) + tenant = optional(string, null) + environment = optional(string, null) + stage = optional(string, null) + component_arn_output = string + })) + default = [] + description = <<-DOC + A list of Atmos component selectors to get from the remote state and associate their ARNs with the web ACL. + The components must be Application Load Balancers, Amazon API Gateway stages, or AWS AppSync. + + component: + Atmos component name + component_arn_output: + The component output that defines the component ARN + + Do not use this variable to select a Cloudfront Distribution component. + Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource. + For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html + DOC + nullable = false +} + +# Rules variable "byte_match_statement_rules" { type = list(any) default = null @@ -74,6 +213,18 @@ variable "byte_match_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + statement: field_to_match: The part of a web request that you want AWS WAF to inspect. @@ -94,6 +245,53 @@ variable "byte_match_statement_rules" { DOC } +variable "geo_allowlist_statement_rules" { + type = list(any) + default = null + description = <<-DOC + A rule statement used to identify a list of allowed countries which should not be blocked by the WAF. + + name: + A friendly name of the rule. + priority: + If you define more than one Rule in a WebACL, + AWS WAF evaluates each request against the rules in order based on the value of priority. + AWS WAF processes rules with lower priority first. + + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + + statement: + country_codes: + A list of two-character country codes. + forwarded_ip_config: + fallback_behavior: + The match status to assign to the web request if the request doesn't have a valid IP address in the specified position. + Possible values: `MATCH`, `NO_MATCH` + header_name: + The name of the HTTP header to use for the IP address. + + visibility_config: + Defines and enables Amazon CloudWatch metrics and web request sample collection. + + cloudwatch_metrics_enabled: + Whether the associated resource sends metrics to CloudWatch. + metric_name: + A friendly name of the CloudWatch metric. + sampled_requests_enabled: + Whether AWS WAF should store a sampling of the web requests that match the rules. + DOC +} + variable "geo_match_statement_rules" { type = list(any) default = null @@ -109,6 +307,18 @@ variable "geo_match_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + statement: country_codes: A list of two-character country codes. @@ -146,6 +356,18 @@ variable "ip_set_reference_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + statement: arn: The ARN of the IP Set that this statement references. @@ -188,13 +410,29 @@ variable "managed_rule_group_statement_rules" { The override action to apply to the rules in a rule group. Possible values: `count`, `none` + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + statement: name: The name of the managed rule group. vendor_name: The name of the managed rule group vendor. - excluded_rule: - The list of names of the rules to exclude. + version: + The version of the managed rule group. + You can set `Version_1.0` or `Version_1.1` etc. If you want to use the default version, do not set anything. + rule_action_override: + Action settings to use in the place of the rule actions that are configured inside the rule group. + You specify one override for each rule whose action you want to change. visibility_config: Defines and enables Amazon CloudWatch metrics and web request sample collection. @@ -224,6 +462,18 @@ variable "rate_based_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + statement: aggregate_key_type: Setting that indicates how to aggregate the request counts. @@ -264,6 +514,18 @@ variable "regex_pattern_set_reference_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + statement: arn: The Amazon Resource Name (ARN) of the Regex Pattern Set that this statement references. @@ -286,11 +548,11 @@ variable "regex_pattern_set_reference_statement_rules" { DOC } -variable "rule_group_reference_statement_rules" { +variable "regex_match_statement_rules" { type = list(any) default = null description = <<-DOC - A rule statement used to run the rules that are defined in an WAFv2 Rule Group. + A rule statement used to search web request components for a match against a single regular expression. action: The action that AWS WAF should take on a web request when it matches the rule's statement. @@ -301,15 +563,75 @@ variable "rule_group_reference_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + + statement: + regex_string: + String representing the regular expression. Minimum of 1 and maximum of 512 characters. + field_to_match: + The part of a web request that you want AWS WAF to inspect. + See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl.html#field_to_match + text_transformation: + Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection. At least one required. + See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation + + visibility_config: + Defines and enables Amazon CloudWatch metrics and web request sample collection. + + cloudwatch_metrics_enabled: + Whether the associated resource sends metrics to CloudWatch. + metric_name: + A friendly name of the CloudWatch metric. + sampled_requests_enabled: + Whether AWS WAF should store a sampling of the web requests that match the rules. + DOC +} + +variable "rule_group_reference_statement_rules" { + type = list(any) + default = null + description = <<-DOC + A rule statement used to run the rules that are defined in an WAFv2 Rule Group. + + name: + A friendly name of the rule. + priority: + If you define more than one Rule in a WebACL, + AWS WAF evaluates each request against the rules in order based on the value of priority. + AWS WAF processes rules with lower priority first. + override_action: The override action to apply to the rules in a rule group. Possible values: `count`, `none` + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + statement: arn: The ARN of the `aws_wafv2_rule_group` resource. - excluded_rule: - The list of names of the rules to exclude. + rule_action_override: + Action settings to use in the place of the rule actions that are configured inside the rule group. + You specify one override for each rule whose action you want to change. visibility_config: Defines and enables Amazon CloudWatch metrics and web request sample collection. @@ -338,6 +660,18 @@ variable "size_constraint_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + statement: comparison_operator: The operator to use to compare the request part to the size setting. @@ -380,6 +714,18 @@ variable "sqli_match_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. + rule_label: + A List of labels to apply to web requests that match the rule match statement + + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + statement: field_to_match: The part of a web request that you want AWS WAF to inspect. @@ -415,7 +761,19 @@ variable "xss_match_statement_rules" { AWS WAF evaluates each request against the rules in order based on the value of priority. AWS WAF processes rules with lower priority first. - xss_match_statement: + captcha_config: + Specifies how AWS WAF should handle CAPTCHA evaluations. + + immunity_time_property: + Defines custom immunity time. + + immunity_time: + The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300. + + rule_label: + A List of labels to apply to web requests that match the rule match statement + + statement: field_to_match: The part of a web request that you want AWS WAF to inspect. See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match @@ -434,37 +792,3 @@ variable "xss_match_statement_rules" { Whether AWS WAF should store a sampling of the web requests that match the rules. DOC } - -variable "association_resource_arns" { - type = list(string) - default = [] - description = <<-DOC - A list of ARNs of the resources to associate with the web ACL. - This must be an ARN of an Application Load Balancer or an Amazon API Gateway stage. - DOC -} - -variable "log_destination_configs" { - type = list(string) - default = [] - description = "The Amazon Kinesis Data Firehose ARNs." -} - -variable "redacted_fields" { - type = map(any) - default = {} - description = <<-DOC - The parts of the request that you want to keep out of the logs. - method_enabled: - Whether to enable redaction of the HTTP method. - The method indicates the type of operation that the request is asking the origin to perform. - uri_path_enabled: - Whether to enable redaction of the URI path. - This is the part of a web request that identifies a resource. - query_string_enabled: - Whether to enable redaction of the query string. - This is the part of a URL that appears after a `?` character, if any. - single_header: - The list of names of the query headers to redact. - DOC -} diff --git a/modules/waf/versions.tf b/modules/waf/versions.tf index f33ede77f..f0f3f0553 100644 --- a/modules/waf/versions.tf +++ b/modules/waf/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.0" + version = ">= 5.0" } } } From 4e2ce588b6db5d19fdbf3c665e690a2eaed20e7e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 12 Jul 2023 17:21:06 -0700 Subject: [PATCH 177/501] Upstream `gitops` Policy Update (#757) --- modules/gitops/github-actions-iam-policy.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/gitops/github-actions-iam-policy.tf b/modules/gitops/github-actions-iam-policy.tf index 52ef73671..396dd2344 100644 --- a/modules/gitops/github-actions-iam-policy.tf +++ b/modules/gitops/github-actions-iam-policy.tf @@ -39,7 +39,8 @@ data "aws_iam_policy_document" "github_actions_iam_policy" { "dynamodb:PutItem" ] resources = [ - local.dynamodb_table_arn + local.dynamodb_table_arn, + "${local.dynamodb_table_arn}/*" ] } From 8d1f0e5e8cef800af5a8283bc121d889becc93b3 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 13 Jul 2023 18:23:14 -0700 Subject: [PATCH 178/501] Make alb-controller default Ingress actually the default Ingress (#758) --- modules/eks/alb-controller/main.tf | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/eks/alb-controller/main.tf b/modules/eks/alb-controller/main.tf index 44be3b361..c0d726ff9 100644 --- a/modules/eks/alb-controller/main.tf +++ b/modules/eks/alb-controller/main.tf @@ -350,9 +350,8 @@ module "alb_controller" { createIngressClassResource = var.default_ingress_enabled ingressClass = var.default_ingress_class_name ingressClassParams = { - name = var.default_ingress_class_name - create = var.default_ingress_enabled - default = true + name = var.default_ingress_class_name + create = var.default_ingress_enabled spec = { group = { name = var.default_ingress_group @@ -363,6 +362,9 @@ module "alb_controller" { loadBalancerAttributes = var.default_ingress_load_balancer_attributes } } + ingressClassConfig = { + default = var.default_ingress_enabled + } defaultTags = module.this.tags }), # additional values From e5e4fb99a7fb82c4c91dbc8c1d894156a9931264 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Thu, 13 Jul 2023 22:10:22 -0400 Subject: [PATCH 179/501] Better EKS addons support (#723) --- deprecated/eks-iam/.gitignore | 1 + {modules => deprecated}/eks-iam/README.md | 0 .../eks-iam/alb-controller-iam-policy.json | 0 .../eks-iam/alb-controller.tf | 0 {modules => deprecated}/eks-iam/autoscaler.tf | 0 .../eks-iam/cert-manager.tf | 0 {modules => deprecated}/eks-iam/context.tf | 0 .../eks-iam/default.auto.tfvars | 0 .../eks-iam/external-dns.tf | 0 {modules => deprecated}/eks-iam/main.tf | 0 .../modules/service-account/context.tf | 0 .../service-account/default.auto.tfvars | 0 .../eks-iam/modules/service-account/main.tf | 0 .../modules/service-account/outputs.tf | 0 .../modules/service-account/variables.tf | 0 {modules => deprecated}/eks-iam/outputs.tf | 0 {modules => deprecated}/eks-iam/providers.tf | 0 .../eks-iam/tfstate-context.tf | 0 {modules => deprecated}/eks-iam/tfstate.tf | 0 {modules => deprecated}/eks-iam/variables.tf | 0 {modules => deprecated}/eks-iam/versions.tf | 0 modules/eks/cluster/CHANGELOG.md | 63 ++++ modules/eks/cluster/README.md | 316 +++++++++++++----- .../eks/cluster/additional-addon-support.tf | 31 ++ modules/eks/cluster/addons.tf | 173 ++++++++++ .../eks/cluster/{aws_sso.tf => aws-sso.tf} | 0 modules/eks/cluster/eks-node-groups.tf | 2 +- modules/eks/cluster/fargate-profiles.tf | 34 +- modules/eks/cluster/main.tf | 21 +- modules/eks/cluster/outputs.tf | 11 +- modules/eks/cluster/remote-state.tf | 18 +- modules/eks/cluster/variables.tf | 198 +++++++---- modules/eks/cluster/versions.tf | 2 +- 33 files changed, 709 insertions(+), 161 deletions(-) create mode 100644 deprecated/eks-iam/.gitignore rename {modules => deprecated}/eks-iam/README.md (100%) rename {modules => deprecated}/eks-iam/alb-controller-iam-policy.json (100%) rename {modules => deprecated}/eks-iam/alb-controller.tf (100%) rename {modules => deprecated}/eks-iam/autoscaler.tf (100%) rename {modules => deprecated}/eks-iam/cert-manager.tf (100%) rename {modules => deprecated}/eks-iam/context.tf (100%) rename {modules => deprecated}/eks-iam/default.auto.tfvars (100%) rename {modules => deprecated}/eks-iam/external-dns.tf (100%) rename {modules => deprecated}/eks-iam/main.tf (100%) rename {modules => deprecated}/eks-iam/modules/service-account/context.tf (100%) rename {modules => deprecated}/eks-iam/modules/service-account/default.auto.tfvars (100%) rename {modules => deprecated}/eks-iam/modules/service-account/main.tf (100%) rename {modules => deprecated}/eks-iam/modules/service-account/outputs.tf (100%) rename {modules => deprecated}/eks-iam/modules/service-account/variables.tf (100%) rename {modules => deprecated}/eks-iam/outputs.tf (100%) rename {modules => deprecated}/eks-iam/providers.tf (100%) rename {modules => deprecated}/eks-iam/tfstate-context.tf (100%) rename {modules => deprecated}/eks-iam/tfstate.tf (100%) rename {modules => deprecated}/eks-iam/variables.tf (100%) rename {modules => deprecated}/eks-iam/versions.tf (100%) create mode 100644 modules/eks/cluster/CHANGELOG.md create mode 100644 modules/eks/cluster/additional-addon-support.tf create mode 100644 modules/eks/cluster/addons.tf rename modules/eks/cluster/{aws_sso.tf => aws-sso.tf} (100%) diff --git a/deprecated/eks-iam/.gitignore b/deprecated/eks-iam/.gitignore new file mode 100644 index 000000000..a9097e173 --- /dev/null +++ b/deprecated/eks-iam/.gitignore @@ -0,0 +1 @@ +!default.auto.tfvars diff --git a/modules/eks-iam/README.md b/deprecated/eks-iam/README.md similarity index 100% rename from modules/eks-iam/README.md rename to deprecated/eks-iam/README.md diff --git a/modules/eks-iam/alb-controller-iam-policy.json b/deprecated/eks-iam/alb-controller-iam-policy.json similarity index 100% rename from modules/eks-iam/alb-controller-iam-policy.json rename to deprecated/eks-iam/alb-controller-iam-policy.json diff --git a/modules/eks-iam/alb-controller.tf b/deprecated/eks-iam/alb-controller.tf similarity index 100% rename from modules/eks-iam/alb-controller.tf rename to deprecated/eks-iam/alb-controller.tf diff --git a/modules/eks-iam/autoscaler.tf b/deprecated/eks-iam/autoscaler.tf similarity index 100% rename from modules/eks-iam/autoscaler.tf rename to deprecated/eks-iam/autoscaler.tf diff --git a/modules/eks-iam/cert-manager.tf b/deprecated/eks-iam/cert-manager.tf similarity index 100% rename from modules/eks-iam/cert-manager.tf rename to deprecated/eks-iam/cert-manager.tf diff --git a/modules/eks-iam/context.tf b/deprecated/eks-iam/context.tf similarity index 100% rename from modules/eks-iam/context.tf rename to deprecated/eks-iam/context.tf diff --git a/modules/eks-iam/default.auto.tfvars b/deprecated/eks-iam/default.auto.tfvars similarity index 100% rename from modules/eks-iam/default.auto.tfvars rename to deprecated/eks-iam/default.auto.tfvars diff --git a/modules/eks-iam/external-dns.tf b/deprecated/eks-iam/external-dns.tf similarity index 100% rename from modules/eks-iam/external-dns.tf rename to deprecated/eks-iam/external-dns.tf diff --git a/modules/eks-iam/main.tf b/deprecated/eks-iam/main.tf similarity index 100% rename from modules/eks-iam/main.tf rename to deprecated/eks-iam/main.tf diff --git a/modules/eks-iam/modules/service-account/context.tf b/deprecated/eks-iam/modules/service-account/context.tf similarity index 100% rename from modules/eks-iam/modules/service-account/context.tf rename to deprecated/eks-iam/modules/service-account/context.tf diff --git a/modules/eks-iam/modules/service-account/default.auto.tfvars b/deprecated/eks-iam/modules/service-account/default.auto.tfvars similarity index 100% rename from modules/eks-iam/modules/service-account/default.auto.tfvars rename to deprecated/eks-iam/modules/service-account/default.auto.tfvars diff --git a/modules/eks-iam/modules/service-account/main.tf b/deprecated/eks-iam/modules/service-account/main.tf similarity index 100% rename from modules/eks-iam/modules/service-account/main.tf rename to deprecated/eks-iam/modules/service-account/main.tf diff --git a/modules/eks-iam/modules/service-account/outputs.tf b/deprecated/eks-iam/modules/service-account/outputs.tf similarity index 100% rename from modules/eks-iam/modules/service-account/outputs.tf rename to deprecated/eks-iam/modules/service-account/outputs.tf diff --git a/modules/eks-iam/modules/service-account/variables.tf b/deprecated/eks-iam/modules/service-account/variables.tf similarity index 100% rename from modules/eks-iam/modules/service-account/variables.tf rename to deprecated/eks-iam/modules/service-account/variables.tf diff --git a/modules/eks-iam/outputs.tf b/deprecated/eks-iam/outputs.tf similarity index 100% rename from modules/eks-iam/outputs.tf rename to deprecated/eks-iam/outputs.tf diff --git a/modules/eks-iam/providers.tf b/deprecated/eks-iam/providers.tf similarity index 100% rename from modules/eks-iam/providers.tf rename to deprecated/eks-iam/providers.tf diff --git a/modules/eks-iam/tfstate-context.tf b/deprecated/eks-iam/tfstate-context.tf similarity index 100% rename from modules/eks-iam/tfstate-context.tf rename to deprecated/eks-iam/tfstate-context.tf diff --git a/modules/eks-iam/tfstate.tf b/deprecated/eks-iam/tfstate.tf similarity index 100% rename from modules/eks-iam/tfstate.tf rename to deprecated/eks-iam/tfstate.tf diff --git a/modules/eks-iam/variables.tf b/deprecated/eks-iam/variables.tf similarity index 100% rename from modules/eks-iam/variables.tf rename to deprecated/eks-iam/variables.tf diff --git a/modules/eks-iam/versions.tf b/deprecated/eks-iam/versions.tf similarity index 100% rename from modules/eks-iam/versions.tf rename to deprecated/eks-iam/versions.tf diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md new file mode 100644 index 000000000..84582aa50 --- /dev/null +++ b/modules/eks/cluster/CHANGELOG.md @@ -0,0 +1,63 @@ +## Components PR [#723](https://github.com/cloudposse/terraform-aws-components/pull/723/files) + + +### Improved support for EKS Add-Ons + +This has improved support for EKS Add-Ons. + +##### Configuration and Timeouts + +The `addons` input now accepts a `configuration_values` input to allow you +to configure the add-ons, and various timeout inputs to allow you to fine-tune +the timeouts for the add-ons. + +##### Automatic IAM Role Creation + +If you enable `aws-ebs-csi-driver` or `vpc-cni` add-ons, the module will +automatically create the required Service Account IAM Role and attach it to +the add-on. + +##### Add-Ons can be deployed to Fargate + +If you are using Karpenter and not provisioning any nodes with this module, +the `coredns` and `aws-ebs-csi-driver` add-ons can be deployed to Fargate. +(They must be able to run somewhere in the cluster or else the deployment +will fail.) + +To cause the add-ons to be deployed to Fargate, set the `deploy_addons_to_fargate` +input to `true`. + +**Note about CoreDNS**: If you want to deploy CoreDNS to Fargate, as of this +writing you must set the `configuration_values` input for CoreDNS to +`'{"computeType": "Fargate"}'`. If you want to deploy CoreDNS to EC2 instances, +you must NOT include the `computeType` configuration value. + +### Availability Zones implied by Private Subnets + +You can now avoid specifying Availability Zones for the cluster anywhere. +If all of the possible Availability Zones inputs are empty, the module will +use the Availability Zones implied by the private subnets. That is, it will +deploy the cluster to all of the Availability Zones in which the VPC has +private subnets. + +### Optional support for 1 Fargate Pod Execution Role per Cluster + +Previously, this module created a separate Fargate Pod Execution Role for each +Fargate Profile it created. This is unnecessary, excessive, and can cause +problems due to name collisions, but is otherwise merely inefficient, so it is +not important to fix this on existiong, working clusters. +This update brings a feature that causes the module to create at +most 1 Fargate Pod Execution Role per cluster. + +**This change is recommended for all NEW clusters, but only NEW clusters**. +Because it is a breaking change, it is not enabled by default. To enable it, set the +`legacy_fargate_1_role_per_profile_enabled` variable to `false`. + +**WARNING**: If you enable this feature on an existing cluster, and that +cluster is using Karpenter, the update could destroy all of your existing +Karpenter-provisioned nodes. Depending on your Karpenter version, this +could leave you with stranded EC2 instances (still running, but not managed by +Karpenter or visible to the cluster) and an interruption of service, and +possibly other problems. If you are using Karpenter and want to enable this +feature, the safest way is to destroy the existing cluster and create a new +one with this feature enabled. diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 543d0ac25..65ed6304f 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -2,10 +2,8 @@ This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups and Fargate profiles. - :::warning - This component should only be deployed after logging into AWS via Federated login with SAML (e.g. GSuite) or assuming an IAM role (e.g. from a CI/CD system). It should not be deployed if you log into AWS via AWS SSO, the reason being that on initial deployment, the EKS cluster will be owned by the assumed role that provisioned it, @@ -27,7 +25,7 @@ In addition, this example has the GitHub OIDC integration added and makes use of For more on these requirements, see [Identity Reference Architecture](https://docs.cloudposse.com/reference-architecture/quickstart/iam-identity/), [Network Reference Architecture](https://docs.cloudposse.com/reference-architecture/scaffolding/setup/network/), -the [Github OIDC component](https://docs.cloudposse.com/components/catalog/aws/github-oidc-provider/), +the [GitHub OIDC component](https://docs.cloudposse.com/components/catalog/aws/github-oidc-provider/), and the [Karpenter component](https://docs.cloudposse.com/components/catalog/aws/eks/karpenter/). ```yaml @@ -39,6 +37,10 @@ components: name: eks iam_primary_roles_tenant_name: core cluster_kubernetes_version: "1.25" + + vpc_component_name: "vpc" + eks_component_name: "eks/cluster" + # Your choice of availability zones or availability zone ids # availability_zones: ["us-east-1a", "us-east-1b", "us-east-1c"] availability_zone_ids: ["use1-az4", "use1-az5", "use1-az6"] @@ -50,9 +52,11 @@ components: stage: corp - tenant: core stage: network + public_access_cidrs: [] allowed_cidr_blocks: [] allowed_security_groups: [] + enabled_cluster_log_types: # Caution: enabling `api` log events may lead to a substantial increase in Cloudwatch Logs expenses. - api @@ -60,6 +64,7 @@ components: - authenticator - controllerManager - scheduler + oidc_provider_enabled: true # Allows GitHub OIDC role @@ -84,40 +89,39 @@ components: cluster_endpoint_private_access: true cluster_endpoint_public_access: false cluster_log_retention_period: 90 + # List of `aws-teams` to map to Kubernetes RBAC groups. # This gives teams direct access to Kubernetes without having to assume a team-role. # RBAC groups must be created elsewhere. The "system:" groups are predefined by Kubernetes. aws_teams_rbac: - - groups: - - system:masters - aws_team: admin - - groups: - - idp:poweruser - - system:authenticated - aws_team: poweruser - - groups: - - idp:observer - - system:authenticated - aws_team: observer + - aws_team: managers + groups: + - system:masters + - aws_team: devops + groups: + - system:masters + # List of `aws-teams-roles` (in the account where the EKS cluster is deployed) to map to Kubernetes RBAC groups aws_team_roles_rbac: - - groups: - - system:masters - aws_team_role: admin - - groups: - - idp:poweruser - - system:authenticated - aws_team_role: poweruser - - groups: - - idp:observer - - system:authenticated - aws_team_role: observer - - groups: - - system:masters - aws_team_role: terraform - - groups: - - system:masters - aws_team_role: helm + - aws_team: admin + groups: + - system:masters + - aws_team_role: poweruser + groups: + - idp:poweruser + - system:authenticated + - aws_team_role: observer + groups: + - idp:observer + - system:authenticated + - aws_team_role: planner + groups: + - idp:observer + - system:authenticated + - aws_team: terraform + groups: + - system:masters + # Permission sets from AWS SSO allowing cluster access # See `aws-sso` component. aws_sso_permission_sets_rbac: @@ -125,66 +129,136 @@ components: groups: - idp:poweruser - system:authenticated + + # Fargate Profiles fargate_profiles: karpenter: kubernetes_namespace: karpenter kubernetes_labels: null karpenter_iam_role_enabled: true + + # EKS addons + # https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html + addons: + vpc-cni: + addon_version: "v1.12.2-eksbuild.1" + kube-proxy: + addon_version: "v1.25.6-eksbuild.1" + coredns: + addon_version: "v1.9.3-eksbuild.2" + aws-ebs-csi-driver: + addon_version: "v1.19.0-eksbuild.2" ``` ### Usage with Node Groups -The `eks/cluster` component also supports managed node groups. In order to add a set list of nodes to -provision with the cluster, add values for `var.managed_node_groups_enabled` and `var.node_groups`. +The `eks/cluster` component also supports managed Node Groups. In order to add a set of nodes to +provision with the cluster, provide values for `var.managed_node_groups_enabled` and `var.node_groups`. :::info -You can use managed node groups in conjunction with Karpenter node groups, though in most cases, -Karpenter is all you need. + +You can use managed Node Groups in conjunction with Karpenter, though in most cases, Karpenter is all you need. ::: For example: ```yaml - managed_node_groups_enabled: true - node_groups: # for most attributes, setting null here means use setting from node_group_defaults - main: - # availability_zones = null will create one autoscaling group - # in every private subnet in the VPC - availability_zones: null - - desired_group_size: 3 # number of instances to start with, must be >= number of AZs - min_group_size: 3 # must be >= number of AZs - max_group_size: 6 - - # Can only set one of ami_release_version or kubernetes_version - # Leave both null to use latest AMI for Cluster Kubernetes version - kubernetes_version: null # use cluster Kubernetes version - ami_release_version: null # use latest AMI for Kubernetes version - - attributes: [] - create_before_destroy: true - disk_size: 100 - cluster_autoscaler_enabled: true - instance_types: - - t3.medium - ami_type: AL2_x86_64 # use "AL2_x86_64" for standard instances, "AL2_x86_64_GPU" for GPU instances - kubernetes_labels: {} - kubernetes_taints: {} - resources_to_tag: - - instance - - volume - tags: null +managed_node_groups_enabled: true +node_groups: # for most attributes, setting null here means use setting from node_group_defaults + main: + # availability_zones = null will create one autoscaling group + # in every private subnet in the VPC + availability_zones: null + + desired_group_size: 3 # number of instances to start with, must be >= number of AZs + min_group_size: 3 # must be >= number of AZs + max_group_size: 6 + + # Can only set one of ami_release_version or kubernetes_version + # Leave both null to use latest AMI for Cluster Kubernetes version + kubernetes_version: null # use cluster Kubernetes version + ami_release_version: null # use latest AMI for Kubernetes version + + attributes: [] + create_before_destroy: true + disk_size: 100 + cluster_autoscaler_enabled: true + instance_types: + - t3.medium + ami_type: AL2_x86_64 # use "AL2_x86_64" for standard instances, "AL2_x86_64_GPU" for GPU instances + kubernetes_labels: {} + kubernetes_taints: {} + resources_to_tag: + - instance + - volume + tags: null ``` ### Using Addons -EKS clusters support “Addons” that can be automatically installed on a cluster. Install these addons with the [`var.addons` input](https://docs.cloudposse.com/components/library/aws/eks/cluster/#input_addons). +EKS clusters support “Addons” that can be automatically installed on a cluster. +Install these addons with the [`var.addons` input](https://docs.cloudposse.com/components/library/aws/eks/cluster/#input_addons). + +:::info + +Run the following command to see all available addons, their type, and their publisher. +You can also see the URL for addons that are available through the AWS Marketplace. Replace 1.25 with the version of your cluster. +See [Creating an addon](https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on) for more details. + +::: + +```shell +aws eks describe-addon-versions --kubernetes-version 1.25 \ + --query 'addons[].{MarketplaceProductUrl: marketplaceInformation.productUrl, Name: addonName, Owner: owner Publisher: publisher, Type: type}' --output table +``` + +:::info + +You can see which versions are available for each addon by executing the following commands. +Replace 1.25 with the version of your cluster. + +::: + +```shell +EKS_K8S_VERSION=1.24 # replace with your cluster version +echo "vpc-cni:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name vpc-cni \ + --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table + +echo "kube-proxy:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name kube-proxy \ + --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table + +echo "coredns:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name coredns \ + --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table + +echo "aws-ebs-csi-driver:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name aws-ebs-csi-driver \ + --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table +``` + +Configure the addons like the following example: ```yaml +# https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html +# https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on addons: - - addon_name: vpc-cni - addon_version: v1.12.6-eksbuild.2 + # https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html + # https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html + # https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role + # https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/#deploy-vpc-cni-managed-add-on + vpc-cni: + addon_version: "v1.12.2-eksbuild.1" # set `addon_version` to `null` to use the latest version + # https://docs.aws.amazon.com/eks/latest/userguide/managing-kube-proxy.html + kube-proxy: + addon_version: "v1.25.6-eksbuild.1" # set `addon_version` to `null` to use the latest version + # https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html + coredns: + addon_version: "v1.9.3-eksbuild.2" # set `addon_version` to `null` to use the latest version + # https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html + # https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons + # https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role + # https://github.com/kubernetes-sigs/aws-ebs-csi-driver + aws-ebs-csi-driver: + addon_version: "v1.19.0-eksbuild.2" # set `addon_version` to `null` to use the latest version ``` Some addons, such as CoreDNS, require at least one node to be fully provisioned first. @@ -194,8 +268,8 @@ Set `var.addons_depends_on` to `true` to require the Node Groups to be provision ```yaml addons_depends_on: true addons: - - addon_name: coredns - addon_version: v1.25 + coredns: + addon_version: "v1.8.7-eksbuild.1" ``` :::warning @@ -205,16 +279,83 @@ these nodes will never be available before the cluster component is deployed. ::: -For more on upgrading these EKS Addons, see +For more information on upgrading EKS Addons, see ["How to Upgrade EKS Cluster Addons"](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons/) +### Adding and Configuring a new EKS Addon + +Add a new EKS addon to the `addons` map (`addons` variable): + +```yaml +addons: + my-addon: + addon_version: "..." +``` + +If the new addon requires an EKS IAM Role for Kubernetes Service Account, perform the following steps: + +- Add a file `addons-custom.tf` to the `eks/cluster` folder + +- In the file, add an IAM policy document with the permissions required for the addon, + and use the `eks-iam-role` module to provision an IAM Role for Kubernetes Service Account for the addon: + + ```hcl + data "aws_iam_policy_document" "my_addon" { + statement { + sid = "..." + effect = "Allow" + resources = ["..."] + + actions = [ + "...", + "..." + ] + } + } + + module "my_addon_eks_iam_role" { + source = "cloudposse/eks-iam-role/aws" + version = "2.1.0" + + eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url + + service_account_name = "..." + service_account_namespace = "..." + + aws_iam_policy_document = [one(data.aws_iam_policy_document.my_addon[*].json)] + + context = module.this.context + } + ``` + + For reference on how to configure the IAM role and IAM permissions for EKS addons, see [addons.tf](addons.tf). + +- Add a file `additional-addon-support_override.tf` to the `eks/cluster` folder + +- In the file, add the IAM Role for Kubernetes Service Account for the addon to the `overridable_additional_addon_service_account_role_arn_map` map: + + ```hcl + locals { + overridable_additional_addon_service_account_role_arn_map = { + my-addon = module.my_addon_eks_iam_role.service_account_role_arn + } + } + ``` + +- This map will override the default map in the [additional-addon-support.tf](additional-addon-support.tf) file, + and will be merged into the final map together with the default EKS addons `vpc-cni` and `aws-ebs-csi-driver` + (which this component configures and creates IAM Roles for Kubernetes Service Accounts) + +- Follow the instructions in the [additional-addon-support.tf](additional-addon-support.tf) file + if the addon may need to be deployed to Fargate, or has dependencies that Terraform cannot detect automatically. + ## Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | ## Providers @@ -227,16 +368,21 @@ For more on upgrading these EKS Addons, see | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | -| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.8.1 | -| [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.2.0 | +| [aws\_ebs\_csi\_driver\_eks\_iam\_role](#module\_aws\_ebs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.0 | +| [aws\_ebs\_csi\_driver\_fargate\_profile](#module\_aws\_ebs\_csi\_driver\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | +| [coredns\_fargate\_profile](#module\_coredns\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.9.0 | +| [fargate\_pod\_execution\_role](#module\_fargate\_pod\_execution\_role) | cloudposse/eks-fargate-profile/aws | 1.3.0 | +| [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [iam\_arns](#module\_iam\_arns) | ../../account-map/modules/roles-to-principals | n/a | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [karpenter\_label](#module\_karpenter\_label) | cloudposse/label/null | 0.25.0 | | [region\_node\_group](#module\_region\_node\_group) | ./modules/node_group_by_region | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [vpc\_cni\_eks\_iam\_role](#module\_vpc\_cni\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.0 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | ## Resources @@ -247,9 +393,12 @@ For more on upgrading these EKS Addons, see | [aws_iam_role_policy_attachment.amazon_ec2_container_registry_readonly](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.amazon_eks_worker_node_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.amazon_ssm_managed_instance_core](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.aws_ebs_csi_driver](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.ipv6_eks_cni_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.vpc_cni](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ipv6_eks_cni_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.vpc_cni_ipv6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_roles.sso_roles](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_roles) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | @@ -258,7 +407,7 @@ For more on upgrading these EKS Addons, see | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [addons](#input\_addons) | Manages [`aws_eks_addon`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
list(object({
addon_name = string
addon_version = string
resolve_conflicts = string
service_account_role_arn = string
}))
| `[]` | no | +| [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
map(object({
addon_version = optional(string, null)
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | | [addons\_depends\_on](#input\_addons\_depends\_on) | If set `true`, all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned.
See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. | `bool` | `false` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | @@ -266,7 +415,7 @@ For more on upgrading these EKS Addons, see | [apply\_config\_map\_aws\_auth](#input\_apply\_config\_map\_aws\_auth) | Whether to execute `kubectl apply` to apply the ConfigMap to allow worker nodes to join the EKS cluster | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [availability\_zone\_abbreviation\_type](#input\_availability\_zone\_abbreviation\_type) | Type of Availability Zone abbreviation (either `fixed` or `short`) to use in names. See https://github.com/cloudposse/terraform-aws-utils for details. | `string` | `"fixed"` | no | -| [availability\_zones](#input\_availability\_zones) | AWS Availability Zones in which to deploy multi-AZ resources.
If not provided, resources will be provisioned in every private subnet in the VPC. | `list(string)` | `[]` | no | +| [availability\_zones](#input\_availability\_zones) | AWS Availability Zones in which to deploy multi-AZ resources.
If not provided, resources will be provisioned in every zone with a private subnet in the VPC. | `list(string)` | `[]` | no | | [aws\_auth\_yaml\_strip\_quotes](#input\_aws\_auth\_yaml\_strip\_quotes) | If true, remove double quotes from the generated aws-auth ConfigMap YAML to reduce spurious diffs in plans | `bool` | `true` | no | | [aws\_ssm\_agent\_enabled](#input\_aws\_ssm\_agent\_enabled) | Set true to attach the required IAM policy for AWS SSM agent to each EC2 instance's IAM Role | `bool` | `false` | no | | [aws\_sso\_permission\_sets\_rbac](#input\_aws\_sso\_permission\_sets\_rbac) | (Not Recommended): AWS SSO (IAM Identity Center) permission sets in the EKS deployment account to add to `aws-auth` ConfigMap.
Unfortunately, `aws-auth` ConfigMap does not support SSO permission sets, so we map the generated
IAM Role ARN corresponding to the permission set at the time Terraform runs. This is subject to change
when any changes are made to the AWS SSO configuration, invalidating the mapping, and requiring a
`terraform apply` in this project to update the `aws-auth` ConfigMap and restore access. |
list(object({
aws_sso_permission_set = string
groups = list(string)
}))
| `[]` | no | @@ -277,7 +426,7 @@ For more on upgrading these EKS Addons, see | [cluster\_encryption\_config\_kms\_key\_enable\_key\_rotation](#input\_cluster\_encryption\_config\_kms\_key\_enable\_key\_rotation) | Cluster Encryption Config KMS Key Resource argument - enable kms key rotation | `bool` | `true` | no | | [cluster\_encryption\_config\_kms\_key\_id](#input\_cluster\_encryption\_config\_kms\_key\_id) | KMS Key ID to use for cluster encryption config | `string` | `""` | no | | [cluster\_encryption\_config\_kms\_key\_policy](#input\_cluster\_encryption\_config\_kms\_key\_policy) | Cluster Encryption Config KMS Key Resource argument - key policy | `string` | `null` | no | -| [cluster\_encryption\_config\_resources](#input\_cluster\_encryption\_config\_resources) | Cluster Encryption Config Resources to encrypt, e.g. ['secrets'] | `list(any)` |
[
"secrets"
]
| no | +| [cluster\_encryption\_config\_resources](#input\_cluster\_encryption\_config\_resources) | Cluster Encryption Config Resources to encrypt, e.g. `["secrets"]` | `list(string)` |
[
"secrets"
]
| no | | [cluster\_endpoint\_private\_access](#input\_cluster\_endpoint\_private\_access) | Indicates whether or not the Amazon EKS private API server endpoint is enabled. Default to AWS EKS resource and it is `false` | `bool` | `false` | no | | [cluster\_endpoint\_public\_access](#input\_cluster\_endpoint\_public\_access) | Indicates whether or not the Amazon EKS public API server endpoint is enabled. Default to AWS EKS resource and it is `true` | `bool` | `true` | no | | [cluster\_kubernetes\_version](#input\_cluster\_kubernetes\_version) | Desired Kubernetes master version. If you do not specify a value, the latest available version is used | `string` | `null` | no | @@ -286,6 +435,7 @@ For more on upgrading these EKS Addons, see | [color](#input\_color) | The cluster stage represented by a color; e.g. blue, green | `string` | `""` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [deploy\_addons\_to\_fargate](#input\_deploy\_addons\_to\_fargate) | Set to `true` to deploy addons to Fargate instead of initial node pool | `bool` | `true` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -303,6 +453,7 @@ For more on upgrading these EKS Addons, see | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [legacy\_fargate\_1\_role\_per\_profile\_enabled](#input\_legacy\_fargate\_1\_role\_per\_profile\_enabled) | Set to `false` for new clusters to create a single Fargate Pod Execution role for the cluster.
Set to `true` for existing clusters to preserve the old behavior of creating
a Fargate Pod Execution role for each Fargate Profile. | `bool` | `true` | no | | [managed\_node\_groups\_enabled](#input\_managed\_node\_groups\_enabled) | Set false to prevent the creation of EKS managed node groups. | `bool` | `true` | no | | [map\_additional\_aws\_accounts](#input\_map\_additional\_aws\_accounts) | Additional AWS account numbers to add to `aws-auth` ConfigMap | `list(string)` | `[]` | no | | [map\_additional\_iam\_roles](#input\_map\_additional\_iam\_roles) | Additional IAM roles to add to `config-map-aws-auth` ConfigMap |
list(object({
rolearn = string
username = string
groups = list(string)
}))
| `[]` | no | @@ -311,8 +462,8 @@ For more on upgrading these EKS Addons, see | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = string
ami_type = string
attributes = list(string)
availability_zones = list(string) # set to null to use var.availability_zones
cluster_autoscaler_enabled = bool
create_before_destroy = bool
desired_group_size = number
disk_encryption_enabled = bool
disk_size = number
instance_types = list(string)
kubernetes_labels = map(string)
kubernetes_taints = list(object({
key = string
value = string
effect = string
}))
kubernetes_version = string # set to null to use cluster_kubernetes_version
max_group_size = number
min_group_size = number
resources_to_tag = list(string)
tags = map(string)
})
|
{
"ami_release_version": null,
"ami_type": null,
"attributes": null,
"availability_zones": null,
"cluster_autoscaler_enabled": true,
"create_before_destroy": true,
"desired_group_size": 1,
"disk_encryption_enabled": true,
"disk_size": 20,
"instance_types": [
"t3.medium"
],
"kubernetes_labels": null,
"kubernetes_taints": null,
"kubernetes_version": null,
"max_group_size": 100,
"min_group_size": null,
"resources_to_tag": null,
"tags": null
}
| no | -| [node\_groups](#input\_node\_groups) | List of objects defining a node group for the cluster |
map(object({
# EKS AMI version to use, e.g. "1.16.13-20200821" (no "v").
ami_release_version = string
# Type of Amazon Machine Image (AMI) associated with the EKS Node Group
ami_type = string
# Additional attributes (e.g. `1`) for the node group
attributes = list(string)
# will create 1 auto scaling group in each specified availability zone
availability_zones = list(string)
# Whether to enable Node Group to scale its AutoScaling Group
cluster_autoscaler_enabled = bool
# True to create new node_groups before deleting old ones, avoiding a temporary outage
create_before_destroy = bool
# Desired number of worker nodes when initially provisioned
desired_group_size = number
# Enable disk encryption for the created launch template (if we aren't provided with an existing launch template)
disk_encryption_enabled = bool
# Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided.
disk_size = number
# Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided.
instance_types = list(string)
# Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed
kubernetes_labels = map(string)
# List of objects describing Kubernetes taints.
kubernetes_taints = list(object({
key = string
value = string
effect = string
}))
# Desired Kubernetes master version. If you do not specify a value, the latest available version is used
kubernetes_version = string
# The maximum size of the AutoScaling Group
max_group_size = number
# The minimum size of the AutoScaling Group
min_group_size = number
# List of auto-launched resource types to tag
resources_to_tag = list(string)
tags = map(string)
}))
| `{}` | no | -| [oidc\_provider\_enabled](#input\_oidc\_provider\_enabled) | Create an IAM OIDC identity provider for the cluster, then you can create IAM roles to associate with a service account in the cluster, instead of using kiam or kube2iam. For more information, see https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html | `bool` | n/a | yes | +| [node\_groups](#input\_node\_groups) | List of objects defining a node group for the cluster |
map(object({
# EKS AMI version to use, e.g. "1.16.13-20200821" (no "v").
ami_release_version = string
# Type of Amazon Machine Image (AMI) associated with the EKS Node Group
ami_type = string
# Additional attributes (e.g. `1`) for the node group
attributes = list(string)
# will create 1 auto scaling group in each specified availability zone
# or all AZs with subnets if none are specified anywhere
availability_zones = list(string)
# Whether to enable Node Group to scale its AutoScaling Group
cluster_autoscaler_enabled = bool
# True to create new node_groups before deleting old ones, avoiding a temporary outage
create_before_destroy = bool
# Desired number of worker nodes when initially provisioned
desired_group_size = number
# Enable disk encryption for the created launch template (if we aren't provided with an existing launch template)
disk_encryption_enabled = bool
# Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided.
disk_size = number
# Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided.
instance_types = list(string)
# Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed
kubernetes_labels = map(string)
# List of objects describing Kubernetes taints.
kubernetes_taints = list(object({
key = string
value = string
effect = string
}))
# Desired Kubernetes master version. If you do not specify a value, the latest available version is used
kubernetes_version = string
# The maximum size of the AutoScaling Group
max_group_size = number
# The minimum size of the AutoScaling Group
min_group_size = number
# List of auto-launched resource types to tag
resources_to_tag = list(string)
tags = map(string)
}))
| `{}` | no | +| [oidc\_provider\_enabled](#input\_oidc\_provider\_enabled) | Create an IAM OIDC identity provider for the cluster, then you can create IAM roles to associate with a service account in the cluster, instead of using kiam or kube2iam. For more information, see https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html | `bool` | `true` | no | | [public\_access\_cidrs](#input\_public\_access\_cidrs) | Indicates which CIDR blocks can access the Amazon EKS public API server endpoint when enabled. EKS defaults this to a list with 0.0.0.0/0. | `list(string)` |
[
"0.0.0.0/0"
]
| no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | @@ -320,6 +471,7 @@ For more on upgrading these EKS Addons, see | [subnet\_type\_tag\_key](#input\_subnet\_type\_tag\_key) | The tag used to find the private subnets to find by availability zone. If null, will be looked up in vpc outputs. | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | The name of the vpc component | `string` | `"vpc"` | no | ## Outputs @@ -358,6 +510,6 @@ For more on upgrading these EKS Addons, see ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/cluster) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/cluster) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/eks/cluster/additional-addon-support.tf b/modules/eks/cluster/additional-addon-support.tf new file mode 100644 index 000000000..3fa6f4b40 --- /dev/null +++ b/modules/eks/cluster/additional-addon-support.tf @@ -0,0 +1,31 @@ +locals { + # If you have custom addons, create a file called `additional-addon-support_override.tf` + # and in that file override any of the following declarations as needed. + + + # Set `overridable_deploy_additional_addons_to_fargate` to indicate whether or not + # there are custom addons that should be deployed to Fargate on nodeless clusters. + overridable_deploy_additional_addons_to_fargate = false + + # `overridable_additional_addon_service_account_role_arn_map` is a map of addon names + # to the service account role ARNs they use. + # See the README for more details. + overridable_additional_addon_service_account_role_arn_map = { + # Example: + # my-addon = module.my_addon_eks_iam_role.service_account_role_arn + } + + # If you are creating Fargate profiles for your addons, + # use "cloudposse/eks-fargate-profile/aws" to create them + # and set `overridable_additional_addon_fargate_profiles` to a map of addon names + # to the corresponding eks-fargate-profile module output. + overridable_additional_addon_fargate_profiles = { + # Example: + # my-addon = module.my_addon_fargate_profile + } + + # If you have additional dependencies that must be created before the addons are deployed, + # override this declaration by creating a file called `additional-addon-support_override.tf` + # and setting `overridable_addons_depends_on` appropriately. + overridable_addons_depends_on = [] +} diff --git a/modules/eks/cluster/addons.tf b/modules/eks/cluster/addons.tf new file mode 100644 index 000000000..5f6f5965a --- /dev/null +++ b/modules/eks/cluster/addons.tf @@ -0,0 +1,173 @@ +# https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html +# https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on + +locals { + eks_cluster_oidc_issuer_url = replace(local.eks_outputs.eks_cluster_identity_oidc_issuer, "https://", "") + + addon_names = keys(var.addons) + vpc_cni_addon_enabled = local.enabled && contains(local.addon_names, "vpc-cni") + aws_ebs_csi_driver_enabled = local.enabled && contains(local.addon_names, "aws-ebs-csi-driver") + coredns_enabled = local.enabled && contains(local.addon_names, "coredns") + + # The `vpc-cni` and `aws-ebs-csi-driver` addons are special as they always require an IAM role for Kubernetes Service Account (IRSA). + # The roles are created by this component. + addon_service_account_role_arn_map = { + vpc-cni = module.vpc_cni_eks_iam_role.service_account_role_arn + aws-ebs-csi-driver = module.aws_ebs_csi_driver_eks_iam_role.service_account_role_arn + } + + final_addon_service_account_role_arn_map = merge(local.addon_service_account_role_arn_map, local.overridable_additional_addon_service_account_role_arn_map) + + addons = [ + for k, v in var.addons : { + addon_name = k + addon_version = lookup(v, "addon_version", null) + configuration_values = lookup(v, "configuration_values", null) + resolve_conflicts = lookup(v, "resolve_conflicts", null) + service_account_role_arn = try(coalesce(lookup(v, "service_account_role_arn", null), lookup(local.final_addon_service_account_role_arn_map, k, null)), null) + } + ] + + addons_depends_on = concat([ + module.aws_ebs_csi_driver_fargate_profile, + module.coredns_fargate_profile, + ], local.overridable_addons_depends_on) + + addons_require_fargate = var.deploy_addons_to_fargate && ( + local.aws_ebs_csi_driver_enabled || + local.coredns_enabled || + local.overridable_deploy_additional_addons_to_fargate + ) + addon_fargate_profiles = merge( + (local.aws_ebs_csi_driver_enabled && var.deploy_addons_to_fargate ? { + aws_ebs_csi_driver = one(module.aws_ebs_csi_driver_fargate_profile[*]) + } : {}), + (local.coredns_enabled && var.deploy_addons_to_fargate ? { + coredns = one(module.coredns_fargate_profile[*]) + } : {}), + local.overridable_additional_addon_fargate_profiles + ) +} + +# `vpc-cni` EKS addon +# https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html +# https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html +# https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role +# https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/#deploy-vpc-cni-managed-add-on +data "aws_iam_policy_document" "vpc_cni_ipv6" { + count = local.vpc_cni_addon_enabled ? 1 : 0 + + # See https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-ipv6-policy + statement { + sid = "" + effect = "Allow" + resources = ["*"] + + actions = [ + "ec2:AssignIpv6Addresses", + "ec2:DescribeInstances", + "ec2:DescribeTags", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeInstanceTypes" + ] + } + + statement { + sid = "" + effect = "Allow" + resources = ["arn:aws:ec2:*:*:network-interface/*"] + actions = ["ec2:CreateTags"] + } +} + +resource "aws_iam_role_policy_attachment" "vpc_cni" { + count = local.vpc_cni_addon_enabled ? 1 : 0 + + role = module.vpc_cni_eks_iam_role.service_account_role_name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" +} + +module "vpc_cni_eks_iam_role" { + source = "cloudposse/eks-iam-role/aws" + version = "2.1.0" + + enabled = local.vpc_cni_addon_enabled + + eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url + + service_account_name = "aws-node" + service_account_namespace = "kube-system" + + aws_iam_policy_document = [one(data.aws_iam_policy_document.vpc_cni_ipv6[*].json)] + + context = module.this.context +} + +# `aws-ebs-csi-driver` EKS addon +# https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html +# https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons +# https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role +# https://github.com/kubernetes-sigs/aws-ebs-csi-driver +resource "aws_iam_role_policy_attachment" "aws_ebs_csi_driver" { + count = local.aws_ebs_csi_driver_enabled ? 1 : 0 + + role = module.aws_ebs_csi_driver_eks_iam_role.service_account_role_name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy" +} + +module "aws_ebs_csi_driver_eks_iam_role" { + source = "cloudposse/eks-iam-role/aws" + version = "2.1.0" + + enabled = local.aws_ebs_csi_driver_enabled + + eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url + + service_account_name = "ebs-csi-controller" + service_account_namespace = "kube-system" + + context = module.this.context +} + +module "aws_ebs_csi_driver_fargate_profile" { + count = local.aws_ebs_csi_driver_enabled && var.deploy_addons_to_fargate ? 1 : 0 + + source = "cloudposse/eks-fargate-profile/aws" + version = "1.3.0" + + subnet_ids = local.private_subnet_ids + cluster_name = module.eks_cluster.eks_cluster_id + kubernetes_namespace = "kube-system" + kubernetes_labels = { app = "ebs-csi-controller" } + permissions_boundary = var.fargate_profile_iam_role_permissions_boundary + iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter + + fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-ebs-csi" + fargate_pod_execution_role_enabled = false + fargate_pod_execution_role_arn = one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) + + attributes = ["ebs-csi"] + context = module.this.context +} + +module "coredns_fargate_profile" { + count = local.coredns_enabled && var.deploy_addons_to_fargate ? 1 : 0 + + source = "cloudposse/eks-fargate-profile/aws" + version = "1.3.0" + + + subnet_ids = local.private_subnet_ids + cluster_name = module.eks_cluster.eks_cluster_id + kubernetes_namespace = "kube-system" + kubernetes_labels = { k8s-app = "kube-dns" } + permissions_boundary = var.fargate_profile_iam_role_permissions_boundary + iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter + + fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-coredns" + fargate_pod_execution_role_enabled = false + fargate_pod_execution_role_arn = one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) + + attributes = ["coredns"] + context = module.this.context +} diff --git a/modules/eks/cluster/aws_sso.tf b/modules/eks/cluster/aws-sso.tf similarity index 100% rename from modules/eks/cluster/aws_sso.tf rename to modules/eks/cluster/aws-sso.tf diff --git a/modules/eks/cluster/eks-node-groups.tf b/modules/eks/cluster/eks-node-groups.tf index f061f7cf1..433923810 100644 --- a/modules/eks/cluster/eks-node-groups.tf +++ b/modules/eks/cluster/eks-node-groups.tf @@ -1,7 +1,7 @@ locals { node_groups_enabled = local.enabled && var.managed_node_groups_enabled - node_group_default_availability_zones = var.node_group_defaults.availability_zones == null ? var.availability_zones : var.node_group_defaults.availability_zones + node_group_default_availability_zones = var.node_group_defaults.availability_zones == null ? local.availability_zones : var.node_group_defaults.availability_zones node_group_default_kubernetes_version = var.node_group_defaults.kubernetes_version == null ? var.cluster_kubernetes_version : var.node_group_defaults.kubernetes_version # values(module.region_node_group) is an array of `region_node_group` objects diff --git a/modules/eks/cluster/fargate-profiles.tf b/modules/eks/cluster/fargate-profiles.tf index 04e0714d2..1dfd2dd12 100644 --- a/modules/eks/cluster/fargate-profiles.tf +++ b/modules/eks/cluster/fargate-profiles.tf @@ -1,10 +1,36 @@ locals { - fargate_profiles = local.enabled ? var.fargate_profiles : {} + fargate_profiles = local.enabled ? var.fargate_profiles : {} + fargate_cluster_pod_execution_role_name = "${module.eks_cluster.eks_cluster_id}-fargate" + fargate_cluster_pod_execution_role_needed = local.enabled && ( + local.addons_require_fargate || + ((length(var.fargate_profiles) > 0) && !var.legacy_fargate_1_role_per_profile_enabled) + ) } +module "fargate_pod_execution_role" { + count = local.fargate_cluster_pod_execution_role_needed ? 1 : 0 + + source = "cloudposse/eks-fargate-profile/aws" + version = "1.3.0" + + subnet_ids = local.private_subnet_ids + cluster_name = module.eks_cluster.eks_cluster_id + permissions_boundary = var.fargate_profile_iam_role_permissions_boundary + + fargate_profile_enabled = false + fargate_pod_execution_role_enabled = true + fargate_pod_execution_role_name = local.fargate_cluster_pod_execution_role_name + + context = module.this.context +} + + +############################################################################### +### Both New and Legacy behavior, use caution when modifying +############################################################################### module "fargate_profile" { source = "cloudposse/eks-fargate-profile/aws" - version = "1.2.0" + version = "1.3.0" for_each = local.fargate_profiles @@ -15,5 +41,9 @@ module "fargate_profile" { permissions_boundary = var.fargate_profile_iam_role_permissions_boundary iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter + ## Legacy switch + fargate_pod_execution_role_enabled = var.legacy_fargate_1_role_per_profile_enabled + fargate_pod_execution_role_arn = var.legacy_fargate_1_role_per_profile_enabled ? null : one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) + context = module.this.context } diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index e98ad654a..402841c24 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -16,7 +16,7 @@ locals { aws_teams_auth = [for role in var.aws_teams_rbac : { rolearn = module.iam_arns.principals_map[local.identity_account_name][role.aws_team] - # Include session name in the username for auditing purposes. + # Session name included in audit trail automatically starting with Kubernetes v1.20. # See https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-iam-roles-when-multiple-users-need-identical-access-to-the-cluster username = format("%s-%s", local.identity_account_name, role.aws_team) groups = role.groups @@ -80,16 +80,19 @@ locals { # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` # `az_public_subnets_map` is a map of AZ names to list of public subnet IDs in the AZs - public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k)]) + public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]) # Get only the private subnets that correspond to the AZs provided in `var.availability_zones` # `az_private_subnets_map` is a map of AZ names to list of private subnet IDs in the AZs - private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k)]) + private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]) + + # Infer the availability zones from the private subnets if var.availability_zones is empty: + availability_zones = length(var.availability_zones) == 0 ? keys(local.vpc_outputs.az_private_subnets_map) : var.availability_zones } module "eks_cluster" { source = "cloudposse/eks-cluster/aws" - version = "2.8.1" + version = "2.9.0" region = var.region attributes = local.attributes @@ -121,11 +124,17 @@ module "eks_cluster" { public_access_cidrs = var.public_access_cidrs subnet_ids = var.cluster_private_subnets_only ? local.private_subnet_ids : concat(local.private_subnet_ids, local.public_subnet_ids) vpc_id = local.vpc_id - addons = var.addons - addons_depends_on = var.addons_depends_on ? [module.region_node_group] : null kubernetes_config_map_ignore_role_changes = false + # EKS addons + addons = local.addons + + addons_depends_on = var.addons_depends_on ? concat( + [module.region_node_group], local.addons_depends_on, + values(local.final_addon_service_account_role_arn_map) + ) : null + # Managed Node Groups do not expose nor accept any Security Groups. # Instead, EKS creates a Security Group and applies it to ENI that is attached to EKS Control Plane master nodes and to any managed workloads. #workers_security_group_ids = compact([local.vpn_allowed_cidr_sg]) diff --git a/modules/eks/cluster/outputs.tf b/modules/eks/cluster/outputs.tf index a286038e6..c240fc5a7 100644 --- a/modules/eks/cluster/outputs.tf +++ b/modules/eks/cluster/outputs.tf @@ -80,17 +80,22 @@ output "karpenter_iam_role_name" { output "fargate_profiles" { description = "Fargate Profiles" - value = module.fargate_profile + value = merge(module.fargate_profile, local.addon_fargate_profiles) } output "fargate_profile_role_arns" { description = "Fargate Profile Role ARNs" - value = values(module.fargate_profile)[*].eks_fargate_profile_role_arn + value = distinct(compact(concat(values(module.fargate_profile)[*].eks_fargate_profile_role_arn, + [one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn)] + ))) + } output "fargate_profile_role_names" { description = "Fargate Profile Role names" - value = values(module.fargate_profile)[*].eks_fargate_profile_role_name + value = distinct(compact(concat(values(module.fargate_profile)[*].eks_fargate_profile_role_name, + [one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_name)] + ))) } output "vpc_cidr" { diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf index af1f02457..43e8cc5ff 100644 --- a/modules/eks/cluster/remote-state.tf +++ b/modules/eks/cluster/remote-state.tf @@ -1,5 +1,7 @@ locals { - accounts_with_vpc = { for i, account in var.allow_ingress_from_vpc_accounts : try(account.tenant, module.this.tenant) != null ? format("%s-%s", account.tenant, account.stage) : account.stage => account } + accounts_with_vpc = { + for i, account in var.allow_ingress_from_vpc_accounts : try(account.tenant, module.this.tenant) != null ? format("%s-%s", account.tenant, account.stage) : account.stage => account + } } module "iam_arns" { @@ -12,22 +14,22 @@ module "iam_arns" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.4.3" - component = "vpc" + component = var.vpc_component_name context = module.this.context } module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.4.3" for_each = local.accounts_with_vpc - component = "vpc" + component = var.vpc_component_name environment = try(each.value.environment, module.this.environment) - stage = try(each.value.stage, module.this.environment) + stage = try(each.value.stage, module.this.stage) tenant = try(each.value.tenant, module.this.tenant) context = module.this.context @@ -38,13 +40,15 @@ module "vpc_ingress" { # to it rather than overwrite it (specifically the aws-auth configMap) module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.4.3" component = var.eks_component_name defaults = { eks_managed_node_workers_role_arns = [] fargate_profile_role_arns = [] + fargate_profile_role_names = [] + eks_cluster_identity_oidc_issuer = "" } context = module.this.context diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 8b08bbd30..80473c011 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -7,15 +7,18 @@ variable "availability_zones" { type = list(string) description = <<-EOT AWS Availability Zones in which to deploy multi-AZ resources. - If not provided, resources will be provisioned in every private subnet in the VPC. + If not provided, resources will be provisioned in every zone with a private subnet in the VPC. EOT default = [] + nullable = false } variable "availability_zone_abbreviation_type" { type = string description = "Type of Availability Zone abbreviation (either `fixed` or `short`) to use in names. See https://github.com/cloudposse/terraform-aws-utils for details." default = "fixed" + nullable = false + validation { condition = contains(["fixed", "short"], var.availability_zone_abbreviation_type) error_message = "The availability_zone_abbreviation_type must be either \"fixed\" or \"short\"." @@ -26,93 +29,110 @@ variable "managed_node_groups_enabled" { type = bool description = "Set false to prevent the creation of EKS managed node groups." default = true + nullable = false } variable "oidc_provider_enabled" { type = bool description = "Create an IAM OIDC identity provider for the cluster, then you can create IAM roles to associate with a service account in the cluster, instead of using kiam or kube2iam. For more information, see https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html" + default = true + nullable = false } variable "cluster_endpoint_private_access" { type = bool - default = false description = "Indicates whether or not the Amazon EKS private API server endpoint is enabled. Default to AWS EKS resource and it is `false`" + default = false + nullable = false } variable "cluster_endpoint_public_access" { type = bool - default = true description = "Indicates whether or not the Amazon EKS public API server endpoint is enabled. Default to AWS EKS resource and it is `true`" + default = true + nullable = false } variable "cluster_kubernetes_version" { type = string - default = null description = "Desired Kubernetes master version. If you do not specify a value, the latest available version is used" + default = null } variable "public_access_cidrs" { type = list(string) - default = ["0.0.0.0/0"] description = "Indicates which CIDR blocks can access the Amazon EKS public API server endpoint when enabled. EKS defaults this to a list with 0.0.0.0/0." + default = ["0.0.0.0/0"] + nullable = false } variable "enabled_cluster_log_types" { type = list(string) - default = [] description = "A list of the desired control plane logging to enable. For more information, see https://docs.aws.amazon.com/en_us/eks/latest/userguide/control-plane-logs.html. Possible values [`api`, `audit`, `authenticator`, `controllerManager`, `scheduler`]" + default = [] + nullable = false } variable "cluster_log_retention_period" { type = number - default = 0 description = "Number of days to retain cluster logs. Requires `enabled_cluster_log_types` to be set. See https://docs.aws.amazon.com/en_us/eks/latest/userguide/control-plane-logs.html." + default = 0 + nullable = false } variable "apply_config_map_aws_auth" { type = bool - default = true description = "Whether to execute `kubectl apply` to apply the ConfigMap to allow worker nodes to join the EKS cluster" + default = true + nullable = false } variable "map_additional_aws_accounts" { - description = "Additional AWS account numbers to add to `aws-auth` ConfigMap" type = list(string) + description = "Additional AWS account numbers to add to `aws-auth` ConfigMap" default = [] + nullable = false } variable "map_additional_worker_roles" { - description = "AWS IAM Role ARNs of worker nodes to add to `aws-auth` ConfigMap" type = list(string) + description = "AWS IAM Role ARNs of worker nodes to add to `aws-auth` ConfigMap" default = [] + nullable = false } variable "aws_teams_rbac" { - description = <<-EOT - List of `aws-teams` to map to Kubernetes RBAC groups. - This gives teams direct access to Kubernetes without having to assume a team-role. - EOT - type = list(object({ aws_team = string groups = list(string) })) - default = [] + description = <<-EOT + List of `aws-teams` to map to Kubernetes RBAC groups. + This gives teams direct access to Kubernetes without having to assume a team-role. + EOT + + default = [] + nullable = false } variable "aws_team_roles_rbac" { - description = "List of `aws-team-roles` (in the target AWS account) to map to Kubernetes RBAC groups." - type = list(object({ aws_team_role = string groups = list(string) })) - default = [] + description = "List of `aws-team-roles` (in the target AWS account) to map to Kubernetes RBAC groups." + default = [] + nullable = false } variable "aws_sso_permission_sets_rbac" { + type = list(object({ + aws_sso_permission_set = string + groups = list(string) + })) + description = <<-EOT (Not Recommended): AWS SSO (IAM Identity Center) permission sets in the EKS deployment account to add to `aws-auth` ConfigMap. Unfortunately, `aws-auth` ConfigMap does not support SSO permission sets, so we map the generated @@ -121,60 +141,59 @@ variable "aws_sso_permission_sets_rbac" { `terraform apply` in this project to update the `aws-auth` ConfigMap and restore access. EOT - type = list(object({ - aws_sso_permission_set = string - groups = list(string) - })) - - default = [] + default = [] + nullable = false } variable "map_additional_iam_roles" { - description = "Additional IAM roles to add to `config-map-aws-auth` ConfigMap" - type = list(object({ rolearn = string username = string groups = list(string) })) - default = [] + description = "Additional IAM roles to add to `config-map-aws-auth` ConfigMap" + default = [] + nullable = false } variable "map_additional_iam_users" { - description = "Additional IAM users to add to `aws-auth` ConfigMap" - type = list(object({ userarn = string username = string groups = list(string) })) - default = [] + description = "Additional IAM users to add to `aws-auth` ConfigMap" + default = [] + nullable = false } variable "allowed_security_groups" { type = list(string) - default = [] description = "List of Security Group IDs to be allowed to connect to the EKS cluster" + default = [] + nullable = false } variable "allowed_cidr_blocks" { type = list(string) - default = [] description = "List of CIDR blocks to be allowed to connect to the EKS cluster" + default = [] + nullable = false } variable "subnet_type_tag_key" { type = string - default = null description = "The tag used to find the private subnets to find by availability zone. If null, will be looked up in vpc outputs." + default = null } variable "color" { type = string - default = "" description = "The cluster stage represented by a color; e.g. blue, green" + default = "" + nullable = false } variable "node_groups" { @@ -187,6 +206,7 @@ variable "node_groups" { # Additional attributes (e.g. `1`) for the node group attributes = list(string) # will create 1 auto scaling group in each specified availability zone + # or all AZs with subnets if none are specified anywhere availability_zones = list(string) # Whether to enable Node Group to scale its AutoScaling Group cluster_autoscaler_enabled = bool @@ -218,8 +238,10 @@ variable "node_groups" { resources_to_tag = list(string) tags = map(string) })) + description = "List of objects defining a node group for the cluster" default = {} + nullable = false } variable "node_group_defaults" { @@ -248,7 +270,9 @@ variable "node_group_defaults" { resources_to_tag = list(string) tags = map(string) }) + description = "Defaults for node groups in the cluster" + default = { ami_release_version = null ami_type = null @@ -268,86 +292,98 @@ variable "node_group_defaults" { resources_to_tag = null tags = null } + nullable = false } variable "cluster_encryption_config_enabled" { type = bool - default = true description = "Set to `true` to enable Cluster Encryption Configuration" + default = true + nullable = false } variable "cluster_encryption_config_kms_key_id" { type = string - default = "" description = "KMS Key ID to use for cluster encryption config" + default = "" + nullable = false } variable "cluster_encryption_config_kms_key_enable_key_rotation" { type = bool - default = true description = "Cluster Encryption Config KMS Key Resource argument - enable kms key rotation" + default = true + nullable = false } variable "cluster_encryption_config_kms_key_deletion_window_in_days" { type = number - default = 10 description = "Cluster Encryption Config KMS Key Resource argument - key deletion windows in days post destruction" + default = 10 + nullable = false } variable "cluster_encryption_config_kms_key_policy" { type = string - default = null description = "Cluster Encryption Config KMS Key Resource argument - key policy" + default = null } variable "cluster_encryption_config_resources" { - type = list(any) + type = list(string) + description = "Cluster Encryption Config Resources to encrypt, e.g. `[\"secrets\"]`" default = ["secrets"] - description = "Cluster Encryption Config Resources to encrypt, e.g. ['secrets']" + nullable = false } variable "aws_ssm_agent_enabled" { type = bool description = "Set true to attach the required IAM policy for AWS SSM agent to each EC2 instance's IAM Role" default = false + nullable = false } variable "kubeconfig_file" { type = string - default = "" description = "Name of `kubeconfig` file to use to configure Kubernetes provider" + default = "" } variable "kubeconfig_file_enabled" { - type = bool - default = false + type = bool + description = <<-EOF Set true to configure Kubernetes provider with a `kubeconfig` file specified by `kubeconfig_file`. Mainly for when the standard configuration produces a Terraform error. EOF + + default = false + nullable = false } variable "kube_exec_auth_role_arn" { type = string - default = null description = "The role ARN for `aws eks get-token` to use. Defaults to the current caller's role." + default = null } variable "aws_auth_yaml_strip_quotes" { type = bool - default = true description = "If true, remove double quotes from the generated aws-auth ConfigMap YAML to reduce spurious diffs in plans" + default = true + nullable = false } variable "cluster_private_subnets_only" { type = bool - default = false description = "Whether or not to enable private subnets or both public and private subnets" + default = false + nullable = false } variable "allow_ingress_from_vpc_accounts" { - type = any - default = [] + type = any + description = <<-EOF List of account contexts to pull VPC ingress CIDR and add to cluster security group. @@ -359,18 +395,30 @@ variable "allow_ingress_from_vpc_accounts" { tenant = "core" } EOF + + default = [] + nullable = false } variable "eks_component_name" { type = string description = "The name of the eks component" default = "eks/cluster" + nullable = false +} + +variable "vpc_component_name" { + type = string + description = "The name of the vpc component" + default = "vpc" + nullable = false } variable "karpenter_iam_role_enabled" { type = bool description = "Flag to enable/disable creation of IAM role for EC2 Instance Profile that is attached to the nodes launched by Karpenter" default = false + nullable = false } variable "fargate_profiles" { @@ -378,14 +426,17 @@ variable "fargate_profiles" { kubernetes_namespace = string kubernetes_labels = map(string) })) + description = "Fargate Profiles config" default = {} + nullable = false } variable "fargate_profile_iam_role_kubernetes_namespace_delimiter" { type = string description = "Delimiter for the Kubernetes namespace in the IAM Role name for Fargate Profiles" default = "-" + nullable = false } variable "fargate_profile_iam_role_permissions_boundary" { @@ -395,21 +446,50 @@ variable "fargate_profile_iam_role_permissions_boundary" { } variable "addons" { - type = list(object({ - addon_name = string - addon_version = string - resolve_conflicts = string - service_account_role_arn = string + type = map(object({ + addon_version = optional(string, null) + configuration_values = optional(string, null) + # Set default resolve_conflicts to OVERWRITE because it is required on initial installation of + # add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and + # because any custom configuration that you would want to preserve should be managed by Terraform. + resolve_conflicts = optional(string, "OVERWRITE") + service_account_role_arn = optional(string, null) + create_timeout = optional(string, null) + update_timeout = optional(string, null) + delete_timeout = optional(string, null) })) - description = "Manages [`aws_eks_addon`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources" - default = [] + + description = "Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources" + default = {} + nullable = false } -variable "addons_depends_on" { +variable "deploy_addons_to_fargate" { type = bool + description = "Set to `true` to deploy addons to Fargate instead of initial node pool" + default = true + nullable = false +} + +variable "addons_depends_on" { + type = bool + description = <<-EOT If set `true`, all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned. See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. EOT - default = false + + default = false + nullable = false +} + +variable "legacy_fargate_1_role_per_profile_enabled" { + type = bool + description = <<-EOT + Set to `false` for new clusters to create a single Fargate Pod Execution role for the cluster. + Set to `true` for existing clusters to preserve the old behavior of creating + a Fargate Pod Execution role for each Fargate Profile. + EOT + default = true + nullable = false } diff --git a/modules/eks/cluster/versions.tf b/modules/eks/cluster/versions.tf index cc73ffd35..b5920b7b1 100644 --- a/modules/eks/cluster/versions.tf +++ b/modules/eks/cluster/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { From 990ef52f71cffb3da1de2f8508848abf4e304008 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 13 Jul 2023 19:14:23 -0700 Subject: [PATCH 180/501] [eks/karpenter-provisioner] minor improvements (#759) --- modules/eks/karpenter-provisioner/README.md | 8 +++++- modules/eks/karpenter-provisioner/main.tf | 15 +++++++++-- .../eks/karpenter-provisioner/variables.tf | 27 ++++++++++++------- modules/eks/karpenter/README.md | 7 ++--- modules/eks/karpenter/main.tf | 5 +++- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/modules/eks/karpenter-provisioner/README.md b/modules/eks/karpenter-provisioner/README.md index d49c06e23..33ab50b81 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/modules/eks/karpenter-provisioner/README.md @@ -42,6 +42,12 @@ components: # and capacity type (such as AWS spot or on-demand). # See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details requirements: + - key: "karpenter.k8s.aws/instance-category" + operator: "In" + values: ["c", "m", "r"] + - key: "karpenter.k8s.aws/instance-generation" + operator: "Gt" + values: ["2"] - key: "karpenter.sh/capacity-type" operator: "In" values: @@ -157,7 +163,7 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [provisioners](#input\_provisioners) | Karpenter provisioners config |
map(object({
# The name of the Karpenter provisioner
name = string
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets
private_subnets_enabled = bool
# Configures Karpenter to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the value to `null` (never scales down if not set)
ttl_seconds_after_empty = number
# Configures Karpenter to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` (never expires if not set)
ttl_seconds_until_expired = number
# Continuously binpack containers into least possible number of nodes. Mutually exclusive with ttl_seconds_after_empty.
consolidation = object({
enabled = bool
})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
values = list(string)
}))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter provisioner metadata options. See https://karpenter.sh/v0.18.0/aws/provisioning/#metadata-options for more details
metadata_options = optional(map(string), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter provisioner block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes. Karpenter uses default block device mappings for the AMI Family specified. For example, the Bottlerocket AMI Family defaults with two block device mappings. See https://karpenter.sh/v0.18.0/aws/provisioning/#block-device-mappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
}))
| n/a | yes | +| [provisioners](#input\_provisioners) | Karpenter provisioners config |
map(object({
# The name of the Karpenter provisioner
name = string
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets
private_subnets_enabled = optional(bool, true)
# Configures Karpenter to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the value to `null` (never scales down if not set)
# Conflicts with `consolidation.enabled`, which is usually a better option.
ttl_seconds_after_empty = optional(number, null)
# Configures Karpenter to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` (never expires if not set)
ttl_seconds_until_expired = optional(number, null)
# Continuously binpack containers into least possible number of nodes. Mutually exclusive with ttl_seconds_after_empty.
# Ideally `true` by default, but conflicts with `ttl_seconds_after_empty`, which was previously the only option.
consolidation = optional(object({
enabled = bool
}), { enabled = false })
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
values = list(string)
}))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})), [])
startup_taints = optional(list(object({
key = string
effect = string
value = string
})), [])
# Karpenter provisioner metadata options. See https://karpenter.sh/v0.18.0/aws/provisioning/#metadata-options for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled"), # valid values: enabled, disabled
httpProtocolIPv6 = optional(string, "disabled"), # valid values: enabled, disabled
httpPutResponseHopLimit = optional(number, 2), # limit of 1 discouraged because it keeps Pods from reaching metadata service
httpTokens = optional(string, "required") # valid values: required, optional
})),
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter provisioner block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes. Karpenter uses default block device mappings for the AMI Family specified. For example, the Bottlerocket AMI Family defaults with two block device mappings. See https://karpenter.sh/v0.18.0/aws/provisioning/#block-device-mappings for more details
block_device_mappings = optional(list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
})), [])
}))
| n/a | yes | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/eks/karpenter-provisioner/main.tf b/modules/eks/karpenter-provisioner/main.tf index 36cce759c..5285f42aa 100644 --- a/modules/eks/karpenter-provisioner/main.tf +++ b/modules/eks/karpenter-provisioner/main.tf @@ -53,7 +53,17 @@ resource "kubernetes_manifest" "provisioner" { ) } + # spec.requirements counts as a computed field because defaults may be added by the admission webhook. + computed_fields = ["spec.requirements"] + depends_on = [kubernetes_manifest.provider] + + lifecycle { + precondition { + condition = each.value.consolidation.enabled == false || each.value.ttl_seconds_after_empty == null + error_message = "Consolidation and TTL Seconds After Empty are mutually exclusive." + } + } } locals { @@ -87,8 +97,9 @@ resource "kubernetes_manifest" "provider" { "aws:eks:cluster-name" = local.eks_cluster_id } # https://karpenter.sh/v0.18.0/aws/provisioning/#amazon-machine-image-ami-family - amiFamily = each.value.ami_family - tags = module.this.tags + amiFamily = each.value.ami_family + metadataOptions = each.value.metadata_options + tags = module.this.tags }, try(length(local.provisioner_block_device_mappings[each.key]), 0) == 0 ? {} : { blockDeviceMappings = local.provisioner_block_device_mappings[each.key] }) diff --git a/modules/eks/karpenter-provisioner/variables.tf b/modules/eks/karpenter-provisioner/variables.tf index f132b7529..3e31e75aa 100644 --- a/modules/eks/karpenter-provisioner/variables.tf +++ b/modules/eks/karpenter-provisioner/variables.tf @@ -14,15 +14,17 @@ variable "provisioners" { # The name of the Karpenter provisioner name = string # Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets - private_subnets_enabled = bool + private_subnets_enabled = optional(bool, true) # Configures Karpenter to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the value to `null` (never scales down if not set) - ttl_seconds_after_empty = number + # Conflicts with `consolidation.enabled`, which is usually a better option. + ttl_seconds_after_empty = optional(number, null) # Configures Karpenter to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` (never expires if not set) - ttl_seconds_until_expired = number + ttl_seconds_until_expired = optional(number, null) # Continuously binpack containers into least possible number of nodes. Mutually exclusive with ttl_seconds_after_empty. - consolidation = object({ + # Ideally `true` by default, but conflicts with `ttl_seconds_after_empty`, which was previously the only option. + consolidation = optional(object({ enabled = bool - }) + }), { enabled = false }) # Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter total_cpu_limit = string # Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter @@ -38,18 +40,23 @@ variable "provisioners" { key = string effect = string value = string - }))) + })), []) startup_taints = optional(list(object({ key = string effect = string value = string - }))) + })), []) # Karpenter provisioner metadata options. See https://karpenter.sh/v0.18.0/aws/provisioning/#metadata-options for more details - metadata_options = optional(map(string), {}) + metadata_options = optional(object({ + httpEndpoint = optional(string, "enabled"), # valid values: enabled, disabled + httpProtocolIPv6 = optional(string, "disabled"), # valid values: enabled, disabled + httpPutResponseHopLimit = optional(number, 2), # limit of 1 discouraged because it keeps Pods from reaching metadata service + httpTokens = optional(string, "required") # valid values: required, optional + })), # The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM) ami_family = string # Karpenter provisioner block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes. Karpenter uses default block device mappings for the AMI Family specified. For example, the Bottlerocket AMI Family defaults with two block device mappings. See https://karpenter.sh/v0.18.0/aws/provisioning/#block-device-mappings for more details - block_device_mappings = list(object({ + block_device_mappings = optional(list(object({ deviceName = string ebs = optional(object({ volumeSize = string @@ -61,7 +68,7 @@ variable "provisioners" { snapshotID = optional(string) throughput = optional(number) })) - })) + })), []) })) description = "Karpenter provisioners config" } diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 39ccf242b..c92a3d5eb 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -1,6 +1,8 @@ # Component: `eks/karpenter` This component provisions [Karpenter](https://karpenter.sh) on an EKS cluster. +It requires at least version 0.19.0 of Karpenter, though you are encouraged to +use the latest version. ## Usage @@ -85,7 +87,6 @@ Service role name AWSServiceRoleForEC2Spot has been taken in this account, pleas ``` For more details, see: - - https://karpenter.sh/v0.18.0/getting-started/getting-started-with-terraform/ - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html - https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html @@ -279,8 +280,8 @@ For your cluster, you will need to review the following configurations for the K For more details, refer to: - - https://karpenter.sh/v0.18.0/provisioner/#specrequirements - - https://karpenter.sh/v0.18.0/aws/provisioning + - https://karpenter.sh/v0.28.0/provisioner/#specrequirements + - https://karpenter.sh/v0.28.0/aws/provisioning - https://aws.github.io/aws-eks-best-practices/karpenter/#creating-provisioners - https://aws.github.io/aws-eks-best-practices/karpenter - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index 169393087..5d70e6526 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -136,10 +136,13 @@ module "karpenter" { # karpenter-specific values yamlencode({ settings = { + # This configuration of settings requires Karpenter chart v0.19.0 or later aws = { defaultInstanceProfile = one(aws_iam_instance_profile.default[*].name) clusterName = local.eks_cluster_id - clusterEndpoint = local.eks_cluster_endpoint + # clusterEndpoint not needed as of v0.25.0 + clusterEndpoint = local.eks_cluster_endpoint + tags = module.this.tags } } }), From ca80ce2a735a1004d2bfac6c1c497cacb3817d48 Mon Sep 17 00:00:00 2001 From: Nuru Date: Fri, 14 Jul 2023 16:33:54 -0700 Subject: [PATCH 181/501] [aws-teams] Remove obsolete restriction on assuming roles in identity account (#761) --- modules/aws-teams/policy-team-role-access.tf | 9 --------- 1 file changed, 9 deletions(-) diff --git a/modules/aws-teams/policy-team-role-access.tf b/modules/aws-teams/policy-team-role-access.tf index d207bf335..bdbc88a93 100644 --- a/modules/aws-teams/policy-team-role-access.tf +++ b/modules/aws-teams/policy-team-role-access.tf @@ -22,15 +22,6 @@ data "aws_iam_policy_document" "team_role_access" { actions = ["sts:GetCallerIdentity"] resources = ["*"] } - - statement { - sid = "DenyIdentityAssumeRole" - effect = "Deny" - actions = ["sts:AssumeRole"] - resources = [ - format("arn:%s:iam::%s:role/*", local.aws_partition, local.identity_account_id), - ] - } } resource "aws_iam_policy" "team_role_access" { From 62e29b4a341e6ca3fa5fb4db5ce5ae12aa4348a8 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Fri, 14 Jul 2023 20:31:17 -0400 Subject: [PATCH 182/501] Update `alb` and `eks/alb-controller` components (#760) --- modules/alb/main.tf | 12 ++++++++++++ modules/eks/alb-controller/README.md | 3 +-- modules/eks/alb-controller/main.tf | 6 +----- modules/eks/alb-controller/variables.tf | 6 ------ 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/modules/alb/main.tf b/modules/alb/main.tf index 426ac9df4..11249a917 100644 --- a/modules/alb/main.tf +++ b/modules/alb/main.tf @@ -7,12 +7,20 @@ module "alb" { certificate_arn = module.remote_dns.outputs.certificate.arn internal = var.internal + http_port = var.http_port + http_ingress_cidr_blocks = var.http_ingress_cidr_blocks + http_ingress_prefix_list_ids = var.http_ingress_prefix_list_ids + https_port = var.https_port + https_ingress_cidr_blocks = var.https_ingress_cidr_blocks + https_ingress_prefix_list_ids = var.https_ingress_prefix_list_ids http_enabled = var.http_enabled https_enabled = var.https_enabled http2_enabled = var.http2_enabled http_redirect = var.http_redirect https_ssl_policy = var.https_ssl_policy access_logs_enabled = var.access_logs_enabled + access_logs_prefix = var.access_logs_prefix + access_logs_s3_bucket_id = var.access_logs_s3_bucket_id alb_access_logs_s3_bucket_force_destroy = var.alb_access_logs_s3_bucket_force_destroy cross_zone_load_balancing_enabled = var.cross_zone_load_balancing_enabled idle_timeout = var.idle_timeout @@ -20,14 +28,18 @@ module "alb" { deletion_protection_enabled = var.deletion_protection_enabled deregistration_delay = var.deregistration_delay health_check_path = var.health_check_path + health_check_port = var.health_check_port health_check_timeout = var.health_check_timeout health_check_healthy_threshold = var.health_check_healthy_threshold health_check_unhealthy_threshold = var.health_check_unhealthy_threshold health_check_interval = var.health_check_interval health_check_matcher = var.health_check_matcher target_group_port = var.target_group_port + target_group_protocol = var.target_group_protocol + target_group_name = var.target_group_name target_group_target_type = var.target_group_target_type stickiness = var.stickiness + lifecycle_rule_enabled = var.lifecycle_rule_enabled context = module.this.context } diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index ae2bc99f8..301b6243b 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -67,7 +67,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.7.0 | +| [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.9.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -132,7 +132,6 @@ components: | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | -| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `null` | no | ## Outputs diff --git a/modules/eks/alb-controller/main.tf b/modules/eks/alb-controller/main.tf index c0d726ff9..76bef6f2a 100644 --- a/modules/eks/alb-controller/main.tf +++ b/modules/eks/alb-controller/main.tf @@ -1,10 +1,6 @@ -locals { - enabled = module.this.enabled -} - module "alb_controller" { source = "cloudposse/helm-release/aws" - version = "0.7.0" + version = "0.9.1" name = "" # avoids hitting length restrictions on IAM Role names chart = var.chart diff --git a/modules/eks/alb-controller/variables.tf b/modules/eks/alb-controller/variables.tf index 70e6a32b5..0c14e0a45 100644 --- a/modules/eks/alb-controller/variables.tf +++ b/modules/eks/alb-controller/variables.tf @@ -68,12 +68,6 @@ variable "atomic" { default = true } -variable "wait" { - type = bool - description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`." - default = null -} - variable "chart_values" { type = any description = "Additional values to yamlencode as `helm_release` values." From e0da7cd4bdf01c1f6bf422a58c44ee8e2cd28a2b Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 14 Jul 2023 23:47:10 -0400 Subject: [PATCH 183/501] fix: ecs capacity provider typing (#762) --- modules/ecs/README.md | 2 +- modules/ecs/variables.tf | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/ecs/README.md b/modules/ecs/README.md index 5ff2e74ae..5cb8eb16f 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -87,7 +87,7 @@ components: | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [capacity\_providers\_ec2](#input\_capacity\_providers\_ec2) | EC2 autoscale groups capacity providers |
map(object({
instance_type = string
max_size = number
security_group_ids = optional(list(string), [])
min_size = optional(number, 0)
image_id = optional(string)
instance_initiated_shutdown_behavior = optional(string, "terminate")
key_name = optional(string, "")
user_data = optional(string, "")
enable_monitoring = optional(bool, true)
instance_warmup_period = optional(number, 300)
maximum_scaling_step_size = optional(number, 1)
minimum_scaling_step_size = optional(number, 1)
target_capacity_utilization = optional(number, 100)
ebs_optimized = optional(bool, false)
block_device_mappings = optional(list(object({
device_name = string
no_device = bool
virtual_name = string
ebs = object({
delete_on_termination = bool
encrypted = bool
iops = number
kms_key_id = string
snapshot_id = string
volume_size = number
volume_ = string
})
})), [])
instance_market_options = optional(object({
market_ = string
spot_options = object({
block_duration_minutes = number
instance_interruption_behavior = string
max_price = number
spot_instance_ = string
valid_until = string
})
}))
instance_refresh = optional(object({
strategy = string
preferences = object({
instance_warmup = number
min_healthy_percentage = number
})
triggers = list(string)
}))
mixed_instances_policy = optional(object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
}), {
instances_distribution = null
})
placement = optional(object({
affinity = string
availability_zone = string
group_name = string
host_id = string
tenancy = string
}))
credit_specification = optional(object({
cpu_credits = string
}))
elastic_gpu_specifications = optional(object({
type = string
}))
disable_api_termination = optional(bool, false)
default_cooldown = optional(number, 300)
health_check_grace_period = optional(number, 300)
force_delete = optional(bool, false)
termination_policies = optional(list(string), ["Default"])
suspended_processes = optional(list(string), [])
placement_group = optional(string, "")
metrics_granularity = optional(string, "1Minute")
enabled_metrics = optional(list(string), [
"GroupMinSize",
"GroupMaxSize",
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupPendingInstances",
"GroupStandbyInstances",
"GroupTerminatingInstances",
"GroupTotalInstances",
"GroupInServiceCapacity",
"GroupPendingCapacity",
"GroupStandbyCapacity",
"GroupTerminatingCapacity",
"GroupTotalCapacity",
"WarmPoolDesiredCapacity",
"WarmPoolWarmedCapacity",
"WarmPoolPendingCapacity",
"WarmPoolTerminatingCapacity",
"WarmPoolTotalCapacity",
"GroupAndWarmPoolDesiredCapacity",
"GroupAndWarmPoolTotalCapacity",
])
wait_for_capacity_timeout = optional(string, "10m")
service_linked_role_arn = optional(string, "")
metadata_http_endpoint_enabled = optional(bool, true)
metadata_http_put_response_hop_limit = optional(number, 2)
metadata_http_tokens_required = optional(bool, true)
metadata_http_protocol_ipv6_enabled = optional(bool, false)
tag_specifications_resource_types = optional(set(string), ["instance", "volume"])
max_instance_lifetime = optional(number, null)
capacity_rebalance = optional(bool, false)
warm_pool = optional(object({
pool_state = string
min_size = number
max_group_prepared_capacity = number
}))
}))
| `{}` | no | +| [capacity\_providers\_ec2](#input\_capacity\_providers\_ec2) | EC2 autoscale groups capacity providers |
map(object({
instance_type = string
max_size = number
security_group_ids = optional(list(string), [])
min_size = optional(number, 0)
image_id = optional(string)
instance_initiated_shutdown_behavior = optional(string, "terminate")
key_name = optional(string, "")
user_data = optional(string, "")
enable_monitoring = optional(bool, true)
instance_warmup_period = optional(number, 300)
maximum_scaling_step_size = optional(number, 1)
minimum_scaling_step_size = optional(number, 1)
target_capacity_utilization = optional(number, 100)
ebs_optimized = optional(bool, false)
block_device_mappings = optional(list(object({
device_name = string
no_device = bool
virtual_name = string
ebs = object({
delete_on_termination = bool
encrypted = bool
iops = number
kms_key_id = string
snapshot_id = string
volume_size = number
volume_type = string
})
})), [])
instance_market_options = optional(object({
market_type = string
spot_options = object({
block_duration_minutes = number
instance_interruption_behavior = string
max_price = number
spot_instance_type = string
valid_until = string
})
}))
instance_refresh = optional(object({
strategy = string
preferences = object({
instance_warmup = number
min_healthy_percentage = number
})
triggers = list(string)
}))
mixed_instances_policy = optional(object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
}), {
instances_distribution = null
})
placement = optional(object({
affinity = string
availability_zone = string
group_name = string
host_id = string
tenancy = string
}))
credit_specification = optional(object({
cpu_credits = string
}))
elastic_gpu_specifications = optional(object({
type = string
}))
disable_api_termination = optional(bool, false)
default_cooldown = optional(number, 300)
health_check_grace_period = optional(number, 300)
force_delete = optional(bool, false)
termination_policies = optional(list(string), ["Default"])
suspended_processes = optional(list(string), [])
placement_group = optional(string, "")
metrics_granularity = optional(string, "1Minute")
enabled_metrics = optional(list(string), [
"GroupMinSize",
"GroupMaxSize",
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupPendingInstances",
"GroupStandbyInstances",
"GroupTerminatingInstances",
"GroupTotalInstances",
"GroupInServiceCapacity",
"GroupPendingCapacity",
"GroupStandbyCapacity",
"GroupTerminatingCapacity",
"GroupTotalCapacity",
"WarmPoolDesiredCapacity",
"WarmPoolWarmedCapacity",
"WarmPoolPendingCapacity",
"WarmPoolTerminatingCapacity",
"WarmPoolTotalCapacity",
"GroupAndWarmPoolDesiredCapacity",
"GroupAndWarmPoolTotalCapacity",
])
wait_for_capacity_timeout = optional(string, "10m")
service_linked_role_arn = optional(string, "")
metadata_http_endpoint_enabled = optional(bool, true)
metadata_http_put_response_hop_limit = optional(number, 2)
metadata_http_tokens_required = optional(bool, true)
metadata_http_protocol_ipv6_enabled = optional(bool, false)
tag_specifications_resource_types = optional(set(string), ["instance", "volume"])
max_instance_lifetime = optional(number, null)
capacity_rebalance = optional(bool, false)
warm_pool = optional(object({
pool_state = string
min_size = number
max_group_prepared_capacity = number
}))
}))
| `{}` | no | | [capacity\_providers\_fargate](#input\_capacity\_providers\_fargate) | Use FARGATE capacity provider | `bool` | `true` | no | | [capacity\_providers\_fargate\_spot](#input\_capacity\_providers\_fargate\_spot) | Use FARGATE\_SPOT capacity provider | `bool` | `false` | no | | [container\_insights\_enabled](#input\_container\_insights\_enabled) | Whether or not to enable container insights | `bool` | `true` | no | diff --git a/modules/ecs/variables.tf b/modules/ecs/variables.tf index 89498b493..6c0ccd171 100644 --- a/modules/ecs/variables.tf +++ b/modules/ecs/variables.tf @@ -160,16 +160,16 @@ variable "capacity_providers_ec2" { kms_key_id = string snapshot_id = string volume_size = number - volume_ = string + volume_type = string }) })), []) instance_market_options = optional(object({ - market_ = string + market_type = string spot_options = object({ block_duration_minutes = number instance_interruption_behavior = string max_price = number - spot_instance_ = string + spot_instance_type = string valid_until = string }) })) From 884ab908f3afa0e60bfea9dbacb842e2d8c727a5 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 18 Jul 2023 12:13:56 -0400 Subject: [PATCH 184/501] fix: argocd flags, versions, and expressions (#753) Co-authored-by: aknysh --- modules/ecr/README.md | 1 + modules/ecr/outputs.tf | 5 + modules/eks/argocd/README.md | 366 ++++++++++++++++-- modules/eks/argocd/data.tf | 65 ++-- modules/eks/argocd/main.tf | 44 +-- modules/eks/argocd/outputs.tf | 7 +- modules/eks/argocd/remote-state.tf | 8 +- .../argocd/variables-argocd-notifications.tf | 22 +- modules/eks/argocd/variables-argocd.tf | 32 +- modules/eks/platform/README.md | 6 +- modules/eks/platform/main.tf | 13 +- modules/eks/platform/variables.tf | 2 +- 12 files changed, 436 insertions(+), 135 deletions(-) diff --git a/modules/ecr/README.md b/modules/ecr/README.md index 155e24e36..62def8f65 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -127,6 +127,7 @@ components: | [ecr\_user\_arn](#output\_ecr\_user\_arn) | ECR user ARN | | [ecr\_user\_name](#output\_ecr\_user\_name) | ECR user name | | [ecr\_user\_unique\_id](#output\_ecr\_user\_unique\_id) | ECR user unique ID assigned by AWS | +| [repository\_host](#output\_repository\_host) | ECR repository name | ## Related diff --git a/modules/ecr/outputs.tf b/modules/ecr/outputs.tf index bb574f900..790d8a423 100644 --- a/modules/ecr/outputs.tf +++ b/modules/ecr/outputs.tf @@ -3,6 +3,11 @@ output "ecr_repo_arn_map" { description = "Map of image names to ARNs" } +output "repository_host" { + value = try(split("/", module.ecr.repository_url)[0], null) + description = "ECR repository name" +} + output "ecr_repo_url_map" { value = module.ecr.repository_url_map description = "Map of image names to URLs" diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index ede4126e7..20f870be4 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -14,14 +14,132 @@ kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=v2.4.9" ## Usage -**Stack Level**: Regional +### Preparing AppProject repos: +First, make sure you have a GitHub repo ready to go. We have a component for this +called the `argocd-repo` component. It will create a GitHub repo and adds +some secrets and code owners. Most importantly, it configures an `applicationset.yaml` +that includes all the details for helm to create ArgoCD CRDs. These CRDs +let ArgoCD know how to fulfill changes to its repo. + +```yaml +components: + terraform: + argocd-repo-defaults: + metadata: + type: abstract + vars: + enabled: true + github_user: acme_admin + github_user_email: infra@acme.com + github_organization: ACME + github_codeowner_teams: + - "@ACME/acme-admins" + - "@ACME/CloudPosse" + - "@ACME/developers" + gitignore_entries: + - "**/.DS_Store" + - ".DS_Store" + - "**/.vscode" + - "./vscode" + - ".idea/" + - ".vscode/" + permissions: + - team_slug: acme-admins + permission: admin + - team_slug: CloudPosse + permission: admin + - team_slug: developers + permission: push +``` + +### Injecting infrastructure details into applications +Second, your application repos could use values to best configure their +helm releases. We have an `eks/platform` component for exposing various +infra outputs. It takes remote state lookups and stores them into SSM. +We demonstrate how to pull the platform SSM parameters later. Here's an +example `eks/platform` config: + +```yaml +components: + terraform: + eks/platform: + metadata: + type: abstract + component: eks/platform + backend: + s3: + workspace_key_prefix: platform + deps: + - catalog/eks/cluster + - catalog/eks/alb-controller-ingress-group + - catalog/acm + vars: + enabled: true + name: "platform" + eks_component_name: eks/cluster + ssm_platform_path: /platform/%s/%s + references: + default_alb_ingress_group: + component: eks/alb-controller-ingress-group + output: .group_name + default_ingress_domain: + component: dns-delegated + environment: gbl + output: "[.zones[].name][-1]" + + eks/platform/acm: + metadata: + component: eks/platform + inherits: + - eks/platform + vars: + eks_component_name: eks/cluster + references: + default_ingress_domain: + component: acm + environment: use2 + output: .domain_name + + eks/platform/dev: + metadata: + component: eks/platform + inherits: + - eks/platform + vars: + platform_environment: dev + + acm/qa2: + settings: + spacelift: + workspace_enabled: true + metadata: + component: acm + vars: + enabled: true + name: acm-qa2 + tags: + Team: sre + Service: acm + process_domain_validation_options: true + validation_method: DNS + dns_private_zone_enabled: false + certificate_authority_enabled: false +``` + +In the previous sample we create platform settings for a `dev` platform and a +`qa2` platform. Understand that these are arbitrary titles that are used to separate +the SSM parameters so that if, say, a particular hostname is needed, we can safely +select the right hostname using a moniker such as `qa2`. These otherwise are meaningless +and do not need to align with any particular stage or tenant. + +### ArgoCD on SAML / AWS Identity Center (formerly aws-sso) Here's an example snippet for how to use this component: ```yaml components: terraform: - argocd: + eks/argocd: settings: spacelift: workspace_enabled: true @@ -42,31 +160,210 @@ components: saml_admin_role: ArgoCD-non-prod-admin saml_readonly_role: ArgoCD-non-prod-observer argocd_repo_name: argocd-deploy-non-prod - chart_values: {} + # Note: the IDs for AWS Identity Center groups will change if you alter/replace them: + argocd_rbac_groups: + - group: deadbeef-dead-beef-dead-beefdeadbeef + role: admin + - group: badca7sb-add0-65ba-dca7-sbadd065badc + role: reader + chart_values: + global: + logging: + format: json + level: warn + + sso-saml/aws-sso: + settings: + spacelift: + workspace_enabled: true + metadata: + component: sso-saml-provider + vars: + enabled: true + ssm_path_prefix: "/sso/saml/aws-sso" + usernameAttr: email + emailAttr: email + groupsAttr: groups +``` + +Note, if you set up `sso-saml-provider`, you will need to restart DEX on your EKS cluster +manually: +```bash +kubectl delete pod -n argocd +``` + +The configuration above will work for AWS Identity Center if you have +the following attributes in a +[Custom SAML 2.0 application](https://docs.aws.amazon.com/singlesignon/latest/userguide/samlapps.html): + +| attribute name | value | type | +|:---------------|:----------------|:------------| +| Subject | ${user:subject} | persistent | +| email | ${user:email} | unspecified | +| groups | ${user:groups} | unspecified | + +You will also need to assign AWS Identity Center groups to your Custom SAML 2.0 +application. Make a note of each group and replace the IDs in the `argocd_rbac_groups` +var accordingly. + +### Google Workspace OIDC + +To use Google OIDC: + +```yaml + oidc_enabled: true + saml_enabled: false + oidc_providers: + google: + uses_dex: true + type: google + id: google + name: Google + serviceAccountAccess: + enabled: true + key: googleAuth.json + value: /sso/oidc/google/serviceaccount + admin_email: an_actual_user@acme.com + config: + # This filters emails when signing in with Google to only this domain. helpful for picking the right one. + hostedDomains: + - acme.com + clientID: /sso/saml/google/clientid + clientSecret: /sso/saml/google/clientsecret ``` -to use google OIDC: +### Working with ArgoCD and GitHub + +Here's a simple GitHub action that will trigger a deployment in ArgoCD: ```yaml - oidc_enabled: true - saml_enabled: false - oidc_providers: - google: - uses_dex: true - type: google - id: google - name: Google - serviceAccountAccess: - enabled: true - key: googleAuth.json - value: /sso/oidc/google/serviceaccount - admin_email: an_actual_user@acme.com - config: - # This filters emails when signing in with Google to only this domain. helpful for picking the right one. - hostedDomains: - - acme.com - clientID: /sso/saml/google/clientid - clientSecret: /sso/saml/google/clientsecret +# NOTE: Example will show dev, and qa2 +name: argocd-deploy +on: + push: + branches: + - main +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2.1.0 + with: + aws-region: us-east-2 + role-to-assume: arn:aws:iam::123456789012:role/github-action-worker + - name: Build + shell: bash + run: docker build -t some.docker.repo/acme/app . & docker push some.docker.repo/acmo/app + - name: Checkout Argo Configuration + uses: actions/checkout@v3 + with: + repository: acme/argocd-deploy-non-prod + ref: main + path: argocd-deploy-non-prod + - name: Deploy to dev + shell: bash + run: | + echo Rendering helmfile: + helmfile \ + --namespace acme-app \ + --environment dev \ + --file deploy/app/release.yaml \ + --state-values-file <(aws ssm get-parameter --name /platform/dev),<(docker image inspect some.docker.repo/acme/app) \ + template > argocd-deploy-non-prod/plat/use2-dev/apps/my-preview-acme-app/manifests/resources.yaml + echo Updating sha for app: + yq e '' -i argocd-deploy-non-prod/plat/use2-dev/apps/my-preview-acme-app/config.yaml + echo Committing new helmfile + pushd argocd-deploy-non-prod + git add --all + git commit --message 'Updating acme-app' + git push + popd +``` + +In the above example, we make a few assumptions: +- You've already made the app in ArgoCD by creating a YAML file + in your non-prod ArgoCD repo at the path + `plat/use2-dev/apps/my-preview-acme-app/config.yaml` with contents: + +```yaml +app_repository: acme/app +app_commit: deadbeefdeadbeef +app_hostname: https://some.app.endpoint/landing_page +name: my-feature-branch.acme-app +namespace: my-feature-branch +manifests: plat/use2-dev/apps/my-preview-acme-app/manifests +``` + +- you have set up `ecr` with permissions for github to push docker images to it +- you already have your `ApplicationSet` and `AppProject` crd's in + `plat/use2-dev/argocd/applicationset.yaml`, which should be generated by our `argocd-repo` + component. +- your app has a [helmfile template](https://helmfile.readthedocs.io/en/latest/#templating) + in `deploy/app/release.yaml` +- that helmfile template can accept both the `eks/platform` config which is pulled from + ssm at the path configured in `eks/platform/defaults` +- the helmfile template can update container resources using the output of `docker image inspect` + +### Notifications + +Here's a configuration for letting argocd send notifications back to GitHub: + +```yaml +components: + terraform: + eks/argocd/notifications: + metadata: + type: abstract + component: eks/argocd + vars: + notifications_triggers: + trigger_on-deployed: + - when: app.status.operationState.phase in ['Succeeded'] + oncePer: app.status.sync.revision + send: [app-deployed, github-commit-status] + + notifications_templates: + template_app-deployed: + message: | + Application {{ .app.metadata.name }} is now running new version of deployments manifests. + github: + status: + state: success + label: "continuous-delivery/{{ .app.metadata.name }}" + targetURL: "{{ .context.argocdUrl }}/applications/{{ .app.metadata.name }}?operation=true" + + template_github-commit-status: + message: | + Application {{ .app.metadata.name }} is now running new version of deployments manifests. + webhook: + github-commit-status: + method: POST + path: /repos/{{call .repo.FullNameByRepoURL .app.metadata.annotations.app_repository}}/statuses/{{.app.metadata.annotations.app_commit}} + body: | + { + {{if eq .app.status.operationState.phase "Running"}} "state": "pending"{{end}} + {{if eq .app.status.operationState.phase "Succeeded"}} "state": "success"{{end}} + {{if eq .app.status.operationState.phase "Error"}} "state": "error"{{end}} + {{if eq .app.status.operationState.phase "Failed"}} "state": "error"{{end}}, + "description": "ArgoCD", + "target_url": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", + "context": "continuous-delivery/{{.app.metadata.name}}" + } + + notifications_notifiers: + service_github: + appID: 123456 + installationID: 12345678 + service_webhook: + github-commit-status: + url: https://api.github.com + headers: + - name: Authorization + value: token $service_webhook_github-commit-status_github-token + ``` @@ -91,16 +388,16 @@ to use google OIDC: | Name | Source | Version | |------|--------|---------| -| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.3.0 | -| [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.3.0 | -| [argocd\_repo](#module\_argocd\_repo) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.9.1 | +| [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.9.1 | +| [argocd\_repo](#module\_argocd\_repo) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles\_config\_secrets](#module\_iam\_roles\_config\_secrets) | ../../account-map/modules/iam-roles | n/a | | [oidc\_gsuite\_service\_providers\_providers\_store\_read](#module\_oidc\_gsuite\_service\_providers\_providers\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.10.0 | | [oidc\_providers\_store\_read](#module\_oidc\_providers\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.10.0 | -| [saml\_sso\_providers](#module\_saml\_sso\_providers) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [saml\_sso\_providers](#module\_saml\_sso\_providers) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -108,12 +405,8 @@ to use google OIDC: | Name | Type | |------|------| | [kubernetes_secret.oidc_gsuite_service_account](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | -| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_eks_cluster_auth.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | | [aws_ssm_parameter.github_deploy_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [aws_ssm_parameter.oidc_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | -| [aws_ssm_parameter.oidc_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameters_by_path.argocd_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | ## Inputs @@ -176,13 +469,10 @@ to use google OIDC: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [notifications\_default\_triggers](#input\_notifications\_default\_triggers) | Default notification Triggers to configure.

See: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/triggers/#default-triggers
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/790438efebf423c2d56cb4b93471f4adb3fcd448/charts/argo-cd/values.yaml#L2841) | `map(list(string))` | `{}` | no | | [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = optional(number)
installationID = optional(number)
privateKey = optional(string)
}))
})
| `null` | no | | [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
}))
| `{}` | no | | [notifications\_triggers](#input\_notifications\_triggers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
map(list(
object({
oncePer = optional(string)
send = list(string)
when = string
})
))
| `{}` | no | | [oidc\_enabled](#input\_oidc\_enabled) | Toggles OIDC integration in the deployed chart | `bool` | `false` | no | -| [oidc\_issuer](#input\_oidc\_issuer) | OIDC issuer URL | `string` | `""` | no | -| [oidc\_name](#input\_oidc\_name) | Name of the OIDC resource | `string` | `""` | no | | [oidc\_providers](#input\_oidc\_providers) | OIDC providers components, clientID and clientSecret should be passed as SSM parameters (denoted by leading slash) | `any` | `{}` | no | | [oidc\_rbac\_scopes](#input\_oidc\_rbac\_scopes) | OIDC RBAC scopes to request | `string` | `"[argocd_realm_access]"` | no | | [oidc\_requested\_scopes](#input\_oidc\_requested\_scopes) | Set of OIDC scopes to request | `string` | `"[\"openid\", \"profile\", \"email\", \"groups\"]"` | no | @@ -191,7 +481,6 @@ to use google OIDC: | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| `null` | no | | [saml\_enabled](#input\_saml\_enabled) | Toggles SAML integration in the deployed chart | `bool` | `false` | no | -| [saml\_okta\_app\_name](#input\_saml\_okta\_app\_name) | Name of the Okta SAML Integration | `string` | `"ArgoCD"` | no | | [saml\_rbac\_scopes](#input\_saml\_rbac\_scopes) | SAML RBAC scopes to request | `string` | `"[email,groups]"` | no | | [saml\_sso\_providers](#input\_saml\_sso\_providers) | SAML SSO providers components |
map(object({
component = string
}))
| `{}` | no | | [slack\_notifications\_enabled](#input\_slack\_notifications\_enabled) | Whether or not to enable Slack notifications. | `bool` | `false` | no | @@ -212,7 +501,8 @@ to use google OIDC: | Name | Description | |------|-------------| -| [metadata](#output\_metadata) | Block status of the deployed release | +| [argocd\_apps\_metadata](#output\_argocd\_apps\_metadata) | Block status of the deployed ArgoCD apps release | +| [metadata](#output\_metadata) | Block status of the deployed ArgoCD release | ## References diff --git a/modules/eks/argocd/data.tf b/modules/eks/argocd/data.tf index 7803bfb55..9f1d8ed16 100644 --- a/modules/eks/argocd/data.tf +++ b/modules/eks/argocd/data.tf @@ -1,9 +1,9 @@ locals { - kubernetes_host = local.enabled ? data.aws_eks_cluster.kubernetes[0].endpoint : "" - kubernetes_token = local.enabled ? data.aws_eks_cluster_auth.kubernetes[0].token : "" - kubernetes_cluster_ca_certificate = local.enabled ? base64decode(data.aws_eks_cluster.kubernetes[0].certificate_authority[0].data) : "" - oidc_client_id = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_id[0].value : "" - oidc_client_secret = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_secret[0].value : "" + # kubernetes_host = local.enabled ? data.aws_eks_cluster.kubernetes[0].endpoint : "" + # kubernetes_token = local.enabled ? data.aws_eks_cluster_auth.kubernetes[0].token : "" + # kubernetes_cluster_ca_certificate = local.enabled ? base64decode(data.aws_eks_cluster.kubernetes[0].certificate_authority[0].data) : "" + # oidc_client_id = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_id[0].value : "" + # oidc_client_secret = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_secret[0].value : "" # saml_certificate = base64encode(format("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", module.okta_saml_apps.outputs.certificates[var.saml_okta_app_name])) # @@ -13,31 +13,35 @@ locals { # NOTE: OIDC parameters are global, hence why they use a separate AWS provider -data "aws_ssm_parameter" "oidc_client_id" { - count = local.oidc_enabled_count - name = var.ssm_oidc_client_id - with_decryption = true - - provider = aws.config_secrets -} - -data "aws_ssm_parameter" "oidc_client_secret" { - count = local.oidc_enabled_count - name = var.ssm_oidc_client_secret - with_decryption = true - - provider = aws.config_secrets -} - -data "aws_eks_cluster" "kubernetes" { - count = local.count_enabled - name = module.eks.outputs.eks_cluster_id -} - -data "aws_eks_cluster_auth" "kubernetes" { - count = local.count_enabled - name = module.eks.outputs.eks_cluster_id -} +# +# These variables are depreciated but should not yet be removed. Future iterations of this component will delete these variables +# + +#data "aws_ssm_parameter" "oidc_client_id" { +# count = local.oidc_enabled_count +# name = var.ssm_oidc_client_id +# with_decryption = true +# +# provider = aws.config_secrets +#} + +#data "aws_ssm_parameter" "oidc_client_secret" { +# count = local.oidc_enabled_count +# name = var.ssm_oidc_client_secret +# with_decryption = true +# +# provider = aws.config_secrets +#} + +#data "aws_eks_cluster" "kubernetes" { +# count = local.count_enabled +# name = module.eks.outputs.eks_cluster_id +#} + +#data "aws_eks_cluster_auth" "kubernetes" { +# count = local.count_enabled +# name = module.eks.outputs.eks_cluster_id +#} data "aws_ssm_parameter" "github_deploy_key" { for_each = local.enabled ? var.argocd_repositories : {} @@ -51,6 +55,7 @@ data "aws_ssm_parameter" "github_deploy_key" { module.this.stage ) ) : null + with_decryption = true provider = aws.config_secrets diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index c5e3a3819..1c9c5724b 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -26,9 +26,7 @@ locals { locals { kubernetes_namespace = var.kubernetes_namespace - count_enabled = local.enabled ? 1 : 0 oidc_enabled = local.enabled && var.oidc_enabled - oidc_enabled_count = local.oidc_enabled ? 1 : 0 saml_enabled = local.enabled && var.saml_enabled argocd_repositories = local.enabled ? { for k, v in var.argocd_repositories : k => { @@ -53,10 +51,9 @@ locals { regional_service_discovery_domain = "${module.this.environment}.${module.dns_gbl_delegated.outputs.default_domain_name}" host = var.host != "" ? var.host : format("%s.%s", coalesce(var.alb_name, var.name), local.regional_service_discovery_domain) enable_argo_workflows_auth = local.saml_enabled && var.argo_enable_workflows_auth - enable_argo_workflows_auth_count = local.enable_argo_workflows_auth ? 1 : 0 - argo_workflows_host = "${var.argo_workflows_name}.${local.regional_service_discovery_domain}" + # argo_workflows_host = "${var.argo_workflows_name}.${local.regional_service_discovery_domain}" - oidc_values = values(local.oidc_providers_merged)[0] + oidc_values = local.oidc_enabled ? values(local.oidc_providers_merged)[0] : {} oidc_config_map = local.oidc_enabled && !lookup(local.oidc_values, "uses_dex", true) ? { server : { @@ -98,8 +95,9 @@ locals { caData = base64encode(format("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", module.saml_sso_providers[name].outputs.ca)) redirectURI = format("https://%s/api/dex/callback", local.host) entityIssuer = format("https://%s/api/dex/callback", local.host) - usernameAttr = "name" - emailAttr = "email" + usernameAttr = module.saml_sso_providers[name].outputs.usernameAttr + emailAttr = module.saml_sso_providers[name].outputs.emailAttr + groupsAttr = module.saml_sso_providers[name].outputs.groupsAttr ssoIssuer = module.saml_sso_providers[name].outputs.issuer } } @@ -164,7 +162,7 @@ locals { } } -#https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/google/#configure-dex +# https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/google/#configure-dex module "oidc_gsuite_service_providers_providers_store_read" { for_each = { for k, v in var.oidc_providers : k => v if lookup(v.serviceAccountAccess, "enabled", false) } source = "cloudposse/ssm-parameter-store/aws" @@ -175,6 +173,7 @@ module "oidc_gsuite_service_providers_providers_store_read" { if v.id == "google" && lookup(v.serviceAccountAccess, "enabled", false) ] } + resource "kubernetes_secret" "oidc_gsuite_service_account" { for_each = { for k, v in var.oidc_providers : k => v if lookup(v.serviceAccountAccess, "enabled", false) } metadata { @@ -189,7 +188,7 @@ resource "kubernetes_secret" "oidc_gsuite_service_account" { module "argocd" { source = "cloudposse/helm-release/aws" - version = "0.3.0" + version = "0.9.1" name = "argocd" # avoids hitting length restrictions on IAM Role names chart = var.chart @@ -303,20 +302,21 @@ module "argocd_apps" { count = local.enabled && var.argocd_apps_enabled ? 1 : 0 source = "cloudposse/helm-release/aws" - version = "0.3.0" + version = "0.9.1" - name = "" # avoids hitting length restrictions on IAM Role names - chart = var.argocd_apps_chart - repository = var.argocd_apps_chart_repository - description = var.argocd_apps_chart_description - chart_version = var.argocd_apps_chart_version - kubernetes_namespace = var.kubernetes_namespace - create_namespace = var.create_namespace - wait = var.wait - atomic = var.atomic - cleanup_on_fail = var.cleanup_on_fail - timeout = var.timeout - enabled = local.enabled && var.argocd_apps_enabled + name = "" # avoids hitting length restrictions on IAM Role names + chart = var.argocd_apps_chart + repository = var.argocd_apps_chart_repository + description = var.argocd_apps_chart_description + chart_version = var.argocd_apps_chart_version + kubernetes_namespace = var.kubernetes_namespace + create_namespace = var.create_namespace + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + enabled = local.enabled && var.argocd_apps_enabled + eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer values = compact([ templatefile( "${path.module}/resources/argocd-apps-values.yaml.tpl", diff --git a/modules/eks/argocd/outputs.tf b/modules/eks/argocd/outputs.tf index 26b6561e7..782874758 100644 --- a/modules/eks/argocd/outputs.tf +++ b/modules/eks/argocd/outputs.tf @@ -1,4 +1,9 @@ output "metadata" { value = module.argocd.metadata - description = "Block status of the deployed release" + description = "Block status of the deployed ArgoCD release" +} + +output "argocd_apps_metadata" { + value = module.argocd_apps.metadata + description = "Block status of the deployed ArgoCD apps release" } diff --git a/modules/eks/argocd/remote-state.tf b/modules/eks/argocd/remote-state.tf index 9024293ae..fc6d9abf5 100644 --- a/modules/eks/argocd/remote-state.tf +++ b/modules/eks/argocd/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" environment = "gbl" component = "dns-delegated" @@ -20,7 +20,7 @@ module "dns_gbl_delegated" { module "saml_sso_providers" { for_each = local.enabled ? var.saml_sso_providers : {} source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = each.value.component @@ -31,7 +31,7 @@ module "argocd_repo" { for_each = local.enabled ? var.argocd_repositories : {} source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.4.3" component = each.key environment = each.value.environment diff --git a/modules/eks/argocd/variables-argocd-notifications.tf b/modules/eks/argocd/variables-argocd-notifications.tf index f03b2791d..bf83f51ed 100644 --- a/modules/eks/argocd/variables-argocd-notifications.tf +++ b/modules/eks/argocd/variables-argocd-notifications.tf @@ -58,18 +58,16 @@ variable "notifications_notifiers" { EOT } - -variable "notifications_default_triggers" { - type = map(list(string)) - default = {} - description = <<-EOT - Default notification Triggers to configure. - - See: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/triggers/#default-triggers - See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/790438efebf423c2d56cb4b93471f4adb3fcd448/charts/argo-cd/values.yaml#L2841) - EOT -} - +#variable "notifications_default_triggers" { +# type = map(list(string)) +# default = {} +# description = <<-EOT +# Default notification Triggers to configure. +# +# See: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/triggers/#default-triggers +# See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/790438efebf423c2d56cb4b93471f4adb3fcd448/charts/argo-cd/values.yaml#L2841) +# EOT +#} variable "slack_notifications_enabled" { type = bool diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index 6c0a787ca..be085d5da 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -101,17 +101,17 @@ variable "oidc_enabled" { default = false } -variable "oidc_issuer" { - type = string - description = "OIDC issuer URL" - default = "" -} - -variable "oidc_name" { - type = string - description = "Name of the OIDC resource" - default = "" -} +#variable "oidc_issuer" { +# type = string +# description = "OIDC issuer URL" +# default = "" +#} + +#variable "oidc_name" { +# type = string +# description = "Name of the OIDC resource" +# default = "" +#} variable "oidc_rbac_scopes" { type = string @@ -131,11 +131,11 @@ variable "saml_enabled" { default = false } -variable "saml_okta_app_name" { - type = string - description = "Name of the Okta SAML Integration" - default = "ArgoCD" -} +#variable "saml_okta_app_name" { +# type = string +# description = "Name of the Okta SAML Integration" +# default = "ArgoCD" +#} variable "saml_rbac_scopes" { type = string diff --git a/modules/eks/platform/README.md b/modules/eks/platform/README.md index 0fec581a7..9ed341e03 100644 --- a/modules/eks/platform/README.md +++ b/modules/eks/platform/README.md @@ -1,4 +1,4 @@ -# Component: `platform` +# Component: `eks/platform` This component maps another components' outputs into SSM parameter store to declare platform context used by CI/CD workflows. @@ -60,7 +60,6 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.9.0 | | [jq](#provider\_jq) | >= 0.2.1 | ## Modules @@ -77,7 +76,6 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | Name | Type | |------|------| -| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | | [jq_query.default](https://registry.terraform.io/providers/massdriver-cloud/jq/latest/docs/data-sources/query) | data source | ## Inputs @@ -89,7 +87,7 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/eks"` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | diff --git a/modules/eks/platform/main.tf b/modules/eks/platform/main.tf index d66f63e19..e9435de98 100644 --- a/modules/eks/platform/main.tf +++ b/modules/eks/platform/main.tf @@ -1,6 +1,4 @@ locals { - enabled = module.this.enabled - partition = join("", data.aws_partition.current[*].partition) metadata = { kube_version = { component = var.eks_component_name @@ -9,10 +7,6 @@ locals { } } -data "aws_partition" "current" { - count = local.enabled ? 1 : 0 -} - module "store_write" { source = "cloudposse/ssm-parameter-store/aws" version = "0.10.0" @@ -36,13 +30,18 @@ module "store_write" { description = "Platform metadata for ${module.eks.outputs.eks_cluster_id} cluster" } ]) + context = module.this.context } data "jq_query" "default" { for_each = var.references data = jsonencode(module.remote[each.key].outputs) - query = ".${each.value.output}" + # Query is left to be free form since setting this to something like `.` would + # mean you cannot handle arrays. For example, if you wanted to get the first + # element of an array, you would need to use `[0]` as the query, but having a + # query of `.` would not allow you to do that. It would render as '.[0]' + query = each.value.output } locals { diff --git a/modules/eks/platform/variables.tf b/modules/eks/platform/variables.tf index cd4bc163b..d905e7f31 100644 --- a/modules/eks/platform/variables.tf +++ b/modules/eks/platform/variables.tf @@ -19,7 +19,7 @@ variable "references" { variable "eks_component_name" { type = string description = "The name of the eks component" - default = "eks/eks" + default = "eks/cluster" } variable "ssm_platform_path" { From 0d8115a16012e0396fd7af76d4f97e0bb45651b2 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 18 Jul 2023 10:44:56 -0700 Subject: [PATCH 185/501] `alb` and `ssm-parameters` Upstream for Basic Use (#763) Co-authored-by: Andriy Knysh --- modules/alb/README.md | 3 +++ modules/alb/main.tf | 2 +- modules/alb/remote-state.tf | 15 +++++++++++++++ modules/alb/variables.tf | 12 ++++++++++++ modules/ssm-parameters/README.md | 4 ++-- modules/ssm-parameters/main.tf | 10 +++++++--- modules/ssm-parameters/variables.tf | 4 +++- modules/waf/README.md | 4 ++++ 8 files changed, 47 insertions(+), 7 deletions(-) diff --git a/modules/alb/README.md b/modules/alb/README.md index 2d134f3ae..ccc471ed1 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -36,6 +36,7 @@ No providers. |------|--------|---------| | [alb](#module\_alb) | cloudposse/alb/aws | 1.10.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [remote\_acm](#module\_remote\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [remote\_dns](#module\_remote\_dns) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [remote\_vpc](#module\_remote\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -51,6 +52,7 @@ No resources. | [access\_logs\_enabled](#input\_access\_logs\_enabled) | A boolean flag to enable/disable access\_logs | `bool` | `true` | no | | [access\_logs\_prefix](#input\_access\_logs\_prefix) | The S3 log bucket prefix | `string` | `""` | no | | [access\_logs\_s3\_bucket\_id](#input\_access\_logs\_s3\_bucket\_id) | An external S3 Bucket name to store access logs in. If specified, no logging bucket will be created. | `string` | `null` | no | +| [acm\_component\_name](#input\_acm\_component\_name) | Atmos `acm` component name | `string` | `"acm"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [alb\_access\_logs\_s3\_bucket\_force\_destroy](#input\_alb\_access\_logs\_s3\_bucket\_force\_destroy) | A boolean that indicates all objects should be deleted from the ALB access logs S3 bucket so that the bucket can be destroyed without error | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | @@ -60,6 +62,7 @@ No resources. | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [deregistration\_delay](#input\_deregistration\_delay) | The amount of time to wait in seconds before changing the state of a deregistering target to unused | `number` | `15` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_acm\_enabled](#input\_dns\_acm\_enabled) | If `true`, use the ACM ARN created by the given `dns-delegated` component. Otherwise, use the ACM ARN created by the given `acm` component. | `bool` | `false` | no | | [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | Atmos `dns-delegated` component name | `string` | `"dns-delegated"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | diff --git a/modules/alb/main.tf b/modules/alb/main.tf index 11249a917..50a708087 100644 --- a/modules/alb/main.tf +++ b/modules/alb/main.tf @@ -4,7 +4,7 @@ module "alb" { vpc_id = module.remote_vpc.outputs.vpc_id subnet_ids = module.remote_vpc.outputs.public_subnet_ids - certificate_arn = module.remote_dns.outputs.certificate.arn + certificate_arn = var.dns_acm_enabled ? module.remote_dns.outputs.certificate.arn : module.remote_acm.outputs.arn internal = var.internal http_port = var.http_port diff --git a/modules/alb/remote-state.tf b/modules/alb/remote-state.tf index 8414dda9d..9a5c1642a 100644 --- a/modules/alb/remote-state.tf +++ b/modules/alb/remote-state.tf @@ -13,5 +13,20 @@ module "remote_dns" { component = var.dns_delegated_component_name + # Ignore errors if component doesn't exist + ignore_errors = true + + context = module.this.context +} + +module "remote_acm" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + component = var.acm_component_name + + # Ignore errors if component doesn't exist + ignore_errors = true + context = module.this.context } diff --git a/modules/alb/variables.tf b/modules/alb/variables.tf index 71a6016b9..1968a6d4f 100644 --- a/modules/alb/variables.tf +++ b/modules/alb/variables.tf @@ -221,3 +221,15 @@ variable "dns_delegated_component_name" { default = "dns-delegated" description = "Atmos `dns-delegated` component name" } + +variable "acm_component_name" { + type = string + default = "acm" + description = "Atmos `acm` component name" +} + +variable "dns_acm_enabled" { + type = bool + default = false + description = "If `true`, use the ACM ARN created by the given `dns-delegated` component. Otherwise, use the ACM ARN created by the given `acm` component." +} diff --git a/modules/ssm-parameters/README.md b/modules/ssm-parameters/README.md index 5bfdf6ebd..f41fd4a75 100644 --- a/modules/ssm-parameters/README.md +++ b/modules/ssm-parameters/README.md @@ -77,8 +77,8 @@ components: | [params](#input\_params) | A map of parameter values to write to SSM Parameter Store |
map(object({
value = string
description = string
overwrite = optional(bool, false)
tier = optional(string, "Standard")
type = string
}))
| n/a | yes | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [sops\_source\_file](#input\_sops\_source\_file) | The relative path to the SOPS file which is consumed as the source for creating parameter resources. | `string` | n/a | yes | -| [sops\_source\_key](#input\_sops\_source\_key) | The SOPS key to pull from the source file. | `string` | n/a | yes | +| [sops\_source\_file](#input\_sops\_source\_file) | The relative path to the SOPS file which is consumed as the source for creating parameter resources. | `string` | `""` | no | +| [sops\_source\_key](#input\_sops\_source\_key) | The SOPS key to pull from the source file. | `string` | `""` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/ssm-parameters/main.tf b/modules/ssm-parameters/main.tf index 895737987..989a3c7a5 100644 --- a/modules/ssm-parameters/main.tf +++ b/modules/ssm-parameters/main.tf @@ -1,6 +1,9 @@ locals { - sops_yaml = yamldecode(data.sops_file.source.raw) - secret_params = nonsensitive(local.sops_yaml[var.sops_source_key]) + enabled = module.this.enabled + sops_enabled = local.enabled && length(var.sops_source_file) > 0 + + sops_yaml = local.sops_enabled ? yamldecode(data.sops_file.source[0].raw) : "" + secret_params = local.sops_enabled ? nonsensitive(local.sops_yaml[var.sops_source_key]) : {} secret_params_normalized = { for key, value in local.secret_params : @@ -13,11 +16,12 @@ locals { } } - params = var.enabled ? merge(var.params, local.secret_params_normalized) : {} + params = local.enabled ? merge(var.params, local.secret_params_normalized) : {} param_keys = keys(local.params) } data "sops_file" "source" { + count = local.sops_enabled ? 1 : 0 source_file = "${path.root}/${var.sops_source_file}" } diff --git a/modules/ssm-parameters/variables.tf b/modules/ssm-parameters/variables.tf index 51733adbc..e956f8a50 100644 --- a/modules/ssm-parameters/variables.tf +++ b/modules/ssm-parameters/variables.tf @@ -6,17 +6,19 @@ variable "region" { variable "sops_source_file" { type = string description = "The relative path to the SOPS file which is consumed as the source for creating parameter resources." + default = "" } variable "sops_source_key" { type = string description = "The SOPS key to pull from the source file." + default = "" } variable "kms_arn" { type = string - default = "" description = "The ARN of a KMS key used to encrypt and decrypt SecretString values" + default = "" } variable "params" { diff --git a/modules/waf/README.md b/modules/waf/README.md index a9f5da3d2..6a3524685 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -19,6 +19,10 @@ components: acl_name: default default_action: allow description: Default web ACL + visibility_config: + cloudwatch_metrics_enabled: false + metric_name: "default" + sampled_requests_enabled: false managed_rule_group_statement_rules: - name: "OWASP-10" # Rules are processed in order based on the value of priority, lowest number first From b4ba66cb443b05d88c5c896a5063174cc4274ed3 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 18 Jul 2023 17:00:05 -0400 Subject: [PATCH 186/501] feat: acm no longer requires zone (#765) --- modules/acm/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/acm/main.tf b/modules/acm/main.tf index 7683e90a8..a2234c44d 100644 --- a/modules/acm/main.tf +++ b/modules/acm/main.tf @@ -14,7 +14,7 @@ locals { } data "aws_route53_zone" "default" { - count = local.enabled ? 1 : 0 + count = local.enabled && var.process_domain_validation_options ? 1 : 0 name = length(var.zone_name) > 0 ? var.zone_name : module.dns_delegated.outputs.default_domain_name private_zone = local.private_enabled } From eb6c3f360b9f889092e7295011e2f246657a9070 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 18 Jul 2023 15:53:33 -0700 Subject: [PATCH 187/501] Aurora Postgres Enhanced Monitoring Input (#766) --- modules/aurora-mysql-resources/README.md | 2 +- modules/aurora-mysql-resources/variables.tf | 1 + modules/aurora-postgres/README.md | 1 + modules/aurora-postgres/cluster-regional.tf | 1 + modules/aurora-postgres/variables.tf | 6 ++++++ 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/aurora-mysql-resources/README.md b/modules/aurora-mysql-resources/README.md index ea61b3f4a..bb13ac3cd 100644 --- a/modules/aurora-mysql-resources/README.md +++ b/modules/aurora-mysql-resources/README.md @@ -92,7 +92,7 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [additional\_users](#input\_additional\_users) | Create additional database user for a service, specifying username, grants, and optional password.
If no password is specified, one will be generated. Username and password will be stored in
SSM parameter store under the service's key. |
map(object({
db_user : string
db_password : string
grants : list(object({
grant : list(string)
db : string
}))
}))
| `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [aurora\_mysql\_component\_name](#input\_aurora\_mysql\_component\_name) | Aurora MySQL component name to read the remote state from | `string` | n/a | yes | +| [aurora\_mysql\_component\_name](#input\_aurora\_mysql\_component\_name) | Aurora MySQL component name to read the remote state from | `string` | `"aurora-mysql"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/aurora-mysql-resources/variables.tf b/modules/aurora-mysql-resources/variables.tf index 142b453ea..816f1bccd 100644 --- a/modules/aurora-mysql-resources/variables.tf +++ b/modules/aurora-mysql-resources/variables.tf @@ -6,6 +6,7 @@ variable "region" { variable "aurora_mysql_component_name" { type = string description = "Aurora MySQL component name to read the remote state from" + default = "aurora-mysql" } variable "read_passwords_from_ssm" { diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index c51bb620d..c2bb93aac 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -161,6 +161,7 @@ components: | [engine](#input\_engine) | Name of the database engine to be used for the DB cluster | `string` | `"postgresql"` | no | | [engine\_mode](#input\_engine\_mode) | The database engine mode. Valid values: `global`, `multimaster`, `parallelquery`, `provisioned`, `serverless` | `string` | n/a | yes | | [engine\_version](#input\_engine\_version) | Engine version of the Aurora global database | `string` | `"13.4"` | no | +| [enhanced\_monitoring\_attributes](#input\_enhanced\_monitoring\_attributes) | Attributes used to format the Enhanced Monitoring IAM role. If this role hits IAM role length restrictions (max 64 characters), consider shortening these strings. | `list(string)` |
[
"enhanced-monitoring"
]
| no | | [enhanced\_monitoring\_role\_enabled](#input\_enhanced\_monitoring\_role\_enabled) | A boolean flag to enable/disable the creation of the enhanced monitoring IAM role. If set to `false`, the module will not create a new role and will use `rds_monitoring_role_arn` for enhanced monitoring | `bool` | `true` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [iam\_database\_authentication\_enabled](#input\_iam\_database\_authentication\_enabled) | Specifies whether or mappings of AWS Identity and Access Management (IAM) accounts to database accounts is enabled | `bool` | `false` | no | diff --git a/modules/aurora-postgres/cluster-regional.tf b/modules/aurora-postgres/cluster-regional.tf index abdf00214..2400f7f35 100644 --- a/modules/aurora-postgres/cluster-regional.tf +++ b/modules/aurora-postgres/cluster-regional.tf @@ -34,6 +34,7 @@ module "aurora_postgres_cluster" { maintenance_window = var.maintenance_window enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports enhanced_monitoring_role_enabled = var.enhanced_monitoring_role_enabled + enhanced_monitoring_attributes = var.enhanced_monitoring_attributes performance_insights_enabled = var.performance_insights_enabled rds_monitoring_interval = var.rds_monitoring_interval autoscaling_enabled = var.autoscaling_enabled diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 1681670ff..4c9d0d93f 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -193,6 +193,12 @@ variable "enhanced_monitoring_role_enabled" { default = true } +variable "enhanced_monitoring_attributes" { + type = list(string) + description = "Attributes used to format the Enhanced Monitoring IAM role. If this role hits IAM role length restrictions (max 64 characters), consider shortening these strings." + default = ["enhanced-monitoring"] +} + variable "rds_monitoring_interval" { type = number description = "The interval, in seconds, between points when enhanced monitoring metrics are collected for the DB instance. To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. Valid Values: 0, 1, 5, 10, 15, 30, 60" From b8c73752d4b954939d2d2ef523de159837e684ad Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 18 Jul 2023 16:57:21 -0700 Subject: [PATCH 188/501] Bump `elasticache-redis` Module (#767) --- modules/elasticache-redis/modules/redis_cluster/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/elasticache-redis/modules/redis_cluster/main.tf b/modules/elasticache-redis/modules/redis_cluster/main.tf index a82d1288b..ec7e8eb48 100644 --- a/modules/elasticache-redis/modules/redis_cluster/main.tf +++ b/modules/elasticache-redis/modules/redis_cluster/main.tf @@ -10,7 +10,7 @@ locals { module "redis" { source = "cloudposse/elasticache-redis/aws" - version = "0.44.0" + version = "0.52.0" name = var.cluster_name From 77eb9bee7f90628dae2713f012e5ba392672f727 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Thu, 20 Jul 2023 06:04:30 +0300 Subject: [PATCH 189/501] Bump ECS cluster module (#752) Co-authored-by: cloudpossebot --- modules/ecs/README.md | 2 +- modules/ecs/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ecs/README.md b/modules/ecs/README.md index 5cb8eb16f..dba55187a 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -56,7 +56,7 @@ components: | Name | Source | Version | |------|--------|---------| | [alb](#module\_alb) | cloudposse/alb/aws | 1.5.0 | -| [cluster](#module\_cluster) | cloudposse/ecs-cluster/aws | 0.2.2 | +| [cluster](#module\_cluster) | cloudposse/ecs-cluster/aws | 0.4.1 | | [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [target\_group\_label](#module\_target\_group\_label) | cloudposse/label/null | 0.25.0 | diff --git a/modules/ecs/main.tf b/modules/ecs/main.tf index 3347576c2..33a848bfa 100644 --- a/modules/ecs/main.tf +++ b/modules/ecs/main.tf @@ -66,7 +66,7 @@ resource "aws_security_group_rule" "egress" { module "cluster" { source = "cloudposse/ecs-cluster/aws" - version = "0.2.2" + version = "0.4.1" context = module.this.context From ccf55dc4caaec139ea74f12d309855b98b022b8a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 19 Jul 2023 21:17:09 -0700 Subject: [PATCH 190/501] Bump `lambda-elasticsearch-cleanup` module (#768) --- modules/elasticsearch/README.md | 2 +- modules/elasticsearch/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index a6ad798c3..f8dd62bdb 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -49,7 +49,7 @@ components: |------|--------|---------| | [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [elasticsearch](#module\_elasticsearch) | cloudposse/elasticsearch/aws | 0.42.0 | -| [elasticsearch\_log\_cleanup](#module\_elasticsearch\_log\_cleanup) | cloudposse/lambda-elasticsearch-cleanup/aws | 0.13.0 | +| [elasticsearch\_log\_cleanup](#module\_elasticsearch\_log\_cleanup) | cloudposse/lambda-elasticsearch-cleanup/aws | 0.14.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | diff --git a/modules/elasticsearch/main.tf b/modules/elasticsearch/main.tf index 28577a6bd..b2570fcc0 100644 --- a/modules/elasticsearch/main.tf +++ b/modules/elasticsearch/main.tf @@ -99,7 +99,7 @@ resource "aws_ssm_parameter" "elasticsearch_kibana_endpoint" { module "elasticsearch_log_cleanup" { source = "cloudposse/lambda-elasticsearch-cleanup/aws" - version = "0.13.0" + version = "0.14.0" es_endpoint = module.elasticsearch.domain_endpoint es_domain_arn = module.elasticsearch.domain_arn From 39d56a609c56770d4bddda337b2a994efebf1e2e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 19 Jul 2023 21:31:50 -0700 Subject: [PATCH 191/501] `elasticsearch` DNS Component Lookup (#769) --- modules/elasticsearch/README.md | 1 + modules/elasticsearch/remote-state.tf | 3 ++- modules/elasticsearch/variables.tf | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index f8dd62bdb..fb07cf624 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -74,6 +74,7 @@ components: | [dedicated\_master\_enabled](#input\_dedicated\_master\_enabled) | Indicates whether dedicated master nodes are enabled for the cluster | `bool` | n/a | yes | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_delegated\_environment\_name](#input\_dns\_delegated\_environment\_name) | The name of the environment where the `dns-delegated` component is deployed | `string` | `"gbl"` | no | | [domain\_hostname\_enabled](#input\_domain\_hostname\_enabled) | Explicit flag to enable creating a DNS hostname for ES. If `true`, then `var.dns_zone_id` is required. | `bool` | n/a | yes | | [ebs\_volume\_size](#input\_ebs\_volume\_size) | EBS volumes for data storage in GB | `number` | n/a | yes | | [elasticsearch\_iam\_actions](#input\_elasticsearch\_iam\_actions) | List of actions to allow for the IAM roles, _e.g._ `es:ESHttpGet`, `es:ESHttpPut`, `es:ESHttpPost` | `list(string)` |
[
"es:ESHttpGet",
"es:ESHttpPut",
"es:ESHttpPost",
"es:ESHttpHead",
"es:Describe*",
"es:List*"
]
| no | diff --git a/modules/elasticsearch/remote-state.tf b/modules/elasticsearch/remote-state.tf index 1d2ce7056..73a773ce2 100644 --- a/modules/elasticsearch/remote-state.tf +++ b/modules/elasticsearch/remote-state.tf @@ -11,7 +11,8 @@ module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.1" - component = "dns-delegated" + component = "dns-delegated" + environment = var.dns_delegated_environment_name context = module.this.context } diff --git a/modules/elasticsearch/variables.tf b/modules/elasticsearch/variables.tf index a5328ce3b..09e876725 100644 --- a/modules/elasticsearch/variables.tf +++ b/modules/elasticsearch/variables.tf @@ -87,3 +87,9 @@ variable "elasticsearch_password" { error_message = "Password must be between 8 and 128 characters. If null is provided then a random password will be used." } } + +variable "dns_delegated_environment_name" { + type = string + description = "The name of the environment where the `dns-delegated` component is deployed" + default = "gbl" +} From e3f31bedf10e949f36b5a2df2f54b948cad09d58 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Sun, 23 Jul 2023 19:08:31 -0400 Subject: [PATCH 192/501] Update `alb` component (#773) --- modules/alb/README.md | 7 ++++--- modules/alb/main.tf | 16 +++++++++++++--- modules/alb/remote-state.tf | 22 ++++++++++++++++++---- modules/alb/variables.tf | 6 ++++++ modules/eks/cluster/README.md | 1 + modules/eks/cluster/outputs.tf | 5 +++++ 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/modules/alb/README.md b/modules/alb/README.md index ccc471ed1..1389f912f 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -34,12 +34,12 @@ No providers. | Name | Source | Version | |------|--------|---------| +| [acm](#module\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [alb](#module\_alb) | cloudposse/alb/aws | 1.10.0 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [remote\_acm](#module\_remote\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | -| [remote\_dns](#module\_remote\_dns) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | -| [remote\_vpc](#module\_remote\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | ## Resources @@ -64,6 +64,7 @@ No resources. | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_acm\_enabled](#input\_dns\_acm\_enabled) | If `true`, use the ACM ARN created by the given `dns-delegated` component. Otherwise, use the ACM ARN created by the given `acm` component. | `bool` | `false` | no | | [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | Atmos `dns-delegated` component name | `string` | `"dns-delegated"` | no | +| [dns\_delegated\_environment\_name](#input\_dns\_delegated\_environment\_name) | `dns-delegated` component environment name | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [health\_check\_healthy\_threshold](#input\_health\_check\_healthy\_threshold) | The number of consecutive health checks successes required before considering an unhealthy target healthy | `number` | `2` | no | diff --git a/modules/alb/main.tf b/modules/alb/main.tf index 50a708087..91f9c530b 100644 --- a/modules/alb/main.tf +++ b/modules/alb/main.tf @@ -1,10 +1,20 @@ +locals { + dns_delegated_outputs = module.dns_delegated.outputs + dns_delegated_default_domain_name = local.dns_delegated_outputs.default_domain_name + dns_delegated_certificate = local.dns_delegated_outputs.certificate + dns_delegated_certificate_obj = lookup(local.dns_delegated_certificate, local.dns_delegated_default_domain_name, {}) + dns_delegated_certificate_arn = lookup(local.dns_delegated_certificate_obj, "arn", "") + + certificate_arn = var.dns_acm_enabled ? module.acm.outputs.arn : local.dns_delegated_certificate_arn +} + module "alb" { source = "cloudposse/alb/aws" version = "1.10.0" - vpc_id = module.remote_vpc.outputs.vpc_id - subnet_ids = module.remote_vpc.outputs.public_subnet_ids - certificate_arn = var.dns_acm_enabled ? module.remote_dns.outputs.certificate.arn : module.remote_acm.outputs.arn + vpc_id = module.vpc.outputs.vpc_id + subnet_ids = module.vpc.outputs.public_subnet_ids + certificate_arn = local.certificate_arn internal = var.internal http_port = var.http_port diff --git a/modules/alb/remote-state.tf b/modules/alb/remote-state.tf index 9a5c1642a..a6a9ab35c 100644 --- a/modules/alb/remote-state.tf +++ b/modules/alb/remote-state.tf @@ -1,4 +1,4 @@ -module "remote_vpc" { +module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.3" @@ -7,26 +7,40 @@ module "remote_vpc" { context = module.this.context } -module "remote_dns" { +module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.3" - component = var.dns_delegated_component_name + component = var.dns_delegated_component_name + environment = coalesce(var.dns_delegated_environment_name, module.iam_roles.global_environment_name) + + bypass = var.dns_acm_enabled # Ignore errors if component doesn't exist ignore_errors = true + defaults = { + default_domain_name = "" + certificate = {} + } + context = module.this.context } -module "remote_acm" { +module "acm" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.4.3" component = var.acm_component_name + bypass = !var.dns_acm_enabled + # Ignore errors if component doesn't exist ignore_errors = true + defaults = { + arn = "" + } + context = module.this.context } diff --git a/modules/alb/variables.tf b/modules/alb/variables.tf index 1968a6d4f..3b196ab92 100644 --- a/modules/alb/variables.tf +++ b/modules/alb/variables.tf @@ -222,6 +222,12 @@ variable "dns_delegated_component_name" { description = "Atmos `dns-delegated` component name" } +variable "dns_delegated_environment_name" { + type = string + default = null + description = "`dns-delegated` component environment name" +} + variable "acm_component_name" { type = string default = "acm" diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 65ed6304f..b550539ea 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -477,6 +477,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | Name | Description | |------|-------------| +| [availability\_zones](#output\_availability\_zones) | Availability Zones in which the cluster is provisioned | | [eks\_auth\_worker\_roles](#output\_eks\_auth\_worker\_roles) | List of worker IAM roles that were included in the `auth-map` ConfigMap. | | [eks\_cluster\_arn](#output\_eks\_cluster\_arn) | The Amazon Resource Name (ARN) of the cluster | | [eks\_cluster\_certificate\_authority\_data](#output\_eks\_cluster\_certificate\_authority\_data) | The Kubernetes cluster certificate authority data | diff --git a/modules/eks/cluster/outputs.tf b/modules/eks/cluster/outputs.tf index c240fc5a7..6430de8f9 100644 --- a/modules/eks/cluster/outputs.tf +++ b/modules/eks/cluster/outputs.tf @@ -102,3 +102,8 @@ output "vpc_cidr" { description = "The CIDR of the VPC where this cluster is deployed." value = local.vpc_outputs.vpc_cidr } + +output "availability_zones" { + description = "Availability Zones in which the cluster is provisioned" + value = local.availability_zones +} From a2ae6afd101f75f1e5db4f3816938e382afde6ce Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 24 Jul 2023 22:09:57 -0700 Subject: [PATCH 193/501] [vpc] bugfix, [aurora-postgres] & [cloudtrail-bucket] Tflint fixes (#776) --- modules/aurora-postgres/variables.tf | 1 + modules/cloudtrail-bucket/README.md | 2 +- modules/cloudtrail-bucket/variables.tf | 15 ++++++++++----- modules/vpc/main.tf | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 4c9d0d93f..5d001f805 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -68,6 +68,7 @@ variable "cluster_family" { # AWS KMS alias used for encryption/decryption of SSM secure strings variable "kms_alias_name_ssm" { + type = string default = "alias/aws/ssm" description = "KMS alias name for SSM" } diff --git a/modules/cloudtrail-bucket/README.md b/modules/cloudtrail-bucket/README.md index d7c9a8cc8..ef943c2df 100644 --- a/modules/cloudtrail-bucket/README.md +++ b/modules/cloudtrail-bucket/README.md @@ -71,7 +71,7 @@ No resources. | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [noncurrent\_version\_expiration\_days](#input\_noncurrent\_version\_expiration\_days) | Specifies when noncurrent object versions expire | `number` | `90` | no | -| [noncurrent\_version\_transition\_days](#input\_noncurrent\_version\_transition\_days) | Specifies when noncurrent object versions transitions | `number` | `30` | no | +| [noncurrent\_version\_transition\_days](#input\_noncurrent\_version\_transition\_days) | Specifies when noncurrent object versions transition to a different storage tier | `number` | `30` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/cloudtrail-bucket/variables.tf b/modules/cloudtrail-bucket/variables.tf index 350b12fc3..479c1e91c 100644 --- a/modules/cloudtrail-bucket/variables.tf +++ b/modules/cloudtrail-bucket/variables.tf @@ -10,28 +10,33 @@ variable "lifecycle_rule_enabled" { } variable "noncurrent_version_expiration_days" { - description = "Specifies when noncurrent object versions expire" + type = number default = 90 + description = "Specifies when noncurrent object versions expire" } variable "noncurrent_version_transition_days" { - description = "Specifies when noncurrent object versions transitions" + type = number default = 30 + description = "Specifies when noncurrent object versions transition to a different storage tier" } variable "standard_transition_days" { - description = "Number of days to persist in the standard storage tier before moving to the infrequent access tier" + type = number default = 30 + description = "Number of days to persist in the standard storage tier before moving to the infrequent access tier" } variable "glacier_transition_days" { - description = "Number of days after which to move the data to the glacier storage tier" + type = number default = 60 + description = "Number of days after which to move the data to the glacier storage tier" } variable "expiration_days" { - description = "Number of days after which to expunge the objects" + type = number default = 90 + description = "Number of days after which to expunge the objects" } variable "create_access_log_bucket" { diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index 51ce85f10..19045b024 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -128,7 +128,7 @@ module "vpc_endpoints" { source = "cloudposse/vpc/aws//modules/vpc-endpoints" version = "2.1.0" - enabled = (length(var.interface_vpc_endpoints) + length(var.gateway_vpc_endpoints)) > 0 + enabled = local.enabled && (length(var.interface_vpc_endpoints) + length(var.gateway_vpc_endpoints)) > 0 vpc_id = module.vpc.vpc_id gateway_vpc_endpoints = local.gateway_endpoint_map From c72de34db4c4461943697122d287eb249f9d32dd Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 26 Jul 2023 09:20:15 -0700 Subject: [PATCH 194/501] Upstream `spa-s3-cloudfront` (#778) --- modules/spa-s3-cloudfront/README.md | 10 +++++----- modules/spa-s3-cloudfront/main.tf | 6 +++--- modules/spa-s3-cloudfront/remote-state.tf | 9 +++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 5ad68239b..e5d59b6cd 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -112,18 +112,18 @@ an extensive explanation for how these preview environments work. | Name | Source | Version | |------|--------|---------| -| [acm\_request\_certificate](#module\_acm\_request\_certificate) | cloudposse/acm-request-certificate/aws | 0.17.0 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [acm\_request\_certificate](#module\_acm\_request\_certificate) | cloudposse/acm-request-certificate/aws | 0.16.3 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | | [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | -| [github\_runners](#module\_github\_runners) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [github\_runners](#module\_github\_runners) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [lambda\_edge\_preview](#module\_lambda\_edge\_preview) | ./modules/lambda-edge-preview | n/a | | [lambda\_edge\_redirect\_404](#module\_lambda\_edge\_redirect\_404) | ./modules/lambda_edge_redirect_404 | n/a | -| [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.83.0 | +| [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.91.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 0.8.1 | -| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf index 2c5785575..60e6318c2 100644 --- a/modules/spa-s3-cloudfront/main.tf +++ b/modules/spa-s3-cloudfront/main.tf @@ -54,7 +54,7 @@ locals { # Create an ACM and explicitly set it to us-east-1 (requirement of CloudFront) module "acm_request_certificate" { source = "cloudposse/acm-request-certificate/aws" - version = "0.17.0" + version = "0.16.3" providers = { aws = aws.us-east-1 } @@ -70,13 +70,13 @@ module "acm_request_certificate" { module "spa_web" { source = "cloudposse/cloudfront-s3-cdn/aws" - version = "0.83.0" + version = "0.91.0" block_origin_public_access_enabled = local.block_origin_public_access_enabled encryption_enabled = var.origin_encryption_enabled origin_force_destroy = var.origin_force_destroy versioning_enabled = var.origin_versioning_enabled - web_acl_id = local.aws_waf_enabled ? module.waf.outputs.acl.arn : null + web_acl_id = local.aws_waf_enabled ? module.waf[0].outputs.acl.arn : null cloudfront_access_log_create_bucket = var.cloudfront_access_log_create_bucket cloudfront_access_log_bucket_name = local.cloudfront_access_log_bucket_name diff --git a/modules/spa-s3-cloudfront/remote-state.tf b/modules/spa-s3-cloudfront/remote-state.tf index ef44ee892..1f9f90575 100644 --- a/modules/spa-s3-cloudfront/remote-state.tf +++ b/modules/spa-s3-cloudfront/remote-state.tf @@ -1,8 +1,9 @@ module "waf" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" + + count = local.aws_waf_enabled ? 1 : 0 - bypass = !local.aws_waf_enabled component = var.cloudfront_aws_waf_component_name privileged = false environment = var.cloudfront_aws_waf_environment @@ -18,7 +19,7 @@ module "waf" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = var.dns_delegated_environment_name @@ -28,7 +29,7 @@ module "dns_delegated" { module "github_runners" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" count = local.github_runners_enabled ? 1 : 0 From 76679ad362d8f709e089551fd2723665c8cafd7e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 27 Jul 2023 10:05:10 -0700 Subject: [PATCH 195/501] Upstream `spa-s3-cloudfront` (#780) --- modules/spa-s3-cloudfront/README.md | 2 +- .../spa-s3-cloudfront/github-actions-iam-policy.tf | 11 +++++++++++ modules/spa-s3-cloudfront/main.tf | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index e5d59b6cd..d67935e36 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -120,7 +120,7 @@ an extensive explanation for how these preview environments work. | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [lambda\_edge\_preview](#module\_lambda\_edge\_preview) | ./modules/lambda-edge-preview | n/a | | [lambda\_edge\_redirect\_404](#module\_lambda\_edge\_redirect\_404) | ./modules/lambda_edge_redirect_404 | n/a | -| [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.91.0 | +| [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.92.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 0.8.1 | | [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | diff --git a/modules/spa-s3-cloudfront/github-actions-iam-policy.tf b/modules/spa-s3-cloudfront/github-actions-iam-policy.tf index a1c4d3764..79ceec702 100644 --- a/modules/spa-s3-cloudfront/github-actions-iam-policy.tf +++ b/modules/spa-s3-cloudfront/github-actions-iam-policy.tf @@ -26,4 +26,15 @@ data "aws_iam_policy_document" "github_actions_iam_policy" { ] resources = [format("%s/*", module.spa_web.s3_bucket_arn)] } + + statement { + sid = "CloudfrontActions" + effect = "Allow" + actions = [ + "cloudfront:CreateInvalidation" + ] + resources = [ + module.spa_web.cf_arn + ] + } } diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf index 60e6318c2..4298b7ee7 100644 --- a/modules/spa-s3-cloudfront/main.tf +++ b/modules/spa-s3-cloudfront/main.tf @@ -70,7 +70,7 @@ module "acm_request_certificate" { module "spa_web" { source = "cloudposse/cloudfront-s3-cdn/aws" - version = "0.91.0" + version = "0.92.0" block_origin_public_access_enabled = local.block_origin_public_access_enabled encryption_enabled = var.origin_encryption_enabled From ac404447b3e2f73b1a962dc32e634ad5c3b97747 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 28 Jul 2023 14:12:06 -0400 Subject: [PATCH 196/501] fix: restore notifications config in argocd (#782) --- modules/eks/argocd/README.md | 19 +- modules/eks/argocd/data.tf | 32 ++-- modules/eks/argocd/main.tf | 169 +++++++----------- modules/eks/argocd/outputs.tf | 7 +- modules/eks/argocd/providers.tf | 2 +- .../argocd/variables-argocd-notifications.tf | 30 +++- modules/eks/argocd/variables-argocd.tf | 52 +++--- 7 files changed, 146 insertions(+), 165 deletions(-) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 20f870be4..1e7d35770 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -395,8 +395,6 @@ components: | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles\_config\_secrets](#module\_iam\_roles\_config\_secrets) | ../../account-map/modules/iam-roles | n/a | -| [oidc\_gsuite\_service\_providers\_providers\_store\_read](#module\_oidc\_gsuite\_service\_providers\_providers\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.10.0 | -| [oidc\_providers\_store\_read](#module\_oidc\_providers\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.10.0 | | [saml\_sso\_providers](#module\_saml\_sso\_providers) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -404,28 +402,31 @@ components: | Name | Type | |------|------| -| [kubernetes_secret.oidc_gsuite_service_account](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | | [aws_ssm_parameter.github_deploy_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.oidc_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.oidc_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameters_by_path.argocd_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | +| [kubernetes_resources.crd](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/resources) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_enabled](#input\_admin\_enabled) | Toggles Admin user creation the deployed chart | `bool` | `false` | no | | [alb\_group\_name](#input\_alb\_group\_name) | A name used in annotations to reuse an ALB (e.g. `argocd`) or to generate a new one | `string` | `null` | no | | [alb\_logs\_bucket](#input\_alb\_logs\_bucket) | The name of the bucket for ALB access logs. The bucket must have policy allowing the ELB logging principal | `string` | `""` | no | | [alb\_logs\_prefix](#input\_alb\_logs\_prefix) | `alb_logs_bucket` s3 bucket prefix | `string` | `""` | no | | [alb\_name](#input\_alb\_name) | The name of the ALB (e.g. `argocd`) provisioned by `alb-controller`. Works together with `var.alb_group_name` | `string` | `null` | no | | [argo\_enable\_workflows\_auth](#input\_argo\_enable\_workflows\_auth) | Allow argo-workflows to use Dex instance for SAML auth | `bool` | `false` | no | -| [argo\_workflows\_name](#input\_argo\_workflows\_name) | Name of argo-workflows instance | `string` | `"argo-workflows"` | no | | [argocd\_apps\_chart](#input\_argocd\_apps\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"argocd-apps"` | no | | [argocd\_apps\_chart\_description](#input\_argocd\_apps\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"A Helm chart for managing additional Argo CD Applications and Projects"` | no | | [argocd\_apps\_chart\_repository](#input\_argocd\_apps\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://argoproj.github.io/argo-helm"` | no | | [argocd\_apps\_chart\_version](#input\_argocd\_apps\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"0.0.3"` | no | | [argocd\_apps\_enabled](#input\_argocd\_apps\_enabled) | Enable argocd apps | `bool` | `true` | no | | [argocd\_create\_namespaces](#input\_argocd\_create\_namespaces) | ArgoCD create namespaces policy | `bool` | `false` | no | +| [argocd\_rbac\_default\_policy](#input\_argocd\_rbac\_default\_policy) | Default ArgoCD RBAC default role.

See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#basic-built-in-roles for more information. | `string` | `"role:readonly"` | no | | [argocd\_rbac\_groups](#input\_argocd\_rbac\_groups) | List of ArgoCD Group Role Assignment strings to be added to the argocd-rbac configmap policy.csv item.
e.g.
[
{
group: idp-group-name,
role: argocd-role-name
},
]
becomes: `g, idp-group-name, role:argocd-role-name`
See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. |
list(object({
group = string,
role = string
}))
| `[]` | no | | [argocd\_rbac\_policies](#input\_argocd\_rbac\_policies) | List of ArgoCD RBAC Permission strings to be added to the argocd-rbac configmap policy.csv item.

See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ for more information. | `list(string)` | `[]` | no | | [argocd\_repositories](#input\_argocd\_repositories) | Map of objects defining an `argocd_repo` to configure. The key is the name of the ArgoCD repository. |
map(object({
environment = string # The environment where the `argocd_repo` component is deployed.
stage = string # The stage where the `argocd_repo` component is deployed.
tenant = string # The tenant where the `argocd_repo` component is deployed.
}))
| `{}` | no | @@ -469,11 +470,12 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = optional(number)
installationID = optional(number)
privateKey = optional(string)
}))
})
| `null` | no | -| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
}))
| `{}` | no | +| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = number
installationID = number
privateKey = optional(string)
}))
# service.webhook.:
service_webhook = optional(map(
object({
url = string
headers = optional(list(
object({
name = string
value = string
})
), [])
basicAuth = optional(object({
username = string
password = string
}))
insecureSkipVerify = optional(bool, false)
})
))
})
| `{}` | no | +| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
webhook = optional(map(
object({
method = optional(string)
path = optional(string)
body = optional(string)
})
))
}))
| `{}` | no | | [notifications\_triggers](#input\_notifications\_triggers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
map(list(
object({
oncePer = optional(string)
send = list(string)
when = string
})
))
| `{}` | no | | [oidc\_enabled](#input\_oidc\_enabled) | Toggles OIDC integration in the deployed chart | `bool` | `false` | no | -| [oidc\_providers](#input\_oidc\_providers) | OIDC providers components, clientID and clientSecret should be passed as SSM parameters (denoted by leading slash) | `any` | `{}` | no | +| [oidc\_issuer](#input\_oidc\_issuer) | OIDC issuer URL | `string` | `""` | no | +| [oidc\_name](#input\_oidc\_name) | Name of the OIDC resource | `string` | `""` | no | | [oidc\_rbac\_scopes](#input\_oidc\_rbac\_scopes) | OIDC RBAC scopes to request | `string` | `"[argocd_realm_access]"` | no | | [oidc\_requested\_scopes](#input\_oidc\_requested\_scopes) | Set of OIDC scopes to request | `string` | `"[\"openid\", \"profile\", \"email\", \"groups\"]"` | no | | [rbac\_enabled](#input\_rbac\_enabled) | Enable Service Account for pods. | `bool` | `true` | no | @@ -501,8 +503,7 @@ components: | Name | Description | |------|-------------| -| [argocd\_apps\_metadata](#output\_argocd\_apps\_metadata) | Block status of the deployed ArgoCD apps release | -| [metadata](#output\_metadata) | Block status of the deployed ArgoCD release | +| [metadata](#output\_metadata) | Block status of the deployed release | ## References diff --git a/modules/eks/argocd/data.tf b/modules/eks/argocd/data.tf index 9f1d8ed16..5cb530e3f 100644 --- a/modules/eks/argocd/data.tf +++ b/modules/eks/argocd/data.tf @@ -2,8 +2,8 @@ locals { # kubernetes_host = local.enabled ? data.aws_eks_cluster.kubernetes[0].endpoint : "" # kubernetes_token = local.enabled ? data.aws_eks_cluster_auth.kubernetes[0].token : "" # kubernetes_cluster_ca_certificate = local.enabled ? base64decode(data.aws_eks_cluster.kubernetes[0].certificate_authority[0].data) : "" - # oidc_client_id = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_id[0].value : "" - # oidc_client_secret = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_secret[0].value : "" + oidc_client_id = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_id[0].value : "" + oidc_client_secret = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_secret[0].value : "" # saml_certificate = base64encode(format("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", module.okta_saml_apps.outputs.certificates[var.saml_okta_app_name])) # @@ -17,21 +17,21 @@ locals { # These variables are depreciated but should not yet be removed. Future iterations of this component will delete these variables # -#data "aws_ssm_parameter" "oidc_client_id" { -# count = local.oidc_enabled_count -# name = var.ssm_oidc_client_id -# with_decryption = true -# -# provider = aws.config_secrets -#} +data "aws_ssm_parameter" "oidc_client_id" { + count = local.oidc_enabled_count + name = var.ssm_oidc_client_id + with_decryption = true -#data "aws_ssm_parameter" "oidc_client_secret" { -# count = local.oidc_enabled_count -# name = var.ssm_oidc_client_secret -# with_decryption = true -# -# provider = aws.config_secrets -#} + provider = aws.config_secrets +} + +data "aws_ssm_parameter" "oidc_client_secret" { + count = local.oidc_enabled_count + name = var.ssm_oidc_client_secret + with_decryption = true + + provider = aws.config_secrets +} #data "aws_eks_cluster" "kubernetes" { # count = local.count_enabled diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 1c9c5724b..8b879f867 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -1,32 +1,8 @@ locals { - enabled = module.this.enabled -} - -module "oidc_providers_store_read" { - for_each = local.enabled ? var.oidc_providers : {} - source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" - - parameter_read = [for k, v in each.value.config : v if try(startswith(v, "/"), false)] -} - -locals { - param_store_values = [for k, v in module.oidc_providers_store_read : module.oidc_providers_store_read[k].map] - vals = flatten([for k, v in local.param_store_values : [for k2, v2 in v : v2]]) - keys = flatten([for k, v in local.param_store_values : [for k2, v2 in v : k2]]) - map = zipmap(local.keys, local.vals) - oidc_providers_merged = { - for name, values in var.oidc_providers : name => merge(values, { - config = { - for k, v in values.config : k => try(startswith(v, "/"), false) ? local.map[v] : v - } - }) - } -} - -locals { + enabled = module.this.enabled kubernetes_namespace = var.kubernetes_namespace oidc_enabled = local.enabled && var.oidc_enabled + oidc_enabled_count = local.oidc_enabled ? 1 : 0 saml_enabled = local.enabled && var.saml_enabled argocd_repositories = local.enabled ? { for k, v in var.argocd_repositories : k => { @@ -34,7 +10,7 @@ locals { github_deploy_key = data.aws_ssm_parameter.github_deploy_key[k].value } } : {} - credential_templates = flatten([ + credential_templates = flatten(concat([ for k, v in local.argocd_repositories : [ { name = "configs.credentialTemplates.${k}.url" @@ -43,33 +19,43 @@ locals { }, { name = "configs.credentialTemplates.${k}.sshPrivateKey" - value = v.github_deploy_key + value = nonsensitive(v.github_deploy_key) type = "string" }, ] - ]) + ], + [ + for s, v in local.notifications_notifiers_ssm_configs : [ + for k, i in v : [ + { + name = "notifications.secret.items.${s}_${k}" + value = i + type = "string" + } + ] + ] + ])) regional_service_discovery_domain = "${module.this.environment}.${module.dns_gbl_delegated.outputs.default_domain_name}" host = var.host != "" ? var.host : format("%s.%s", coalesce(var.alb_name, var.name), local.regional_service_discovery_domain) enable_argo_workflows_auth = local.saml_enabled && var.argo_enable_workflows_auth + # enable_argo_workflows_auth_count = local.enable_argo_workflows_auth ? 1 : 0 # argo_workflows_host = "${var.argo_workflows_name}.${local.regional_service_discovery_domain}" - oidc_values = local.oidc_enabled ? values(local.oidc_providers_merged)[0] : {} - - oidc_config_map = local.oidc_enabled && !lookup(local.oidc_values, "uses_dex", true) ? { + oidc_config_map = local.oidc_enabled ? { server : { config : { "oidc.config" = <<-EOT - name: ${lookup(local.oidc_values, "name", null)} - issuer: ${lookup(local.oidc_values.config, "issuer", null)} - clientID: ${lookup(local.oidc_values.config, "clientID", null)} - clientSecret: ${lookup(local.oidc_values.config, "clientSecret", null)} + name: ${var.oidc_name} + issuer: ${var.oidc_issuer} + clientID: ${local.oidc_client_id} + clientSecret: ${local.oidc_client_secret} requestedScopes: ${var.oidc_requested_scopes} EOT } } } : {} - dex_config_map = { + saml_config_map = local.saml_enabled ? { configs : { params : { "dexserver.disable.tls" = true @@ -81,11 +67,10 @@ locals { ]) } } - } + } : {} dex_config_connectors = yamlencode({ - connectors = concat([ - for name, config in(local.enabled && local.saml_enabled ? var.saml_sso_providers : {}) : + connectors = [for name, config in(local.enabled ? var.saml_sso_providers : {}) : { type = "saml" id = "saml" @@ -101,31 +86,14 @@ locals { ssoIssuer = module.saml_sso_providers[name].outputs.issuer } } - ], - [ - for name, config in(local.enabled && local.oidc_enabled ? local.oidc_providers_merged : {}) : - { - type = lookup(config, "type", "oidc") - id = lookup(config, "id", name) - name = coalesce(lookup(config, "name", null), name) - config = merge(config.config, { - redirectURI = format("https://%s/api/dex/callback", local.host) - clientID = lookup(config.config, "clientID", null) - clientSecret = lookup(config.config, "clientSecret", null) - serviceAccountFilePath = lookup(config.serviceAccountAccess, "enabled", false) ? "/tmp/oidc/googleAuth.json" : null - adminEmail = lookup(config.serviceAccountAccess, "enabled", false) ? lookup(config.serviceAccountAccess, "admin_email", null) : null - }) - } if lookup(config, "uses_dex", true) - ] - ) - }) + ] + } + ) post_render_script = local.enable_argo_workflows_auth ? "./resources/kustomize/post-render.sh" : null kustomize_files_values = local.enable_argo_workflows_auth ? { __ignore = { - kustomize_files = { - for f in fileset("./resources/kustomize", "[^_]*.{sh,yaml}") : f => filesha256("./resources/kustomize/${f}") - } + kustomize_files = { for f in fileset("./resources/kustomize", "[^_]*.{sh,yaml}") : f => filesha256("./resources/kustomize/${f}") } } } : {} } @@ -137,52 +105,35 @@ data "aws_ssm_parameters_by_path" "argocd_notifications" { } locals { - notifications_notifiers_ssm_path = var.notifications_notifiers == null ? {} : { - for key, value in var.notifications_notifiers : + notifications_notifiers_ssm_path = { for key, value in local.notifications_notifiers_variables : key => format("%s/%s/", var.notifications_notifiers.ssm_path_prefix, key) } - notifications_notifiers_ssm_configs = { - for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : - key => nonsensitive(zipmap( + notifications_notifiers_ssm_configs = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : + key => zipmap( [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], value.values - )) + ) } - notifications_notifiers_variables = var.notifications_notifiers == null ? {} : { - for key, value in var.notifications_notifiers : - key => { for param_name, param_value in value : param_name => param_value if param_value != null } - if key != "ssm_path_prefix" + notifications_notifiers_ssm_configs_keys = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : + key => zipmap( + [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], + [for name in value.names : format("$%s_%s", key, trimprefix(name, local.notifications_notifiers_ssm_path[key]))] + ) } + notifications_notifiers_variables = merge({ for key, value in var.notifications_notifiers : + key => { for param_name, param_value in value : param_name => param_value if param_value != null } + if key != "ssm_path_prefix" && key != "service_webhook" + }, + { for key, value in coalesce(var.notifications_notifiers.service_webhook, {}) : + format("service_webhook_%s", key) => { for param_name, param_value in value : param_name => param_value if param_value != null } + }) + notifications_notifiers = { for key, value in local.notifications_notifiers_variables : - replace(key, "_", ".") => yamlencode(merge(local.notifications_notifiers_ssm_configs[key], value)) - } -} - -# https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/google/#configure-dex -module "oidc_gsuite_service_providers_providers_store_read" { - for_each = { for k, v in var.oidc_providers : k => v if lookup(v.serviceAccountAccess, "enabled", false) } - source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" - - parameter_read = [ - for k, v in var.oidc_providers : v.serviceAccountAccess.value - if v.id == "google" && lookup(v.serviceAccountAccess, "enabled", false) - ] -} - -resource "kubernetes_secret" "oidc_gsuite_service_account" { - for_each = { for k, v in var.oidc_providers : k => v if lookup(v.serviceAccountAccess, "enabled", false) } - metadata { - name = "argocd-google-groups-json" - namespace = local.kubernetes_namespace - } - - data = { - (each.value.serviceAccountAccess.key) = trimspace(module.oidc_gsuite_service_providers_providers_store_read[each.key].map[each.value.serviceAccountAccess.value]) + replace(key, "_", ".") => yamlencode(merge(local.notifications_notifiers_ssm_configs_keys[key], value)) } } @@ -203,12 +154,12 @@ module "argocd" { timeout = var.timeout postrender_binary_path = local.post_render_script - eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") service_account_name = module.this.name service_account_namespace = var.kubernetes_namespace - set_sensitive = local.credential_templates + set_sensitive = nonsensitive(local.credential_templates) values = compact([ # standard k8s object settings @@ -226,7 +177,7 @@ module "argocd" { templatefile( "${path.module}/resources/argocd-values.yaml.tpl", { - admin_enabled = true + admin_enabled = var.admin_enabled alb_group_name = var.alb_group_name == null ? "" : var.alb_group_name alb_logs_bucket = var.alb_logs_bucket alb_logs_prefix = var.alb_logs_prefix @@ -242,6 +193,7 @@ module "argocd" { organization = var.github_organization saml_enabled = local.saml_enabled saml_rbac_scopes = var.saml_rbac_scopes + rbac_default_policy = var.argocd_rbac_default_policy rbac_policies = var.argocd_rbac_policies rbac_groups = var.argocd_rbac_groups enable_argo_workflows_auth = local.enable_argo_workflows_auth @@ -269,8 +221,7 @@ module "argocd" { yamlencode( { notifications = { - triggers = { - for key, value in var.notifications_triggers : + triggers = { for key, value in var.notifications_triggers : replace(key, "_", ".") => yamlencode(value) } } @@ -285,22 +236,22 @@ module "argocd" { ), yamlencode(merge( local.oidc_config_map, - local.dex_config_map, + local.saml_config_map, )), yamlencode(local.kustomize_files_values), yamlencode(var.chart_values) ]) context = module.this.context +} - depends_on = [ - kubernetes_secret.oidc_gsuite_service_account - ] +data "kubernetes_resources" "crd" { + api_version = "apiextensions.k8s.io/v1" + kind = "CustomResourceDefinition" + field_selector = "metadata.name==applications.argoproj.io" } module "argocd_apps" { - count = local.enabled && var.argocd_apps_enabled ? 1 : 0 - source = "cloudposse/helm-release/aws" version = "0.9.1" @@ -315,8 +266,8 @@ module "argocd_apps" { atomic = var.atomic cleanup_on_fail = var.cleanup_on_fail timeout = var.timeout - enabled = local.enabled && var.argocd_apps_enabled - eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer + enabled = local.enabled && var.argocd_apps_enabled && length(data.kubernetes_resources.crd.objects) > 0 + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") values = compact([ templatefile( "${path.module}/resources/argocd-apps-values.yaml.tpl", diff --git a/modules/eks/argocd/outputs.tf b/modules/eks/argocd/outputs.tf index 782874758..26b6561e7 100644 --- a/modules/eks/argocd/outputs.tf +++ b/modules/eks/argocd/outputs.tf @@ -1,9 +1,4 @@ output "metadata" { value = module.argocd.metadata - description = "Block status of the deployed ArgoCD release" -} - -output "argocd_apps_metadata" { - value = module.argocd_apps.metadata - description = "Block status of the deployed ArgoCD apps release" + description = "Block status of the deployed release" } diff --git a/modules/eks/argocd/providers.tf b/modules/eks/argocd/providers.tf index 89ed50a98..45d458575 100644 --- a/modules/eks/argocd/providers.tf +++ b/modules/eks/argocd/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = assume_role.value + role_arn = module.iam_roles.terraform_role_arn } } } diff --git a/modules/eks/argocd/variables-argocd-notifications.tf b/modules/eks/argocd/variables-argocd-notifications.tf index bf83f51ed..603741896 100644 --- a/modules/eks/argocd/variables-argocd-notifications.tf +++ b/modules/eks/argocd/variables-argocd-notifications.tf @@ -13,6 +13,13 @@ variable "notifications_templates" { targetURL = string }) })) + webhook = optional(map( + object({ + method = optional(string) + path = optional(string) + body = optional(string) + }) + )) })) default = {} description = <<-EOT @@ -44,12 +51,29 @@ variable "notifications_notifiers" { type = object({ ssm_path_prefix = optional(string, "/argocd/notifications/notifiers") service_github = optional(object({ - appID = optional(number) - installationID = optional(number) + appID = number + installationID = number privateKey = optional(string) })) + # service.webhook.: + service_webhook = optional(map( + object({ + url = string + headers = optional(list( + object({ + name = string + value = string + }) + ), []) + basicAuth = optional(object({ + username = string + password = string + })) + insecureSkipVerify = optional(bool, false) + }) + )) }) - default = null + default = {} description = <<-EOT Notification Triggers to configure. diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index be085d5da..0ee31dc8f 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -95,23 +95,29 @@ variable "forecastle_enabled" { default = false } +variable "admin_enabled" { + type = bool + description = "Toggles Admin user creation the deployed chart" + default = false +} + variable "oidc_enabled" { type = bool description = "Toggles OIDC integration in the deployed chart" default = false } -#variable "oidc_issuer" { -# type = string -# description = "OIDC issuer URL" -# default = "" -#} +variable "oidc_issuer" { + type = string + description = "OIDC issuer URL" + default = "" +} -#variable "oidc_name" { -# type = string -# description = "Name of the OIDC resource" -# default = "" -#} +variable "oidc_name" { + type = string + description = "Name of the OIDC resource" + default = "" +} variable "oidc_rbac_scopes" { type = string @@ -149,11 +155,11 @@ variable "argo_enable_workflows_auth" { description = "Allow argo-workflows to use Dex instance for SAML auth" } -variable "argo_workflows_name" { - type = string - default = "argo-workflows" - description = "Name of argo-workflows instance" -} +# variable "argo_workflows_name" { +# type = string +# default = "argo-workflows" +# description = "Name of argo-workflows instance" +# } variable "argocd_rbac_policies" { type = list(string) @@ -165,6 +171,16 @@ variable "argocd_rbac_policies" { EOT } +variable "argocd_rbac_default_policy" { + type = string + default = "role:readonly" + description = <<-EOT + Default ArgoCD RBAC default role. + + See https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#basic-built-in-roles for more information. + EOT +} + variable "argocd_rbac_groups" { type = list(object({ group = string, @@ -199,9 +215,3 @@ variable "saml_sso_providers" { default = {} description = "SAML SSO providers components" } - -variable "oidc_providers" { - type = any - default = {} - description = "OIDC providers components, clientID and clientSecret should be passed as SSM parameters (denoted by leading slash)" -} From 849a4036a8001cad8106aaf18c7cca744184b92b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 28 Jul 2023 11:57:06 -0700 Subject: [PATCH 197/501] Aurora Resource Submodule Requirements (#775) --- modules/aurora-mysql-resources/modules/mysql-user/main.tf | 3 +-- .../aurora-postgres-resources/modules/postgresql-user/main.tf | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/aurora-mysql-resources/modules/mysql-user/main.tf b/modules/aurora-mysql-resources/modules/mysql-user/main.tf index 976684740..70d071c08 100644 --- a/modules/aurora-mysql-resources/modules/mysql-user/main.tf +++ b/modules/aurora-mysql-resources/modules/mysql-user/main.tf @@ -4,7 +4,6 @@ locals { db_user = length(var.db_user) > 0 ? var.db_user : var.service_name db_password = length(var.db_password) > 0 ? var.db_password : join("", random_password.db_password.*.result) - create_db_user = local.enabled && var.service_name != local.db_user save_password_in_ssm = local.enabled && var.save_password_in_ssm db_password_key = format("%s/%s/passwords/%s", var.ssm_path_prefix, var.service_name, local.db_user) @@ -16,7 +15,7 @@ locals { overwrite = true } : null - parameter_write = (local.create_db_user && local.save_password_in_ssm) ? [local.db_password_ssm] : [] + parameter_write = local.save_password_in_ssm ? [local.db_password_ssm] : [] # You cannot grant "ALL" to an RDS user because "ALL" includes privileges that Master does not have (because this is a managed database). # Instead, use "ALL PRIVILEGES" diff --git a/modules/aurora-postgres-resources/modules/postgresql-user/main.tf b/modules/aurora-postgres-resources/modules/postgresql-user/main.tf index 494789461..0cb4c2b95 100644 --- a/modules/aurora-postgres-resources/modules/postgresql-user/main.tf +++ b/modules/aurora-postgres-resources/modules/postgresql-user/main.tf @@ -5,7 +5,6 @@ locals { db_password = length(var.db_password) > 0 ? var.db_password : join("", random_password.db_password.*.result) save_password_in_ssm = local.enabled && var.save_password_in_ssm - create_db_user = local.enabled && var.service_name != local.db_user db_password_key = format("%s/%s/passwords/%s", var.ssm_path_prefix, var.service_name, local.db_user) db_password_ssm = local.save_password_in_ssm ? { @@ -16,7 +15,7 @@ locals { overwrite = true } : null - parameter_write = (local.create_db_user && local.save_password_in_ssm) ? [local.db_password_ssm] : [] + parameter_write = local.save_password_in_ssm ? [local.db_password_ssm] : [] # ALL grant always shows Terraform drift: # https://github.com/cyrilgdn/terraform-provider-postgresql/issues/32 From 5669f714d95f25796eb87abe5fae339ba43a38e4 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 28 Jul 2023 14:34:54 -0700 Subject: [PATCH 198/501] `bastion` support for `availability_zones` and public IP and subnets (#783) --- modules/bastion/README.md | 9 ++---- modules/bastion/default.auto.tfvars | 1 - modules/bastion/main.tf | 23 ++++++++------- modules/bastion/outputs.tf | 2 +- modules/bastion/variables.tf | 46 +++++++---------------------- 5 files changed, 27 insertions(+), 54 deletions(-) delete mode 100644 modules/bastion/default.auto.tfvars diff --git a/modules/bastion/README.md b/modules/bastion/README.md index 488208d6f..4ef76ac19 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -19,6 +19,8 @@ components: vars: enabled: true name: bastion-ssm + # Your choice of availability zones. If not specified, all private subnets are used. + availability_zones: ["us-east-1a", "us-east-1b", "us-east-1c"] instance_type: t3.micro image_container: infrastructure:latest image_repository: "111111111111.dkr.ecr.us-east-1.amazonaws.com/example/infrastructure" @@ -97,13 +99,11 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [associate\_public\_ip\_address](#input\_associate\_public\_ip\_address) | Whether to associate public IP to the instance. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [availability\_zones](#input\_availability\_zones) | AWS Availability Zones in which to deploy multi-AZ resources.
If not provided, resources will be provisioned in every private subnet in the VPC. | `list(string)` | `[]` | no | | [container\_command](#input\_container\_command) | The container command passed in after `docker run --rm -it bash -c`. | `string` | `"bash"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [custom\_bastion\_hostname](#input\_custom\_bastion\_hostname) | Hostname to assign with bastion instance | `string` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [ebs\_block\_device\_volume\_size](#input\_ebs\_block\_device\_volume\_size) | The volume size (in GiB) to provision for the EBS block device. Creation skipped if size is 0 | `number` | `0` | no | -| [ebs\_delete\_on\_termination](#input\_ebs\_delete\_on\_termination) | Whether the EBS volume should be destroyed on instance termination | `bool` | `false` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | @@ -119,13 +119,10 @@ components: | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS region | `string` | n/a | yes | -| [root\_block\_device\_volume\_size](#input\_root\_block\_device\_volume\_size) | The volume size (in GiB) to provision for the root block device. It cannot be smaller than the AMI it refers to. | `number` | `8` | no | | [security\_group\_rules](#input\_security\_group\_rules) | A list of maps of Security Group rules.
The values of map is fully complated with `aws_security_group_rule` resource.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` |
[
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 0,
"protocol": -1,
"to_port": 0,
"type": "egress"
},
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 22,
"protocol": "tcp",
"to_port": 22,
"type": "ingress"
}
]
| no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [user\_data](#input\_user\_data) | User data content | `list(string)` | `[]` | no | -| [vanity\_domain](#input\_vanity\_domain) | Vanity domain | `string` | `null` | no | ## Outputs diff --git a/modules/bastion/default.auto.tfvars b/modules/bastion/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/bastion/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/bastion/main.tf b/modules/bastion/main.tf index 34f736dce..846d0e69c 100644 --- a/modules/bastion/main.tf +++ b/modules/bastion/main.tf @@ -1,10 +1,13 @@ locals { - enabled = module.this.enabled - vpc_id = module.vpc.outputs.vpc_id - vpc_private_subnet_ids = module.vpc.outputs.private_subnet_ids - vpc_public_subnet_ids = module.vpc.outputs.public_subnet_ids + enabled = module.this.enabled + vpc_id = module.vpc.outputs.vpc_id + vpc_outputs = module.vpc.outputs + + # Get only the subnets that correspond to the AZs provided in `var.availability_zones` if set. + # `az_private_subnets_map` and `az_public_subnets_map` are a map of AZ names to list of subnet IDs in the AZs + vpc_private_subnet_ids = length(var.availability_zones) == 0 ? module.vpc.outputs.private_subnet_ids : flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k)]) + vpc_public_subnet_ids = length(var.availability_zones) == 0 ? module.vpc.outputs.public_subnet_ids : flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k)]) vpc_subnet_ids = var.associate_public_ip_address ? local.vpc_public_subnet_ids : local.vpc_private_subnet_ids - route52_enabled = var.associate_public_ip_address && var.custom_bastion_hostname != null && var.vanity_domain != null userdata_template = "${path.module}/templates/user-data.sh" container_template = "${path.module}/templates/container.sh" @@ -93,21 +96,21 @@ module "bastion_autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" version = "0.35.0" - image_id = join("", data.aws_ami.bastion_image.*.id) + image_id = join("", data.aws_ami.bastion_image[*].id) instance_type = var.instance_type - subnet_ids = local.vpc_private_subnet_ids + subnet_ids = local.vpc_subnet_ids health_check_type = "EC2" min_size = 1 max_size = 2 default_cooldown = 300 scale_down_cooldown_seconds = 300 wait_for_capacity_timeout = "10m" - user_data_base64 = join("", data.cloudinit_config.config[0].*.rendered) + user_data_base64 = join("", data.cloudinit_config.config[0][*].rendered) tags = module.this.tags security_group_ids = [module.sg.id] - iam_instance_profile_name = join("", aws_iam_instance_profile.default.*.name) + iam_instance_profile_name = join("", aws_iam_instance_profile.default[*].name) block_device_mappings = [] - associate_public_ip_address = false + associate_public_ip_address = var.associate_public_ip_address # Auto-scaling policies and CloudWatch metric alarms autoscaling_policies_enabled = true diff --git a/modules/bastion/outputs.tf b/modules/bastion/outputs.tf index 545ae6333..b9753f8f3 100644 --- a/modules/bastion/outputs.tf +++ b/modules/bastion/outputs.tf @@ -4,7 +4,7 @@ output "autoscaling_group_id" { } output "iam_instance_profile" { - value = join("", aws_iam_instance_profile.default.*.name) + value = join("", aws_iam_instance_profile.default[*].name) description = "Name of AWS IAM Instance Profile" } diff --git a/modules/bastion/variables.tf b/modules/bastion/variables.tf index 5e1444e65..3ed81c211 100644 --- a/modules/bastion/variables.tf +++ b/modules/bastion/variables.tf @@ -3,42 +3,27 @@ variable "region" { description = "AWS region" } +variable "availability_zones" { + type = list(string) + description = <<-EOT + AWS Availability Zones in which to deploy multi-AZ resources. + If not provided, resources will be provisioned in every private subnet in the VPC. + EOT + default = [] +} + variable "instance_type" { type = string default = "t2.micro" description = "Bastion instance type" } -variable "root_block_device_volume_size" { - type = number - default = 8 - description = "The volume size (in GiB) to provision for the root block device. It cannot be smaller than the AMI it refers to." -} - variable "associate_public_ip_address" { type = bool default = false description = "Whether to associate public IP to the instance." } -variable "ebs_block_device_volume_size" { - type = number - default = 0 - description = "The volume size (in GiB) to provision for the EBS block device. Creation skipped if size is 0" -} - -variable "ebs_delete_on_termination" { - type = bool - default = false - description = "Whether the EBS volume should be destroyed on instance termination" -} - -variable "user_data" { - type = list(string) - default = [] - description = "User data content" -} - variable "security_group_rules" { type = list(any) default = [ @@ -64,20 +49,9 @@ variable "security_group_rules" { EOT } -variable "custom_bastion_hostname" { - type = string - default = null - description = "Hostname to assign with bastion instance" -} - -variable "vanity_domain" { - type = string - default = null - description = "Vanity domain" -} - # AWS KMS alias used for encryption/decryption of SSM secure strings variable "kms_alias_name_ssm" { + type = string default = "alias/aws/ssm" description = "KMS alias name for SSM" } From 87f74f1e2acec2575eb99d29cd15c06aeae702a6 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Sat, 29 Jul 2023 14:00:06 -0400 Subject: [PATCH 199/501] Use s3_object_ownership variable (#779) --- modules/s3-bucket/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/s3-bucket/main.tf b/modules/s3-bucket/main.tf index e248c35d4..10a47f478 100644 --- a/modules/s3-bucket/main.tf +++ b/modules/s3-bucket/main.tf @@ -54,6 +54,7 @@ module "s3_bucket" { source_policy_documents = [local.bucket_policy] privileged_principal_actions = var.privileged_principal_actions privileged_principal_arns = var.privileged_principal_arns + s3_object_ownership = var.s3_object_ownership # Static website configuration cors_configuration = var.cors_configuration From 8b89bc3c45d8568f090c362db4e735872f8cab27 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 31 Jul 2023 12:41:21 -0700 Subject: [PATCH 200/501] Spacelift `admin-stack` `var.description` (#787) --- modules/spacelift/admin-stack/child-stacks.tf | 2 +- modules/spacelift/admin-stack/root-admin-stack.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index 316131bc3..d7f976ed0 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -88,7 +88,7 @@ module "child_stack" { component_root = try(join("/", [var.component_root, try(each.value.metadata.component, each.value.component)])) component_vars = try(each.value.vars, null) context_attachments = try(each.value.context_attachments, []) - description = try(each.value.description, null) + description = try(each.value.description, var.description) drift_detection_enabled = try(each.value.settings.spacelift.drift_detection_enabled, var.drift_detection_enabled) drift_detection_reconcile = try(each.value.settings.spacelift.drift_detection_reconcile, var.drift_detection_reconcile) drift_detection_schedule = try(each.value.settings.spacelift.drift_detection_schedule, var.drift_detection_schedule) diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf index 8d3b70f75..3258dde17 100644 --- a/modules/spacelift/admin-stack/root-admin-stack.tf +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -55,7 +55,7 @@ module "root_admin_stack" { component_root = try(join("/", [var.component_root, local.root_admin_stack_config.metadata.component]), null) component_vars = try(local.root_admin_stack_config.vars, null) context_attachments = try(local.root_admin_stack_config.context_attachments, []) - description = try(local.root_admin_stack_config.description, null) + description = try(local.root_admin_stack_config.description, var.description) drift_detection_enabled = try(local.root_admin_stack_config.settings.spacelift.drift_detection_enabled, var.drift_detection_enabled) drift_detection_reconcile = try(local.root_admin_stack_config.settings.spacelift.drift_detection_reconcile, var.drift_detection_reconcile) drift_detection_schedule = try(local.root_admin_stack_config.settings.spacelift.drift_detection_schedule, var.drift_detection_schedule) From 657c6375fc12feb7c7c315043048b2437dcf9b82 Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Tue, 1 Aug 2023 08:04:14 +0300 Subject: [PATCH 201/501] Added new variable into `argocd-repo` component to configure ArgoCD's `ignore-differences` (#785) --- modules/argocd-repo/README.md | 2 +- modules/argocd-repo/applicationset.tf | 13 ++++++----- .../templates/applicationset.yaml.tpl | 12 ++++++++++ modules/argocd-repo/variables.tf | 22 +++++++++++++++++++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index f9b0214c8..19e09fd8e 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -133,7 +133,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [environments](#input\_environments) | Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for.

`auto-sync` determines whether or not the ArgoCD application will be automatically synced. |
list(object({
tenant = string
environment = string
stage = string
auto-sync = bool
}))
| `[]` | no | +| [environments](#input\_environments) | Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for.

`auto-sync` determines whether or not the ArgoCD application will be automatically synced.

`ignore-differences` determines whether or not the ArgoCD application will ignore the number of
replicas in the deployment. Read more on ignore differences here:
https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs

Example:
tenant: plat
environment: use1
stage: sandbox
auto-sync: true
ignore-differences:
- group: apps
kind: Deployment
json-pointers:
- /spec/replicas
|
list(object({
tenant = string
environment = string
stage = string
auto-sync = bool
ignore-differences = list(object({
group = string,
kind = string,
json-pointers = list(string)
}))
}))
| `[]` | no | | [github\_base\_url](#input\_github\_base\_url) | This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/` | `string` | `null` | no | | [github\_codeowner\_teams](#input\_github\_codeowner\_teams) | List of teams to use when populating the CODEOWNERS file.

For example: `["@ACME/cloud-admins", "@ACME/cloud-developers"]`. | `list(string)` | n/a | yes | | [github\_organization](#input\_github\_organization) | GitHub Organization | `string` | n/a | yes | diff --git a/modules/argocd-repo/applicationset.tf b/modules/argocd-repo/applicationset.tf index 39c1f5990..b664271e6 100644 --- a/modules/argocd-repo/applicationset.tf +++ b/modules/argocd-repo/applicationset.tf @@ -5,12 +5,13 @@ resource "github_repository_file" "application_set" { branch = join("", github_repository.default.*.default_branch) file = "${each.value.tenant != null ? format("%s/", each.value.tenant) : ""}${each.value.environment}-${each.value.stage}/${local.manifest_kubernetes_namespace}/applicationset.yaml" content = templatefile("${path.module}/templates/applicationset.yaml.tpl", { - environment = each.key - auto-sync = each.value.auto-sync - name = module.this.namespace - namespace = local.manifest_kubernetes_namespace - ssh_url = join("", github_repository.default.*.ssh_clone_url) - slack_channel = var.slack_channel + environment = each.key + auto-sync = each.value.auto-sync + ignore-differences = each.value.ignore-differences + name = module.this.namespace + namespace = local.manifest_kubernetes_namespace + ssh_url = join("", github_repository.default.*.ssh_clone_url) + slack_channel = var.slack_channel }) commit_message = "Initialize environment: `${each.key}`." commit_author = var.github_user diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index 62efbeff6..d4727ac60 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -85,3 +85,15 @@ spec: %{ endif ~} syncOptions: - CreateNamespace=true +%{if length(ignore-differences) > 0 ~} + - RespectIgnoreDifferences=true + ignoreDifferences: +%{for item in ignore-differences ~} + - group: "${item.group}" + kind: "${item.kind}" + jsonPointers: +%{for pointer in item.json-pointers ~} + - ${pointer} +%{ endfor ~} +%{ endfor ~} +%{ endif ~} diff --git a/modules/argocd-repo/variables.tf b/modules/argocd-repo/variables.tf index 739c6ef13..9114bcbd3 100644 --- a/modules/argocd-repo/variables.tf +++ b/modules/argocd-repo/variables.tf @@ -15,11 +15,33 @@ variable "environments" { environment = string stage = string auto-sync = bool + ignore-differences = list(object({ + group = string, + kind = string, + json-pointers = list(string) + })) })) description = <<-EOT Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for. `auto-sync` determines whether or not the ArgoCD application will be automatically synced. + + `ignore-differences` determines whether or not the ArgoCD application will ignore the number of + replicas in the deployment. Read more on ignore differences here: + https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs + + Example: + ``` + tenant: plat + environment: use1 + stage: sandbox + auto-sync: true + ignore-differences: + - group: apps + kind: Deployment + json-pointers: + - /spec/replicas + ``` EOT default = [] } From ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 3 Aug 2023 13:47:33 -0700 Subject: [PATCH 202/501] upstream `api-gateway` and `api-gateway-settings` (#788) Co-authored-by: Dan Miller --- .../api-gateway-account-settings/README.md | 85 ++++++ .../api-gateway-account-settings/context.tf | 279 ++++++++++++++++++ modules/api-gateway-account-settings/main.tf | 6 + .../api-gateway-account-settings/outputs.tf | 4 + .../api-gateway-account-settings/providers.tf | 19 ++ .../api-gateway-account-settings/variables.tf | 4 + .../api-gateway-account-settings/versions.tf | 10 + modules/api-gateway-rest-api/README.md | 125 ++++++++ modules/api-gateway-rest-api/context.tf | 279 ++++++++++++++++++ modules/api-gateway-rest-api/main.tf | 77 +++++ modules/api-gateway-rest-api/nlb.tf | 29 ++ modules/api-gateway-rest-api/outputs.tf | 33 +++ modules/api-gateway-rest-api/providers.tf | 19 ++ modules/api-gateway-rest-api/remote-state.tf | 28 ++ modules/api-gateway-rest-api/variables.tf | 128 ++++++++ modules/api-gateway-rest-api/versions.tf | 10 + 16 files changed, 1135 insertions(+) create mode 100644 modules/api-gateway-account-settings/README.md create mode 100644 modules/api-gateway-account-settings/context.tf create mode 100644 modules/api-gateway-account-settings/main.tf create mode 100644 modules/api-gateway-account-settings/outputs.tf create mode 100644 modules/api-gateway-account-settings/providers.tf create mode 100644 modules/api-gateway-account-settings/variables.tf create mode 100644 modules/api-gateway-account-settings/versions.tf create mode 100644 modules/api-gateway-rest-api/README.md create mode 100644 modules/api-gateway-rest-api/context.tf create mode 100644 modules/api-gateway-rest-api/main.tf create mode 100644 modules/api-gateway-rest-api/nlb.tf create mode 100644 modules/api-gateway-rest-api/outputs.tf create mode 100644 modules/api-gateway-rest-api/providers.tf create mode 100644 modules/api-gateway-rest-api/remote-state.tf create mode 100644 modules/api-gateway-rest-api/variables.tf create mode 100644 modules/api-gateway-rest-api/versions.tf diff --git a/modules/api-gateway-account-settings/README.md b/modules/api-gateway-account-settings/README.md new file mode 100644 index 000000000..c716057ec --- /dev/null +++ b/modules/api-gateway-account-settings/README.md @@ -0,0 +1,85 @@ +# Component: `api-gateway-account-settings` + +This component is responsible for setting the global, regional settings required to allow API Gateway to write to CloudWatch logs. + +Every AWS region you want to deploy an API Gateway to must be configured with an IAM Role that gives API Gateway permissions to create and write to CloudWatch logs. Without this configuration, API Gateway will not be able to send logs to CloudWatch. This configuration is done once per region regardless of the number of API Gateways deployed in that region. This module creates an IAM role, assigns it the necessary permissions to write logs and sets it as the "CloudWatch log role ARN" in the API Gateway configuration. + +## Usage + +**Stack Level**: Regional + +The following is a snippet for how to use this component: + +```yaml +components: + terraform: + api-gateway-account-settings: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + tags: + Service: api-gateway +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [api\_gateway\_account\_settings](#module\_api\_gateway\_account\_settings) | cloudposse/api-gateway/aws//modules/account-settings | 0.3.1 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [role\_arn](#output\_role\_arn) | Role ARN of the API Gateway logging role | + + +## References +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/api-gateway-settings) - Cloud Posse's upstream component + + +[](https://cpco.io/component) diff --git a/modules/api-gateway-account-settings/context.tf b/modules/api-gateway-account-settings/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/api-gateway-account-settings/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/api-gateway-account-settings/main.tf b/modules/api-gateway-account-settings/main.tf new file mode 100644 index 000000000..1a812109d --- /dev/null +++ b/modules/api-gateway-account-settings/main.tf @@ -0,0 +1,6 @@ +module "api_gateway_account_settings" { + source = "cloudposse/api-gateway/aws//modules/account-settings" + version = "0.3.1" + + context = module.this.context +} diff --git a/modules/api-gateway-account-settings/outputs.tf b/modules/api-gateway-account-settings/outputs.tf new file mode 100644 index 000000000..1b0b7a57d --- /dev/null +++ b/modules/api-gateway-account-settings/outputs.tf @@ -0,0 +1,4 @@ +output "role_arn" { + description = "Role ARN of the API Gateway logging role" + value = module.api_gateway_account_settings.role_arn +} diff --git a/modules/api-gateway-account-settings/providers.tf b/modules/api-gateway-account-settings/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/api-gateway-account-settings/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/api-gateway-account-settings/variables.tf b/modules/api-gateway-account-settings/variables.tf new file mode 100644 index 000000000..0753180bf --- /dev/null +++ b/modules/api-gateway-account-settings/variables.tf @@ -0,0 +1,4 @@ +variable "region" { + type = string + description = "AWS Region" +} diff --git a/modules/api-gateway-account-settings/versions.tf b/modules/api-gateway-account-settings/versions.tf new file mode 100644 index 000000000..f33ede77f --- /dev/null +++ b/modules/api-gateway-account-settings/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} diff --git a/modules/api-gateway-rest-api/README.md b/modules/api-gateway-rest-api/README.md new file mode 100644 index 000000000..5e475a297 --- /dev/null +++ b/modules/api-gateway-rest-api/README.md @@ -0,0 +1,125 @@ +# Component: `api-gateway-account-settings` + +This component is responsible for deploying an API Gateway REST API. +## Usage + +**Stack Level**: Regional + +The following is a snippet for how to use this component: + +```yaml +components: + terraform: + api-gateway-rest-api: + vars: + enabled: true + name: api + openapi_config: + openapi: 3.0.1 + info: + title: Example API Gateway + version: 1.0.0 + paths: + "/": + get: + x-amazon-apigateway-integration: + httpMethod: GET + payloadFormatVersion: 1.0 + type: HTTP_PROXY + uri: https://api.ipify.org + "/{proxy+}": + get: + x-amazon-apigateway-integration: + httpMethod: GET + payloadFormatVersion: 1.0 + type: HTTP_PROXY + uri: https://api.ipify.org +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [acm](#module\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [api\_gateway\_rest\_api](#module\_api\_gateway\_rest\_api) | cloudposse/api-gateway/aws | 0.3.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [nlb](#module\_nlb) | cloudposse/nlb/aws | 0.12.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | + +## Resources + +| Name | Type | +|------|------| +| [aws_api_gateway_base_path_mapping.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_base_path_mapping) | resource | +| [aws_api_gateway_domain_name.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_domain_name) | resource | +| [aws_route53_record.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | +| [aws_acm_certificate.issued](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/acm_certificate) | data source | +| [aws_route53_zone.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [access\_log\_format](#input\_access\_log\_format) | The format of the access log file. | `string` | `" {\n \"requestTime\": \"$context.requestTime\",\n \"requestId\": \"$context.requestId\",\n \"httpMethod\": \"$context.httpMethod\",\n \"path\": \"$context.path\",\n \"resourcePath\": \"$context.resourcePath\",\n \"status\": $context.status,\n \"responseLatency\": $context.responseLatency,\n \"xrayTraceId\": \"$context.xrayTraceId\",\n \"integrationRequestId\": \"$context.integration.requestId\",\n \"functionResponseStatus\": \"$context.integration.status\",\n \"integrationLatency\": \"$context.integration.latency\",\n \"integrationServiceStatus\": \"$context.integration.integrationStatus\",\n \"authorizeResultStatus\": \"$context.authorize.status\",\n \"authorizerServiceStatus\": \"$context.authorizer.status\",\n \"authorizerLatency\": \"$context.authorizer.latency\",\n \"authorizerRequestId\": \"$context.authorizer.requestId\",\n \"ip\": \"$context.identity.sourceIp\",\n \"userAgent\": \"$context.identity.userAgent\",\n \"principalId\": \"$context.authorizer.principalId\",\n \"cognitoUser\": \"$context.identity.cognitoIdentityId\",\n \"user\": \"$context.identity.user\"\n}\n"` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [deregistration\_delay](#input\_deregistration\_delay) | The amount of time to wait in seconds before changing the state of a deregistering target to unused | `number` | `15` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enable\_private\_link\_nlb\_deletion\_protection](#input\_enable\_private\_link\_nlb\_deletion\_protection) | A flag to indicate whether to enable private link deletion protection. | `bool` | `false` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [endpoint\_type](#input\_endpoint\_type) | The type of the endpoint. One of - PUBLIC, PRIVATE, REGIONAL | `string` | `"REGIONAL"` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [fully\_qualified\_domain\_name](#input\_fully\_qualified\_domain\_name) | The fully qualified domain name of the API. | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [logging\_level](#input\_logging\_level) | The logging level of the API. One of - OFF, INFO, ERROR | `string` | `"INFO"` | no | +| [metrics\_enabled](#input\_metrics\_enabled) | A flag to indicate whether to enable metrics collection. | `bool` | `true` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [openapi\_config](#input\_openapi\_config) | The OpenAPI specification for the API | `any` | `{}` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [rest\_api\_policy](#input\_rest\_api\_policy) | The IAM policy document for the API. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [xray\_tracing\_enabled](#input\_xray\_tracing\_enabled) | A flag to indicate whether to enable X-Ray tracing. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | The ARN of the REST API | +| [created\_date](#output\_created\_date) | The date the REST API was created | +| [execution\_arn](#output\_execution\_arn) | The execution ARN part to be used in lambda\_permission's source\_arn when allowing API Gateway to invoke a Lambda
function, e.g., arn:aws:execute-api:eu-west-2:123456789012:z4675bid1j, which can be concatenated with allowed stage,
method and resource path.The ARN of the Lambda function that will be executed. | +| [id](#output\_id) | The ID of the REST API | +| [invoke\_url](#output\_invoke\_url) | The URL to invoke the REST API | +| [root\_resource\_id](#output\_root\_resource\_id) | The resource ID of the REST API's root | + + +## References +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component + + +[](https://cpco.io/component) diff --git a/modules/api-gateway-rest-api/context.tf b/modules/api-gateway-rest-api/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/api-gateway-rest-api/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/api-gateway-rest-api/main.tf b/modules/api-gateway-rest-api/main.tf new file mode 100644 index 000000000..b083b360e --- /dev/null +++ b/modules/api-gateway-rest-api/main.tf @@ -0,0 +1,77 @@ +locals { + enabled = module.this.enabled + + sub_domain = var.name + root_domain = coalesce(module.acm.outputs.domain_name, join(".", [ + module.this.environment, module.dns_delegated.outputs.default_domain_name + ]), module.dns_delegated.outputs.default_domain_name) + domain_name = join(".", [local.sub_domain, local.root_domain]) +} + +module "api_gateway_rest_api" { + source = "cloudposse/api-gateway/aws" + version = "0.3.1" + + enabled = local.enabled + + openapi_config = var.openapi_config + endpoint_type = var.endpoint_type + logging_level = var.logging_level + metrics_enabled = var.metrics_enabled + xray_tracing_enabled = var.xray_tracing_enabled + access_log_format = var.access_log_format + rest_api_policy = var.rest_api_policy + private_link_target_arns = module.nlb[*].nlb_arn + + context = module.this.context +} + +data "aws_acm_certificate" "issued" { + count = local.enabled ? 1 : 0 + domain = local.root_domain + statuses = ["ISSUED"] +} + +data "aws_route53_zone" "this" { + count = local.enabled ? 1 : 0 + name = module.dns_delegated.outputs.default_domain_name + private_zone = false +} + +resource "aws_api_gateway_domain_name" "this" { + count = local.enabled ? 1 : 0 + domain_name = local.domain_name + regional_certificate_arn = data.aws_acm_certificate.issued[0].arn + + endpoint_configuration { + types = ["REGIONAL"] + } + + tags = module.this.tags +} + +resource "aws_api_gateway_base_path_mapping" "this" { + count = local.enabled ? 1 : 0 + api_id = module.api_gateway_rest_api.id + domain_name = aws_api_gateway_domain_name.this[0].domain_name + stage_name = module.this.stage + + depends_on = [ + aws_api_gateway_domain_name.this, + module.api_gateway_rest_api + ] + +} + +resource "aws_route53_record" "this" { + count = local.enabled ? 1 : 0 + name = aws_api_gateway_domain_name.this[0].domain_name + type = "A" + zone_id = data.aws_route53_zone.this[0].id + + alias { + evaluate_target_health = true + name = aws_api_gateway_domain_name.this[0].regional_domain_name + zone_id = aws_api_gateway_domain_name.this[0].regional_zone_id + } +} diff --git a/modules/api-gateway-rest-api/nlb.tf b/modules/api-gateway-rest-api/nlb.tf new file mode 100644 index 000000000..ceadf8f36 --- /dev/null +++ b/modules/api-gateway-rest-api/nlb.tf @@ -0,0 +1,29 @@ +module "nlb" { + source = "cloudposse/nlb/aws" + version = "0.12.0" + + enabled = local.enabled + + vpc_id = module.vpc.outputs.vpc.id + subnet_ids = module.vpc.outputs.private_subnet_ids + internal = true + tcp_enabled = true + cross_zone_load_balancing_enabled = true + ip_address_type = "ipv4" + deletion_protection_enabled = var.enable_private_link_nlb_deletion_protection + tcp_port = 443 + target_group_port = 443 + target_group_target_type = "alb" + health_check_protocol = "HTTPS" + nlb_access_logs_s3_bucket_force_destroy = true + deregistration_delay = var.deregistration_delay + + context = module.this.context +} + +## You can use a target attachment like below to point the nlb at an ecs alb. +#resource "aws_lb_target_group_attachment" "alb" { +# target_group_arn = one(module.nlb[*].default_target_group_arn) +# target_id = module.ecs.outputs.alb_arn +# port = 443 +#} diff --git a/modules/api-gateway-rest-api/outputs.tf b/modules/api-gateway-rest-api/outputs.tf new file mode 100644 index 000000000..0fd29402a --- /dev/null +++ b/modules/api-gateway-rest-api/outputs.tf @@ -0,0 +1,33 @@ +output "id" { + description = "The ID of the REST API" + value = module.this.enabled ? module.api_gateway_rest_api.id : null +} + +output "root_resource_id" { + description = "The resource ID of the REST API's root" + value = module.this.enabled ? module.api_gateway_rest_api.root_resource_id : null +} + +output "created_date" { + description = "The date the REST API was created" + value = module.this.enabled ? module.api_gateway_rest_api.created_date : null +} + +output "execution_arn" { + description = < Date: Mon, 7 Aug 2023 14:40:47 -0700 Subject: [PATCH 203/501] [eks/karpenter] Script to update Karpenter CRDs (#793) --- modules/eks/karpenter/karpenter-crd-upgrade | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100755 modules/eks/karpenter/karpenter-crd-upgrade diff --git a/modules/eks/karpenter/karpenter-crd-upgrade b/modules/eks/karpenter/karpenter-crd-upgrade new file mode 100755 index 000000000..a3e3ce05c --- /dev/null +++ b/modules/eks/karpenter/karpenter-crd-upgrade @@ -0,0 +1,28 @@ +#!/bin/bash + +function usage() { + cat >&2 <<'EOF' +./karpenter-crd-upgrade + +Use this script to upgrade the Karpenter CRDs by installing or upgrading the karpenter-crd helm chart. + +EOF +} + +function upgrade() { + VERSION="${1}" + [[ $VERSION =~ ^v ]] || VERSION="v${VERSION}" + + set -x + + kubectl label crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh app.kubernetes.io/managed-by=Helm --overwrite + kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-name=karpenter-crd --overwrite + kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-namespace=karpenter --overwrite + helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version "$VERSION" --namespace karpenter +} + +if (($# == 0)); then + usage +else + upgrade $1 +fi From 4f2dc2522ce53903c03acaa56aa75110bf9a9b85 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 7 Aug 2023 14:52:16 -0700 Subject: [PATCH 204/501] [eks/storage-class] Initial implementation (#794) --- modules/eks/storage-class/README.md | 197 +++++++++++++++ modules/eks/storage-class/context.tf | 279 +++++++++++++++++++++ modules/eks/storage-class/main.tf | 88 +++++++ modules/eks/storage-class/outputs.tf | 4 + modules/eks/storage-class/provider-helm.tf | 166 ++++++++++++ modules/eks/storage-class/providers.tf | 19 ++ modules/eks/storage-class/remote-state.tf | 19 ++ modules/eks/storage-class/variables.tf | 87 +++++++ modules/eks/storage-class/versions.tf | 18 ++ 9 files changed, 877 insertions(+) create mode 100644 modules/eks/storage-class/README.md create mode 100644 modules/eks/storage-class/context.tf create mode 100644 modules/eks/storage-class/main.tf create mode 100644 modules/eks/storage-class/outputs.tf create mode 100644 modules/eks/storage-class/provider-helm.tf create mode 100644 modules/eks/storage-class/providers.tf create mode 100644 modules/eks/storage-class/remote-state.tf create mode 100644 modules/eks/storage-class/variables.tf create mode 100644 modules/eks/storage-class/versions.tf diff --git a/modules/eks/storage-class/README.md b/modules/eks/storage-class/README.md new file mode 100644 index 000000000..1be842269 --- /dev/null +++ b/modules/eks/storage-class/README.md @@ -0,0 +1,197 @@ +# Component: `eks/storage-class` + +This component is responsible for provisioning `StorageClasses` in an EKS cluster. +See the list of guides and references linked at the bottom of this README for more information. + +A StorageClass provides part of the configuration for a PersistentVolumeClaim, +which copies the configuration when it is created. Thus, you can delete a StorageClass +without affecting existing PersistentVolumeClaims, and changes to a StorageClass +do not propagate to existing PersistentVolumeClaims. + +## Usage + +**Stack Level**: Regional, per cluster + +This component can create storage classes backed by EBS or EFS, and is intended to be used +with the corresponding EKS add-ons `aws-ebs-csi-driver` and `aws-efs-csi-driver` respectively. +In the case of EFS, this component also requires that you have provisioned an EFS filesystem +in the same region as your cluster, and expects you have used the `eks/efs` component to do so. +The EFS storage classes will get the file system ID from the EFS component's output. + +### Note: Default Storage Class + +Exactly one StorageClass can be designated as the default StorageClass for a cluster. +This default StorageClass is then used by PersistentVolumeClaims that do not specify a storage class. + +Prior to Kubernetes 1.26, if more than one StorageClass is marked as default, +a PersistentVolumeClaim without `storageClassName` explicitly specified cannot be created. +In Kubernetes 1.26 and later, if more than one StorageClass is marked as default, +the last one created will be used. + +EKS always creates a default storage class for the cluster, typically an EBS backed class named `gp2`. Find out +what the default storage class is for your cluster by running this command: + +```bash +kubectl get storageclass +``` + +This will list the available storage classes, with the default one marked with `(default)` next to its name. + +If you want to change the default, you can unset the existing default manually, like this: + +```bash +SC_NAME=gp2 # Replace with the name of the storage class you want to unset as default +kubectl patch storageclass $SC_NAME -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' +``` + +Or you can import the existing default storage class into Terraform and manage or delete it entirely, like this: + +```bash +SC_NAME=gp2 # Replace with the name of the storage class you want to unset as default +atmos terraform import eks/storage-class 'kubernetes_storage_class_v1.ebs["'${SC_NAME}'"]' $SC_NAME -s=core-usw2-dev +``` + +View the parameters of a storage class by running this command: + +```bash +SC_NAME=gp2 # Replace with the name of the storage class you want to view +kubectl get storageclass $SC_NAME -o yaml +``` + +You can then match that configuration, except that you cannot omit `allow_volume_exansion`. + +```yaml +ebs_storage_classes: + gp2: + make_default_storage_class: true + include_tags: false + # Preserve values originally set by eks/cluster. + # Set to "" to omit. + provisioner: kubernetes.io/aws-ebs + parameters: + type: gp2 + encrypted: "" +``` + +Here's an example snippet for how to use this component. + +```yaml + eks/storage-class: + vars: + ebs_storage_classes: + gp2: + make_default_storage_class: false + include_tags: false + # Preserve values originally set by eks/cluster. + # Set to "" to omit. + provisioner: kubernetes.io/aws-ebs + parameters: + type: gp2 + encrypted: "" + gp3: + make_default_storage_class: true + parameters: + type: gp3 + efs_storage_classes: + efs-sc: + make_default_storage_class: false +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.22.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.22.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [efs](#module\_efs) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_storage_class_v1.ebs](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/storage_class_v1) | resource | +| [kubernetes_storage_class_v1.efs](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/storage_class_v1) | resource | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [ebs\_storage\_classes](#input\_ebs\_storage\_classes) | A map of storage class name to EBS parameters to create |
map(object({
make_default_storage_class = optional(bool, false)
include_tags = optional(bool, true) # If true, StorageClass will set our tags on created EBS volumes
labels = optional(map(string), null)
reclaim_policy = optional(string, "Delete")
volume_binding_mode = optional(string, "WaitForFirstConsumer")
mount_options = optional(list(string), null)
# Allowed topologies are poorly documented, and poorly implemented.
# According to the API spec https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#storageclass-v1-storage-k8s-io
# it should be a list of objects with a `matchLabelExpressions` key, which is a list of objects with `key` and `values` keys.
# However, the Terraform resource only allows a single object in a matchLabelExpressions block, not a list,
# the EBS driver appears to only allow a single matchLabelExpressions block, and it is entirely unclear
# what should happen if either of the lists has more than one element.
# So we simplify it here to be singletons, not lists, and allow for a future change to the resource to support lists,
# and a future replacement for this flattened object which can maintain backward compatibility.
allowed_topologies_match_label_expressions = optional(object({
key = optional(string, "topology.ebs.csi.aws.com/zone")
values = list(string)
}), null)
allow_volume_expansion = optional(bool, true)
# parameters, see https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/parameters.md
parameters = object({
fstype = optional(string, "ext4") # "csi.storage.k8s.io/fstype"
type = optional(string, "gp3")
iopsPerGB = optional(string, null)
allowAutoIOPSPerGBIncrease = optional(string, null) # "true" or "false"
iops = optional(string, null)
throughput = optional(string, null)

encrypted = optional(string, "true")
kmsKeyId = optional(string, null) # ARN of the KMS key to use for encryption. If not specified, the default key is used.
blockExpress = optional(string, null) # "true" or "false"
blockSize = optional(string, null)
})
provisioner = optional(string, "ebs.csi.aws.com")

# TODO: support tags
# https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/tagging.md
}))
| `{}` | no | +| [efs\_storage\_classes](#input\_efs\_storage\_classes) | A map of storage class name to EFS parameters to create |
map(object({
make_default_storage_class = optional(bool, false)
labels = optional(map(string), null)
efs_component_name = optional(string, "eks/efs")
reclaim_policy = optional(string, "Delete")
volume_binding_mode = optional(string, "Immediate")
# Mount options are poorly documented.
# TLS is now the default and need not be specified. https://github.com/kubernetes-sigs/aws-efs-csi-driver/tree/master/docs#encryption-in-transit
# Other options include `lookupcache` and `iam`.
mount_options = optional(list(string), null)
parameters = optional(object({
basePath = optional(string, "/efs_controller")
directoryPerms = optional(string, "700")
provisioningMode = optional(string, "efs-ap")
gidRangeStart = optional(string, null)
gidRangeEnd = optional(string, null)
# Support for cross-account EFS mounts
# See https://github.com/kubernetes-sigs/aws-efs-csi-driver/tree/master/examples/kubernetes/cross_account_mount
# and for gritty details on secrets: https://kubernetes-csi.github.io/docs/secrets-and-credentials-storage-class.html
az = optional(string, null)
provisioner-secret-name = optional(string, null) # "csi.storage.k8s.io/provisioner-secret-name"
provisioner-secret-namespace = optional(string, null) # "csi.storage.k8s.io/provisioner-secret-namespace"
}), {})
provisioner = optional(string, "efs.csi.aws.com")
}))
| `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the EKS component for the cluster in which to create the storage classes | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region. | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [storage\_classes](#output\_storage\_classes) | Storage classes created by this module | + + +## Related How-to Guides + +- [EBS CSI Migration FAQ](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi-migration-faq.html) +- [Migrating Clusters From gp2 to gp3 EBS Volumes](https://aws.amazon.com/blogs/containers/migrating-amazon-eks-clusters-from-gp2-to-gp3-ebs-volumes/) +- [Kubernetes: Change the Default StorageClass](https://kubernetes.io/docs/tasks/administer-cluster/change-default-storage-class/) + +## References + +- [Kubernetes Storage Classes](https://kubernetes.io/docs/concepts/storage/storage-classes) +- +- [EBS CSI driver (Amazon)](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html) +- [EBS CSI driver (GitHub)](https://github.com/kubernetes-sigs/aws-ebs-csi-driver#documentation) +- [EBS CSI StorageClass Parameters](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/parameters.md) +- [EFS CSI driver (Amazon)](https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html) +- [EFS CSI driver (GitHub)](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/README.md#examples) +- [EFS CSI StorageClass Parameters](https://github.com/kubernetes-sigs/aws-efs-csi-driver/tree/master/docs#storage-class-parameters-for-dynamic-provisioning) +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/cluster) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/eks/storage-class/context.tf b/modules/eks/storage-class/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/storage-class/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/storage-class/main.tf b/modules/eks/storage-class/main.tf new file mode 100644 index 000000000..e4abdd8fb --- /dev/null +++ b/modules/eks/storage-class/main.tf @@ -0,0 +1,88 @@ +locals { + enabled = module.this.enabled + + efs_components = local.enabled ? toset([for k, v in var.efs_storage_classes : v.efs_component_name]) : [] + + # In order to use `optional()`, the variable must be an object, but + # object keys must be valid identifiers and cannot be like "csi.storage.k8s.io/fstype" + # See https://github.com/hashicorp/terraform/issues/22681 + # So we have to convert the object to a map with the keys the StorageClass expects + ebs_key_map = { + fstype = "csi.storage.k8s.io/fstype" + } + old_ebs_key_map = { + fstype = "fsType" + } + + efs_key_map = { + provisioner-secret-name = "csi.storage.k8s.io/provisioner-secret-name" + provisioner-secret-namespace = "csi.storage.k8s.io/provisioner-secret-namespace" + } + + # Tag with cluster name rather than just stage ID. + tags = merge(module.this.tags, { Name = module.eks.outputs.eks_cluster_id }) +} + +resource "kubernetes_storage_class_v1" "ebs" { + for_each = local.enabled ? var.ebs_storage_classes : {} + + metadata { + name = each.key + annotations = { + "storageclass.kubernetes.io/is-default-class" = each.value.make_default_storage_class ? "true" : "false" + } + labels = each.value.labels + } + + # Tags are implemented via parameters. We use "tagSpecification_n" as the key, starting at 1. + # See https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/tagging.md#storageclass-tagging + parameters = merge({ for k, v in each.value.parameters : ( + # provisioner kubernetes.io/aws-ebs uses the key "fsType" instead of "csi.storage.k8s.io/fstype" + lookup((each.value.provisioner == "kubernetes.io/aws-ebs" ? local.old_ebs_key_map : local.ebs_key_map), k, k)) => v if v != null && v != "" }, + each.value.include_tags ? { for i, k in keys(local.tags) : "tagSpecification_${i + 1}" => "${k}=${local.tags[k]}" } : {}, + ) + + storage_provisioner = each.value.provisioner + reclaim_policy = each.value.reclaim_policy + volume_binding_mode = each.value.volume_binding_mode + mount_options = each.value.mount_options + + # Allowed topologies are poorly documented, and poorly implemented. + # According to the API spec https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#storageclass-v1-storage-k8s-io + # it should be a list of objects with a `matchLabelExpressions` key, which is a list of objects with `key` and `values` keys. + # However, the Terraform resource only allows a single object in a matchLabelExpressions block, not a list,, + # the EBS driver appears to only allow a single matchLabelExpressions block, and it is entirely unclear + # what should happen if either of the lists has more than one element. So we simplify it here to be singletons, not lists. + dynamic "allowed_topologies" { + for_each = each.value.allowed_topologies_match_label_expressions != null ? ["zones"] : [] + content { + match_label_expressions { + key = each.value.allowed_topologies_match_label_expressions.key + values = each.value.allowed_topologies_match_label_expressions.values + } + } + } + + # Unfortunately, the provider always sets allow_volume_expansion to something whether you provide it or not. + # There is no way to omit it. + allow_volume_expansion = each.value.allow_volume_expansion +} + +resource "kubernetes_storage_class_v1" "efs" { + for_each = local.enabled ? var.efs_storage_classes : {} + + metadata { + name = each.key + annotations = { + "storageclass.kubernetes.io/is-default-class" = each.value.make_default_storage_class ? "true" : "false" + } + labels = each.value.labels + } + parameters = merge({ fileSystemId = module.efs[each.value.efs_component_name].outputs.efs_id }, + { for k, v in each.value.parameters : lookup(local.efs_key_map, k, k) => v if v != null && v != "" }) + + storage_provisioner = each.value.provisioner + reclaim_policy = each.value.reclaim_policy + volume_binding_mode = each.value.volume_binding_mode + mount_options = each.value.mount_options +} diff --git a/modules/eks/storage-class/outputs.tf b/modules/eks/storage-class/outputs.tf new file mode 100644 index 000000000..5d7a7e70f --- /dev/null +++ b/modules/eks/storage-class/outputs.tf @@ -0,0 +1,4 @@ +output "storage_classes" { + value = merge(kubernetes_storage_class_v1.ebs, kubernetes_storage_class_v1.efs) + description = "Storage classes created by this module" +} diff --git a/modules/eks/storage-class/provider-helm.tf b/modules/eks/storage-class/provider-helm.tf new file mode 100644 index 000000000..64459d4f4 --- /dev/null +++ b/modules/eks/storage-class/provider-helm.tf @@ -0,0 +1,166 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/storage-class/providers.tf b/modules/eks/storage-class/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/eks/storage-class/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/eks/storage-class/remote-state.tf b/modules/eks/storage-class/remote-state.tf new file mode 100644 index 000000000..e4db4d0b2 --- /dev/null +++ b/modules/eks/storage-class/remote-state.tf @@ -0,0 +1,19 @@ +module "efs" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + for_each = local.efs_components + + component = each.value + + context = module.this.context +} + +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + context = module.this.context +} diff --git a/modules/eks/storage-class/variables.tf b/modules/eks/storage-class/variables.tf new file mode 100644 index 000000000..597970e54 --- /dev/null +++ b/modules/eks/storage-class/variables.tf @@ -0,0 +1,87 @@ +variable "region" { + description = "AWS Region." + type = string +} + +variable "eks_component_name" { + type = string + description = "The name of the EKS component for the cluster in which to create the storage classes" + default = "eks/cluster" + nullable = false +} + +variable "ebs_storage_classes" { + type = map(object({ + make_default_storage_class = optional(bool, false) + include_tags = optional(bool, true) # If true, StorageClass will set our tags on created EBS volumes + labels = optional(map(string), null) + reclaim_policy = optional(string, "Delete") + volume_binding_mode = optional(string, "WaitForFirstConsumer") + mount_options = optional(list(string), null) + # Allowed topologies are poorly documented, and poorly implemented. + # According to the API spec https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#storageclass-v1-storage-k8s-io + # it should be a list of objects with a `matchLabelExpressions` key, which is a list of objects with `key` and `values` keys. + # However, the Terraform resource only allows a single object in a matchLabelExpressions block, not a list, + # the EBS driver appears to only allow a single matchLabelExpressions block, and it is entirely unclear + # what should happen if either of the lists has more than one element. + # So we simplify it here to be singletons, not lists, and allow for a future change to the resource to support lists, + # and a future replacement for this flattened object which can maintain backward compatibility. + allowed_topologies_match_label_expressions = optional(object({ + key = optional(string, "topology.ebs.csi.aws.com/zone") + values = list(string) + }), null) + allow_volume_expansion = optional(bool, true) + # parameters, see https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/parameters.md + parameters = object({ + fstype = optional(string, "ext4") # "csi.storage.k8s.io/fstype" + type = optional(string, "gp3") + iopsPerGB = optional(string, null) + allowAutoIOPSPerGBIncrease = optional(string, null) # "true" or "false" + iops = optional(string, null) + throughput = optional(string, null) + + encrypted = optional(string, "true") + kmsKeyId = optional(string, null) # ARN of the KMS key to use for encryption. If not specified, the default key is used. + blockExpress = optional(string, null) # "true" or "false" + blockSize = optional(string, null) + }) + provisioner = optional(string, "ebs.csi.aws.com") + + # TODO: support tags + # https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/tagging.md + })) + description = "A map of storage class name to EBS parameters to create" + default = {} + nullable = false +} + +variable "efs_storage_classes" { + type = map(object({ + make_default_storage_class = optional(bool, false) + labels = optional(map(string), null) + efs_component_name = optional(string, "eks/efs") + reclaim_policy = optional(string, "Delete") + volume_binding_mode = optional(string, "Immediate") + # Mount options are poorly documented. + # TLS is now the default and need not be specified. https://github.com/kubernetes-sigs/aws-efs-csi-driver/tree/master/docs#encryption-in-transit + # Other options include `lookupcache` and `iam`. + mount_options = optional(list(string), null) + parameters = optional(object({ + basePath = optional(string, "/efs_controller") + directoryPerms = optional(string, "700") + provisioningMode = optional(string, "efs-ap") + gidRangeStart = optional(string, null) + gidRangeEnd = optional(string, null) + # Support for cross-account EFS mounts + # See https://github.com/kubernetes-sigs/aws-efs-csi-driver/tree/master/examples/kubernetes/cross_account_mount + # and for gritty details on secrets: https://kubernetes-csi.github.io/docs/secrets-and-credentials-storage-class.html + az = optional(string, null) + provisioner-secret-name = optional(string, null) # "csi.storage.k8s.io/provisioner-secret-name" + provisioner-secret-namespace = optional(string, null) # "csi.storage.k8s.io/provisioner-secret-namespace" + }), {}) + provisioner = optional(string, "efs.csi.aws.com") + })) + description = "A map of storage class name to EFS parameters to create" + default = {} + nullable = false +} diff --git a/modules/eks/storage-class/versions.tf b/modules/eks/storage-class/versions.tf new file mode 100644 index 000000000..fba2b45f9 --- /dev/null +++ b/modules/eks/storage-class/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.22.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + } +} From 2d6425794e797d1fc23bd87e5c65f5ffba0ae649 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 7 Aug 2023 14:54:28 -0700 Subject: [PATCH 205/501] [eks/cluster] Add support for BottleRocket and EFS add-on (#795) --- .../eks/ebs-controller/README.md | 0 .../eks/ebs-controller/context.tf | 0 .../eks/ebs-controller/main.tf | 0 .../eks/ebs-controller/outputs.tf | 0 .../eks/ebs-controller/provider-helm.tf | 0 .../eks/ebs-controller/providers.tf | 0 .../eks/ebs-controller/remote-state.tf | 0 .../eks/ebs-controller/variables.tf | 0 .../eks/ebs-controller/versions.tf | 0 .../eks/efs-controller/README.md | 0 .../eks/efs-controller/context.tf | 0 .../eks/efs-controller/main.tf | 0 .../eks/efs-controller/outputs.tf | 0 .../eks/efs-controller/provider-helm.tf | 0 .../eks/efs-controller/providers.tf | 0 .../eks/efs-controller/remote-state.tf | 0 .../resources/iam_policy_statements.yaml | 0 .../eks/efs-controller/resources/values.yaml | 0 .../eks/efs-controller/variables.tf | 0 .../eks/efs-controller/versions.tf | 0 .../eks/eks-without-spotinst/README.md | 0 .../eks/eks-without-spotinst/context.tf | 0 .../eks-without-spotinst/default.auto.tfvars | 0 .../eks-without-spotinst/eks-node-groups.tf | 0 .../eks/eks-without-spotinst/main.tf | 0 .../modules/node_group_by_az/context.tf | 0 .../modules/node_group_by_az/main.tf | 0 .../modules/node_group_by_az/outputs.tf | 0 .../modules/node_group_by_az/variables.tf | 0 .../modules/node_group_by_region/context.tf | 0 .../modules/node_group_by_region/main.tf | 0 .../modules/node_group_by_region/outputs.tf | 0 .../modules/node_group_by_region/variables.tf | 0 .../eks/eks-without-spotinst/outputs.tf | 0 .../eks/eks-without-spotinst/providers.tf | 0 .../eks/eks-without-spotinst/remote-state.tf | 0 .../eks/eks-without-spotinst/variables.tf | 0 .../eks/eks-without-spotinst/versions.tf | 0 modules/eks/cluster/CHANGELOG.md | 90 +++++++- modules/eks/cluster/README.md | 106 ++++++--- modules/eks/cluster/addons.tf | 97 +++++++-- modules/eks/cluster/eks-node-groups.tf | 84 +++++++- modules/eks/cluster/karpenter.tf | 10 + modules/eks/cluster/main.tf | 74 +++++-- .../cluster/modules/node_group_by_az/main.tf | 10 +- .../modules/node_group_by_az/variables.tf | 26 ++- .../modules/node_group_by_region/variables.tf | 27 ++- modules/eks/cluster/variables.tf | 204 +++++++++++++----- modules/eks/cluster/versions.tf | 4 + modules/eks/karpenter/README.md | 4 + modules/eks/karpenter/main.tf | 7 +- modules/eks/karpenter/variables.tf | 13 ++ modules/vpc/README.md | 4 +- modules/vpc/variables.tf | 3 + 54 files changed, 615 insertions(+), 148 deletions(-) rename {modules => deprecated}/eks/ebs-controller/README.md (100%) rename {modules => deprecated}/eks/ebs-controller/context.tf (100%) rename {modules => deprecated}/eks/ebs-controller/main.tf (100%) rename {modules => deprecated}/eks/ebs-controller/outputs.tf (100%) rename {modules => deprecated}/eks/ebs-controller/provider-helm.tf (100%) rename {modules => deprecated}/eks/ebs-controller/providers.tf (100%) rename {modules => deprecated}/eks/ebs-controller/remote-state.tf (100%) rename {modules => deprecated}/eks/ebs-controller/variables.tf (100%) rename {modules => deprecated}/eks/ebs-controller/versions.tf (100%) rename {modules => deprecated}/eks/efs-controller/README.md (100%) rename {modules => deprecated}/eks/efs-controller/context.tf (100%) rename {modules => deprecated}/eks/efs-controller/main.tf (100%) rename {modules => deprecated}/eks/efs-controller/outputs.tf (100%) rename {modules => deprecated}/eks/efs-controller/provider-helm.tf (100%) rename {modules => deprecated}/eks/efs-controller/providers.tf (100%) rename {modules => deprecated}/eks/efs-controller/remote-state.tf (100%) rename {modules => deprecated}/eks/efs-controller/resources/iam_policy_statements.yaml (100%) rename {modules => deprecated}/eks/efs-controller/resources/values.yaml (100%) rename {modules => deprecated}/eks/efs-controller/variables.tf (100%) rename {modules => deprecated}/eks/efs-controller/versions.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/README.md (100%) rename {modules => deprecated}/eks/eks-without-spotinst/context.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/default.auto.tfvars (100%) rename {modules => deprecated}/eks/eks-without-spotinst/eks-node-groups.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/main.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/modules/node_group_by_az/context.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/modules/node_group_by_az/main.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/modules/node_group_by_az/outputs.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/modules/node_group_by_az/variables.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/modules/node_group_by_region/context.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/modules/node_group_by_region/main.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/modules/node_group_by_region/outputs.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/modules/node_group_by_region/variables.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/outputs.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/providers.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/remote-state.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/variables.tf (100%) rename {modules => deprecated}/eks/eks-without-spotinst/versions.tf (100%) diff --git a/modules/eks/ebs-controller/README.md b/deprecated/eks/ebs-controller/README.md similarity index 100% rename from modules/eks/ebs-controller/README.md rename to deprecated/eks/ebs-controller/README.md diff --git a/modules/eks/ebs-controller/context.tf b/deprecated/eks/ebs-controller/context.tf similarity index 100% rename from modules/eks/ebs-controller/context.tf rename to deprecated/eks/ebs-controller/context.tf diff --git a/modules/eks/ebs-controller/main.tf b/deprecated/eks/ebs-controller/main.tf similarity index 100% rename from modules/eks/ebs-controller/main.tf rename to deprecated/eks/ebs-controller/main.tf diff --git a/modules/eks/ebs-controller/outputs.tf b/deprecated/eks/ebs-controller/outputs.tf similarity index 100% rename from modules/eks/ebs-controller/outputs.tf rename to deprecated/eks/ebs-controller/outputs.tf diff --git a/modules/eks/ebs-controller/provider-helm.tf b/deprecated/eks/ebs-controller/provider-helm.tf similarity index 100% rename from modules/eks/ebs-controller/provider-helm.tf rename to deprecated/eks/ebs-controller/provider-helm.tf diff --git a/modules/eks/ebs-controller/providers.tf b/deprecated/eks/ebs-controller/providers.tf similarity index 100% rename from modules/eks/ebs-controller/providers.tf rename to deprecated/eks/ebs-controller/providers.tf diff --git a/modules/eks/ebs-controller/remote-state.tf b/deprecated/eks/ebs-controller/remote-state.tf similarity index 100% rename from modules/eks/ebs-controller/remote-state.tf rename to deprecated/eks/ebs-controller/remote-state.tf diff --git a/modules/eks/ebs-controller/variables.tf b/deprecated/eks/ebs-controller/variables.tf similarity index 100% rename from modules/eks/ebs-controller/variables.tf rename to deprecated/eks/ebs-controller/variables.tf diff --git a/modules/eks/ebs-controller/versions.tf b/deprecated/eks/ebs-controller/versions.tf similarity index 100% rename from modules/eks/ebs-controller/versions.tf rename to deprecated/eks/ebs-controller/versions.tf diff --git a/modules/eks/efs-controller/README.md b/deprecated/eks/efs-controller/README.md similarity index 100% rename from modules/eks/efs-controller/README.md rename to deprecated/eks/efs-controller/README.md diff --git a/modules/eks/efs-controller/context.tf b/deprecated/eks/efs-controller/context.tf similarity index 100% rename from modules/eks/efs-controller/context.tf rename to deprecated/eks/efs-controller/context.tf diff --git a/modules/eks/efs-controller/main.tf b/deprecated/eks/efs-controller/main.tf similarity index 100% rename from modules/eks/efs-controller/main.tf rename to deprecated/eks/efs-controller/main.tf diff --git a/modules/eks/efs-controller/outputs.tf b/deprecated/eks/efs-controller/outputs.tf similarity index 100% rename from modules/eks/efs-controller/outputs.tf rename to deprecated/eks/efs-controller/outputs.tf diff --git a/modules/eks/efs-controller/provider-helm.tf b/deprecated/eks/efs-controller/provider-helm.tf similarity index 100% rename from modules/eks/efs-controller/provider-helm.tf rename to deprecated/eks/efs-controller/provider-helm.tf diff --git a/modules/eks/efs-controller/providers.tf b/deprecated/eks/efs-controller/providers.tf similarity index 100% rename from modules/eks/efs-controller/providers.tf rename to deprecated/eks/efs-controller/providers.tf diff --git a/modules/eks/efs-controller/remote-state.tf b/deprecated/eks/efs-controller/remote-state.tf similarity index 100% rename from modules/eks/efs-controller/remote-state.tf rename to deprecated/eks/efs-controller/remote-state.tf diff --git a/modules/eks/efs-controller/resources/iam_policy_statements.yaml b/deprecated/eks/efs-controller/resources/iam_policy_statements.yaml similarity index 100% rename from modules/eks/efs-controller/resources/iam_policy_statements.yaml rename to deprecated/eks/efs-controller/resources/iam_policy_statements.yaml diff --git a/modules/eks/efs-controller/resources/values.yaml b/deprecated/eks/efs-controller/resources/values.yaml similarity index 100% rename from modules/eks/efs-controller/resources/values.yaml rename to deprecated/eks/efs-controller/resources/values.yaml diff --git a/modules/eks/efs-controller/variables.tf b/deprecated/eks/efs-controller/variables.tf similarity index 100% rename from modules/eks/efs-controller/variables.tf rename to deprecated/eks/efs-controller/variables.tf diff --git a/modules/eks/efs-controller/versions.tf b/deprecated/eks/efs-controller/versions.tf similarity index 100% rename from modules/eks/efs-controller/versions.tf rename to deprecated/eks/efs-controller/versions.tf diff --git a/modules/eks/eks-without-spotinst/README.md b/deprecated/eks/eks-without-spotinst/README.md similarity index 100% rename from modules/eks/eks-without-spotinst/README.md rename to deprecated/eks/eks-without-spotinst/README.md diff --git a/modules/eks/eks-without-spotinst/context.tf b/deprecated/eks/eks-without-spotinst/context.tf similarity index 100% rename from modules/eks/eks-without-spotinst/context.tf rename to deprecated/eks/eks-without-spotinst/context.tf diff --git a/modules/eks/eks-without-spotinst/default.auto.tfvars b/deprecated/eks/eks-without-spotinst/default.auto.tfvars similarity index 100% rename from modules/eks/eks-without-spotinst/default.auto.tfvars rename to deprecated/eks/eks-without-spotinst/default.auto.tfvars diff --git a/modules/eks/eks-without-spotinst/eks-node-groups.tf b/deprecated/eks/eks-without-spotinst/eks-node-groups.tf similarity index 100% rename from modules/eks/eks-without-spotinst/eks-node-groups.tf rename to deprecated/eks/eks-without-spotinst/eks-node-groups.tf diff --git a/modules/eks/eks-without-spotinst/main.tf b/deprecated/eks/eks-without-spotinst/main.tf similarity index 100% rename from modules/eks/eks-without-spotinst/main.tf rename to deprecated/eks/eks-without-spotinst/main.tf diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_az/context.tf b/deprecated/eks/eks-without-spotinst/modules/node_group_by_az/context.tf similarity index 100% rename from modules/eks/eks-without-spotinst/modules/node_group_by_az/context.tf rename to deprecated/eks/eks-without-spotinst/modules/node_group_by_az/context.tf diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_az/main.tf b/deprecated/eks/eks-without-spotinst/modules/node_group_by_az/main.tf similarity index 100% rename from modules/eks/eks-without-spotinst/modules/node_group_by_az/main.tf rename to deprecated/eks/eks-without-spotinst/modules/node_group_by_az/main.tf diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_az/outputs.tf b/deprecated/eks/eks-without-spotinst/modules/node_group_by_az/outputs.tf similarity index 100% rename from modules/eks/eks-without-spotinst/modules/node_group_by_az/outputs.tf rename to deprecated/eks/eks-without-spotinst/modules/node_group_by_az/outputs.tf diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_az/variables.tf b/deprecated/eks/eks-without-spotinst/modules/node_group_by_az/variables.tf similarity index 100% rename from modules/eks/eks-without-spotinst/modules/node_group_by_az/variables.tf rename to deprecated/eks/eks-without-spotinst/modules/node_group_by_az/variables.tf diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_region/context.tf b/deprecated/eks/eks-without-spotinst/modules/node_group_by_region/context.tf similarity index 100% rename from modules/eks/eks-without-spotinst/modules/node_group_by_region/context.tf rename to deprecated/eks/eks-without-spotinst/modules/node_group_by_region/context.tf diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_region/main.tf b/deprecated/eks/eks-without-spotinst/modules/node_group_by_region/main.tf similarity index 100% rename from modules/eks/eks-without-spotinst/modules/node_group_by_region/main.tf rename to deprecated/eks/eks-without-spotinst/modules/node_group_by_region/main.tf diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_region/outputs.tf b/deprecated/eks/eks-without-spotinst/modules/node_group_by_region/outputs.tf similarity index 100% rename from modules/eks/eks-without-spotinst/modules/node_group_by_region/outputs.tf rename to deprecated/eks/eks-without-spotinst/modules/node_group_by_region/outputs.tf diff --git a/modules/eks/eks-without-spotinst/modules/node_group_by_region/variables.tf b/deprecated/eks/eks-without-spotinst/modules/node_group_by_region/variables.tf similarity index 100% rename from modules/eks/eks-without-spotinst/modules/node_group_by_region/variables.tf rename to deprecated/eks/eks-without-spotinst/modules/node_group_by_region/variables.tf diff --git a/modules/eks/eks-without-spotinst/outputs.tf b/deprecated/eks/eks-without-spotinst/outputs.tf similarity index 100% rename from modules/eks/eks-without-spotinst/outputs.tf rename to deprecated/eks/eks-without-spotinst/outputs.tf diff --git a/modules/eks/eks-without-spotinst/providers.tf b/deprecated/eks/eks-without-spotinst/providers.tf similarity index 100% rename from modules/eks/eks-without-spotinst/providers.tf rename to deprecated/eks/eks-without-spotinst/providers.tf diff --git a/modules/eks/eks-without-spotinst/remote-state.tf b/deprecated/eks/eks-without-spotinst/remote-state.tf similarity index 100% rename from modules/eks/eks-without-spotinst/remote-state.tf rename to deprecated/eks/eks-without-spotinst/remote-state.tf diff --git a/modules/eks/eks-without-spotinst/variables.tf b/deprecated/eks/eks-without-spotinst/variables.tf similarity index 100% rename from modules/eks/eks-without-spotinst/variables.tf rename to deprecated/eks/eks-without-spotinst/variables.tf diff --git a/modules/eks/eks-without-spotinst/versions.tf b/deprecated/eks/eks-without-spotinst/versions.tf similarity index 100% rename from modules/eks/eks-without-spotinst/versions.tf rename to deprecated/eks/eks-without-spotinst/versions.tf diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 84582aa50..4ba0638c7 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,4 +1,92 @@ -## Components PR [#723](https://github.com/cloudposse/terraform-aws-components/pull/723/files) +## Components PR [#795](https://github.com/cloudposse/terraform-aws-components/pull/723) + +### Removed `identity` roles from cluster RBAC (`aws-auth` ConfigMap) + +Previously, this module added `identity` roles configured by the `aws_teams_rbac` +input to the `aws-auth` ConfigMap. This never worked, and so now `aws_teams_rbac` +is ignored. When upgrading, you may see these roles being removed from the `aws-auth`: +this is expected and harmless. + +### Better support for Manged Node Group Block Device Specifications + +Previously, this module only supported specifying the disk size and encryption state +for the root volume of Managed Node Groups. Now, the full set of block device +specifications is supported, including the ability to specify the device name. +This is particularly important when using BottleRocket, which uses a very small +root volume for storing the OS and configuration, and exposes a second volume +(`/dev/xvdb`) for storing data. + +#### Block Device Migration + +Almost all of the attributes of `node_groups` and `node_group_defaults` are now +optional. This means you can remove from your configuration any attributes that +previously you were setting to `null`. + +The `disk_size` and `disk_encryption_enabled` attributes are deprecated. They +only apply to `/dev/xvda`, and only provision a `gp2` volume. In order to +provide backwards compatibility, they are still supported, and, when specified, +cause the new `block_device_map` attribute to be ignored. + +The new `block_device_map` attribute is a map of objects. The keys are the names +of block devices, and the values are objects with the attributes from the Terraform +[launch_template.block-devices](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template#block-devices) resource. + +Note that the new default, when none of `block_device_map`, `disk_size`, or +`disk_encryption_enabled` are specified, is to provision a 20GB `gp3` volume +for `/dev/xvda`, with encryption enabled. This is a change from the previous +default, which provisioned a `gp2` volume instead. + +### Support for EFS add-on + +This module now supports the EFS CSI driver add-on, in very much the same way +as it supports the EBS CSI driver add-on. The only difference is that the +EFS CSI driver add-on requires that you first provision an EFS file system. + +#### Migration from `eks/efs-controller` to EFS CSI Driver Add-On + +If you are currently using the `eks/efs-controller` module, you can migrate +to the EFS CSI Driver Add-On by following these steps: + +1. Remove or scale to zero Pods any Deployments using the EFS file system. +2. Remove (`terraform destroy`) the `eks/efs-controller` module from your + cluster. This will also remove the `efs-sc` StorageClass. +3. Use the [eks/storage-class](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/storage-class) + module to create a replacement EFS StorageClass `efs-sc`. This component is new and you may need to add it to your cluster. +4. Deploy the EFS CSI Driver Add-On by adding `aws-efs-csi-driver` to the `addons` map (see [README](./README.md)). +5. Restore the Deployments you modified in step 1. + +### More options for specifying Availability Zones + +Previously, this module required you to specify the Availability Zones for the +cluster in one of two ways: + +1. Explicitly, by providing the full AZ names via the `availability_zones` input +2. Implicitly, via private subnets in the VPC + +Option 2 is still usually the best way, but now you have additional options: + +- You can specify the Availability Zones via the `availability_zones` input + without specifying the full AZ names. You can just specify the suffixes of + the AZ names, and the module will find the full names for you, using the + current region. This is useful for using the same configuration in multiple regions. +- You can specify Availability Zone IDs via the `availability_zone_ids` input. + This is useful to ensure that clusters in different accounts are nevertheless + deployed to the same Availability Zones. As with the `availability_zones` input, + you can specify the suffixes of the AZ IDs, and the module will find the full + IDs for you, using the current region. + +### Support for Karpenter Instance Profile + +Previously, this module created an IAM Role for instances launched by Karpenter, +but did not create the corresponding Instance Profile, which was instead created by +the `eks/karpenter` component. This can cause problems if you delete and recreate the cluster, +so for new clusters, this module can now create the Instance Profile as well. + +Because this is disruptive to existing clusters, this is not enabled by default. +To enable it, set the `legacy_do_not_create_karpenter_instance_profile` input to `false`, +and also set the `eks/karpenter` input `legacy_create_karpenter_instance_profile` to `false`. + +## Components PR [#723](https://github.com/cloudposse/terraform-aws-components/pull/723) ### Improved support for EKS Add-Ons diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index b550539ea..5934868a1 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -35,8 +35,7 @@ components: vars: enabled: true name: eks - iam_primary_roles_tenant_name: core - cluster_kubernetes_version: "1.25" + cluster_kubernetes_version: "1.27" vpc_component_name: "vpc" eks_component_name: "eks/cluster" @@ -90,17 +89,6 @@ components: cluster_endpoint_public_access: false cluster_log_retention_period: 90 - # List of `aws-teams` to map to Kubernetes RBAC groups. - # This gives teams direct access to Kubernetes without having to assume a team-role. - # RBAC groups must be created elsewhere. The "system:" groups are predefined by Kubernetes. - aws_teams_rbac: - - aws_team: managers - groups: - - system:masters - - aws_team: devops - groups: - - system:masters - # List of `aws-teams-roles` (in the account where the EKS cluster is deployed) to map to Kubernetes RBAC groups aws_team_roles_rbac: - aws_team: admin @@ -130,24 +118,42 @@ components: - idp:poweruser - system:authenticated - # Fargate Profiles + # Fargate Profiles for Karpenter fargate_profiles: karpenter: kubernetes_namespace: karpenter kubernetes_labels: null karpenter_iam_role_enabled: true + # If you are using Karpenter, disable the legacy instance profile created by the eks/karpenter component + # and use the one created by this component instead by setting the legacy flags to false in both components. + # This is recommended for all new clusters. + legacy_do_not_create_karpenter_instance_profile: false + # All Fargate Profiles will use the same IAM Role when `legacy_fargate_1_role_per_profile_enabled` is set to false. + # Recommended for all new clusters, but will damage existing clusters provisioned with the legacy component. + legacy_fargate_1_role_per_profile_enabled: false # EKS addons # https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html addons: + # https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html vpc-cni: - addon_version: "v1.12.2-eksbuild.1" + addon_version: "v1.12.6-eksbuild.2" # set `addon_version` to `null` to use the latest version + # https://docs.aws.amazon.com/eks/latest/userguide/managing-kube-proxy.html kube-proxy: - addon_version: "v1.25.6-eksbuild.1" + addon_version: "v1.27.1-eksbuild.1" # set `addon_version` to `null` to use the latest version + # https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html coredns: - addon_version: "v1.9.3-eksbuild.2" + addon_version: "v1.10.1-eksbuild.1" # set `addon_version` to `null` to use the latest version + # https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons + # https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html + # https://github.com/kubernetes-sigs/aws-ebs-csi-driver aws-ebs-csi-driver: - addon_version: "v1.19.0-eksbuild.2" + addon_version: "v1.20.0-eksbuild.1" # set `addon_version` to `null` to use the latest version # Only install the EFS driver if you are using EFS and have already created an EFS file system. + # https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html + aws-efs-csi-driver: + addon_version: "v1.5.8-eksbuild.1" + # Set a short timeout in case of conflict with an existing efs-controller deployment + create_timeout: "7m" ``` ### Usage with Node Groups @@ -157,7 +163,10 @@ provision with the cluster, provide values for `var.managed_node_groups_enabled` :::info -You can use managed Node Groups in conjunction with Karpenter, though in most cases, Karpenter is all you need. +You can use managed Node Groups in conjunction with Karpenter. We recommend provisioning a +managed node group with as many nodes as Availability Zones used by your cluster (typically 3), to ensure a +minimum support for a high-availability set of daemons, and then using Karpenter to provision additional nodes +as needed. ::: @@ -182,11 +191,18 @@ node_groups: # for most attributes, setting null here means use setting from nod attributes: [] create_before_destroy: true - disk_size: 100 cluster_autoscaler_enabled: true instance_types: - t3.medium ami_type: AL2_x86_64 # use "AL2_x86_64" for standard instances, "AL2_x86_64_GPU" for GPU instances + block_device_map: + # EBS volume for local ephemeral storage + # IGNORED if legacy `disk_encryption_enabled` or `disk_size` are set! + # "/dev/xvda" most of the instances (without local NVMe) and most of the Linuxes, "/dev/xvdb" BottleRocket + "/dev/xvda": + ebs: + volume_size: 100 # number of GB + volume_type: gp3 kubernetes_labels: {} kubernetes_taints: {} resources_to_tag: @@ -203,25 +219,26 @@ Install these addons with the [`var.addons` input](https://docs.cloudposse.com/c :::info Run the following command to see all available addons, their type, and their publisher. -You can also see the URL for addons that are available through the AWS Marketplace. Replace 1.25 with the version of your cluster. +You can also see the URL for addons that are available through the AWS Marketplace. Replace 1.27 with the version of your cluster. See [Creating an addon](https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on) for more details. ::: ```shell -aws eks describe-addon-versions --kubernetes-version 1.25 \ +EKS_K8S_VERSION=1.27 # replace with your cluster version +aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION \ --query 'addons[].{MarketplaceProductUrl: marketplaceInformation.productUrl, Name: addonName, Owner: owner Publisher: publisher, Type: type}' --output table ``` :::info You can see which versions are available for each addon by executing the following commands. -Replace 1.25 with the version of your cluster. +Replace 1.27 with the version of your cluster. ::: ```shell -EKS_K8S_VERSION=1.24 # replace with your cluster version +EKS_K8S_VERSION=1.27 # replace with your cluster version echo "vpc-cni:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name vpc-cni \ --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table @@ -233,6 +250,25 @@ echo "coredns:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S echo "aws-ebs-csi-driver:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name aws-ebs-csi-driver \ --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table + +echo "aws-efs-csi-driver:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name aws-efs-csi-driver \ + --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table +``` + +Some add-ons accept additional configuration. For example, the `vpc-cni` addon accepts a `disableNetworking` parameter. +View the available configuration options (as JSON Schema) via the `aws eks describe-addon-configuration` command. For example: + +```shell +aws eks describe-addon-configuration \ + --addon-name aws-ebs-csi-driver \ + --addon-version v1.20.0-eksbuild.1 | jq '.configurationSchema | fromjson' +``` + +You can then configure the add-on via the `configuration_values` input. For example: + +```yaml +aws-ebs-csi-driver: + configuration_values: '{"node": {"loggingFormat": "json"}}' ``` Configure the addons like the following example: @@ -357,12 +393,14 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor |------|---------| | [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | +| [random](#requirement\_random) | >= 3.0 | ## Providers | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | +| [random](#provider\_random) | >= 3.0 | ## Modules @@ -370,6 +408,8 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor |------|--------|---------| | [aws\_ebs\_csi\_driver\_eks\_iam\_role](#module\_aws\_ebs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.0 | | [aws\_ebs\_csi\_driver\_fargate\_profile](#module\_aws\_ebs\_csi\_driver\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | +| [aws\_efs\_csi\_driver\_eks\_iam\_role](#module\_aws\_efs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.0 | +| [aws\_efs\_csi\_driver\_fargate\_profile](#module\_aws\_efs\_csi\_driver\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [coredns\_fargate\_profile](#module\_coredns\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.9.0 | @@ -380,6 +420,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [karpenter\_label](#module\_karpenter\_label) | cloudposse/label/null | 0.25.0 | | [region\_node\_group](#module\_region\_node\_group) | ./modules/node_group_by_region | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [vpc\_cni\_eks\_iam\_role](#module\_vpc\_cni\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.0 | | [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | @@ -388,14 +429,18 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | Name | Type | |------|------| +| [aws_iam_instance_profile.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | | [aws_iam_policy.ipv6_eks_cni_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.karpenter](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.amazon_ec2_container_registry_readonly](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.amazon_eks_worker_node_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.amazon_ssm_managed_instance_core](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.aws_ebs_csi_driver](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.aws_efs_csi_driver](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.ipv6_eks_cni_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.vpc_cni](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [random_pet.camel_case_warning](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | +| [aws_availability_zones.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | | [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.ipv6_eks_cni_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.vpc_cni_ipv6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -407,7 +452,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
map(object({
addon_version = optional(string, null)
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | +| [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
map(object({
addon_version = optional(string, null)
# configuration_values is a JSON string, such as '{"computeType": "Fargate"}'.
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | | [addons\_depends\_on](#input\_addons\_depends\_on) | If set `true`, all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned.
See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. | `bool` | `false` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | @@ -415,12 +460,13 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [apply\_config\_map\_aws\_auth](#input\_apply\_config\_map\_aws\_auth) | Whether to execute `kubectl apply` to apply the ConfigMap to allow worker nodes to join the EKS cluster | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [availability\_zone\_abbreviation\_type](#input\_availability\_zone\_abbreviation\_type) | Type of Availability Zone abbreviation (either `fixed` or `short`) to use in names. See https://github.com/cloudposse/terraform-aws-utils for details. | `string` | `"fixed"` | no | -| [availability\_zones](#input\_availability\_zones) | AWS Availability Zones in which to deploy multi-AZ resources.
If not provided, resources will be provisioned in every zone with a private subnet in the VPC. | `list(string)` | `[]` | no | +| [availability\_zone\_ids](#input\_availability\_zone\_ids) | List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`.
Can be the full name, e.g. `use1-az1`, or just the part after the AZ ID region code, e.g. `-az1`,
to allow reusable values across regions. Consider contention for resources and spot pricing in each AZ when selecting.
Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. | `list(string)` | `[]` | no | +| [availability\_zones](#input\_availability\_zones) | AWS Availability Zones in which to deploy multi-AZ resources.
Ignored if `availability_zone_ids` is set.
Can be the full name, e.g. `us-east-1a`, or just the part after the region, e.g. `a` to allow reusable values across regions.
If not provided, resources will be provisioned in every zone with a private subnet in the VPC. | `list(string)` | `[]` | no | | [aws\_auth\_yaml\_strip\_quotes](#input\_aws\_auth\_yaml\_strip\_quotes) | If true, remove double quotes from the generated aws-auth ConfigMap YAML to reduce spurious diffs in plans | `bool` | `true` | no | | [aws\_ssm\_agent\_enabled](#input\_aws\_ssm\_agent\_enabled) | Set true to attach the required IAM policy for AWS SSM agent to each EC2 instance's IAM Role | `bool` | `false` | no | | [aws\_sso\_permission\_sets\_rbac](#input\_aws\_sso\_permission\_sets\_rbac) | (Not Recommended): AWS SSO (IAM Identity Center) permission sets in the EKS deployment account to add to `aws-auth` ConfigMap.
Unfortunately, `aws-auth` ConfigMap does not support SSO permission sets, so we map the generated
IAM Role ARN corresponding to the permission set at the time Terraform runs. This is subject to change
when any changes are made to the AWS SSO configuration, invalidating the mapping, and requiring a
`terraform apply` in this project to update the `aws-auth` ConfigMap and restore access. |
list(object({
aws_sso_permission_set = string
groups = list(string)
}))
| `[]` | no | | [aws\_team\_roles\_rbac](#input\_aws\_team\_roles\_rbac) | List of `aws-team-roles` (in the target AWS account) to map to Kubernetes RBAC groups. |
list(object({
aws_team_role = string
groups = list(string)
}))
| `[]` | no | -| [aws\_teams\_rbac](#input\_aws\_teams\_rbac) | List of `aws-teams` to map to Kubernetes RBAC groups.
This gives teams direct access to Kubernetes without having to assume a team-role. |
list(object({
aws_team = string
groups = list(string)
}))
| `[]` | no | +| [aws\_teams\_rbac](#input\_aws\_teams\_rbac) | OBSOLETE: This feature never worked as intended, and this input is now ignored.
List of `aws-teams` to map to Kubernetes RBAC groups.
This gives teams direct access to Kubernetes without having to assume a team-role. |
list(object({
aws_team = string
groups = list(string)
}))
| `[]` | no | | [cluster\_encryption\_config\_enabled](#input\_cluster\_encryption\_config\_enabled) | Set to `true` to enable Cluster Encryption Configuration | `bool` | `true` | no | | [cluster\_encryption\_config\_kms\_key\_deletion\_window\_in\_days](#input\_cluster\_encryption\_config\_kms\_key\_deletion\_window\_in\_days) | Cluster Encryption Config KMS Key Resource argument - key deletion windows in days post destruction | `number` | `10` | no | | [cluster\_encryption\_config\_kms\_key\_enable\_key\_rotation](#input\_cluster\_encryption\_config\_kms\_key\_enable\_key\_rotation) | Cluster Encryption Config KMS Key Resource argument - enable kms key rotation | `bool` | `true` | no | @@ -453,6 +499,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [legacy\_do\_not\_create\_karpenter\_instance\_profile](#input\_legacy\_do\_not\_create\_karpenter\_instance\_profile) | When `true` (the default), suppresses creation of the IAM Instance Profile
for nodes launched by Karpenter, to preserve the legacy behavior of
the `eks/karpenter` component creating it.
Set to `false` to enable creation of the IAM Instance Profile, which
ensures that both the role and the instance profile have the same lifecycle,
and avoids AWS Provider issue [#32671](https://github.com/hashicorp/terraform-provider-aws/issues/32671).
Use in conjunction with `eks/karpenter` component `legacy_create_karpenter_instance_profile`. | `bool` | `true` | no | | [legacy\_fargate\_1\_role\_per\_profile\_enabled](#input\_legacy\_fargate\_1\_role\_per\_profile\_enabled) | Set to `false` for new clusters to create a single Fargate Pod Execution role for the cluster.
Set to `true` for existing clusters to preserve the old behavior of creating
a Fargate Pod Execution role for each Fargate Profile. | `bool` | `true` | no | | [managed\_node\_groups\_enabled](#input\_managed\_node\_groups\_enabled) | Set false to prevent the creation of EKS managed node groups. | `bool` | `true` | no | | [map\_additional\_aws\_accounts](#input\_map\_additional\_aws\_accounts) | Additional AWS account numbers to add to `aws-auth` ConfigMap | `list(string)` | `[]` | no | @@ -461,8 +508,8 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [map\_additional\_worker\_roles](#input\_map\_additional\_worker\_roles) | AWS IAM Role ARNs of worker nodes to add to `aws-auth` ConfigMap | `list(string)` | `[]` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = string
ami_type = string
attributes = list(string)
availability_zones = list(string) # set to null to use var.availability_zones
cluster_autoscaler_enabled = bool
create_before_destroy = bool
desired_group_size = number
disk_encryption_enabled = bool
disk_size = number
instance_types = list(string)
kubernetes_labels = map(string)
kubernetes_taints = list(object({
key = string
value = string
effect = string
}))
kubernetes_version = string # set to null to use cluster_kubernetes_version
max_group_size = number
min_group_size = number
resources_to_tag = list(string)
tags = map(string)
})
|
{
"ami_release_version": null,
"ami_type": null,
"attributes": null,
"availability_zones": null,
"cluster_autoscaler_enabled": true,
"create_before_destroy": true,
"desired_group_size": 1,
"disk_encryption_enabled": true,
"disk_size": 20,
"instance_types": [
"t3.medium"
],
"kubernetes_labels": null,
"kubernetes_taints": null,
"kubernetes_version": null,
"max_group_size": 100,
"min_group_size": null,
"resources_to_tag": null,
"tags": null
}
| no | -| [node\_groups](#input\_node\_groups) | List of objects defining a node group for the cluster |
map(object({
# EKS AMI version to use, e.g. "1.16.13-20200821" (no "v").
ami_release_version = string
# Type of Amazon Machine Image (AMI) associated with the EKS Node Group
ami_type = string
# Additional attributes (e.g. `1`) for the node group
attributes = list(string)
# will create 1 auto scaling group in each specified availability zone
# or all AZs with subnets if none are specified anywhere
availability_zones = list(string)
# Whether to enable Node Group to scale its AutoScaling Group
cluster_autoscaler_enabled = bool
# True to create new node_groups before deleting old ones, avoiding a temporary outage
create_before_destroy = bool
# Desired number of worker nodes when initially provisioned
desired_group_size = number
# Enable disk encryption for the created launch template (if we aren't provided with an existing launch template)
disk_encryption_enabled = bool
# Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided.
disk_size = number
# Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided.
instance_types = list(string)
# Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed
kubernetes_labels = map(string)
# List of objects describing Kubernetes taints.
kubernetes_taints = list(object({
key = string
value = string
effect = string
}))
# Desired Kubernetes master version. If you do not specify a value, the latest available version is used
kubernetes_version = string
# The maximum size of the AutoScaling Group
max_group_size = number
# The minimum size of the AutoScaling Group
min_group_size = number
# List of auto-launched resource types to tag
resources_to_tag = list(string)
tags = map(string)
}))
| `{}` | no | +| [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = optional(string, null)
ami_type = optional(string, null)
attributes = optional(list(string), null)
availability_zones = optional(list(string)) # set to null to use var.availability_zones
cluster_autoscaler_enabled = optional(bool, null)
create_before_destroy = optional(bool, null)
desired_group_size = optional(number, null)
instance_types = optional(list(string), null)
kubernetes_labels = optional(map(string), null)
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), null)
kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version
max_group_size = optional(number, null)
min_group_size = optional(number, null)
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 20) # disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)
})
|
{
"block_device_map": {
"/dev/xvda": {
"ebs": {
"encrypted": true,
"volume_size": 20,
"volume_type": "gp2"
}
}
},
"desired_group_size": 1,
"instance_types": [
"t3.medium"
],
"kubernetes_version": null,
"max_group_size": 100
}
| no | +| [node\_groups](#input\_node\_groups) | List of objects defining a node group for the cluster |
map(object({
# EKS AMI version to use, e.g. "1.16.13-20200821" (no "v").
ami_release_version = optional(string, null)
# Type of Amazon Machine Image (AMI) associated with the EKS Node Group
ami_type = optional(string, null)
# Additional attributes (e.g. `1`) for the node group
attributes = optional(list(string), null)
# will create 1 auto scaling group in each specified availability zone
# or all AZs with subnets if none are specified anywhere
availability_zones = optional(list(string), null)
# Whether to enable Node Group to scale its AutoScaling Group
cluster_autoscaler_enabled = optional(bool, null)
# True to create new node_groups before deleting old ones, avoiding a temporary outage
create_before_destroy = optional(bool, null)
# Desired number of worker nodes when initially provisioned
desired_group_size = optional(number, null)
# Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided.
instance_types = optional(list(string), null)
# Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed
kubernetes_labels = optional(map(string), null)
# List of objects describing Kubernetes taints.
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), null)
# Desired Kubernetes master version. If you do not specify a value, the latest available version is used
kubernetes_version = optional(string, null)
# The maximum size of the AutoScaling Group
max_group_size = optional(number, null)
# The minimum size of the AutoScaling Group
min_group_size = optional(number, null)
# List of auto-launched resource types to tag
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional.
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 20) # Disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED:
# Enable disk encryption for the created launch template (if we aren't provided with an existing launch template)
# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided.
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)

}))
| `{}` | no | | [oidc\_provider\_enabled](#input\_oidc\_provider\_enabled) | Create an IAM OIDC identity provider for the cluster, then you can create IAM roles to associate with a service account in the cluster, instead of using kiam or kube2iam. For more information, see https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html | `bool` | `true` | no | | [public\_access\_cidrs](#input\_public\_access\_cidrs) | Indicates which CIDR blocks can access the Amazon EKS public API server endpoint when enabled. EKS defaults this to a list with 0.0.0.0/0. | `list(string)` |
[
"0.0.0.0/0"
]
| no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | @@ -507,6 +554,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor - [How to Keep Everything Up to Date](/reference-architecture/how-to-guides/upgrades/how-to-keep-everything-up-to-date) - [How to Tune SpotInst Parameters for EKS](/reference-architecture/how-to-guides/tutorials/how-to-tune-spotinst-parameters-for-eks) - [How to Upgrade EKS Cluster Addons](/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons) +- [EBS CSI Migration FAQ](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi-migration-faq.html) - [How to Upgrade EKS](/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks) ## References diff --git a/modules/eks/cluster/addons.tf b/modules/eks/cluster/addons.tf index 5f6f5965a..7d7a24ed7 100644 --- a/modules/eks/cluster/addons.tf +++ b/modules/eks/cluster/addons.tf @@ -7,13 +7,15 @@ locals { addon_names = keys(var.addons) vpc_cni_addon_enabled = local.enabled && contains(local.addon_names, "vpc-cni") aws_ebs_csi_driver_enabled = local.enabled && contains(local.addon_names, "aws-ebs-csi-driver") + aws_efs_csi_driver_enabled = local.enabled && contains(local.addon_names, "aws-efs-csi-driver") coredns_enabled = local.enabled && contains(local.addon_names, "coredns") - # The `vpc-cni` and `aws-ebs-csi-driver` addons are special as they always require an IAM role for Kubernetes Service Account (IRSA). - # The roles are created by this component. + # The `vpc-cni`, `aws-ebs-csi-driver`, and `aws-efs-csi-driver` addons are special as they always require an + # IAM role for Kubernetes Service Account (IRSA). The roles are created by this component. addon_service_account_role_arn_map = { vpc-cni = module.vpc_cni_eks_iam_role.service_account_role_arn aws-ebs-csi-driver = module.aws_ebs_csi_driver_eks_iam_role.service_account_role_arn + aws-efs-csi-driver = module.aws_efs_csi_driver_eks_iam_role.service_account_role_arn } final_addon_service_account_role_arn_map = merge(local.addon_service_account_role_arn_map, local.overridable_additional_addon_service_account_role_arn_map) @@ -29,21 +31,26 @@ locals { ] addons_depends_on = concat([ - module.aws_ebs_csi_driver_fargate_profile, module.coredns_fargate_profile, + module.aws_ebs_csi_driver_fargate_profile, + module.aws_efs_csi_driver_fargate_profile, ], local.overridable_addons_depends_on) addons_require_fargate = var.deploy_addons_to_fargate && ( - local.aws_ebs_csi_driver_enabled || local.coredns_enabled || + local.aws_ebs_csi_driver_enabled || + local.aws_efs_csi_driver_enabled || local.overridable_deploy_additional_addons_to_fargate ) addon_fargate_profiles = merge( + (local.coredns_enabled && var.deploy_addons_to_fargate ? { + coredns = one(module.coredns_fargate_profile[*]) + } : {}), (local.aws_ebs_csi_driver_enabled && var.deploy_addons_to_fargate ? { aws_ebs_csi_driver = one(module.aws_ebs_csi_driver_fargate_profile[*]) } : {}), - (local.coredns_enabled && var.deploy_addons_to_fargate ? { - coredns = one(module.coredns_fargate_profile[*]) + (local.aws_efs_csi_driver_enabled && var.deploy_addons_to_fargate ? { + aws_efs_csi_driver = one(module.aws_efs_csi_driver_fargate_profile[*]) } : {}), local.overridable_additional_addon_fargate_profiles ) @@ -103,6 +110,27 @@ module "vpc_cni_eks_iam_role" { context = module.this.context } +module "coredns_fargate_profile" { + count = local.coredns_enabled && var.deploy_addons_to_fargate ? 1 : 0 + + source = "cloudposse/eks-fargate-profile/aws" + version = "1.3.0" + + subnet_ids = local.private_subnet_ids + cluster_name = module.eks_cluster.eks_cluster_id + kubernetes_namespace = "kube-system" + kubernetes_labels = { k8s-app = "kube-dns" } + permissions_boundary = var.fargate_profile_iam_role_permissions_boundary + iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter + + fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-coredns" + fargate_pod_execution_role_enabled = false + fargate_pod_execution_role_arn = one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) + + attributes = ["coredns"] + context = module.this.context +} + # `aws-ebs-csi-driver` EKS addon # https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html # https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons @@ -123,7 +151,7 @@ module "aws_ebs_csi_driver_eks_iam_role" { eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url - service_account_name = "ebs-csi-controller" + service_account_name = "ebs-csi-controller-sa" service_account_namespace = "kube-system" context = module.this.context @@ -135,11 +163,12 @@ module "aws_ebs_csi_driver_fargate_profile" { source = "cloudposse/eks-fargate-profile/aws" version = "1.3.0" - subnet_ids = local.private_subnet_ids - cluster_name = module.eks_cluster.eks_cluster_id - kubernetes_namespace = "kube-system" - kubernetes_labels = { app = "ebs-csi-controller" } - permissions_boundary = var.fargate_profile_iam_role_permissions_boundary + subnet_ids = local.private_subnet_ids + cluster_name = module.eks_cluster.eks_cluster_id + kubernetes_namespace = "kube-system" + kubernetes_labels = { app = "ebs-csi-controller" } # Only deploy the controller to Fargate, not the node driver + permissions_boundary = var.fargate_profile_iam_role_permissions_boundary + iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-ebs-csi" @@ -150,24 +179,50 @@ module "aws_ebs_csi_driver_fargate_profile" { context = module.this.context } -module "coredns_fargate_profile" { - count = local.coredns_enabled && var.deploy_addons_to_fargate ? 1 : 0 +# `aws-efs-csi-driver` EKS addon +# https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html +# https://github.com/kubernetes-sigs/aws-efs-csi-driver +resource "aws_iam_role_policy_attachment" "aws_efs_csi_driver" { + count = local.aws_efs_csi_driver_enabled ? 1 : 0 + + role = module.aws_efs_csi_driver_eks_iam_role.service_account_role_name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy" +} + +module "aws_efs_csi_driver_eks_iam_role" { + source = "cloudposse/eks-iam-role/aws" + version = "2.1.0" + + enabled = local.aws_efs_csi_driver_enabled + + eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url + + service_account_namespace_name_list = [ + "kube-system:efs-csi-controller-sa", + "kube-system:efs-csi-node-sa", + ] + + context = module.this.context +} + +module "aws_efs_csi_driver_fargate_profile" { + count = local.aws_efs_csi_driver_enabled && var.deploy_addons_to_fargate ? 1 : 0 source = "cloudposse/eks-fargate-profile/aws" version = "1.3.0" + subnet_ids = local.private_subnet_ids + cluster_name = module.eks_cluster.eks_cluster_id + kubernetes_namespace = "kube-system" + kubernetes_labels = { app = "efs-csi-controller" } # Only deploy the controller to Fargate, not the node driver + permissions_boundary = var.fargate_profile_iam_role_permissions_boundary - subnet_ids = local.private_subnet_ids - cluster_name = module.eks_cluster.eks_cluster_id - kubernetes_namespace = "kube-system" - kubernetes_labels = { k8s-app = "kube-dns" } - permissions_boundary = var.fargate_profile_iam_role_permissions_boundary iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter - fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-coredns" + fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-efs-csi" fargate_pod_execution_role_enabled = false fargate_pod_execution_role_arn = one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) - attributes = ["coredns"] + attributes = ["efs-csi"] context = module.this.context } diff --git a/modules/eks/cluster/eks-node-groups.tf b/modules/eks/cluster/eks-node-groups.tf index 433923810..c9470b134 100644 --- a/modules/eks/cluster/eks-node-groups.tf +++ b/modules/eks/cluster/eks-node-groups.tf @@ -21,7 +21,9 @@ module "region_node_group" { source = "./modules/node_group_by_region" availability_zones = each.value.availability_zones == null ? local.node_group_default_availability_zones : each.value.availability_zones - attributes = flatten(concat(var.attributes, [each.key], [var.color], each.value.attributes == null ? var.node_group_defaults.attributes : each.value.attributes)) + attributes = flatten(concat(var.attributes, [each.key], [ + var.color + ], each.value.attributes == null ? var.node_group_defaults.attributes : each.value.attributes)) node_group_size = module.this.enabled ? { desired_size = each.value.desired_group_size == null ? var.node_group_defaults.desired_group_size : each.value.desired_group_size @@ -40,8 +42,6 @@ module "region_node_group" { cluster_autoscaler_enabled = each.value.cluster_autoscaler_enabled == null ? var.node_group_defaults.cluster_autoscaler_enabled : each.value.cluster_autoscaler_enabled cluster_name = module.eks_cluster.eks_cluster_id create_before_destroy = each.value.create_before_destroy == null ? var.node_group_defaults.create_before_destroy : each.value.create_before_destroy - disk_encryption_enabled = each.value.disk_encryption_enabled == null ? var.node_group_defaults.disk_encryption_enabled : each.value.disk_encryption_enabled - disk_size = each.value.disk_size == null ? var.node_group_defaults.disk_size : each.value.disk_size instance_types = each.value.instance_types == null ? var.node_group_defaults.instance_types : each.value.instance_types kubernetes_labels = each.value.kubernetes_labels == null ? var.node_group_defaults.kubernetes_labels : each.value.kubernetes_labels kubernetes_taints = each.value.kubernetes_taints == null ? var.node_group_defaults.kubernetes_taints : each.value.kubernetes_taints @@ -51,6 +51,8 @@ module "region_node_group" { aws_ssm_agent_enabled = var.aws_ssm_agent_enabled vpc_id = local.vpc_id + block_device_map = lookup(local.legacy_converted_block_device_map, each.key, local.block_device_map_w_defaults[each.key]) + # See "Ensure ordering of resource creation" comment above for explanation # of "module_depends_on" module_depends_on = module.eks_cluster.kubernetes_config_map_id @@ -58,3 +60,79 @@ module "region_node_group" { context = module.this.context } + +## Warn if you are using camelCase in the `block_device_map` argument. +## Without this warning, camelCase inputs will be silently ignored and replaced with defaults, +## which is very hard to notice and debug. +# +## We just need some kind of data source or resource to trigger the warning. +## Because we need it to run for each node group, there are no good options +## among actually useful data sources or resources. We also have to ensure +## that Terraform updates it when the `block_device_map` argument changes, +## and does not skip the checks because it can use the cached value. +resource "random_pet" "camel_case_warning" { + for_each = local.node_groups_enabled ? var.node_groups : {} + + keepers = { + hash = base64sha256(jsonencode(local.block_device_map_w_defaults[each.key])) + } + + lifecycle { + precondition { + condition = length(compact(flatten([for device_name, device_map in local.block_device_map_w_defaults[each.key] : [ + lookup(device_map.ebs, "volumeSize", null), + lookup(device_map.ebs, "volumeType", null), + lookup(device_map.ebs, "kmsKeyId", null), + lookup(device_map.ebs, "deleteOnTermination", null), + lookup(device_map.ebs, "snapshotId", null), + ] + ]))) == 0 + error_message = <<-EOT + The `block_device_map` argument in the `node_groups[${each.key}]` module + does not support the `volumeSize`, `volumeType`, `kmsKeyId`, `deleteOnTermination`, or `snapshotId` arguments. + Please use `volume_size`, `volume_type`, `kms_key_id`, `delete_on_termination`, and `snapshot_id` instead." + EOT + } + } +} + +# DEPRECATION SUPPORT +# `disk_size` and `disk_encryption_enabled are deprecated in favor of `block_device_map`. +# Convert legacy use to new format. + +locals { + legacy_disk_inputs = { + for k, v in(local.node_groups_enabled ? var.node_groups : {}) : k => { + disk_encryption_enabled = v.disk_encryption_enabled == null ? var.node_group_defaults.disk_encryption_enabled : v.disk_encryption_enabled + disk_size = v.disk_size == null ? var.node_group_defaults.disk_size : v.disk_size + } if( + ((v.disk_encryption_enabled == null ? var.node_group_defaults.disk_encryption_enabled : v.disk_encryption_enabled) != null) + || ((v.disk_size == null ? var.node_group_defaults.disk_size : v.disk_size) != null) + ) + } + + legacy_converted_block_device_map = { + for k, v in local.legacy_disk_inputs : k => { + "/dev/xvda" = { + no_device = null + virtual_name = null + ebs = { + delete_on_termination = true + encrypted = v.disk_encryption_enabled + iops = null + kms_key_id = null + snapshot_id = null + throughput = null + volume_size = v.disk_size + volume_type = "gp2" + } # ebs + } # "/dev/xvda" + } # k => { "/dev/xvda" = { ... } } + } + + block_device_map_w_defaults = { + for k, v in(local.node_groups_enabled ? var.node_groups : {}) : k => + v.block_device_map == null ? var.node_group_defaults.block_device_map : v.block_device_map + } + +} diff --git a/modules/eks/cluster/karpenter.tf b/modules/eks/cluster/karpenter.tf index 46cec385c..b57efe889 100644 --- a/modules/eks/cluster/karpenter.tf +++ b/modules/eks/cluster/karpenter.tf @@ -13,6 +13,8 @@ locals { karpenter_iam_role_enabled = local.enabled && var.karpenter_iam_role_enabled + karpenter_instance_profile_enabled = local.karpenter_iam_role_enabled && !var.legacy_do_not_create_karpenter_instance_profile + # Used to determine correct partition (i.e. - `aws`, `aws-gov`, `aws-cn`, etc.) partition = one(data.aws_partition.current[*].partition) } @@ -55,6 +57,14 @@ resource "aws_iam_role" "karpenter" { tags = module.karpenter_label.tags } +resource "aws_iam_instance_profile" "default" { + count = local.karpenter_instance_profile_enabled ? 1 : 0 + + name = one(aws_iam_role.karpenter[*].name) + role = one(aws_iam_role.karpenter[*].name) + tags = module.karpenter_label.tags +} + # AmazonSSMManagedInstanceCore policy is required by Karpenter resource "aws_iam_role_policy_attachment" "amazon_ssm_managed_instance_core" { count = local.karpenter_iam_role_enabled ? 1 : 0 diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 402841c24..a520e0701 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -8,19 +8,7 @@ locals { this_account_name = module.iam_roles.current_account_account_name identity_account_name = module.iam_roles.identity_account_account_name - role_map = merge({ - (local.identity_account_name) = var.aws_teams_rbac[*].aws_team - }, { - (local.this_account_name) = var.aws_team_roles_rbac[*].aws_team_role - }) - - aws_teams_auth = [for role in var.aws_teams_rbac : { - rolearn = module.iam_arns.principals_map[local.identity_account_name][role.aws_team] - # Session name included in audit trail automatically starting with Kubernetes v1.20. - # See https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-iam-roles-when-multiple-users-need-identical-access-to-the-cluster - username = format("%s-%s", local.identity_account_name, role.aws_team) - groups = role.groups - }] + role_map = { (local.this_account_name) = var.aws_team_roles_rbac[*].aws_team_role } aws_team_roles_auth = [for role in var.aws_team_roles_rbac : { rolearn = module.iam_arns.principals_map[local.this_account_name][role.aws_team_role] @@ -47,7 +35,6 @@ locals { ] map_additional_iam_roles = concat( - local.aws_teams_auth, local.aws_team_roles_auth, local.aws_sso_iam_roles_auth, var.map_additional_iam_roles, @@ -78,16 +65,69 @@ locals { vpc_id = local.vpc_outputs.vpc_id + availability_zones_expanded = local.enabled && length(var.availability_zones) > 0 && length(var.availability_zone_ids) == 0 ? ( + (substr( + var.availability_zones[0], + 0, + length(var.region) + ) == var.region) ? var.availability_zones : formatlist("${var.region}%s", var.availability_zones) + ) : [] + + short_region = module.utils.region_az_alt_code_maps["to_short"][var.region] + + availability_zone_ids_expanded = local.enabled && length(var.availability_zone_ids) > 0 ? ( + (substr( + var.availability_zone_ids[0], + 0, + length(local.short_region) + ) == local.short_region) ? var.availability_zone_ids : formatlist("${local.short_region}%s", var.availability_zone_ids) + ) : [] + + # Create a map of AZ IDs to AZ names (and the reverse), + # but fail safely, because AZ IDs are not always available. + az_id_map = length(local.availability_zone_ids_expanded) > 0 ? try(zipmap(data.aws_availability_zones.default[0].zone_ids, data.aws_availability_zones.default[0].names), {}) : {} + + availability_zones_normalized = length(local.availability_zone_ids_expanded) > 0 ? [ + for v in local.availability_zone_ids_expanded : local.az_id_map[v]] : local.availability_zones_expanded + # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` # `az_public_subnets_map` is a map of AZ names to list of public subnet IDs in the AZs - public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]) + # LEGACY PATCH: for legacy VPC with no az_public_subnets_map + # public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]) + public_subnet_ids = try(flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]), + local.vpc_outputs.public_subnet_ids) # Get only the private subnets that correspond to the AZs provided in `var.availability_zones` # `az_private_subnets_map` is a map of AZ names to list of private subnet IDs in the AZs - private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]) + # LEGACY PATCH: for legacy VPC with no az_public_subnets_map + # private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]) + private_subnet_ids = try(flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]), + local.vpc_outputs.private_subnet_ids) # Infer the availability zones from the private subnets if var.availability_zones is empty: - availability_zones = length(var.availability_zones) == 0 ? keys(local.vpc_outputs.az_private_subnets_map) : var.availability_zones + availability_zones = length(local.availability_zones_normalized) == 0 ? keys(local.vpc_outputs.az_private_subnets_map) : local.availability_zones_normalized +} + +data "aws_availability_zones" "default" { + count = length(local.availability_zone_ids_expanded) > 0 ? 1 : 0 + + # Filter out Local Zones. See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones#by-filter + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } + + lifecycle { + postcondition { + condition = length(self.zone_ids) > 0 + error_message = "No availability zones IDs found in region ${var.region}. You must specify availability zones instead." + } + } +} + +module "utils" { + source = "cloudposse/utils/aws" + version = "1.3.0" } module "eks_cluster" { diff --git a/modules/eks/cluster/modules/node_group_by_az/main.tf b/modules/eks/cluster/modules/node_group_by_az/main.tf index 970b315e1..7dcd6902f 100644 --- a/modules/eks/cluster/modules/node_group_by_az/main.tf +++ b/modules/eks/cluster/modules/node_group_by_az/main.tf @@ -32,7 +32,7 @@ locals { module "eks_node_group" { source = "cloudposse/eks-node-group/aws" - version = "2.10.0" + version = "2.11.0" enabled = local.enabled @@ -57,13 +57,7 @@ module "eks_node_group" { resources_to_tag = local.enabled ? var.cluster_context.resources_to_tag : null subnet_ids = local.enabled ? local.subnet_ids : null - block_device_mappings = local.enabled ? [{ - device_name = "/dev/xvda" - volume_size = var.cluster_context.disk_size - volume_type = "gp2" - encrypted = var.cluster_context.disk_encryption_enabled - delete_on_termination = true - }] : [] + block_device_map = local.enabled ? var.cluster_context.block_device_map : null # Prevent the node groups from being created before the Kubernetes aws-auth configMap module_depends_on = var.cluster_context.module_depends_on diff --git a/modules/eks/cluster/modules/node_group_by_az/variables.tf b/modules/eks/cluster/modules/node_group_by_az/variables.tf index adcb1ba0b..ef4e486dc 100644 --- a/modules/eks/cluster/modules/node_group_by_az/variables.tf +++ b/modules/eks/cluster/modules/node_group_by_az/variables.tf @@ -20,10 +20,11 @@ variable "cluster_context" { cluster_autoscaler_enabled = bool cluster_name = string create_before_destroy = bool - disk_encryption_enabled = bool - disk_size = number - instance_types = list(string) - kubernetes_labels = map(string) + # Obsolete, replaced by block_device_map + # disk_encryption_enabled = bool + # disk_size = number + instance_types = list(string) + kubernetes_labels = map(string) kubernetes_taints = list(object({ key = string value = string @@ -35,6 +36,23 @@ variable "cluster_context" { subnet_type_tag_key = string aws_ssm_agent_enabled = bool vpc_id = string + + # block_device_map copied from cloudposse/terraform-aws-eks-node-group + # Really, nothing is optional, but easier to keep in sync via copy and paste + block_device_map = map(object({ + no_device = optional(bool, null) + virtual_name = optional(string, null) + ebs = optional(object({ + delete_on_termination = optional(bool, true) + encrypted = optional(bool, true) + iops = optional(number, null) + kms_key_id = optional(string, null) + snapshot_id = optional(string, null) + throughput = optional(number, null) + volume_size = optional(number, 20) + volume_type = optional(string, "gp3") + })) + })) }) description = "The common settings for all node groups." } diff --git a/modules/eks/cluster/modules/node_group_by_region/variables.tf b/modules/eks/cluster/modules/node_group_by_region/variables.tf index b413c2181..a8be3618c 100644 --- a/modules/eks/cluster/modules/node_group_by_region/variables.tf +++ b/modules/eks/cluster/modules/node_group_by_region/variables.tf @@ -21,10 +21,11 @@ variable "cluster_context" { cluster_autoscaler_enabled = bool cluster_name = string create_before_destroy = bool - disk_encryption_enabled = bool - disk_size = number - instance_types = list(string) - kubernetes_labels = map(string) + # Obsolete, replaced by block_device_map + # disk_encryption_enabled = bool + # disk_size = number + instance_types = list(string) + kubernetes_labels = map(string) kubernetes_taints = list(object({ key = string value = string @@ -36,6 +37,24 @@ variable "cluster_context" { subnet_type_tag_key = string aws_ssm_agent_enabled = bool vpc_id = string + + # block_device_map copied from cloudposse/terraform-aws-eks-node-group + # Really, nothing is optional, but easier to keep in sync via copy and paste + block_device_map = map(object({ + no_device = optional(bool, null) + virtual_name = optional(string, null) + ebs = optional(object({ + delete_on_termination = optional(bool, true) + encrypted = optional(bool, true) + iops = optional(number, null) + kms_key_id = optional(string, null) + snapshot_id = optional(string, null) + throughput = optional(number, null) + volume_size = optional(number, 20) + volume_type = optional(string, "gp3") + })) + })) + }) description = "The common settings for all node groups." } diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 80473c011..ffa28d1f7 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -7,12 +7,25 @@ variable "availability_zones" { type = list(string) description = <<-EOT AWS Availability Zones in which to deploy multi-AZ resources. + Ignored if `availability_zone_ids` is set. + Can be the full name, e.g. `us-east-1a`, or just the part after the region, e.g. `a` to allow reusable values across regions. If not provided, resources will be provisioned in every zone with a private subnet in the VPC. EOT default = [] nullable = false } +variable "availability_zone_ids" { + type = list(string) + description = <<-EOT + List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`. + Can be the full name, e.g. `use1-az1`, or just the part after the AZ ID region code, e.g. `-az1`, + to allow reusable values across regions. Consider contention for resources and spot pricing in each AZ when selecting. + Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. + EOT + default = [] +} + variable "availability_zone_abbreviation_type" { type = string description = "Type of Availability Zone abbreviation (either `fixed` or `short`) to use in names. See https://github.com/cloudposse/terraform-aws-utils for details." @@ -108,6 +121,7 @@ variable "aws_teams_rbac" { })) description = <<-EOT + OBSOLETE: This feature never worked as intended, and this input is now ignored. List of `aws-teams` to map to Kubernetes RBAC groups. This gives teams direct access to Kubernetes without having to assume a team-role. EOT @@ -200,43 +214,77 @@ variable "node_groups" { # will create 1 node group for each item in map type = map(object({ # EKS AMI version to use, e.g. "1.16.13-20200821" (no "v"). - ami_release_version = string + ami_release_version = optional(string, null) # Type of Amazon Machine Image (AMI) associated with the EKS Node Group - ami_type = string + ami_type = optional(string, null) # Additional attributes (e.g. `1`) for the node group - attributes = list(string) + attributes = optional(list(string), null) # will create 1 auto scaling group in each specified availability zone # or all AZs with subnets if none are specified anywhere - availability_zones = list(string) + availability_zones = optional(list(string), null) # Whether to enable Node Group to scale its AutoScaling Group - cluster_autoscaler_enabled = bool + cluster_autoscaler_enabled = optional(bool, null) # True to create new node_groups before deleting old ones, avoiding a temporary outage - create_before_destroy = bool + create_before_destroy = optional(bool, null) # Desired number of worker nodes when initially provisioned - desired_group_size = number - # Enable disk encryption for the created launch template (if we aren't provided with an existing launch template) - disk_encryption_enabled = bool - # Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided. - disk_size = number + desired_group_size = optional(number, null) # Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided. - instance_types = list(string) + instance_types = optional(list(string), null) # Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed - kubernetes_labels = map(string) + kubernetes_labels = optional(map(string), null) # List of objects describing Kubernetes taints. - kubernetes_taints = list(object({ + kubernetes_taints = optional(list(object({ key = string value = string effect = string - })) + })), null) # Desired Kubernetes master version. If you do not specify a value, the latest available version is used - kubernetes_version = string + kubernetes_version = optional(string, null) # The maximum size of the AutoScaling Group - max_group_size = number + max_group_size = optional(number, null) # The minimum size of the AutoScaling Group - min_group_size = number + min_group_size = optional(number, null) # List of auto-launched resource types to tag - resources_to_tag = list(string) - tags = map(string) + resources_to_tag = optional(list(string), null) + tags = optional(map(string), null) + + # block_device_map copied from cloudposse/terraform-aws-eks-node-group + # Keep in sync via copy and paste, but make optional. + # Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb". + block_device_map = optional(map(object({ + no_device = optional(bool, null) + virtual_name = optional(string, null) + ebs = optional(object({ + delete_on_termination = optional(bool, true) + encrypted = optional(bool, true) + iops = optional(number, null) + kms_key_id = optional(string, null) + snapshot_id = optional(string, null) + throughput = optional(number, null) # for gp3, MiB/s, up to 1000 + volume_size = optional(number, 20) # Disk size in GB + volume_type = optional(string, "gp3") + + # Catch common camel case typos. These have no effect, they just generate better errors. + # It would be nice to actually use these, but volumeSize in particular is a number here + # and in most places it is a string with a unit suffix (e.g. 20Gi) + # Without these defined, they would be silently ignored and the default values would be used instead, + # which is difficult to debug. + deleteOnTermination = optional(any, null) + kmsKeyId = optional(any, null) + snapshotId = optional(any, null) + volumeSize = optional(any, null) + volumeType = optional(any, null) + })) + })), null) + + # DEPRECATED: + # Enable disk encryption for the created launch template (if we aren't provided with an existing launch template) + # DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead. + disk_encryption_enabled = optional(bool, null) + # Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided. + # DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead. + disk_size = optional(number, null) + })) description = "List of objects defining a node group for the cluster" @@ -248,49 +296,78 @@ variable "node_group_defaults" { # Any value in the node group that is null will be replaced # by the value in this object, which can also be null type = object({ - ami_release_version = string - ami_type = string - attributes = list(string) - availability_zones = list(string) # set to null to use var.availability_zones - cluster_autoscaler_enabled = bool - create_before_destroy = bool - desired_group_size = number - disk_encryption_enabled = bool - disk_size = number - instance_types = list(string) - kubernetes_labels = map(string) - kubernetes_taints = list(object({ + ami_release_version = optional(string, null) + ami_type = optional(string, null) + attributes = optional(list(string), null) + availability_zones = optional(list(string)) # set to null to use var.availability_zones + cluster_autoscaler_enabled = optional(bool, null) + create_before_destroy = optional(bool, null) + desired_group_size = optional(number, null) + instance_types = optional(list(string), null) + kubernetes_labels = optional(map(string), null) + kubernetes_taints = optional(list(object({ key = string value = string effect = string - })) - kubernetes_version = string # set to null to use cluster_kubernetes_version - max_group_size = number - min_group_size = number - resources_to_tag = list(string) - tags = map(string) + })), null) + kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version + max_group_size = optional(number, null) + min_group_size = optional(number, null) + resources_to_tag = optional(list(string), null) + tags = optional(map(string), null) + + # block_device_map copied from cloudposse/terraform-aws-eks-node-group + # Keep in sync via copy and paste, but make optional + # Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb". + block_device_map = optional(map(object({ + no_device = optional(bool, null) + virtual_name = optional(string, null) + ebs = optional(object({ + delete_on_termination = optional(bool, true) + encrypted = optional(bool, true) + iops = optional(number, null) + kms_key_id = optional(string, null) + snapshot_id = optional(string, null) + throughput = optional(number, null) # for gp3, MiB/s, up to 1000 + volume_size = optional(number, 20) # disk size in GB + volume_type = optional(string, "gp3") + + # Catch common camel case typos. These have no effect, they just generate better errors. + # It would be nice to actually use these, but volumeSize in particular is a number here + # and in most places it is a string with a unit suffix (e.g. 20Gi) + # Without these defined, they would be silently ignored and the default values would be used instead, + # which is difficult to debug. + deleteOnTermination = optional(any, null) + kmsKeyId = optional(any, null) + snapshotId = optional(any, null) + volumeSize = optional(any, null) + volumeType = optional(any, null) + })) + })), null) + + # DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead. + disk_encryption_enabled = optional(bool, null) + # DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead. + disk_size = optional(number, null) }) description = "Defaults for node groups in the cluster" default = { - ami_release_version = null - ami_type = null - attributes = null - availability_zones = null - cluster_autoscaler_enabled = true - create_before_destroy = true - desired_group_size = 1 - disk_encryption_enabled = true - disk_size = 20 - instance_types = ["t3.medium"] - kubernetes_labels = null - kubernetes_taints = null - kubernetes_version = null # set to null to use cluster_kubernetes_version - max_group_size = 100 - min_group_size = null - resources_to_tag = null - tags = null + desired_group_size = 1 + instance_types = ["t3.medium"] + kubernetes_version = null # set to null to use cluster_kubernetes_version + max_group_size = 100 + + block_device_map = { + "/dev/xvda" = { + ebs = { + encrypted = true + volume_size = 20 # GB + volume_type = "gp2" # Should be gp3, but left as gp2 for backwards compatibility + } + } + } } nullable = false } @@ -447,7 +524,8 @@ variable "fargate_profile_iam_role_permissions_boundary" { variable "addons" { type = map(object({ - addon_version = optional(string, null) + addon_version = optional(string, null) + # configuration_values is a JSON string, such as '{"computeType": "Fargate"}'. configuration_values = optional(string, null) # Set default resolve_conflicts to OVERWRITE because it is required on initial installation of # add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and @@ -493,3 +571,17 @@ variable "legacy_fargate_1_role_per_profile_enabled" { default = true nullable = false } + +variable "legacy_do_not_create_karpenter_instance_profile" { + type = bool + description = <<-EOT + When `true` (the default), suppresses creation of the IAM Instance Profile + for nodes launched by Karpenter, to preserve the legacy behavior of + the `eks/karpenter` component creating it. + Set to `false` to enable creation of the IAM Instance Profile, which + ensures that both the role and the instance profile have the same lifecycle, + and avoids AWS Provider issue [#32671](https://github.com/hashicorp/terraform-provider-aws/issues/32671). + Use in conjunction with `eks/karpenter` component `legacy_create_karpenter_instance_profile`. + EOT + default = true +} diff --git a/modules/eks/cluster/versions.tf b/modules/eks/cluster/versions.tf index b5920b7b1..7ed8e615b 100644 --- a/modules/eks/cluster/versions.tf +++ b/modules/eks/cluster/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/aws" version = ">= 4.9.0" } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } } } diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index c92a3d5eb..705a19d29 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -47,6 +47,9 @@ components: atomic: true wait: true rbac_enabled: true + # Set `legacy_create_karpenter_instance_profile` to `false` to allow the `eks/cluster` component + # to manage the instance profile for the nodes launched by Karpenter (recommended for all new clusters). + legacy_create_karpenter_instance_profile: false # Provision `karpenter` component on the blue EKS cluster eks/karpenter-blue: @@ -364,6 +367,7 @@ For more details, refer to: | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [legacy\_create\_karpenter\_instance\_profile](#input\_legacy\_create\_karpenter\_instance\_profile) | When `true` (the default), this component creates an IAM Instance Profile
for nodes launched by Karpenter, to preserve the legacy behavior.
Set to `false` to disable creation of the IAM Instance Profile, which
avoids conflict with having `eks/cluster` create it.
Use in conjunction with `eks/cluster` component `legacy_do_not_create_karpenter_instance_profile`,
which see for further details. | `bool` | `true` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [rbac\_enabled](#input\_rbac\_enabled) | Enable/disable RBAC | `bool` | `true` | no | diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index 5d70e6526..5d42902df 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -13,11 +13,12 @@ locals { eks_cluster_identity_oidc_issuer = try(module.eks.outputs.eks_cluster_identity_oidc_issuer, "") karpenter_iam_role_name = try(module.eks.outputs.karpenter_iam_role_name, "") - karpenter_role_enabled = local.enabled && length(local.karpenter_iam_role_name) > 0 + + karpenter_instance_profile_enabled = local.enabled && var.legacy_create_karpenter_instance_profile && length(local.karpenter_iam_role_name) > 0 } resource "aws_iam_instance_profile" "default" { - count = local.karpenter_role_enabled ? 1 : 0 + count = local.karpenter_instance_profile_enabled ? 1 : 0 name = local.karpenter_iam_role_name role = local.karpenter_iam_role_name @@ -47,7 +48,7 @@ module "karpenter" { service_account_name = module.this.name service_account_namespace = var.kubernetes_namespace - iam_role_enabled = local.karpenter_role_enabled + iam_role_enabled = true # https://karpenter.sh/v0.6.1/getting-started/cloudformation.yaml # https://karpenter.sh/v0.10.1/getting-started/getting-started-with-terraform diff --git a/modules/eks/karpenter/variables.tf b/modules/eks/karpenter/variables.tf index 829a9b6ac..8b366c557 100644 --- a/modules/eks/karpenter/variables.tf +++ b/modules/eks/karpenter/variables.tf @@ -107,3 +107,16 @@ variable "interruption_queue_message_retention" { default = 300 description = "The message retention in seconds for the interruption handler SQS queue." } + +variable "legacy_create_karpenter_instance_profile" { + type = bool + description = <<-EOT + When `true` (the default), this component creates an IAM Instance Profile + for nodes launched by Karpenter, to preserve the legacy behavior. + Set to `false` to disable creation of the IAM Instance Profile, which + avoids conflict with having `eks/cluster` create it. + Use in conjunction with `eks/cluster` component `legacy_do_not_create_karpenter_instance_profile`, + which see for further details. + EOT + default = true +} diff --git a/modules/vpc/README.md b/modules/vpc/README.md index dd986a97e..85a9fe746 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -95,8 +95,8 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [assign\_generated\_ipv6\_cidr\_block](#input\_assign\_generated\_ipv6\_cidr\_block) | When `true`, assign AWS generated IPv6 CIDR block to the VPC. Conflicts with `ipv6_ipam_pool_id`. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [availability\_zone\_ids](#input\_availability\_zone\_ids) | List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`.
Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. | `list(string)` | `[]` | no | -| [availability\_zones](#input\_availability\_zones) | List of Availability Zones (AZs) where subnets will be created. Ignored when `availability_zone_ids` is set.
The order of zones in the list ***must be stable*** or else Terraform will continually make changes.
If no AZs are specified, then `max_subnet_count` AZs will be selected in alphabetical order.
If `max_subnet_count > 0` and `length(var.availability_zones) > max_subnet_count`, the list
will be truncated. We recommend setting `availability_zones` and `max_subnet_count` explicitly as constant
(not computed) values for predictability, consistency, and stability. | `list(string)` | `[]` | no | +| [availability\_zone\_ids](#input\_availability\_zone\_ids) | List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`.
Can be the full name, e.g. `use1-az1`, or just the part after the AZ ID region code, e.g. `-az1`,
to allow reusable values across regions. Consider contention for resources and spot pricing in each AZ when selecting.
Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. | `list(string)` | `[]` | no | +| [availability\_zones](#input\_availability\_zones) | List of Availability Zones (AZs) where subnets will be created. Ignored when `availability_zone_ids` is set.
Can be the full name, e.g. `us-east-1a`, or just the part after the region, e.g. `a` to allow reusable values across regions.
The order of zones in the list ***must be stable*** or else Terraform will continually make changes.
If no AZs are specified, then `max_subnet_count` AZs will be selected in alphabetical order.
If `max_subnet_count > 0` and `length(var.availability_zones) > max_subnet_count`, the list
will be truncated. We recommend setting `availability_zones` and `max_subnet_count` explicitly as constant
(not computed) values for predictability, consistency, and stability. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/vpc/variables.tf b/modules/vpc/variables.tf index dd94b519c..36a72cbb7 100644 --- a/modules/vpc/variables.tf +++ b/modules/vpc/variables.tf @@ -7,6 +7,7 @@ variable "availability_zones" { type = list(string) description = <<-EOT List of Availability Zones (AZs) where subnets will be created. Ignored when `availability_zone_ids` is set. + Can be the full name, e.g. `us-east-1a`, or just the part after the region, e.g. `a` to allow reusable values across regions. The order of zones in the list ***must be stable*** or else Terraform will continually make changes. If no AZs are specified, then `max_subnet_count` AZs will be selected in alphabetical order. If `max_subnet_count > 0` and `length(var.availability_zones) > max_subnet_count`, the list @@ -20,6 +21,8 @@ variable "availability_zone_ids" { type = list(string) description = <<-EOT List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`. + Can be the full name, e.g. `use1-az1`, or just the part after the AZ ID region code, e.g. `-az1`, + to allow reusable values across regions. Consider contention for resources and spot pricing in each AZ when selecting. Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. EOT default = [] From 8c9d4618087baf9a2d1e1bf8fc5729e75780c3ef Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 8 Aug 2023 09:43:57 -0700 Subject: [PATCH 206/501] `eks/karpenter` Readme.md update (#792) Co-authored-by: Dan Miller Co-authored-by: cloudpossebot --- modules/eks/cluster/README.md | 19 +++++++++++++++++++ modules/eks/karpenter/README.md | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 5934868a1..ab53771ab 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -156,6 +156,25 @@ components: create_timeout: "7m" ``` +### Amazon EKS End-of-Life Dates + +When picking a Kubernetes version, be sure to review the [end-of-life dates for Amazon EKS](https://endoflife.date/amazon-eks). Refer to the chart below: + +| cycle | release | latest | latest release | eol | +|:------|:----------:|:------------|:--------------:|:----------:| +| 1.27 | 2023-05-24 | 1.27-eks-3 | 2023-06-30 | 2024-07-01 | +| 1.26 | 2023-04-11 | 1.26-eks-4 | 2023-06-30 | 2024-06-01 | +| 1.25 | 2023-02-21 | 1.25-eks-5 | 2023-06-30 | 2024-05-01 | +| 1.24 | 2022-11-15 | 1.24-eks-8 | 2023-06-30 | 2024-01-01 | +| 1.23 | 2022-08-11 | 1.23-eks-10 | 2023-06-30 | 2023-10-11 | +| 1.22 | 2022-04-04 | 1.22-eks-14 | 2023-06-30 | 2023-06-04 | +| 1.21 | 2021-07-19 | 1.21-eks-18 | 2023-06-09 | 2023-02-15 | +| 1.20 | 2021-05-18 | 1.20-eks-14 | 2023-05-05 | 2022-11-01 | +| 1.19 | 2021-02-16 | 1.19-eks-11 | 2022-08-15 | 2022-08-01 | +| 1.18 | 2020-10-13 | 1.18-eks-13 | 2022-08-15 | 2022-08-15 | + +*This Chart was updated as of 08/04/2023 and is generated with [the `eol` tool](https://github.com/hugovk/norwegianblue). Check the latest updates by running `eol amazon-eks` locally or [on the website directly]((https://endoflife.date/amazon-eks)). + ### Usage with Node Groups The `eks/cluster` component also supports managed Node Groups. In order to add a set of nodes to diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 705a19d29..2e6c8c3a5 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -281,6 +281,12 @@ For your cluster, you will need to review the following configurations for the K ttl_seconds_until_expired: 2592000 ``` +## Troubleshooting + +For Karpenter issues, checkout the [Karpenter Troubleshooting Guide](https://karpenter.sh/docs/troubleshooting/) + +### References + For more details, refer to: - https://karpenter.sh/v0.28.0/provisioner/#specrequirements @@ -290,6 +296,7 @@ For more details, refer to: - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html + ## Requirements From f353a24c1d45fab2164333d85a58f12ac5c4b5de Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:00:10 -0400 Subject: [PATCH 207/501] =?UTF-8?q?feat:=20filter=20out=20=E2=80=9CSUSPEND?= =?UTF-8?q?ED=E2=80=9D=20accounts=20for=20account-map=20(#800)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/account-map/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/account-map/main.tf b/modules/account-map/main.tf index 25785a93b..8431cebbb 100644 --- a/modules/account-map/main.tf +++ b/modules/account-map/main.tf @@ -8,7 +8,7 @@ locals { full_account_map = { for acct in data.aws_organizations_organization.organization.accounts - : acct.name == var.root_account_aws_name ? var.root_account_account_name : acct.name => acct.id + : acct.name == var.root_account_aws_name ? var.root_account_account_name : acct.name => acct.id if acct.status != "SUSPENDED" } iam_role_arn_templates = { From 1db06e7c33c77ba2e321112a2573eb649b86d14d Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:01:00 -0400 Subject: [PATCH 208/501] docs: fix issue with eks/cluster usage snippet (#796) Co-authored-by: Dan Miller --- modules/eks/cluster/README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index ab53771ab..2d0d029df 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -42,7 +42,6 @@ components: # Your choice of availability zones or availability zone ids # availability_zones: ["us-east-1a", "us-east-1b", "us-east-1c"] - availability_zone_ids: ["use1-az4", "use1-az5", "use1-az6"] aws_ssm_agent_enabled: true allow_ingress_from_vpc_accounts: - tenant: core @@ -91,7 +90,7 @@ components: # List of `aws-teams-roles` (in the account where the EKS cluster is deployed) to map to Kubernetes RBAC groups aws_team_roles_rbac: - - aws_team: admin + - aws_team_role: admin groups: - system:masters - aws_team_role: poweruser @@ -113,10 +112,10 @@ components: # Permission sets from AWS SSO allowing cluster access # See `aws-sso` component. aws_sso_permission_sets_rbac: - - aws_sso_permission_set: PowerUserAccess - groups: - - idp:poweruser - - system:authenticated + - aws_sso_permission_set: PowerUserAccess + groups: + - idp:poweruser + - system:authenticated # Fargate Profiles for Karpenter fargate_profiles: @@ -137,7 +136,7 @@ components: addons: # https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html vpc-cni: - addon_version: "v1.12.6-eksbuild.2" # set `addon_version` to `null` to use the latest version + addon_version: v1.13.4-eksbuild.1 # set `addon_version` to `null` to use the latest version # https://docs.aws.amazon.com/eks/latest/userguide/managing-kube-proxy.html kube-proxy: addon_version: "v1.27.1-eksbuild.1" # set `addon_version` to `null` to use the latest version From 993968ee0e365b2764b8c97a5b509c3529b8bdfe Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:03:18 -0400 Subject: [PATCH 209/501] bug: update descriptions *_account_account_name variables (#801) Co-authored-by: cloudpossebot --- modules/account-map/README.md | 10 +++++----- modules/account-map/variables.tf | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index bc2ccc764..be6dfcba2 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -88,19 +88,19 @@ components: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [artifacts\_account\_account\_name](#input\_artifacts\_account\_account\_name) | The stage name for the artifacts account | `string` | `"artifacts"` | no | +| [artifacts\_account\_account\_name](#input\_artifacts\_account\_account\_name) | The short name for the artifacts account | `string` | `"artifacts"` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [audit\_account\_account\_name](#input\_audit\_account\_account\_name) | The stage name for the audit account | `string` | `"audit"` | no | +| [audit\_account\_account\_name](#input\_audit\_account\_account\_name) | The short name for the audit account | `string` | `"audit"` | no | | [aws\_config\_identity\_profile\_name](#input\_aws\_config\_identity\_profile\_name) | The AWS config profile name to use as `source_profile` for credentials. | `string` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [dns\_account\_account\_name](#input\_dns\_account\_account\_name) | The stage name for the primary DNS account | `string` | `"dns"` | no | +| [dns\_account\_account\_name](#input\_dns\_account\_account\_name) | The short name for the primary DNS account | `string` | `"dns"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [iam\_role\_arn\_template\_template](#input\_iam\_role\_arn\_template\_template) | The template for the template used to render Role ARNs.
The template is first used to render a template for the account that takes only the role name.
Then that rendered template is used to create the final Role ARN for the account.
Default is appropriate when using `tenant` and default label order with `null-label`.
Use `"arn:%s:iam::%s:role/%s-%s-%s-%%s"` when not using `tenant`.

Note that if the `null-label` variable `label_order` is truncated or extended with additional labels, this template will
need to be updated to reflect the new number of labels. | `string` | `"arn:%s:iam::%s:role/%s-%s-%s-%s-%%s"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [identity\_account\_account\_name](#input\_identity\_account\_account\_name) | The stage name for the account holding primary IAM roles | `string` | `"identity"` | no | +| [identity\_account\_account\_name](#input\_identity\_account\_account\_name) | The short name for the account holding primary IAM roles | `string` | `"identity"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | @@ -112,7 +112,7 @@ components: | [profiles\_enabled](#input\_profiles\_enabled) | Whether or not to enable profiles instead of roles for the backend. If true, profile must be set. If false, role\_arn must be set. | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [root\_account\_account\_name](#input\_root\_account\_account\_name) | The stage name for the root account | `string` | `"root"` | no | +| [root\_account\_account\_name](#input\_root\_account\_account\_name) | The short name for the root account | `string` | `"root"` | no | | [root\_account\_aws\_name](#input\_root\_account\_aws\_name) | The name of the root account as reported by AWS | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/modules/account-map/variables.tf b/modules/account-map/variables.tf index c944d15bd..7043247bd 100644 --- a/modules/account-map/variables.tf +++ b/modules/account-map/variables.tf @@ -11,31 +11,31 @@ variable "root_account_aws_name" { variable "root_account_account_name" { type = string default = "root" - description = "The stage name for the root account" + description = "The short name for the root account" } variable "identity_account_account_name" { type = string default = "identity" - description = "The stage name for the account holding primary IAM roles" + description = "The short name for the account holding primary IAM roles" } variable "dns_account_account_name" { type = string default = "dns" - description = "The stage name for the primary DNS account" + description = "The short name for the primary DNS account" } variable "artifacts_account_account_name" { type = string default = "artifacts" - description = "The stage name for the artifacts account" + description = "The short name for the artifacts account" } variable "audit_account_account_name" { type = string default = "audit" - description = "The stage name for the audit account" + description = "The short name for the audit account" } variable "iam_role_arn_template_template" { From 8911cd549d4add4034891c7757ae412a9fc304e4 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Tue, 8 Aug 2023 19:11:32 -0500 Subject: [PATCH 210/501] Updated Security Group Component to 2.2.0 (#803) --- modules/bastion/README.md | 2 +- modules/bastion/main.tf | 2 +- modules/rds/README.md | 2 +- modules/rds/main.tf | 2 +- modules/redshift/README.md | 2 +- modules/redshift/main.tf | 2 +- modules/spacelift/worker-pool/README.md | 2 +- modules/spacelift/worker-pool/main.tf | 2 +- modules/vpc/README.md | 2 +- modules/vpc/main.tf | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/bastion/README.md b/modules/bastion/README.md index 4ef76ac19..168062778 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -75,7 +75,7 @@ components: |------|--------|---------| | [bastion\_autoscale\_group](#module\_bastion\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [sg](#module\_sg) | cloudposse/security-group/aws | 2.0.0 | +| [sg](#module\_sg) | cloudposse/security-group/aws | 2.2.0 | | [ssm\_tls\_ssh\_key\_pair](#module\_ssm\_tls\_ssh\_key\_pair) | cloudposse/ssm-tls-ssh-key-pair/aws | 0.10.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | diff --git a/modules/bastion/main.tf b/modules/bastion/main.tf index 846d0e69c..b91c990af 100644 --- a/modules/bastion/main.tf +++ b/modules/bastion/main.tf @@ -35,7 +35,7 @@ locals { module "sg" { source = "cloudposse/security-group/aws" - version = "2.0.0" + version = "2.2.0" rules = var.security_group_rules vpc_id = local.vpc_id diff --git a/modules/rds/README.md b/modules/rds/README.md index fdee4fa61..66c6c291a 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -111,7 +111,7 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | -| [rds\_client\_sg](#module\_rds\_client\_sg) | cloudposse/security-group/aws | 2.0.1 | +| [rds\_client\_sg](#module\_rds\_client\_sg) | cloudposse/security-group/aws | 2.2.0 | | [rds\_instance](#module\_rds\_instance) | cloudposse/rds/aws | 0.38.5 | | [rds\_monitoring\_role](#module\_rds\_monitoring\_role) | cloudposse/iam-role/aws | 0.17.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/rds/main.tf b/modules/rds/main.tf index 941dc4077..d89218c5e 100644 --- a/modules/rds/main.tf +++ b/modules/rds/main.tf @@ -22,7 +22,7 @@ locals { module "rds_client_sg" { source = "cloudposse/security-group/aws" - version = "2.0.1" + version = "2.2.0" name = "${module.this.name}-client" enabled = module.this.enabled && var.client_security_group_enabled diff --git a/modules/redshift/README.md b/modules/redshift/README.md index bc440e033..1bd016c93 100644 --- a/modules/redshift/README.md +++ b/modules/redshift/README.md @@ -61,7 +61,7 @@ components: |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [redshift\_cluster](#module\_redshift\_cluster) | cloudposse/redshift-cluster/aws | 1.0.0 | -| [redshift\_sg](#module\_redshift\_sg) | cloudposse/security-group/aws | 2.0.0-rc1 | +| [redshift\_sg](#module\_redshift\_sg) | cloudposse/security-group/aws | 2.2.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | diff --git a/modules/redshift/main.tf b/modules/redshift/main.tf index b62e45445..7c9a4fcba 100644 --- a/modules/redshift/main.tf +++ b/modules/redshift/main.tf @@ -56,7 +56,7 @@ module "redshift_sg" { count = local.enabled && var.custom_sg_enabled ? 1 : 0 source = "cloudposse/security-group/aws" - version = "2.0.0-rc1" + version = "2.2.0" create_before_destroy = true preserve_security_group_id = true diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index 61a5517cd..751fd8d95 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -137,7 +137,7 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_label](#module\_iam\_label) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.0.0-rc1 | +| [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.2.0 | | [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | diff --git a/modules/spacelift/worker-pool/main.tf b/modules/spacelift/worker-pool/main.tf index d523799ea..93dfa21b3 100644 --- a/modules/spacelift/worker-pool/main.tf +++ b/modules/spacelift/worker-pool/main.tf @@ -78,7 +78,7 @@ data "cloudinit_config" "config" { module "security_group" { source = "cloudposse/security-group/aws" - version = "2.0.0-rc1" + version = "2.2.0" security_group_description = "Security Group for Spacelift worker pool" allow_all_egress = true diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 85a9fe746..e5b0ec080 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -70,7 +70,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [endpoint\_security\_groups](#module\_endpoint\_security\_groups) | cloudposse/security-group/aws | 2.1.0 | +| [endpoint\_security\_groups](#module\_endpoint\_security\_groups) | cloudposse/security-group/aws | 2.2.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [subnets](#module\_subnets) | cloudposse/dynamic-subnets/aws | 2.3.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index 19045b024..e082e94ad 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -99,7 +99,7 @@ module "endpoint_security_groups" { for_each = local.enabled && try(length(var.interface_vpc_endpoints), 0) > 0 ? toset([local.interface_endpoint_security_group_key]) : [] source = "cloudposse/security-group/aws" - version = "2.1.0" + version = "2.2.0" create_before_destroy = true preserve_security_group_id = false From ce9cd63da08fc20189971e174feca3dfd86e777a Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Tue, 8 Aug 2023 19:13:09 -0500 Subject: [PATCH 211/501] Added Enabled Parameter to aws-saml/okta-user and datadog-synthetics-private-location (#805) --- modules/aws-saml/modules/okta-user/main.tf | 6 ++++++ modules/datadog-private-location-ecs/main.tf | 2 ++ 2 files changed, 8 insertions(+) diff --git a/modules/aws-saml/modules/okta-user/main.tf b/modules/aws-saml/modules/okta-user/main.tf index 224b35fe7..146e3717d 100644 --- a/modules/aws-saml/modules/okta-user/main.tf +++ b/modules/aws-saml/modules/okta-user/main.tf @@ -1,4 +1,10 @@ +locals { + enabled = module.this.enabled +} + resource "aws_iam_user" "default" { + count = local.enabled ? 1 : 0 + name = module.this.id tags = module.this.tags force_destroy = true diff --git a/modules/datadog-private-location-ecs/main.tf b/modules/datadog-private-location-ecs/main.tf index ea0f491c6..3e2a730fb 100644 --- a/modules/datadog-private-location-ecs/main.tf +++ b/modules/datadog-private-location-ecs/main.tf @@ -17,6 +17,8 @@ module "roles_to_principals" { } resource "datadog_synthetics_private_location" "private_location" { + count = local.enabled ? 1 : 0 + name = module.this.id description = coalesce(var.private_location_description, format("Private location for %s", module.this.id)) tags = module.datadog_configuration.datadog_tags From f4c3f66e2e476c4b30488fe2dc41892fc36f37dd Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 8 Aug 2023 19:53:16 -0700 Subject: [PATCH 212/501] [eks/cluster] Proper handling of cold start and enabled=false (#806) --- modules/eks/cluster/README.md | 12 ++++++------ modules/eks/cluster/addons.tf | 21 +++++++++++---------- modules/eks/cluster/eks-node-groups.tf | 2 +- modules/eks/cluster/fargate-profiles.tf | 6 +++--- modules/eks/cluster/main.tf | 4 ++-- modules/eks/cluster/remote-state.tf | 23 ++++++++++++++++++----- 6 files changed, 41 insertions(+), 27 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 2d0d029df..2aaea912a 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -424,12 +424,12 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | Name | Source | Version | |------|--------|---------| -| [aws\_ebs\_csi\_driver\_eks\_iam\_role](#module\_aws\_ebs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.0 | +| [aws\_ebs\_csi\_driver\_eks\_iam\_role](#module\_aws\_ebs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.1 | | [aws\_ebs\_csi\_driver\_fargate\_profile](#module\_aws\_ebs\_csi\_driver\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | -| [aws\_efs\_csi\_driver\_eks\_iam\_role](#module\_aws\_efs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.0 | +| [aws\_efs\_csi\_driver\_eks\_iam\_role](#module\_aws\_efs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.1 | | [aws\_efs\_csi\_driver\_fargate\_profile](#module\_aws\_efs\_csi\_driver\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [coredns\_fargate\_profile](#module\_coredns\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.9.0 | | [fargate\_pod\_execution\_role](#module\_fargate\_pod\_execution\_role) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | @@ -439,9 +439,9 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [region\_node\_group](#module\_region\_node\_group) | ./modules/node_group_by_region | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | -| [vpc\_cni\_eks\_iam\_role](#module\_vpc\_cni\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.0 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [vpc\_cni\_eks\_iam\_role](#module\_vpc\_cni\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.1 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/eks/cluster/addons.tf b/modules/eks/cluster/addons.tf index 7d7a24ed7..5c534ab19 100644 --- a/modules/eks/cluster/addons.tf +++ b/modules/eks/cluster/addons.tf @@ -2,7 +2,8 @@ # https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on locals { - eks_cluster_oidc_issuer_url = replace(local.eks_outputs.eks_cluster_identity_oidc_issuer, "https://", "") + eks_cluster_oidc_issuer_url = local.enabled ? replace(module.eks_cluster.eks_cluster_identity_oidc_issuer, "https://", "") : "" + eks_cluster_id = local.enabled ? module.eks_cluster.eks_cluster_id : "" addon_names = keys(var.addons) vpc_cni_addon_enabled = local.enabled && contains(local.addon_names, "vpc-cni") @@ -96,7 +97,7 @@ resource "aws_iam_role_policy_attachment" "vpc_cni" { module "vpc_cni_eks_iam_role" { source = "cloudposse/eks-iam-role/aws" - version = "2.1.0" + version = "2.1.1" enabled = local.vpc_cni_addon_enabled @@ -117,13 +118,13 @@ module "coredns_fargate_profile" { version = "1.3.0" subnet_ids = local.private_subnet_ids - cluster_name = module.eks_cluster.eks_cluster_id + cluster_name = local.eks_cluster_id kubernetes_namespace = "kube-system" kubernetes_labels = { k8s-app = "kube-dns" } permissions_boundary = var.fargate_profile_iam_role_permissions_boundary iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter - fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-coredns" + fargate_profile_name = "${local.eks_cluster_id}-coredns" fargate_pod_execution_role_enabled = false fargate_pod_execution_role_arn = one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) @@ -145,7 +146,7 @@ resource "aws_iam_role_policy_attachment" "aws_ebs_csi_driver" { module "aws_ebs_csi_driver_eks_iam_role" { source = "cloudposse/eks-iam-role/aws" - version = "2.1.0" + version = "2.1.1" enabled = local.aws_ebs_csi_driver_enabled @@ -164,14 +165,14 @@ module "aws_ebs_csi_driver_fargate_profile" { version = "1.3.0" subnet_ids = local.private_subnet_ids - cluster_name = module.eks_cluster.eks_cluster_id + cluster_name = local.eks_cluster_id kubernetes_namespace = "kube-system" kubernetes_labels = { app = "ebs-csi-controller" } # Only deploy the controller to Fargate, not the node driver permissions_boundary = var.fargate_profile_iam_role_permissions_boundary iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter - fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-ebs-csi" + fargate_profile_name = "${local.eks_cluster_id}-ebs-csi" fargate_pod_execution_role_enabled = false fargate_pod_execution_role_arn = one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) @@ -191,7 +192,7 @@ resource "aws_iam_role_policy_attachment" "aws_efs_csi_driver" { module "aws_efs_csi_driver_eks_iam_role" { source = "cloudposse/eks-iam-role/aws" - version = "2.1.0" + version = "2.1.1" enabled = local.aws_efs_csi_driver_enabled @@ -212,14 +213,14 @@ module "aws_efs_csi_driver_fargate_profile" { version = "1.3.0" subnet_ids = local.private_subnet_ids - cluster_name = module.eks_cluster.eks_cluster_id + cluster_name = local.eks_cluster_id kubernetes_namespace = "kube-system" kubernetes_labels = { app = "efs-csi-controller" } # Only deploy the controller to Fargate, not the node driver permissions_boundary = var.fargate_profile_iam_role_permissions_boundary iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter - fargate_profile_name = "${module.eks_cluster.eks_cluster_id}-efs-csi" + fargate_profile_name = "${local.eks_cluster_id}-efs-csi" fargate_pod_execution_role_enabled = false fargate_pod_execution_role_arn = one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) diff --git a/modules/eks/cluster/eks-node-groups.tf b/modules/eks/cluster/eks-node-groups.tf index c9470b134..c22fe2484 100644 --- a/modules/eks/cluster/eks-node-groups.tf +++ b/modules/eks/cluster/eks-node-groups.tf @@ -40,7 +40,7 @@ module "region_node_group" { ami_type = each.value.ami_type == null ? var.node_group_defaults.ami_type : each.value.ami_type az_abbreviation_type = var.availability_zone_abbreviation_type cluster_autoscaler_enabled = each.value.cluster_autoscaler_enabled == null ? var.node_group_defaults.cluster_autoscaler_enabled : each.value.cluster_autoscaler_enabled - cluster_name = module.eks_cluster.eks_cluster_id + cluster_name = local.eks_cluster_id create_before_destroy = each.value.create_before_destroy == null ? var.node_group_defaults.create_before_destroy : each.value.create_before_destroy instance_types = each.value.instance_types == null ? var.node_group_defaults.instance_types : each.value.instance_types kubernetes_labels = each.value.kubernetes_labels == null ? var.node_group_defaults.kubernetes_labels : each.value.kubernetes_labels diff --git a/modules/eks/cluster/fargate-profiles.tf b/modules/eks/cluster/fargate-profiles.tf index 1dfd2dd12..17e494572 100644 --- a/modules/eks/cluster/fargate-profiles.tf +++ b/modules/eks/cluster/fargate-profiles.tf @@ -1,6 +1,6 @@ locals { fargate_profiles = local.enabled ? var.fargate_profiles : {} - fargate_cluster_pod_execution_role_name = "${module.eks_cluster.eks_cluster_id}-fargate" + fargate_cluster_pod_execution_role_name = "${local.eks_cluster_id}-fargate" fargate_cluster_pod_execution_role_needed = local.enabled && ( local.addons_require_fargate || ((length(var.fargate_profiles) > 0) && !var.legacy_fargate_1_role_per_profile_enabled) @@ -14,7 +14,7 @@ module "fargate_pod_execution_role" { version = "1.3.0" subnet_ids = local.private_subnet_ids - cluster_name = module.eks_cluster.eks_cluster_id + cluster_name = local.eks_cluster_id permissions_boundary = var.fargate_profile_iam_role_permissions_boundary fargate_profile_enabled = false @@ -35,7 +35,7 @@ module "fargate_profile" { for_each = local.fargate_profiles subnet_ids = local.private_subnet_ids - cluster_name = module.eks_cluster.eks_cluster_id + cluster_name = local.eks_cluster_id kubernetes_namespace = each.value.kubernetes_namespace kubernetes_labels = each.value.kubernetes_labels permissions_boundary = var.fargate_profile_iam_role_permissions_boundary diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index a520e0701..9c3fa7197 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -105,7 +105,7 @@ locals { local.vpc_outputs.private_subnet_ids) # Infer the availability zones from the private subnets if var.availability_zones is empty: - availability_zones = length(local.availability_zones_normalized) == 0 ? keys(local.vpc_outputs.az_private_subnets_map) : local.availability_zones_normalized + availability_zones = local.enabled ? (length(local.availability_zones_normalized) == 0 ? keys(local.vpc_outputs.az_private_subnets_map) : local.availability_zones_normalized) : [] } data "aws_availability_zones" "default" { @@ -151,7 +151,7 @@ module "eks_cluster" { allowed_security_groups = var.allowed_security_groups allowed_cidr_blocks = local.allowed_cidr_blocks - apply_config_map_aws_auth = var.apply_config_map_aws_auth + apply_config_map_aws_auth = local.enabled && var.apply_config_map_aws_auth cluster_log_retention_period = var.cluster_log_retention_period enabled_cluster_log_types = var.enabled_cluster_log_types endpoint_private_access = var.cluster_endpoint_private_access diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf index 43e8cc5ff..040537ae0 100644 --- a/modules/eks/cluster/remote-state.tf +++ b/modules/eks/cluster/remote-state.tf @@ -1,7 +1,7 @@ locals { - accounts_with_vpc = { + accounts_with_vpc = local.enabled ? { for i, account in var.allow_ingress_from_vpc_accounts : try(account.tenant, module.this.tenant) != null ? format("%s-%s", account.tenant, account.stage) : account.stage => account - } + } : {} } module "iam_arns" { @@ -14,16 +14,29 @@ module "iam_arns" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" + bypass = !local.enabled component = var.vpc_component_name + defaults = { + az_public_subnets_map = {} + az_private_subnets_map = {} + public_subnet_ids = [] + private_subnet_ids = [] + vpc = { + subnet_type_tag_key = "" + } + vpc_cidr = null + vpc_id = null + } + context = module.this.context } module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" for_each = local.accounts_with_vpc @@ -40,7 +53,7 @@ module "vpc_ingress" { # to it rather than overwrite it (specifically the aws-auth configMap) module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.eks_component_name From aef8f0f139867900767575de02ef8425b3328677 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 9 Aug 2023 09:38:15 -0700 Subject: [PATCH 213/501] Transit Gateway Cross-Region Support (#804) Co-authored-by: Max Lobur --- docs/upgrade-guide.md | 55 +++ modules/tgw/README.md | 342 ++++++++++++++++++ .../tgw/cross-region-hub-connector/README.md | 134 +++++++ .../tgw/cross-region-hub-connector/context.tf | 279 ++++++++++++++ .../tgw/cross-region-hub-connector/main.tf | 50 +++ .../tgw/cross-region-hub-connector/outputs.tf | 4 + .../provider-tgw.tf | 15 + .../cross-region-hub-connector/providers.tf | 19 + .../remote-state.tf | 42 +++ .../cross-region-hub-connector/variables.tf | 44 +++ .../cross-region-hub-connector/versions.tf | 14 + modules/tgw/hub/README.md | 10 +- modules/tgw/hub/main.tf | 2 +- modules/tgw/hub/remote-state.tf | 44 ++- modules/tgw/hub/variables.tf | 5 +- modules/tgw/spoke/README.md | 9 +- modules/tgw/spoke/main.tf | 11 +- .../modules/standard_vpc_attachment/main.tf | 84 ++++- .../standard_vpc_attachment/variables.tf | 23 +- modules/tgw/spoke/provider-hub.tf | 27 +- modules/tgw/spoke/remote-state.tf | 33 +- modules/tgw/spoke/variables.tf | 27 +- 22 files changed, 1201 insertions(+), 72 deletions(-) create mode 100644 docs/upgrade-guide.md create mode 100644 modules/tgw/README.md create mode 100644 modules/tgw/cross-region-hub-connector/README.md create mode 100644 modules/tgw/cross-region-hub-connector/context.tf create mode 100644 modules/tgw/cross-region-hub-connector/main.tf create mode 100644 modules/tgw/cross-region-hub-connector/outputs.tf create mode 100644 modules/tgw/cross-region-hub-connector/provider-tgw.tf create mode 100644 modules/tgw/cross-region-hub-connector/providers.tf create mode 100644 modules/tgw/cross-region-hub-connector/remote-state.tf create mode 100644 modules/tgw/cross-region-hub-connector/variables.tf create mode 100644 modules/tgw/cross-region-hub-connector/versions.tf diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md new file mode 100644 index 000000000..2935ecc60 --- /dev/null +++ b/docs/upgrade-guide.md @@ -0,0 +1,55 @@ +## Upgrading to `v1.275.0` + +### Affected Components + +- `tgw/hub` +- `tgw/spoke` +- `tgw/cross-region-hub-connector` + +### Steps + +This change to the Transit Gateway components, [PR #804](https://github.com/cloudposse/terraform-aws-components/pull/804), added support for cross-region connections. + +As part of that change, we've added `environment` to the component identifier used in the Terraform Output created by `tgw/hub`. Because of that map key change, all resources in Terraform now have a new resource identifier and therefore must be recreated with Terraform or removed from state and imported into the new resource ID. + +Recreating the resources is the easiest solution but means that Transit Gateway connectivity will be lost while the changes apply, which typically takes an hour. Alternatively, removing the resources from state and importing back into the new resource ID is much more complex operationally but means no lost Transit Gateway connectivity. + +Since we use Transit Gateway for VPN and GitHub Automation runner access, a temporarily lost connection is not a significant concern, so we choose to accept lost connectivity and recreate all `tgw/spoke` resources. + +### Steps + +1. Notify your team of a temporary VPN and Automation outage for accessing private networks +2. Deploy all `tgw/hub` components. There should be a hub component in each region of your network account connected to Transit Gateway +3. Deploy all `tgw/spoke` components. There should be a spoke component in every account and every region connected to Transit Gateway + +#### Tips + +Use workflows to deploy `tgw` across many accounts with a single command: + +```bash +atmos workflow deploy/tgw -f network +``` + +```yaml +# stacks/workflows/network.yaml +workflows: + deploy/tgw: + description: Provision the Transit Gateway "hub" and "spokes" for connecting VPCs. + steps: + - command: terraform deploy tgw/hub -s core-use1-network + name: hub + - command: terraform deploy tgw/spoke -s core-use1-network + - command: echo 'Creating core spokes for Transit Gateway' + type: shell + name: core-spokes + - command: terraform deploy tgw/spoke -s core-use1-corp + - command: terraform deploy tgw/spoke -s core-use1-auto + - command: terraform deploy tgw/spoke -s plat-use1-sandbox + - command: echo 'Creating platform spokes for Transit Gateway' + type: shell + name: plat-spokes + - command: terraform deploy tgw/spoke -s plat-use1-dev + - command: terraform deploy tgw/spoke -s plat-use1-staging + - command: terraform deploy tgw/spoke -s plat-use1-prod + +``` diff --git a/modules/tgw/README.md b/modules/tgw/README.md new file mode 100644 index 000000000..0947462fe --- /dev/null +++ b/modules/tgw/README.md @@ -0,0 +1,342 @@ +# Transit Gateway: `tgw` + +AWS Transit Gateway connects your Amazon Virtual Private Clouds (VPCs) and on-premises networks through a central hub. This connection simplifies your network and puts an end to complex peering relationships. Transit Gateway acts as a highly scalable cloud router—each new connection is made only once. + +For more on Transit Gateway, see [the AWS documentation](https://aws.amazon.com/transit-gateway/). + +## Requirements + +In order to connect accounts with Transit Gateway, we deploy Transit Gateway to a central account, typically `core-network`, and then deploy Transit Gateway attachments for each connected account. Each connected accounts needs a Transit Gateway attachment for the given account's VPC, either by VPC attachment or by Peering Connection attachment. Furthermore, each private subnet in each connected VPC needs to explicitly list the CIDRs for all allowed connections. + +## Solution + +First we deploy the Transit Gateway Hub, `tgw/hub`, to a central network account. The component prepares the Transit Gateway network with the following steps: + +1. Provision Transit Gateway in the network account +2. Collect VPC and EKS component output from every account connected to Transit Gateway +3. Share the Transit Gateway with the Organization using Resource Access Manager (RAM) + +By using the `tgw/hub` component to collect Terraform output from connected accounts, only this single component requires access to the Terraform state of all connected accounts. + +Next we deploy `tgw/spoke` to the network account and then to every connected account. This spoke component connects the given account to the central hub and any listed connection with the following steps: + +1. Create a Transit Gateway VPC attachment in the spoke account. This connects the account's VPC to the shared Transit Gateway from the hub account. +2. Define all allowed routes for private subnets. Each private subnet in an account's VPC has it's own route table. This route table needs to explicitly list any allowed connection to another account's VPC CIDR. +3. (Optional) Create an EKS Cluster Security Group rule to allow traffic to the cluster in the given account. + +## Implementation + +1. Deploy `tgw/hub` to the network account. List every allowed connection: + +```yaml +# stacks/catalog/tgw/hub +components: + terraform: + tgw/hub/defaults: + metadata: + type: abstract + component: tgw/hub + vars: + enabled: true + name: tgw-hub + tags: + Team: sre + Service: tgw-hub + + tgw/hub: + metadata: + inherits: + - tgw/hub/defaults + component: tgw/hub + vars: + # These are all connections available for spokes in this region + # Defaults environment to this region + connections: + - account: + tenant: core + stage: network + - account: + tenant: core + stage: auto + eks_component_names: + - eks/cluster + - account: + tenant: plat + stage: sandbox + eks_component_names: [] # No clusters deployed for sandbox + - account: + tenant: plat + stage: dev + eks_component_names: + - eks/cluster + - account: + tenant: plat + stage: staging + eks_component_names: + - eks/cluster + - account: + tenant: plat + stage: prod + eks_component_names: + - eks/cluster +``` + +2. Deploy `tgw/spoke` to network. List every account connected to network (all accounts): + +```yaml +# stacks/catalog/tgw/spoke +components: + terraform: + tgw/spoke-defaults: + metadata: + type: abstract + component: tgw/spoke + vars: + enabled: true + name: tgw-spoke + tgw_hub_tenant_name: core + tgw_hub_stage_name: network # default, added for visibility + tags: + Team: sre + Service: tgw-spoke +``` + +```yaml +# stacks/orgs/acme/core/network/us-east-1/network.yaml +tgw/spoke: + metadata: + inherits: + - tgw/spoke-defaults + vars: + # This is what THIS spoke is allowed to connect to + connections: + - account: + tenant: core + stage: network + - account: + tenant: core + stage: auto + - account: + tenant: plat + stage: sandbox + - account: + tenant: plat + stage: dev + - account: + tenant: plat + stage: staging + - account: + tenant: plat + stage: prod +``` + +3. Finally, deploy `tgw/spoke` for each connected account and list the allowed connections: + +```yaml +# stacks/orgs/acme/plat/dev/us-east-1/network.yaml +tgw/spoke: + metadata: + inherits: + - tgw/spoke-defaults + vars: + connections: + # Always list self + - account: + tenant: plat + stage: dev + - account: + tenant: core + stage: network + - account: + tenant: core + stage: auto + +``` + +### Alternate Regions + +In order to connect any account to the network, the given account needs: + +1. Access to the shared Transit Gateway hub +2. An attachment for the given Transit Gateway hub +3. Routes to and from each private subnet + +However, sharing the Transit Gateway hub via RAM is only supported in the same region as the primary hub. Therefore, we must instead deploy a new hub in the alternate region and create a [Transit Gateway Peering Connection](https://docs.aws.amazon.com/vpc/latest/tgw/tgw-peering.html) between the two Transit Gateway hubs. + +Furthermore, since this Transit Gateway hub for the alternate region is now peered, we must create a Peering Transit Gateway attachment, opposed to a VPC Transit Gateway Attachment. + +#### Cross Region Deployment + +1. Deploy `tgw/hub` and `tgw/spoke` into the primary region as described in [Implementation](#implementation) + +2. Deploy `tgw/hub` and `tgw/cross-region-hub` into the new region in the network account. See the following configuration: + +```yaml +# stacks/catalog/tgw/cross-region-hub +import: + - catalog/tgw/hub + +components: + terraform: + # Cross region TGW requires additional hub in the alternate region + tgw/hub: + vars: + # These are all connections available for spokes in this region + # Defaults environment to this region + connections: + # Hub for this region is always required + - account: + tenant: core + stage: network + # VPN source + - account: + tenant: core + stage: network + environment: use1 + # Github Runners + - account: + tenant: core + stage: auto + environment: use1 + eks_component_names: + - eks/cluster + # All stacks where a spoke will be deployed + - account: + tenant: plat + stage: dev + - account: + tenant: plat + stage: staging + - account: + tenant: plat + stage: prod + + # This alternate hub needs to be connected to the primary region's hub + tgw/cross-region-hub-connector: + vars: + enabled: true + primary_tgw_hub_region: us-east-1 +``` + +3. Deploy a `tgw/spoke` for network in the new region. For example: + +```yaml +# stacks/orgs/acme/core/network/us-west-2/network.yaml +tgw/spoke: + metadata: + inherits: + - tgw/spoke-defaults + vars: + peered_region: true # Required for alternate region spokes + connections: + # This stack, always included + - account: + tenant: core + stage: network + # VPN + - account: + tenant: core + environment: use1 + stage: network + # Automation runners + - account: + tenant: core + environment: use1 + stage: auto + eks_component_names: + - eks/cluster + # All other connections + - account: + tenant: plat + stage: dev + - account: + tenant: plat + stage: staging + - account: + tenant: plat + stage: prod +``` + +4. Deploy the `tgw/spoke` components for all connected accounts. For example: + +```yaml +# stacks/orgs/acme/plat/dev/us-west-2/network.yaml +tgw/spoke: + metadata: + inherits: + - tgw/spoke-defaults + vars: + peered_region: true # Required for alternate region spokes + connections: + # This stack, always included + - account: + tenant: plat + stage: dev + # TGW Hub, always included + - account: + tenant: core + stage: network + # VPN + - account: + tenant: core + environment: use1 + stage: network + # Automation runners + - account: + tenant: core + environment: use1 + stage: auto + eks_component_names: + - eks/cluster +``` + +5. Update any existing `tgw/spoke` connections to allow the new account and region. For example: + +```yaml +# stacks/orgs/acme/core/auto/us-east-1/network.yaml +tgw/spoke: + metadata: + inherits: + - tgw/spoke-defaults + vars: + connections: + - account: + tenant: core + stage: network + - account: + tenant: core + stage: corp + - account: + tenant: core + stage: auto + - account: + tenant: plat + stage: sandbox + - account: + tenant: plat + stage: dev + - account: + tenant: plat + stage: staging + - account: + tenant: plat + stage: prod + + # Alternate regions <-------- These are added for alternate region + - account: + tenant: core + stage: network + environment: usw2 + - account: + tenant: plat + stage: dev + environment: usw2 + - account: + tenant: plat + stage: staging + environment: usw2 + - account: + tenant: plat + stage: prod + environment: usw2 +``` diff --git a/modules/tgw/cross-region-hub-connector/README.md b/modules/tgw/cross-region-hub-connector/README.md new file mode 100644 index 000000000..558b6a4d5 --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/README.md @@ -0,0 +1,134 @@ +# Component: `cross-region-hub-connector` + +This component is responsible for provisioning an [AWS Transit Gateway Peering Connection](https://aws.amazon.com/transit-gateway) to connect TGWs from different accounts and(or) regions. + +Transit Gateway does not support sharing the Transit Gateway hub across regions. You must deploy a Transit Gateway hub for each region and connect the alternate hub to the primary hub. + +## Usage + +**Stack Level**: Regional + +This component is deployed to each alternate region with `tgw/hub`. + +For example if your primary region is `us-east-1` and your alternate region is `us-west-2`, deploy another `tgw/hub` in `us-west-2` +and peer the two with `tgw/cross-region-hub-connector` with the following stack config, imported into `us-west-2` + +```yaml +import: + - catalog/tgw/hub + +components: + terraform: + # Cross region TGW requires additional hub in the alternate region + tgw/hub: + vars: + # These are all connections available for spokes in this region + # Defaults environment to this region + connections: + # Hub for this region is always required + - account: + tenant: core + stage: network + # VPN source + - account: + tenant: core + stage: network + environment: use1 + # Github Runners + - account: + tenant: core + stage: auto + environment: use1 + eks_component_names: + - eks/cluster + # All stacks where a spoke will be deployed + - account: + tenant: plat + stage: dev + eks_component_names: [] # Add clusters here once deployed + + # This alternate hub needs to be connected to the primary region's hub + tgw/cross-region-hub-connector: + vars: + enabled: true + primary_tgw_hub_region: us-east-1 +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.1 | +| [utils](#requirement\_utils) | >= 1.8.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.1 | +| [aws.primary\_tgw\_hub\_region](#provider\_aws.primary\_tgw\_hub\_region) | >= 4.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [tgw\_hub\_primary\_region](#module\_tgw\_hub\_primary\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [tgw\_hub\_this\_region](#module\_tgw\_hub\_this\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ec2_transit_gateway_peering_attachment.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_transit_gateway_peering_attachment) | resource | +| [aws_ec2_transit_gateway_peering_attachment_accepter.primary_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_transit_gateway_peering_attachment_accepter) | resource | +| [aws_ec2_transit_gateway_route_table_association.primary_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_transit_gateway_route_table_association) | resource | +| [aws_ec2_transit_gateway_route_table_association.this_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_transit_gateway_route_table_association) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_environment\_name](#input\_account\_map\_environment\_name) | The name of the environment where `account_map` is provisioned | `string` | `"gbl"` | no | +| [account\_map\_stage\_name](#input\_account\_map\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [env\_naming\_convention](#input\_env\_naming\_convention) | The cloudposse/utils naming convention used to translate environment name to AWS region name. Options are `to_short` and `to_fixed` | `string` | `"to_short"` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [primary\_tgw\_hub\_region](#input\_primary\_tgw\_hub\_region) | The name of the AWS region where the primary Transit Gateway hub is deployed. This value is used with `var.env_naming_convention` to determine the primary Transit Gateway hub's environment name. | `string` | n/a | yes | +| [primary\_tgw\_hub\_stage](#input\_primary\_tgw\_hub\_stage) | The name of the stage where the primary Transit Gateway hub is deployed. Defaults to `module.this.stage` | `string` | `""` | no | +| [primary\_tgw\_hub\_tenant](#input\_primary\_tgw\_hub\_tenant) | The name of the tenant where the primary Transit Gateway hub is deployed. Only used if tenants are deployed and defaults to `module.this.tenant` | `string` | `""` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [aws\_ec2\_transit\_gateway\_peering\_attachment\_id](#output\_aws\_ec2\_transit\_gateway\_peering\_attachment\_id) | Transit Gateway Peering Attachment ID | + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/tgw/cross-region-hub-connector) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/tgw/cross-region-hub-connector/context.tf b/modules/tgw/cross-region-hub-connector/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/tgw/cross-region-hub-connector/main.tf b/modules/tgw/cross-region-hub-connector/main.tf new file mode 100644 index 000000000..4a6a0c190 --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/main.tf @@ -0,0 +1,50 @@ +locals { + enabled = module.this.enabled + + primary_tgw_hub_tenant = length(var.primary_tgw_hub_tenant) > 0 ? var.primary_tgw_hub_tenant : module.this.tenant + primary_tgw_hub_stage = length(var.primary_tgw_hub_stage) > 0 ? var.primary_tgw_hub_stage : module.this.stage + primary_tgw_hub_account = module.this.tenant != null ? format("%s-%s", local.primary_tgw_hub_tenant, local.primary_tgw_hub_stage) : local.primary_tgw_hub_stage + primary_tgw_hub_account_id = module.account_map.outputs.full_account_map[local.primary_tgw_hub_account] +} + +# Connect two Transit Gateway Hubs across regions +resource "aws_ec2_transit_gateway_peering_attachment" "this" { + count = local.enabled ? 1 : 0 + + peer_account_id = local.primary_tgw_hub_account_id + peer_region = var.primary_tgw_hub_region + peer_transit_gateway_id = module.tgw_hub_primary_region.outputs.transit_gateway_id + transit_gateway_id = module.tgw_hub_this_region.outputs.transit_gateway_id + + tags = module.this.tags +} + +# Accept the peering attachment in the primary region +resource "aws_ec2_transit_gateway_peering_attachment_accepter" "primary_region" { + count = local.enabled ? 1 : 0 + + provider = aws.primary_tgw_hub_region + + transit_gateway_attachment_id = join("", aws_ec2_transit_gateway_peering_attachment.this[*].id) + tags = module.this.tags +} + +resource "aws_ec2_transit_gateway_route_table_association" "this_region" { + count = local.enabled ? 1 : 0 + + transit_gateway_attachment_id = join("", aws_ec2_transit_gateway_peering_attachment.this[*].id) + transit_gateway_route_table_id = module.tgw_hub_this_region.outputs.transit_gateway_route_table_id + + depends_on = [aws_ec2_transit_gateway_peering_attachment_accepter.primary_region] +} + +resource "aws_ec2_transit_gateway_route_table_association" "primary_region" { + count = local.enabled ? 1 : 0 + + provider = aws.primary_tgw_hub_region + + transit_gateway_attachment_id = join("", aws_ec2_transit_gateway_peering_attachment.this[*].id) + transit_gateway_route_table_id = module.tgw_hub_primary_region.outputs.transit_gateway_route_table_id + + depends_on = [aws_ec2_transit_gateway_peering_attachment_accepter.primary_region] +} diff --git a/modules/tgw/cross-region-hub-connector/outputs.tf b/modules/tgw/cross-region-hub-connector/outputs.tf new file mode 100644 index 000000000..f8beb7d4d --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/outputs.tf @@ -0,0 +1,4 @@ +output "aws_ec2_transit_gateway_peering_attachment_id" { + value = join("", aws_ec2_transit_gateway_peering_attachment.this[*].id) + description = "Transit Gateway Peering Attachment ID" +} diff --git a/modules/tgw/cross-region-hub-connector/provider-tgw.tf b/modules/tgw/cross-region-hub-connector/provider-tgw.tf new file mode 100644 index 000000000..f69825d39 --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/provider-tgw.tf @@ -0,0 +1,15 @@ +provider "aws" { + alias = "primary_tgw_hub_region" + region = var.primary_tgw_hub_region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} diff --git a/modules/tgw/cross-region-hub-connector/providers.tf b/modules/tgw/cross-region-hub-connector/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/tgw/cross-region-hub-connector/remote-state.tf b/modules/tgw/cross-region-hub-connector/remote-state.tf new file mode 100644 index 000000000..50ed6b57f --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/remote-state.tf @@ -0,0 +1,42 @@ +locals { + primary_tgw_hub_environment = module.utils.region_az_alt_code_maps[var.env_naming_convention][var.primary_tgw_hub_region] +} + +# Used to translate region to environment +module "utils" { + source = "cloudposse/utils/aws" + version = "1.3.0" + enabled = local.enabled +} + +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "account-map" + environment = var.account_map_environment_name + stage = var.account_map_stage_name + + context = module.this.context +} + +module "tgw_hub_this_region" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "tgw/hub" + + context = module.this.context +} + +module "tgw_hub_primary_region" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "tgw/hub" + stage = local.primary_tgw_hub_stage + environment = local.primary_tgw_hub_environment + tenant = local.primary_tgw_hub_tenant + + context = module.this.context +} diff --git a/modules/tgw/cross-region-hub-connector/variables.tf b/modules/tgw/cross-region-hub-connector/variables.tf new file mode 100644 index 000000000..f24e1a798 --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/variables.tf @@ -0,0 +1,44 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "env_naming_convention" { + type = string + description = "The cloudposse/utils naming convention used to translate environment name to AWS region name. Options are `to_short` and `to_fixed`" + default = "to_short" + + validation { + condition = var.env_naming_convention != "to_short" || var.env_naming_convention != "to_fixed:" + error_message = "`var.env_naming_convention` must be either `to_short` or `to_fixed`." + } +} + +variable "primary_tgw_hub_tenant" { + type = string + description = "The name of the tenant where the primary Transit Gateway hub is deployed. Only used if tenants are deployed and defaults to `module.this.tenant`" + default = "" +} + +variable "primary_tgw_hub_stage" { + type = string + description = "The name of the stage where the primary Transit Gateway hub is deployed. Defaults to `module.this.stage`" + default = "" +} + +variable "primary_tgw_hub_region" { + type = string + description = "The name of the AWS region where the primary Transit Gateway hub is deployed. This value is used with `var.env_naming_convention` to determine the primary Transit Gateway hub's environment name." +} + +variable "account_map_environment_name" { + type = string + description = "The name of the environment where `account_map` is provisioned" + default = "gbl" +} + +variable "account_map_stage_name" { + type = string + description = "The name of the stage where `account_map` is provisioned" + default = "root" +} diff --git a/modules/tgw/cross-region-hub-connector/versions.tf b/modules/tgw/cross-region-hub-connector/versions.tf new file mode 100644 index 000000000..289af5570 --- /dev/null +++ b/modules/tgw/cross-region-hub-connector/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.1" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.8.0" + } + } +} diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 4f745d524..482c49b4d 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -92,12 +92,12 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [tgw\_hub](#module\_tgw\_hub) | cloudposse/transit-gateway/aws | 0.9.1 | +| [tgw\_hub](#module\_tgw\_hub) | cloudposse/transit-gateway/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources @@ -112,7 +112,7 @@ No resources. | [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | +| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
environment = optional(string, "")
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/tgw/hub/main.tf b/modules/tgw/hub/main.tf index 4ab7e5943..f62ff26cc 100644 --- a/modules/tgw/hub/main.tf +++ b/modules/tgw/hub/main.tf @@ -8,7 +8,7 @@ module "tgw_hub" { source = "cloudposse/transit-gateway/aws" - version = "0.9.1" + version = "0.11.0" ram_resource_share_enabled = true route_keys_enabled = true diff --git a/modules/tgw/hub/remote-state.tf b/modules/tgw/hub/remote-state.tf index 5048794cf..8a8678121 100644 --- a/modules/tgw/hub/remote-state.tf +++ b/modules/tgw/hub/remote-state.tf @@ -1,17 +1,19 @@ locals { vpc_connections = flatten([for connection in var.connections : [ for vpc_component_name in connection.vpc_component_names : { - stage = connection.account.stage - tenant = lookup(connection.account, "tenant", null) - component = vpc_component_name + stage = connection.account.stage + tenant = connection.account.tenant # Defaults to empty string if tenant isnt defined + environment = length(connection.account.environment) > 0 ? connection.account.environment : module.this.environment + component = vpc_component_name }] ]) eks_connections = flatten([for connection in var.connections : [ for eks_component_name in connection.eks_component_names : { - stage = connection.account.stage - tenant = lookup(connection.account, "tenant", null) - component = eks_component_name + stage = connection.account.stage + tenant = connection.account.tenant # Defaults to empty string if tenant isnt defined + environment = length(connection.account.environment) > 0 ? connection.account.environment : module.this.environment + component = eks_component_name }] ]) @@ -19,7 +21,7 @@ locals { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "account-map" environment = var.account_map_environment_name @@ -31,30 +33,38 @@ module "account_map" { module "vpc" { for_each = { for c in local.vpc_connections : - (length(c.tenant) > 0 ? "${c.tenant}-${c.stage}-${c.component}" : "${c.stage}-${c.component}") + (length(c.tenant) > 0 ? "${c.tenant}-${c.environment}-${c.stage}-${c.component}" : "${c.environment}-${c.stage}-${c.component}") => c } source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" - component = each.value.component - stage = each.value.stage - tenant = lookup(each.value, "tenant", null) + component = each.value.component + stage = each.value.stage + environment = each.value.environment + tenant = lookup(each.value, "tenant", null) + + defaults = { + stage = each.value.stage + environment = each.value.environment + tenant = lookup(each.value, "tenant", null) + } context = module.this.context } module "eks" { for_each = { for c in local.eks_connections : - (length(c.tenant) > 0 ? "${c.tenant}-${c.stage}-${c.component}" : "${c.stage}-${c.component}") + (length(c.tenant) > 0 ? "${c.tenant}-${c.environment}-${c.stage}-${c.component}" : "${c.environment}-${c.stage}-${c.component}") => c } source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" - component = each.value.component - stage = each.value.stage - tenant = lookup(each.value, "tenant", null) + component = each.value.component + stage = each.value.stage + environment = each.value.environment + tenant = lookup(each.value, "tenant", null) defaults = { eks_cluster_managed_security_group_id = null diff --git a/modules/tgw/hub/variables.tf b/modules/tgw/hub/variables.tf index 6b5603c83..0bf4d7c71 100644 --- a/modules/tgw/hub/variables.tf +++ b/modules/tgw/hub/variables.tf @@ -12,8 +12,9 @@ variable "expose_eks_sg" { variable "connections" { type = list(object({ account = object({ - stage = string - tenant = optional(string, "") + stage = string + environment = optional(string, "") + tenant = optional(string, "") }) vpc_component_names = optional(list(string), ["vpc"]) eks_component_names = optional(list(string), []) diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index 663ef7d5c..2afd19e68 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -104,8 +104,9 @@ No providers. | Name | Source | Version | |------|--------|---------| +| [cross\_region\_hub\_connector](#module\_cross\_region\_hub\_connector) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [tgw\_hub](#module\_tgw\_hub) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [tgw\_hub](#module\_tgw\_hub) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [tgw\_hub\_role](#module\_tgw\_hub\_role) | ../../account-map/modules/iam-roles | n/a | | [tgw\_hub\_routes](#module\_tgw\_hub\_routes) | cloudposse/transit-gateway/aws | 0.10.0 | | [tgw\_spoke\_vpc\_attachment](#module\_tgw\_spoke\_vpc\_attachment) | ./modules/standard_vpc_attachment | n/a | @@ -121,7 +122,7 @@ No resources. |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | +| [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
environment = optional(string, "")
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | @@ -137,14 +138,14 @@ No resources. | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [own\_eks\_component\_names](#input\_own\_eks\_component\_names) | The name of the eks components in the owning account. | `list(string)` | `[]` | no | | [own\_vpc\_component\_name](#input\_own\_vpc\_component\_name) | The name of the vpc component in the owning account. Defaults to "vpc" | `string` | `"vpc"` | no | +| [peered\_region](#input\_peered\_region) | Set `true` if this region is not the primary region | `bool` | `false` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [tgw\_hub\_component\_name](#input\_tgw\_hub\_component\_name) | The name of the transit-gateway component | `string` | `"tgw/hub"` | no | -| [tgw\_hub\_environment\_name](#input\_tgw\_hub\_environment\_name) | The name of the environment where `tgw/gateway` is provisioned | `string` | `"ue2"` | no | -| [tgw\_hub\_stage\_name](#input\_tgw\_hub\_stage\_name) | The name of the stage where `tgw/gateway` is provisioned | `string` | `"network"` | no | +| [tgw\_hub\_stage\_name](#input\_tgw\_hub\_stage\_name) | The name of the stage where `tgw/hub` is provisioned | `string` | `"network"` | no | | [tgw\_hub\_tenant\_name](#input\_tgw\_hub\_tenant\_name) | The name of the tenant where `tgw/hub` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | ## Outputs diff --git a/modules/tgw/spoke/main.tf b/modules/tgw/spoke/main.tf index 607969f5b..32ef7792e 100644 --- a/modules/tgw/spoke/main.tf +++ b/modules/tgw/spoke/main.tf @@ -7,7 +7,7 @@ # https://docs.aws.amazon.com/ram/latest/userguide/getting-started-sharing.html locals { - spoke_account = module.this.tenant != null ? format("%s-%s", module.this.tenant, module.this.stage) : module.this.stage + spoke_account = module.this.tenant != null ? format("%s-%s-%s", module.this.tenant, module.this.environment, module.this.stage) : format("%s-%s", module.this.environment, module.this.stage) } module "tgw_hub_routes" { @@ -42,9 +42,12 @@ module "tgw_spoke_vpc_attachment" { own_vpc_component_name = var.own_vpc_component_name own_eks_component_names = var.own_eks_component_names - tgw_config = module.tgw_hub.outputs.tgw_config - connections = var.connections - expose_eks_sg = var.expose_eks_sg + tgw_config = module.tgw_hub.outputs.tgw_config + tgw_connector_config = module.cross_region_hub_connector + connections = var.connections + expose_eks_sg = var.expose_eks_sg + peered_region = var.peered_region + context = module.this.context } diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf index 7e09fe853..96a28208d 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf @@ -4,6 +4,7 @@ locals { own_account_vpc_key = "${var.owning_account}-${var.own_vpc_component_name}" own_vpc = local.vpcs[local.own_account_vpc_key].outputs + is_network_hub = (module.this.stage == var.network_account_stage_name) ? true : false # Create a list of all VPC component keys. Key includes stack + component # @@ -17,6 +18,13 @@ locals { # - account: # tenant: core # stage: auto + # - account: + # tenant: plat + # stage: dev + # - account: + # tenant: plat + # stage: dev + # environment: usw2 connected_vpc_component_keys = flatten( [ for c in var.connections : @@ -25,7 +33,14 @@ locals { for vpc in c.vpc_component_names : # This component key needs to match the key created by tgw/hub # See components/terraform/tgw/hub/remote-state.tf - length(c.account.tenant) > 0 ? "${c.account.tenant}-${c.account.stage}-${vpc}" : "${c.account.stage}-${vpc}" + length(c.account.environment) > 0 ? + (length(c.account.tenant) > 0 ? + "${c.account.tenant}-${c.account.environment}-${c.account.stage}-${vpc}" : + "${c.account.environment}-${c.account.stage}-${vpc}") + : + (length(c.account.tenant) > 0 ? + "${c.account.tenant}-${module.this.environment}-${c.account.stage}-${vpc}" : + "${module.this.environment}-${c.account.stage}-${vpc}") ] ] ) @@ -37,7 +52,14 @@ locals { for c in var.connections : [ for eks in c.eks_component_names : - length(c.account.tenant) > 0 ? "${c.account.tenant}-${c.account.stage}-${eks}" : "${c.account.stage}-${eks}" + length(c.account.environment) > 0 ? + (length(c.account.tenant) > 0 ? + "${c.account.tenant}-${c.account.environment}-${c.account.stage}-${eks}" : + "${c.account.environment}-${c.account.stage}-${eks}") + : + (length(c.account.tenant) > 0 ? + "${c.account.tenant}-${module.this.environment}-${c.account.stage}-${eks}" : + "${module.this.environment}-${c.account.stage}-${eks}") ] ] ) @@ -48,7 +70,9 @@ locals { allowed_vpcs = { for vpc_key, vpc_remote_state in local.vpcs : vpc_key => { - cidr = vpc_remote_state.outputs.vpc_cidr + cidr = vpc_remote_state.outputs.vpc_cidr + cross_region = (vpc_remote_state.outputs.environment != module.this.environment) + environment = vpc_remote_state.outputs.environment } if vpc_key != local.own_account_vpc_key && contains(local.connected_vpc_component_keys, vpc_key) } @@ -66,13 +90,35 @@ locals { } ]...) + cross_region_vpcs = flatten([ + for vpc_key, vpc in local.allowed_vpcs : [ + { + vpc_key = vpc_key + cidr = vpc.cidr + environment = vpc.environment + } + ] if vpc.cross_region + ]) + + cross_region_vpc_route_table_ids = flatten([ + for vpc_key, vpc in local.allowed_vpcs : [ + for route_table_key, route_table_id in local.own_vpc.private_route_table_ids : [ + { + vpc_key = vpc_key + rt_key = route_table_key + cidr = vpc.cidr + route_table_id = route_table_id + } + ] + ] if vpc.cross_region + ]) } # Create a TGW attachment from this account's VPC to the TGW Hub # This includes a merged list of all CIDRs from allowed VPCs in connected accounts module "standard_vpc_attachment" { source = "cloudposse/transit-gateway/aws" - version = "0.9.1" + version = "0.11.0" existing_transit_gateway_id = var.tgw_config.existing_transit_gateway_id existing_transit_gateway_route_table_id = var.tgw_config.existing_transit_gateway_route_table_id @@ -92,13 +138,41 @@ module "standard_vpc_attachment" { route_to = null static_routes = null transit_gateway_vpc_attachment_id = null - route_to_cidr_blocks = [for vpc in local.allowed_vpcs : vpc.cidr] + route_to_cidr_blocks = [for vpc in local.allowed_vpcs : vpc.cidr if !vpc.cross_region] } } context = module.this.context } +# Create a TGW attachment for a Peering Connection +# This in only necessary in the hub accounts +resource "aws_ec2_transit_gateway_route" "peering_connection" { + for_each = local.is_network_hub ? { + for vpc in local.cross_region_vpcs : vpc.cidr => vpc + } : {} + + # Use the TGW Attachment in the alternate, peered region + transit_gateway_attachment_id = var.peered_region ? var.tgw_connector_config[local.own_vpc.environment].outputs.aws_ec2_transit_gateway_peering_attachment_id : var.tgw_connector_config[each.value.environment].outputs.aws_ec2_transit_gateway_peering_attachment_id + + blackhole = false + destination_cidr_block = each.value.cidr + transit_gateway_route_table_id = var.tgw_config.existing_transit_gateway_route_table_id +} + +# Route this VPC to the destination CIDR +# This is only necessary in cross-region connections +resource "aws_route" "peering_connection" { + for_each = { + for vpc_rt in local.cross_region_vpc_route_table_ids : "${vpc_rt.route_table_id}:${vpc_rt.cidr}" => vpc_rt + } + + transit_gateway_id = var.tgw_config.existing_transit_gateway_id + + route_table_id = each.value.route_table_id + destination_cidr_block = each.value.cidr +} + # Define a Security Group Rule to allow traffic from # Expose traffic from EKS VPC CIDRs in other accounts to this accounts EKS cluster SG resource "aws_security_group_rule" "ingress_cidr_blocks" { diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf index 79d6384a0..08f294f5d 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf @@ -26,11 +26,18 @@ variable "tgw_config" { description = "Object to pass common data from root module to this submodule. See root module for details" } +variable "tgw_connector_config" { + type = map(any) + description = "Map of output from all `tgw/cross-region-hub-connector` components. See root module for details" + default = {} +} + variable "connections" { type = list(object({ account = object({ - stage = string - tenant = optional(string, "") + stage = string + environment = optional(string, "") + tenant = optional(string, "") }) vpc_component_names = optional(list(string), ["vpc"]) eks_component_names = optional(list(string), []) @@ -48,3 +55,15 @@ variable "expose_eks_sg" { description = "Set true to allow EKS clusters to accept traffic from source accounts" default = true } + +variable "peered_region" { + type = bool + description = "Set `true` if this region is not the primary region" + default = false +} + +variable "network_account_stage_name" { + type = string + description = "The name of the stage designated as the network hub" + default = "network" +} diff --git a/modules/tgw/spoke/provider-hub.tf b/modules/tgw/spoke/provider-hub.tf index e969db12e..a1f429f85 100644 --- a/modules/tgw/spoke/provider-hub.tf +++ b/modules/tgw/spoke/provider-hub.tf @@ -14,34 +14,11 @@ provider "aws" { } } -variable "tgw_hub_environment_name" { - type = string - description = "The name of the environment where `tgw/gateway` is provisioned" - default = "ue2" -} - -variable "tgw_hub_stage_name" { - type = string - description = "The name of the stage where `tgw/gateway` is provisioned" - default = "network" -} - -variable "tgw_hub_tenant_name" { - type = string - description = <<-EOT - The name of the tenant where `tgw/hub` is provisioned. - - If the `tenant` label is not used, leave this as `null`. - EOT - default = null -} - module "tgw_hub_role" { source = "../../account-map/modules/iam-roles" - stage = var.tgw_hub_stage_name - environment = var.tgw_hub_environment_name - tenant = var.tgw_hub_tenant_name + stage = var.tgw_hub_stage_name + tenant = var.tgw_hub_tenant_name context = module.this.context } diff --git a/modules/tgw/spoke/remote-state.tf b/modules/tgw/spoke/remote-state.tf index d074fca2d..f6c60563c 100644 --- a/modules/tgw/spoke/remote-state.tf +++ b/modules/tgw/spoke/remote-state.tf @@ -1,11 +1,34 @@ +locals { + # Any cross region connection requires a TGW Hub connector deployed + # If any connections given are cross-region, get the `tgw/cross-region-hub-connector` component from that region + connected_environments = distinct(compact(concat([for c in var.connections : c.account.environment], [module.this.environment]))) +} + module "tgw_hub" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" + + component = var.tgw_hub_component_name + tenant = length(var.tgw_hub_tenant_name) > 0 ? var.tgw_hub_tenant_name : module.this.tenant + stage = length(var.tgw_hub_stage_name) > 0 ? var.tgw_hub_stage_name : module.this.stage + + context = module.this.context +} + +module "cross_region_hub_connector" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + for_each = toset(local.connected_environments) + + component = "tgw/cross-region-hub-connector" + tenant = length(var.tgw_hub_tenant_name) > 0 ? var.tgw_hub_tenant_name : module.this.tenant + stage = length(var.tgw_hub_stage_name) > 0 ? var.tgw_hub_stage_name : module.this.stage + environment = each.value - component = var.tgw_hub_component_name - stage = var.tgw_hub_stage_name - environment = var.tgw_hub_environment_name - tenant = var.tgw_hub_tenant_name + # Ignore if hub connector doesnt exist (it doesnt exist in primary region) + ignore_errors = true + defaults = {} context = module.this.context } diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index 6d4075445..a9b66e42f 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -6,8 +6,9 @@ variable "region" { variable "connections" { type = list(object({ account = object({ - stage = string - tenant = optional(string, "") + stage = string + environment = optional(string, "") + tenant = optional(string, "") }) vpc_component_names = optional(list(string), ["vpc"]) eks_component_names = optional(list(string), []) @@ -26,6 +27,22 @@ variable "tgw_hub_component_name" { default = "tgw/hub" } +variable "tgw_hub_stage_name" { + type = string + description = "The name of the stage where `tgw/hub` is provisioned" + default = "network" +} + +variable "tgw_hub_tenant_name" { + type = string + description = <<-EOT + The name of the tenant where `tgw/hub` is provisioned. + + If the `tenant` label is not used, leave this as `null`. + EOT + default = null +} + variable "expose_eks_sg" { type = bool description = "Set true to allow EKS clusters to accept traffic from source accounts" @@ -43,3 +60,9 @@ variable "own_eks_component_names" { default = [] description = "The name of the eks components in the owning account." } + +variable "peered_region" { + type = bool + description = "Set `true` if this region is not the primary region" + default = false +} From 4c8b7f573ccabba6840928c2ab797fe449fd427d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 9 Aug 2023 11:21:19 -0700 Subject: [PATCH 214/501] Update upgrade-guide.md Version (#807) --- docs/upgrade-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md index 2935ecc60..e89e40ba7 100644 --- a/docs/upgrade-guide.md +++ b/docs/upgrade-guide.md @@ -1,4 +1,4 @@ -## Upgrading to `v1.275.0` +## Upgrading to `v1.276.0` ### Affected Components From 3f2957d37557c64bd75925e2ef411f004c23ec8d Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:30:12 -0400 Subject: [PATCH 215/501] feat: allow email to be configured at account level (#799) --- modules/account/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/account/main.tf b/modules/account/main.tf index 093a53647..fa3c46684 100644 --- a/modules/account/main.tf +++ b/modules/account/main.tf @@ -13,7 +13,7 @@ locals { # Organizational Units' Accounts list and map configuration organizational_units_accounts = flatten([ for ou in local.organizational_units : [ - for account in lookup(ou, "accounts", []) : merge(account, { "ou" = ou.name, "account_email_format" = lookup(ou, "account_email_format", var.account_email_format) }) + for account in lookup(ou, "accounts", []) : merge({ "ou" = ou.name, "account_email_format" = lookup(ou, "account_email_format", var.account_email_format) }, account) ] ]) organizational_units_accounts_map = { for acc in local.organizational_units_accounts : acc.name => acc } @@ -139,7 +139,7 @@ resource "aws_organizations_account" "organizational_units_accounts" { for_each = local.organizational_units_accounts_map name = each.value.name parent_id = aws_organizations_organizational_unit.this[local.account_names_organizational_unit_names_map[each.value.name]].id - email = format(each.value.account_email_format, each.value.name) + email = try(format(each.value.account_email_format, each.value.name), each.value.account_email_format) iam_user_access_to_billing = var.account_iam_user_access_to_billing tags = merge(module.this.tags, try(each.value.tags, {}), { Name : each.value.name }) From 58b846239df877c2e9a1cdc9fe8d5fd72c10b3fa Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 9 Aug 2023 13:38:57 -0700 Subject: [PATCH 216/501] Added Inputs for `elasticsearch` and `cognito` (#786) --- modules/cognito/README.md | 1 + modules/cognito/main.tf | 1 + modules/cognito/variables.tf | 6 ++++++ modules/elasticsearch/README.md | 2 ++ modules/elasticsearch/main.tf | 2 ++ modules/elasticsearch/variables.tf | 12 ++++++++++++ 6 files changed, 24 insertions(+) diff --git a/modules/cognito/README.md b/modules/cognito/README.md index ec89b7b59..25f440db1 100644 --- a/modules/cognito/README.md +++ b/modules/cognito/README.md @@ -101,6 +101,7 @@ components: | [client\_write\_attributes](#input\_client\_write\_attributes) | List of user pool attributes the application client can write to | `list(string)` | `[]` | no | | [clients](#input\_clients) | User Pool clients configuration | `any` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [deletion\_protection](#input\_deletion\_protection) | (Optional) When active, DeletionProtection prevents accidental deletion of your user pool. Before you can delete a user pool that you have protected against deletion, you must deactivate this feature. Valid values are ACTIVE and INACTIVE, Default value is INACTIVE. | `string` | `"INACTIVE"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [device\_configuration](#input\_device\_configuration) | The configuration for the user pool's device tracking | `map(any)` | `{}` | no | diff --git a/modules/cognito/main.tf b/modules/cognito/main.tf index bf05fd81c..aec78d656 100644 --- a/modules/cognito/main.tf +++ b/modules/cognito/main.tf @@ -149,6 +149,7 @@ resource "aws_cognito_user_pool" "pool" { mfa_configuration = var.mfa_configuration sms_authentication_message = var.sms_authentication_message sms_verification_message = var.sms_verification_message + deletion_protection = var.deletion_protection dynamic "username_configuration" { for_each = local.username_configuration diff --git a/modules/cognito/variables.tf b/modules/cognito/variables.tf index 5dfc5671c..687cd36de 100644 --- a/modules/cognito/variables.tf +++ b/modules/cognito/variables.tf @@ -559,3 +559,9 @@ variable "identity_providers" { type = list(any) default = [] } + +variable "deletion_protection" { + description = "(Optional) When active, DeletionProtection prevents accidental deletion of your user pool. Before you can delete a user pool that you have protected against deletion, you must deactivate this feature. Valid values are ACTIVE and INACTIVE, Default value is INACTIVE." + type = string + default = "INACTIVE" +} diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index fb07cf624..461324c17 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -71,7 +71,9 @@ components: | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_iam\_service\_linked\_role](#input\_create\_iam\_service\_linked\_role) | Whether to create `AWSServiceRoleForAmazonElasticsearchService` service-linked role.
Set this to `false` if you already have an ElasticSearch cluster created in the AWS account and `AWSServiceRoleForAmazonElasticsearchService` already exists.
See https://github.com/terraform-providers/terraform-provider-aws/issues/5218 for more information. | `bool` | n/a | yes | +| [dedicated\_master\_count](#input\_dedicated\_master\_count) | Number of dedicated master nodes in the cluster | `number` | `0` | no | | [dedicated\_master\_enabled](#input\_dedicated\_master\_enabled) | Indicates whether dedicated master nodes are enabled for the cluster | `bool` | n/a | yes | +| [dedicated\_master\_type](#input\_dedicated\_master\_type) | Instance type of the dedicated master nodes in the cluster | `string` | `"t2.small.elasticsearch"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_delegated\_environment\_name](#input\_dns\_delegated\_environment\_name) | The name of the environment where the `dns-delegated` component is deployed | `string` | `"gbl"` | no | diff --git a/modules/elasticsearch/main.tf b/modules/elasticsearch/main.tf index b2570fcc0..25f8d5756 100644 --- a/modules/elasticsearch/main.tf +++ b/modules/elasticsearch/main.tf @@ -30,6 +30,8 @@ module "elasticsearch" { availability_zone_count = length(local.vpc_private_subnet_ids) encrypt_at_rest_enabled = var.encrypt_at_rest_enabled dedicated_master_enabled = var.dedicated_master_enabled + dedicated_master_count = var.dedicated_master_enabled ? var.dedicated_master_count : null + dedicated_master_type = var.dedicated_master_enabled ? var.dedicated_master_type : null create_iam_service_linked_role = var.create_iam_service_linked_role kibana_subdomain_name = module.this.environment ebs_volume_size = var.ebs_volume_size diff --git a/modules/elasticsearch/variables.tf b/modules/elasticsearch/variables.tf index 09e876725..c47487d09 100644 --- a/modules/elasticsearch/variables.tf +++ b/modules/elasticsearch/variables.tf @@ -23,6 +23,18 @@ variable "dedicated_master_enabled" { description = "Indicates whether dedicated master nodes are enabled for the cluster" } +variable "dedicated_master_count" { + type = number + description = "Number of dedicated master nodes in the cluster" + default = 0 +} + +variable "dedicated_master_type" { + type = string + default = "t2.small.elasticsearch" + description = "Instance type of the dedicated master nodes in the cluster" +} + variable "elasticsearch_subdomain_name" { type = string description = "The name of the subdomain for Elasticsearch in the DNS zone (_e.g._ `elasticsearch`, `ui`, `ui-es`, `search-ui`)" From 020ad29406d0ebaa805de6132b6277faa16e1d37 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 9 Aug 2023 14:53:48 -0700 Subject: [PATCH 217/501] Upstream `eks/keda` (#808) --- modules/eks/keda/README.md | 123 +++++++++++++ modules/eks/keda/context.tf | 279 ++++++++++++++++++++++++++++++ modules/eks/keda/main.tf | 40 +++++ modules/eks/keda/outputs.tf | 4 + modules/eks/keda/provider-helm.tf | 166 ++++++++++++++++++ modules/eks/keda/providers.tf | 19 ++ modules/eks/keda/remote-state.tf | 8 + modules/eks/keda/variables.tf | 89 ++++++++++ modules/eks/keda/versions.tf | 18 ++ 9 files changed, 746 insertions(+) create mode 100644 modules/eks/keda/README.md create mode 100644 modules/eks/keda/context.tf create mode 100644 modules/eks/keda/main.tf create mode 100644 modules/eks/keda/outputs.tf create mode 100644 modules/eks/keda/provider-helm.tf create mode 100644 modules/eks/keda/providers.tf create mode 100644 modules/eks/keda/remote-state.tf create mode 100644 modules/eks/keda/variables.tf create mode 100644 modules/eks/keda/versions.tf diff --git a/modules/eks/keda/README.md b/modules/eks/keda/README.md new file mode 100644 index 000000000..753e05f72 --- /dev/null +++ b/modules/eks/keda/README.md @@ -0,0 +1,123 @@ +# Component: `keda` + +This component is used to install the KEDA operator. + +[See this overview of how Keda works with triggers with a `ScaledObject`, which is a light wrapper around HPAs](https://keda.sh/docs/2.9/concepts/scaling-deployments/#overview). + +## Usage + +**Stack Level**: Regional + +Use this in the catalog or use these variables to overwrite the catalog values. + +```yaml +components: + terraform: + eks/keda: + vars: + enabled: true + name: "keda" + kubernetes_namespace: "keda" + create_namespace: true + timeout: 90 + wait: true + atomic: true + cleanup_on_fail: true + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 1000m + memory: 1024Mi + +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [helm](#requirement\_helm) | >= 2.6.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.9.0, != 2.21.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [keda](#module\_keda) | cloudposse/helm-release/aws | 0.9.3 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"keda"` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"2.8"` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input\_description) | Set release description attribute (visible in the history). | `string` | `"Used for autoscaling from external metrics configured as triggers."` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [rbac\_enabled](#input\_rbac\_enabled) | Service Account for pods. | `bool` | `true` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [repository](#input\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://kedacore.github.io/charts"` | no | +| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [metadata](#output\_metadata) | Block status of the deployed release | + + +## References +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/keda) - Cloud Posse's upstream component diff --git a/modules/eks/keda/context.tf b/modules/eks/keda/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/keda/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/keda/main.tf b/modules/eks/keda/main.tf new file mode 100644 index 000000000..20cf5010d --- /dev/null +++ b/modules/eks/keda/main.tf @@ -0,0 +1,40 @@ +module "keda" { + source = "cloudposse/helm-release/aws" + version = "0.9.3" + + name = module.this.name + description = var.description + + repository = var.repository + chart = var.chart + chart_version = var.chart_version + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") + + kubernetes_namespace = var.kubernetes_namespace + create_namespace = var.create_namespace + + service_account_name = module.this.name + service_account_namespace = var.kubernetes_namespace + + iam_role_enabled = false + iam_policy_statements = {} + + values = compact([ + yamlencode({ + serviceAccount = { + name = module.this.name + } + resources = var.resources + rbac = { + create = var.rbac_enabled + } + }) + ]) + + context = module.this.context +} diff --git a/modules/eks/keda/outputs.tf b/modules/eks/keda/outputs.tf new file mode 100644 index 000000000..8a5b6e428 --- /dev/null +++ b/modules/eks/keda/outputs.tf @@ -0,0 +1,4 @@ +output "metadata" { + value = try(one(module.keda.metadata), null) + description = "Block status of the deployed release" +} diff --git a/modules/eks/keda/provider-helm.tf b/modules/eks/keda/provider-helm.tf new file mode 100644 index 000000000..64459d4f4 --- /dev/null +++ b/modules/eks/keda/provider-helm.tf @@ -0,0 +1,166 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/keda/providers.tf b/modules/eks/keda/providers.tf new file mode 100644 index 000000000..45d458575 --- /dev/null +++ b/modules/eks/keda/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/eks/keda/remote-state.tf b/modules/eks/keda/remote-state.tf new file mode 100644 index 000000000..c1ec8226d --- /dev/null +++ b/modules/eks/keda/remote-state.tf @@ -0,0 +1,8 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + context = module.this.context +} diff --git a/modules/eks/keda/variables.tf b/modules/eks/keda/variables.tf new file mode 100644 index 000000000..44ab15af5 --- /dev/null +++ b/modules/eks/keda/variables.tf @@ -0,0 +1,89 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "rbac_enabled" { + type = bool + default = true + description = "Service Account for pods." +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "resources" { + type = object({ + limits = object({ + cpu = string + memory = string + }) + requests = object({ + cpu = string + memory = string + }) + }) + description = "The cpu and memory of the deployment's limits and requests." +} + +variable "kubernetes_namespace" { + type = string + description = "The namespace to install the release into." +} + +variable "description" { + type = string + description = "Set release description attribute (visible in the history)." + default = "Used for autoscaling from external metrics configured as triggers." +} + +variable "chart" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." + default = "keda" +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." + default = "2.8" +} + +variable "repository" { + type = string + description = "Repository URL where to locate the requested chart." + default = "https://kedacore.github.io/charts" +} + +variable "create_namespace" { + type = bool + description = "Create the Kubernetes namespace if it does not yet exist" + default = true +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`." + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used." + default = true +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails." + default = true +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = null +} diff --git a/modules/eks/keda/versions.tf b/modules/eks/keda/versions.tf new file mode 100644 index 000000000..3e6c990e3 --- /dev/null +++ b/modules/eks/keda/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.6.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.9.0, != 2.21.0" + } + } +} From d7c4f5603f25c7f81c094e621a632f0c843d2dba Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 11 Aug 2023 12:38:41 -0400 Subject: [PATCH 218/501] fix: restore argocd notification ssm lookups (#764) Co-authored-by: Dan Miller --- modules/argocd-repo/templates/applicationset.yaml.tpl | 3 +-- modules/eks/argocd/README.md | 9 +++++++-- modules/eks/argocd/resources/argocd-values.yaml.tpl | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index d4727ac60..292c1644e 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -61,10 +61,9 @@ spec: template: metadata: annotations: - deployment_id: '{{deployment_id}}' app_repository: '{{app_repository}}' app_commit: '{{app_commit}}' - app_hostname: 'https://{{app_hostname}}' + app_hostname: '{{app_hostname}}' notifications.argoproj.io/subscribe.on-deployed.github: "" notifications.argoproj.io/subscribe.on-deployed.github-commit-status: "" name: '{{name}}' diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 1e7d35770..d89c6201c 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -157,9 +157,14 @@ components: saml_enabled: true ssm_store_account: corp ssm_store_account_region: us-west-2 - saml_admin_role: ArgoCD-non-prod-admin - saml_readonly_role: ArgoCD-non-prod-observer argocd_repo_name: argocd-deploy-non-prod + argocd_rbac_policies: + - "p, role:org-admin, applications, *, */*, allow" + - "p, role:org-admin, clusters, get, *, allow" + - "p, role:org-admin, repositories, get, *, allow" + - "p, role:org-admin, repositories, create, *, allow" + - "p, role:org-admin, repositories, update, *, allow" + - "p, role:org-admin, repositories, delete, *, allow" # Note: the IDs for AWS Identity Center groups will change if you alter/replace them: argocd_rbac_groups: - group: deadbeef-dead-beef-dead-beefdeadbeef diff --git a/modules/eks/argocd/resources/argocd-values.yaml.tpl b/modules/eks/argocd/resources/argocd-values.yaml.tpl index c75ddeabd..16ae99c63 100644 --- a/modules/eks/argocd/resources/argocd-values.yaml.tpl +++ b/modules/eks/argocd/resources/argocd-values.yaml.tpl @@ -115,6 +115,7 @@ server: return hs rbacConfig: + policy.default: ${rbac_default_policy} policy.csv: | %{ for policy in rbac_policies ~} ${policy} @@ -130,7 +131,7 @@ server: scopes: '${saml_rbac_scopes}' %{ endif ~} - policy.default: role:none + policy.default: role:readonly repoServer: replicas: 2 From 7f2f4382ca318360fbc9a60630609d14075a5bb7 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:13:22 -0500 Subject: [PATCH 219/501] Updated ssm parameter versions (#812) Co-authored-by: cloudpossebot --- modules/argocd-repo/README.md | 2 +- modules/argocd-repo/ssm.tf | 2 +- modules/aurora-mysql/README.md | 2 +- modules/aurora-mysql/ssm.tf | 2 +- modules/aurora-postgres/README.md | 2 +- modules/aurora-postgres/ssm.tf | 2 +- modules/datadog-configuration/README.md | 2 +- modules/datadog-configuration/ssm.tf | 2 +- modules/datadog-integration/README.md | 2 +- modules/datadog-integration/main.tf | 2 +- modules/eks/platform/README.md | 2 +- modules/eks/platform/main.tf | 2 +- modules/opsgenie-team/modules/integration/README.md | 2 +- modules/opsgenie-team/modules/integration/main.tf | 2 +- modules/ses/README.md | 2 +- modules/ses/main.tf | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index 19e09fd8e..890177d53 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -99,7 +99,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | Name | Source | Version | |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/argocd-repo/ssm.tf b/modules/argocd-repo/ssm.tf index d85e85cf2..2bfa912af 100644 --- a/modules/argocd-repo/ssm.tf +++ b/modules/argocd-repo/ssm.tf @@ -10,7 +10,7 @@ data "aws_ssm_parameter" "github_api_key" { module "store_write" { source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" + version = "0.11.0" parameter_write = [for k, v in local.environments : { diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index 6ba286211..e4f5d17d0 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -175,7 +175,7 @@ Reploying the component should show no changes. For example, `atmos terraform ap | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | -| [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [primary\_cluster](#module\_primary\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | diff --git a/modules/aurora-mysql/ssm.tf b/modules/aurora-mysql/ssm.tf index 8a6e9220e..40f04b20a 100644 --- a/modules/aurora-mysql/ssm.tf +++ b/modules/aurora-mysql/ssm.tf @@ -71,7 +71,7 @@ data "aws_ssm_parameter" "password" { module "parameter_store_write" { source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" + version = "0.11.0" # kms_arn will only be used for SecureString parameters kms_arn = module.kms_key_rds.key_arn diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index c2bb93aac..6f83bca04 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -107,7 +107,7 @@ components: | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | -| [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | diff --git a/modules/aurora-postgres/ssm.tf b/modules/aurora-postgres/ssm.tf index 16d10a637..322233e36 100644 --- a/modules/aurora-postgres/ssm.tf +++ b/modules/aurora-postgres/ssm.tf @@ -75,7 +75,7 @@ data "aws_ssm_parameter" "password" { module "parameter_store_write" { source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" + version = "0.11.0" # kms_arn will only be used for SecureString parameters kms_arn = module.kms_key_rds.key_arn diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index f97f16511..0da762748 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -77,7 +77,7 @@ provider "datadog" { |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [iam\_roles\_datadog\_secrets](#module\_iam\_roles\_datadog\_secrets) | ../account-map/modules/iam-roles | n/a | -| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/datadog-configuration/ssm.tf b/modules/datadog-configuration/ssm.tf index 28e3ccc72..f4ada17b6 100644 --- a/modules/datadog-configuration/ssm.tf +++ b/modules/datadog-configuration/ssm.tf @@ -27,7 +27,7 @@ data "aws_ssm_parameter" "datadog_app_key" { module "store_write" { source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" + version = "0.11.0" parameter_write = [ { diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index 4b8393a0b..fec94e7fc 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -43,7 +43,7 @@ components: | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [datadog\_integration](#module\_datadog\_integration) | cloudposse/datadog-integration/aws | 1.0.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.9.1 | +| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/datadog-integration/main.tf b/modules/datadog-integration/main.tf index 72de92237..7bd7054a7 100644 --- a/modules/datadog-integration/main.tf +++ b/modules/datadog-integration/main.tf @@ -40,7 +40,7 @@ locals { module "store_write" { count = local.enabled ? 1 : 0 source = "cloudposse/ssm-parameter-store/aws" - version = "0.9.1" + version = "0.11.0" parameter_write = [ { name = "/datadog/datadog_external_id" diff --git a/modules/eks/platform/README.md b/modules/eks/platform/README.md index 9ed341e03..3a9fdad37 100644 --- a/modules/eks/platform/README.md +++ b/modules/eks/platform/README.md @@ -69,7 +69,7 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [remote](#module\_remote) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/platform/main.tf b/modules/eks/platform/main.tf index e9435de98..962dee498 100644 --- a/modules/eks/platform/main.tf +++ b/modules/eks/platform/main.tf @@ -9,7 +9,7 @@ locals { module "store_write" { source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" + version = "0.11.0" parameter_write = concat( [for k, v in var.references : diff --git a/modules/opsgenie-team/modules/integration/README.md b/modules/opsgenie-team/modules/integration/README.md index 227810bbe..21faa8bbe 100644 --- a/modules/opsgenie-team/modules/integration/README.md +++ b/modules/opsgenie-team/modules/integration/README.md @@ -22,7 +22,7 @@ This module creates an OpsGenie integrations for a team. By Default, it creates |------|--------|---------| | [api\_integration](#module\_api\_integration) | cloudposse/incident-management/opsgenie//modules/api_integration | 0.16.0 | | [integration\_name](#module\_integration\_name) | cloudposse/label/null | 0.25.0 | -| [ssm\_parameter\_store](#module\_ssm\_parameter\_store) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [ssm\_parameter\_store](#module\_ssm\_parameter\_store) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/opsgenie-team/modules/integration/main.tf b/modules/opsgenie-team/modules/integration/main.tf index bf3b65466..d45635c8c 100644 --- a/modules/opsgenie-team/modules/integration/main.tf +++ b/modules/opsgenie-team/modules/integration/main.tf @@ -225,7 +225,7 @@ resource "opsgenie_integration_action" "datadog" { # Or they can be used programmatically, if their respective Terraform provider supports it. module "ssm_parameter_store" { source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" + version = "0.11.0" # KMS key is only applied to SecureString params # https://github.com/cloudposse/terraform-aws-ssm-parameter-store/blob/master/main.tf#L17 diff --git a/modules/ses/README.md b/modules/ses/README.md index 3ae8a5afe..8e55ac222 100644 --- a/modules/ses/README.md +++ b/modules/ses/README.md @@ -49,7 +49,7 @@ components: | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_ses](#module\_kms\_key\_ses) | cloudposse/kms-key/aws | 0.12.1 | | [ses](#module\_ses) | cloudposse/ses/aws | 0.22.3 | -| [ssm\_parameter\_store](#module\_ssm\_parameter\_store) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [ssm\_parameter\_store](#module\_ssm\_parameter\_store) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/ses/main.tf b/modules/ses/main.tf index c2b66836d..fb1aa4fa2 100644 --- a/modules/ses/main.tf +++ b/modules/ses/main.tf @@ -37,7 +37,7 @@ module "kms_key_ses" { module "ssm_parameter_store" { source = "cloudposse/ssm-parameter-store/aws" - version = "0.10.0" + version = "0.11.0" count = local.enabled ? 1 : 0 From e2d3ca22afb066c36479340b4754177a8ce82552 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 14 Aug 2023 01:06:22 -0700 Subject: [PATCH 220/501] Fix eks/cluster default values (#813) --- modules/eks/cluster/README.md | 2 +- modules/eks/cluster/variables.tf | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 2aaea912a..ce875e97e 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -526,7 +526,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [map\_additional\_worker\_roles](#input\_map\_additional\_worker\_roles) | AWS IAM Role ARNs of worker nodes to add to `aws-auth` ConfigMap | `list(string)` | `[]` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = optional(string, null)
ami_type = optional(string, null)
attributes = optional(list(string), null)
availability_zones = optional(list(string)) # set to null to use var.availability_zones
cluster_autoscaler_enabled = optional(bool, null)
create_before_destroy = optional(bool, null)
desired_group_size = optional(number, null)
instance_types = optional(list(string), null)
kubernetes_labels = optional(map(string), null)
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), null)
kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version
max_group_size = optional(number, null)
min_group_size = optional(number, null)
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 20) # disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)
})
|
{
"block_device_map": {
"/dev/xvda": {
"ebs": {
"encrypted": true,
"volume_size": 20,
"volume_type": "gp2"
}
}
},
"desired_group_size": 1,
"instance_types": [
"t3.medium"
],
"kubernetes_version": null,
"max_group_size": 100
}
| no | +| [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = optional(string, null)
ami_type = optional(string, null)
attributes = optional(list(string), null)
availability_zones = optional(list(string)) # set to null to use var.availability_zones
cluster_autoscaler_enabled = optional(bool, null)
create_before_destroy = optional(bool, null)
desired_group_size = optional(number, null)
instance_types = optional(list(string), null)
kubernetes_labels = optional(map(string), {})
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), [])
kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version
max_group_size = optional(number, null)
min_group_size = optional(number, null)
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 50) # disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)
})
|
{
"block_device_map": {
"/dev/xvda": {
"ebs": {
"encrypted": true,
"volume_size": 20,
"volume_type": "gp2"
}
}
},
"desired_group_size": 1,
"instance_types": [
"t3.medium"
],
"kubernetes_version": null,
"max_group_size": 100
}
| no | | [node\_groups](#input\_node\_groups) | List of objects defining a node group for the cluster |
map(object({
# EKS AMI version to use, e.g. "1.16.13-20200821" (no "v").
ami_release_version = optional(string, null)
# Type of Amazon Machine Image (AMI) associated with the EKS Node Group
ami_type = optional(string, null)
# Additional attributes (e.g. `1`) for the node group
attributes = optional(list(string), null)
# will create 1 auto scaling group in each specified availability zone
# or all AZs with subnets if none are specified anywhere
availability_zones = optional(list(string), null)
# Whether to enable Node Group to scale its AutoScaling Group
cluster_autoscaler_enabled = optional(bool, null)
# True to create new node_groups before deleting old ones, avoiding a temporary outage
create_before_destroy = optional(bool, null)
# Desired number of worker nodes when initially provisioned
desired_group_size = optional(number, null)
# Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided.
instance_types = optional(list(string), null)
# Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed
kubernetes_labels = optional(map(string), null)
# List of objects describing Kubernetes taints.
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), null)
# Desired Kubernetes master version. If you do not specify a value, the latest available version is used
kubernetes_version = optional(string, null)
# The maximum size of the AutoScaling Group
max_group_size = optional(number, null)
# The minimum size of the AutoScaling Group
min_group_size = optional(number, null)
# List of auto-launched resource types to tag
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional.
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 20) # Disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED:
# Enable disk encryption for the created launch template (if we aren't provided with an existing launch template)
# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided.
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)

}))
| `{}` | no | | [oidc\_provider\_enabled](#input\_oidc\_provider\_enabled) | Create an IAM OIDC identity provider for the cluster, then you can create IAM roles to associate with a service account in the cluster, instead of using kiam or kube2iam. For more information, see https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html | `bool` | `true` | no | | [public\_access\_cidrs](#input\_public\_access\_cidrs) | Indicates which CIDR blocks can access the Amazon EKS public API server endpoint when enabled. EKS defaults this to a list with 0.0.0.0/0. | `list(string)` |
[
"0.0.0.0/0"
]
| no | diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index ffa28d1f7..f5609d8ec 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -304,12 +304,12 @@ variable "node_group_defaults" { create_before_destroy = optional(bool, null) desired_group_size = optional(number, null) instance_types = optional(list(string), null) - kubernetes_labels = optional(map(string), null) + kubernetes_labels = optional(map(string), {}) kubernetes_taints = optional(list(object({ key = string value = string effect = string - })), null) + })), []) kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version max_group_size = optional(number, null) min_group_size = optional(number, null) @@ -329,7 +329,7 @@ variable "node_group_defaults" { kms_key_id = optional(string, null) snapshot_id = optional(string, null) throughput = optional(number, null) # for gp3, MiB/s, up to 1000 - volume_size = optional(number, 20) # disk size in GB + volume_size = optional(number, 50) # disk size in GB volume_type = optional(string, "gp3") # Catch common camel case typos. These have no effect, they just generate better errors. From 3b962d1f34421efea1498e30bca8b218c0762dc4 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 14 Aug 2023 02:10:19 -0700 Subject: [PATCH 221/501] Refactor Changelog (#811) --- modules/eks/cluster/CHANGELOG.md | 7 +++++-- docs/upgrade-guide.md => modules/tgw/CHANGELOG.md | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) rename docs/upgrade-guide.md => modules/tgw/CHANGELOG.md (96%) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 4ba0638c7..dfc1f40e7 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,4 +1,6 @@ -## Components PR [#795](https://github.com/cloudposse/terraform-aws-components/pull/723) +## Upgrading to `v1.270.0` + +Components PR [#795](https://github.com/cloudposse/terraform-aws-components/pull/795) ### Removed `identity` roles from cluster RBAC (`aws-auth` ConfigMap) @@ -86,8 +88,9 @@ Because this is disruptive to existing clusters, this is not enabled by default. To enable it, set the `legacy_do_not_create_karpenter_instance_profile` input to `false`, and also set the `eks/karpenter` input `legacy_create_karpenter_instance_profile` to `false`. -## Components PR [#723](https://github.com/cloudposse/terraform-aws-components/pull/723) +## Upgrading to `v1.250.0` +Components PR [#723](https://github.com/cloudposse/terraform-aws-components/pull/723) ### Improved support for EKS Add-Ons diff --git a/docs/upgrade-guide.md b/modules/tgw/CHANGELOG.md similarity index 96% rename from docs/upgrade-guide.md rename to modules/tgw/CHANGELOG.md index e89e40ba7..41575194d 100644 --- a/docs/upgrade-guide.md +++ b/modules/tgw/CHANGELOG.md @@ -1,12 +1,14 @@ ## Upgrading to `v1.276.0` +Components PR [#804](https://github.com/cloudposse/terraform-aws-components/pull/804) + ### Affected Components - `tgw/hub` - `tgw/spoke` - `tgw/cross-region-hub-connector` -### Steps +### Summary This change to the Transit Gateway components, [PR #804](https://github.com/cloudposse/terraform-aws-components/pull/804), added support for cross-region connections. @@ -51,5 +53,4 @@ workflows: - command: terraform deploy tgw/spoke -s plat-use1-dev - command: terraform deploy tgw/spoke -s plat-use1-staging - command: terraform deploy tgw/spoke -s plat-use1-prod - ``` From 306389699eb5e27aeacad9f659c2f7c0c3a136d1 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Mon, 14 Aug 2023 19:04:45 +0300 Subject: [PATCH 222/501] Upstream the latest ecs-service module (#810) Co-authored-by: cloudpossebot Co-authored-by: Dan Miller --- modules/ecs-service/README.md | 60 ++++-- modules/ecs-service/datadog-agent.tf | 17 +- .../ecs-service/github-actions-iam-policy.tf | 72 ++++++- modules/ecs-service/main.tf | 200 +++++++++++------- modules/ecs-service/remote-state.tf | 148 ++++++++++--- modules/ecs-service/systems-manager.tf | 70 ++++++ modules/ecs-service/variables.tf | 199 ++++++++++++++++- modules/ecs-service/versions.tf | 4 + 8 files changed, 620 insertions(+), 150 deletions(-) create mode 100644 modules/ecs-service/systems-manager.tf diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 999dcb214..8884e4c40 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -151,6 +151,7 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.66.1 | +| [jq](#requirement\_jq) | >=0.2.0 | | [template](#requirement\_template) | >= 2.2 | ## Providers @@ -158,32 +159,38 @@ components: | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.66.1 | +| [jq](#provider\_jq) | >=0.2.0 | | [template](#provider\_template) | >= 2.2 | ## Modules | Name | Source | Version | |------|--------|---------| -| [alb\_ecs\_label](#module\_alb\_ecs\_label) | cloudposse/label/null | 0.25.0 | -| [alb\_ingress](#module\_alb\_ingress) | cloudposse/alb-ingress/aws | 0.24.3 | -| [container\_definition](#module\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | -| [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | +| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [alb\_ingress](#module\_alb\_ingress) | cloudposse/alb-ingress/aws | 0.28.0 | +| [container\_definition](#module\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.60.0 | +| [datadog\_configuration](#module\_datadog\_configuration) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [datadog\_container\_definition](#module\_datadog\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | | [datadog\_fluent\_bit\_container\_definition](#module\_datadog\_fluent\_bit\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | | [datadog\_sidecar\_logs](#module\_datadog\_sidecar\_logs) | cloudposse/cloudwatch-logs/aws | 0.6.6 | -| [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.66.4 | +| [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.71.0 | | [ecs\_cloudwatch\_autoscaling](#module\_ecs\_cloudwatch\_autoscaling) | cloudposse/ecs-cloudwatch-autoscaling/aws | 0.7.3 | | [ecs\_cloudwatch\_sns\_alarms](#module\_ecs\_cloudwatch\_sns\_alarms) | cloudposse/ecs-cloudwatch-sns-alarms/aws | 0.12.3 | -| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | | [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | +| [iam\_role](#module\_iam\_role) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [logs](#module\_logs) | cloudposse/cloudwatch-logs/aws | 0.6.6 | -| [rds](#module\_rds) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [logs](#module\_logs) | cloudposse/cloudwatch-logs/aws | 0.6.8 | +| [nlb](#module\_nlb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [rds](#module\_rds) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [roles\_to\_principals](#module\_roles\_to\_principals) | ../account-map/modules/roles-to-principals | n/a | +| [s3](#module\_s3) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [security\_group](#module\_security\_group) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [service\_domain](#module\_service\_domain) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vanity\_alias](#module\_vanity\_alias) | cloudposse/route53-alias/aws | 0.13.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources @@ -192,6 +199,8 @@ components: | [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_kinesis_stream.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_stream) | resource | +| [aws_ssm_parameter.full_urls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.github_actions_iam_ecspresso_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.github_actions_iam_platform_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -199,7 +208,10 @@ components: | [aws_kms_alias.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | | [aws_route53_zone.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | | [aws_route53_zone.selected_vanity](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | +| [aws_s3_object.task_definition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_object) | data source | +| [aws_s3_objects.mirror](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_objects) | data source | | [aws_ssm_parameters_by_path.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | +| [jq_query.service_domain_query](https://registry.terraform.io/providers/massdriver-cloud/jq/latest/docs/data-sources/query) | data source | | [template_file.envs](https://registry.terraform.io/providers/cloudposse/template/latest/docs/data-sources/file) | data source | ## Inputs @@ -208,12 +220,13 @@ components: |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [alb\_configuration](#input\_alb\_configuration) | The configuration to use for the ALB, specifying which cluster alb configuration to use | `string` | `"default"` | no | +| [alb\_name](#input\_alb\_name) | The name of the ALB this service should attach to | `string` | `null` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [autoscaling\_dimension](#input\_autoscaling\_dimension) | The dimension to use to decide to autoscale | `string` | `"cpu"` | no | | [autoscaling\_enabled](#input\_autoscaling\_enabled) | Should this service autoscale using SNS alarams | `bool` | `true` | no | | [chamber\_service](#input\_chamber\_service) | SSM parameter service name for use with chamber. This is used in chamber\_format where /$chamber\_service/$name/$container\_name/$parameter would be the default. | `string` | `"ecs-service"` | no | | [cluster\_attributes](#input\_cluster\_attributes) | The attributes of the cluster name e.g. if the full name is `namespace-tenant-environment-dev-ecs-b2b` then the `cluster_name` is `ecs` and this value should be `b2b`. | `list(string)` | `[]` | no | -| [containers](#input\_containers) | Feed inputs into container definition module | `any` | `{}` | no | +| [containers](#input\_containers) | Feed inputs into container definition module |
map(object({
name = string
ecr_image = optional(string)
image = optional(string)
memory = optional(number)
memory_reservation = optional(number)
cpu = optional(number)
essential = optional(bool, true)
readonly_root_filesystem = optional(bool, null)
privileged = optional(bool, null)
container_depends_on = optional(list(object({
containerName = string
condition = string # START, COMPLETE, SUCCESS, HEALTHY
})), null)

port_mappings = optional(list(object({
containerPort = number
hostPort = number
protocol = string
})), [])
command = optional(list(string), null)
entrypoint = optional(list(string), null)
healthcheck = optional(object({
command = list(string)
interval = number
retries = number
startPeriod = number
timeout = number
}), null)
ulimits = optional(list(object({
name = string
softLimit = number
hardLimit = number
})), null)
log_configuration = optional(object({
logDriver = string
options = optional(map(string), {})
}))
docker_labels = optional(map(string), null)
map_environment = optional(map(string), {})
map_secrets = optional(map(string), {})
volumes_from = optional(list(object({
sourceContainer = string
readOnly = bool
})), null)
mount_points = optional(list(object({
sourceVolume = string
containerPath = string
readOnly = bool
})), [])
}))
| `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [cpu\_utilization\_high\_alarm\_actions](#input\_cpu\_utilization\_high\_alarm\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization High Alarm action | `list(string)` | `[]` | no | | [cpu\_utilization\_high\_evaluation\_periods](#input\_cpu\_utilization\_high\_evaluation\_periods) | Number of periods to evaluate for the alarm | `number` | `1` | no | @@ -232,9 +245,9 @@ components: | [datadog\_sidecar\_containers\_logs\_enabled](#input\_datadog\_sidecar\_containers\_logs\_enabled) | Enable the Datadog Agent Sidecar to send logs to aws cloudwatch group, requires `datadog_agent_sidecar_enabled` to be true | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [domain\_name](#input\_domain\_name) | The domain name to use as the host header suffix | `string` | `""` | no | | [ecr\_region](#input\_ecr\_region) | The region to use for the fully qualified ECR image URL. Defaults to the current region. | `string` | `""` | no | | [ecr\_stage\_name](#input\_ecr\_stage\_name) | The ecr stage (account) name to use for the fully qualified ECR image URL. | `string` | `"auto"` | no | +| [ecs\_cluster\_name](#input\_ecs\_cluster\_name) | The name of the ECS Cluster this belongs to | `any` | `"ecs"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [github\_actions\_allowed\_repos](#input\_github\_actions\_allowed\_repos) | A list of the GitHub repositories that are allowed to assume this role from GitHub Actions. For example,
["cloudposse/infra-live"]. Can contain "*" as wildcard.
If org part of repo name is omitted, "cloudposse" will be assumed. | `list(string)` | `[]` | no | @@ -242,12 +255,19 @@ components: | [github\_actions\_iam\_role\_attributes](#input\_github\_actions\_iam\_role\_attributes) | Additional attributes to add to the role name | `list(string)` | `[]` | no | | [github\_actions\_iam\_role\_enabled](#input\_github\_actions\_iam\_role\_enabled) | Flag to toggle creation of an IAM Role that GitHub Actions can assume to access AWS resources | `bool` | `false` | no | | [github\_oidc\_trusted\_role\_arns](#input\_github\_oidc\_trusted\_role\_arns) | A list of IAM Role ARNs allowed to assume this cluster's GitHub OIDC role | `list(string)` | `[]` | no | +| [health\_check\_healthy\_threshold](#input\_health\_check\_healthy\_threshold) | The number of consecutive health checks successes required before healthy | `number` | `2` | no | +| [health\_check\_interval](#input\_health\_check\_interval) | The duration in seconds in between health checks | `number` | `15` | no | +| [health\_check\_matcher](#input\_health\_check\_matcher) | The HTTP response codes to indicate a healthy check | `string` | `"200-404"` | no | | [health\_check\_path](#input\_health\_check\_path) | The destination for the health check request | `string` | `"/health"` | no | | [health\_check\_port](#input\_health\_check\_port) | The port to use to connect with the target. Valid values are either ports 1-65536, or `traffic-port`. Defaults to `traffic-port` | `string` | `"traffic-port"` | no | +| [health\_check\_timeout](#input\_health\_check\_timeout) | The amount of time to wait in seconds before failing a health check request | `number` | `10` | no | +| [health\_check\_unhealthy\_threshold](#input\_health\_check\_unhealthy\_threshold) | The number of consecutive health check failures required before unhealthy | `number` | `2` | no | +| [http\_protocol](#input\_http\_protocol) | Which http protocol to use in outputs and SSM url params. This value is ignored if a load balancer is not used. If it is `null`, the redirect value from the ALB determines the protocol. | `string` | `null` | no | | [iam\_policy\_enabled](#input\_iam\_policy\_enabled) | If set to true will create IAM policy in AWS | `bool` | `false` | no | | [iam\_policy\_statements](#input\_iam\_policy\_statements) | Map of IAM policy statements to use in the policy. This can be used with or instead of the `var.iam_source_json_url`. | `any` | `{}` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [kinesis\_enabled](#input\_kinesis\_enabled) | Enable Kinesis | `bool` | `false` | no | +| [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | | [kms\_key\_alias](#input\_kms\_key\_alias) | ID of KMS key | `string` | `"default"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | @@ -267,25 +287,37 @@ components: | [memory\_utilization\_low\_threshold](#input\_memory\_utilization\_low\_threshold) | The minimum percentage of Memory utilization average | `number` | `20` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [nlb\_name](#input\_nlb\_name) | The name of the NLB this service should attach to | `string` | `null` | no | +| [rds\_name](#input\_rds\_name) | The name of the RDS database this service should allow access to | `any` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [retention\_period](#input\_retention\_period) | Length of time data records are accessible after they are added to the stream | `number` | `48` | no | +| [s3\_mirror\_name](#input\_s3\_mirror\_name) | The name of the S3 mirror component | `string` | `null` | no | | [shard\_count](#input\_shard\_count) | Number of shards that the stream will use | `number` | `1` | no | | [shard\_level\_metrics](#input\_shard\_level\_metrics) | List of shard-level CloudWatch metrics which can be enabled for the stream | `list(string)` |
[
"IncomingBytes",
"IncomingRecords",
"IteratorAgeMilliseconds",
"OutgoingBytes",
"OutgoingRecords",
"ReadProvisionedThroughputExceeded",
"WriteProvisionedThroughputExceeded"
]
| no | +| [ssm\_enabled](#input\_ssm\_enabled) | If `true` create SSM keys for the database user and password. | `bool` | `false` | no | +| [ssm\_key\_format](#input\_ssm\_key\_format) | SSM path format. The values will will be used in the following order: `var.ssm_key_prefix`, `var.name`, `var.ssm_key_*` | `string` | `"/%v/%v/%v"` | no | +| [ssm\_key\_prefix](#input\_ssm\_key\_prefix) | SSM path prefix. Omit the leading forward slash `/`. | `string` | `"ecs-service"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [stickiness\_cookie\_duration](#input\_stickiness\_cookie\_duration) | The time period, in seconds, during which requests from a client should be routed to the same target. After this time period expires, the load balancer-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds). The default value is 1 day (86400 seconds) | `number` | `86400` | no | | [stickiness\_enabled](#input\_stickiness\_enabled) | Boolean to enable / disable `stickiness`. Default is `true` | `bool` | `true` | no | | [stickiness\_type](#input\_stickiness\_type) | The type of sticky sessions. The only current possible value is `lb_cookie` | `string` | `"lb_cookie"` | no | | [stream\_mode](#input\_stream\_mode) | Stream mode details for the Kinesis stream | `string` | `"PROVISIONED"` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module | `any` | `{}` | no | +| [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module |
object({
task_cpu = optional(number)
task_memory = optional(number)
task_role_arn = optional(string, "")
pid_mode = optional(string, null)
ipc_mode = optional(string, null)
network_mode = optional(string)
propagate_tags = optional(string)
assign_public_ip = optional(bool, false)
use_alb_security_groups = optional(bool, true)
launch_type = optional(string, "FARGATE")
scheduling_strategy = optional(string, "REPLICA")
capacity_provider_strategies = optional(list(object({
capacity_provider = string
weight = number
base = number
})), [])

deployment_minimum_healthy_percent = optional(number, null)
deployment_maximum_percent = optional(number, null)
desired_count = optional(number, 0)
min_capacity = optional(number, 1)
max_capacity = optional(number, 2)
wait_for_steady_state = optional(bool, true)
circuit_breaker_deployment_enabled = optional(bool, true)
circuit_breaker_rollback_enabled = optional(bool, true)

ecs_service_enabled = optional(bool, true)
bind_mount_volumes = optional(list(object({
name = string
host_path = string
})), [])
})
| `{}` | no | | [task\_enabled](#input\_task\_enabled) | Whether or not to use the ECS task module | `bool` | `true` | no | +| [task\_iam\_role\_component](#input\_task\_iam\_role\_component) | A component that outputs an iam\_role module as 'role' for adding to the service as a whole. | `string` | `null` | no | | [task\_policy\_arns](#input\_task\_policy\_arns) | The IAM policy ARNs to attach to the ECS task IAM role | `list(string)` |
[
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
"arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess"
]
| no | +| [task\_security\_group\_component](#input\_task\_security\_group\_component) | A component that outputs security\_group\_id for adding to the service as a whole. | `string` | `null` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [unauthenticated\_paths](#input\_unauthenticated\_paths) | Unauthenticated path pattern to match | `list(string)` | `[]` | no | +| [unauthenticated\_priority](#input\_unauthenticated\_priority) | The priority for the rules without authentication, between 1 and 50000 (1 being highest priority). Must be different from `authenticated_priority` since a listener can't have multiple rules with the same priority | `string` | `0` | no | | [use\_lb](#input\_use\_lb) | Whether use load balancer for the service | `bool` | `false` | no | | [use\_rds\_client\_sg](#input\_use\_rds\_client\_sg) | Use the RDS client security group | `bool` | `false` | no | | [vanity\_alias](#input\_vanity\_alias) | The vanity aliases to use for the public LB. | `list(string)` | `[]` | no | -| [vanity\_domain\_enabled](#input\_vanity\_domain\_enabled) | Whether to use the vanity domain alias for the service | `bool` | `false` | no | +| [vanity\_domain](#input\_vanity\_domain) | Whether to use the vanity domain alias for the service | `string` | `null` | no | +| [zone\_component](#input\_zone\_component) | The component name to look up service domain remote-state on | `string` | `"dns-delegated"` | no | +| [zone\_component\_output](#input\_zone\_component\_output) | A json query to use to get the zone domain from the remote state. See | `string` | `".default_domain_name"` | no | ## Outputs @@ -301,6 +333,8 @@ components: | [lb\_sg\_id](#output\_lb\_sg\_id) | Selected LB SG ID | | [logs](#output\_logs) | Output of cloudwatch logs module | | [service\_image](#output\_service\_image) | The image of the service container | +| [ssm\_key\_prefix](#output\_ssm\_key\_prefix) | SSM prefix | +| [ssm\_parameters](#output\_ssm\_parameters) | SSM parameters for the ECS Service | | [subnet\_ids](#output\_subnet\_ids) | Selected subnet IDs | | [vpc\_id](#output\_vpc\_id) | Selected VPC ID | | [vpc\_sg\_id](#output\_vpc\_sg\_id) | Selected VPC SG ID | diff --git a/modules/ecs-service/datadog-agent.tf b/modules/ecs-service/datadog-agent.tf index 08466f378..6b654dc22 100644 --- a/modules/ecs-service/datadog-agent.tf +++ b/modules/ecs-service/datadog-agent.tf @@ -40,8 +40,8 @@ locals { logDriver = "awsfirelens" options = var.datadog_agent_sidecar_enabled ? { Name = "datadog", - apikey = one(module.datadog_configuration[*].datadog_api_key), - Host = format("http-intake.logs.%s", one(module.datadog_configuration[*].datadog_site)) + apikey = one(module.datadog_configuration[*].outputs.datadog_api_key), + Host = format("http-intake.logs.%s", one(module.datadog_configuration[*].outputs.datadog_site)) dd_service = module.this.name, dd_tags = local.all_dd_tags, dd_source = "ecs", @@ -57,7 +57,7 @@ module "datadog_sidecar_logs" { version = "0.6.6" # if we are using datadog firelens we don't need to create a log group - count = local.enabled && var.datadog_sidecar_containers_logs_enabled ? 1 : 0 + count = local.enabled && var.datadog_agent_sidecar_enabled && var.datadog_sidecar_containers_logs_enabled ? 1 : 0 stream_names = lookup(var.logs, "stream_names", []) retention_in_days = lookup(var.logs, "retention_in_days", 90) @@ -87,8 +87,8 @@ module "datadog_container_definition" { essential = true map_environment = { "ECS_FARGATE" = var.task.launch_type == "FARGATE" ? true : false - "DD_API_KEY" = one(module.datadog_configuration[*].datadog_api_key) - "DD_SITE" = one(module.datadog_configuration[*].datadog_site) + "DD_API_KEY" = one(module.datadog_configuration[*].outputs.datadog_api_key) + "DD_SITE" = one(module.datadog_configuration[*].outputs.datadog_site) "DD_ENV" = module.this.stage "DD_LOGS_ENABLED" = true "DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL" = true @@ -154,10 +154,3 @@ module "datadog_fluent_bit_container_definition" { } } : null } - -module "datadog_configuration" { - count = var.datadog_agent_sidecar_enabled ? 1 : 0 - source = "../datadog-configuration/modules/datadog_keys" - enabled = true - context = module.this.context -} diff --git a/modules/ecs-service/github-actions-iam-policy.tf b/modules/ecs-service/github-actions-iam-policy.tf index d2158b0db..99a3d3a98 100644 --- a/modules/ecs-service/github-actions-iam-policy.tf +++ b/modules/ecs-service/github-actions-iam-policy.tf @@ -17,7 +17,7 @@ locals { data "aws_iam_policy_document" "github_actions_iam_policy" { source_policy_documents = compact([ data.aws_iam_policy_document.github_actions_iam_platform_policy.json, - join("", data.aws_iam_policy_document.github_actions_iam_ecspresso_policy.*.json) + join("", data.aws_iam_policy_document.github_actions_iam_ecspresso_policy[*]["json"]) ]) } @@ -36,6 +36,7 @@ data "aws_iam_policy_document" "github_actions_iam_platform_policy" { } } + # Allow chamber to read secrets statement { sid = "AllowKMSAccess" effect = "Allow" @@ -56,9 +57,9 @@ data "aws_iam_policy_document" "github_actions_iam_platform_policy" { "ssm:GetParameter", "ssm:PutParameter" ] - resources = [ + resources = concat([ "arn:aws:ssm:*:*:parameter${format("/%s/%s/*", var.chamber_service, var.name)}" - ] + ], formatlist("arn:aws:ssm:*:*:parameter%s", keys(local.url_params))) } statement { @@ -74,6 +75,13 @@ data "aws_iam_policy_document" "github_actions_iam_platform_policy" { } } +data "aws_caller_identity" "current" {} + +locals { + aws_partition = module.iam_roles.aws_partition + account_id = data.aws_caller_identity.current.account_id +} + data "aws_iam_policy_document" "github_actions_iam_ecspresso_policy" { count = var.github_actions_ecspresso_enabled ? 1 : 0 @@ -81,17 +89,31 @@ data "aws_iam_policy_document" "github_actions_iam_ecspresso_policy" { effect = "Allow" actions = [ "ecs:DescribeServices", - "ecs:UpdateService" + "ecs:UpdateService", + "ecs:ListTagsForResource" + ] + resources = [ + join("", module.ecs_alb_service_task[*]["service_arn"]) + ] + } + + statement { + effect = "Allow" + actions = [ + "ecs:RunTask", ] resources = [ - join("", module.ecs_alb_service_task.*.service_arn) + format("arn:%s:ecs:%s:%s:task-definition/%s:*", local.aws_partition, var.region, local.account_id, join("", module.ecs_alb_service_task.*.task_definition_family)), ] } statement { effect = "Allow" actions = [ - "ecs:RegisterTaskDefinition" + "ecs:RegisterTaskDefinition", + "ecs:DescribeTaskDefinition", + "ecs:DescribeTasks", + "application-autoscaling:DescribeScalableTargets" ] resources = [ "*" @@ -99,25 +121,53 @@ data "aws_iam_policy_document" "github_actions_iam_ecspresso_policy" { } statement { + sid = "logs" effect = "Allow" actions = [ - "iam:PassRole" + "logs:Describe*", + "logs:Get*", + "logs:List*", + "logs:StartQuery", + "logs:StopQuery", + "logs:TestMetricFilter", + "logs:FilterLogEvents", + "oam:ListSinks" ] resources = [ - join("", module.ecs_alb_service_task.*.task_exec_role_arn), - join("", module.ecs_alb_service_task.*.task_role_arn), + "*" ] } statement { effect = "Allow" actions = [ - "application-autoscaling:DescribeScalableTargets" + "iam:PassRole" ] resources = [ - "*", + join("", module.ecs_alb_service_task[*]["task_exec_role_arn"]), + join("", module.ecs_alb_service_task[*]["task_role_arn"]), ] } + dynamic "statement" { + for_each = local.s3_mirroring_enabled ? ["enabled"] : [] + content { + effect = "Allow" + actions = ["s3:ListBucket"] + resources = ["*"] + } + } + dynamic "statement" { + for_each = local.s3_mirroring_enabled ? ["enabled"] : [] + content { + effect = "Allow" + actions = [ + "s3:PutObject" + ] + resources = [ + format("%s/%s/%s/*", lookup(module.s3[0].outputs, "bucket_arn", null), module.ecs_cluster.outputs.cluster_name, module.this.id) + ] + } + } } diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index ab70382d2..920d84657 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -2,11 +2,13 @@ locals { enabled = module.this.enabled + s3_mirroring_enabled = local.enabled && try(length(var.s3_mirror_name) > 0, false) + service_container = lookup(var.containers, "service") # Get the first containerPort in var.container["service"]["port_mappings"] - container_port = lookup(local.service_container, "port_mappings")[0].containerPort + container_port = try(lookup(local.service_container, "port_mappings")[0].containerPort, null) - assign_public_ip = lookup(var.task, "assign_public_ip", false) + assign_public_ip = lookup(local.task, "assign_public_ip", false) container_definition = concat([ for container in module.container_definition : @@ -21,14 +23,43 @@ locals { ) kinesis_kms_id = try(one(data.aws_kms_alias.selected[*].id), null) + + use_alb_security_group = local.is_alb ? lookup(local.task, "use_alb_security_group", true) : false + + task_definition_s3_key = format("%s/%s/task-definition.json", module.ecs_cluster.outputs.cluster_name, module.this.id) + task_definition_use_s3 = local.enabled && local.s3_mirroring_enabled && contains(flatten(data.aws_s3_objects.mirror[*].keys), local.task_definition_s3_key) + task_definition_s3_objects = flatten(data.aws_s3_objects.mirror[*].keys) + + task_definition_s3 = try(jsondecode(data.aws_s3_object.task_definition[0].body), {}) + + task_s3 = local.task_definition_use_s3 ? { + launch_type = try(local.task_definition_s3.requiresCompatibilities[0], null) + network_mode = lookup(local.task_definition_s3, "networkMode", null) + task_memory = try(tonumber(lookup(local.task_definition_s3, "memory")), null) + task_cpu = try(tonumber(lookup(local.task_definition_s3, "cpu")), null) + } : {} + + task = merge(var.task, local.task_s3) +} + +data "aws_s3_objects" "mirror" { + count = local.s3_mirroring_enabled ? 1 : 0 + bucket = lookup(module.s3[0].outputs, "bucket_id", null) + prefix = format("%s/%s", module.ecs_cluster.outputs.cluster_name, module.this.id) +} + +data "aws_s3_object" "task_definition" { + count = local.task_definition_use_s3 ? 1 : 0 + bucket = lookup(module.s3[0].outputs, "bucket_id", null) + key = try(element(local.task_definition_s3_objects, index(local.task_definition_s3_objects, local.task_definition_s3_key)), null) } module "logs" { source = "cloudposse/cloudwatch-logs/aws" - version = "0.6.6" + version = "0.6.8" # if we are using datadog firelens we don't need to create a log group - count = local.enabled && !var.datadog_log_method_is_firelens ? 1 : 0 + count = local.enabled && (!var.datadog_agent_sidecar_enabled || !var.datadog_log_method_is_firelens) ? 1 : 0 stream_names = lookup(var.logs, "stream_names", []) retention_in_days = lookup(var.logs, "retention_in_days", 90) @@ -56,8 +87,16 @@ locals { name => { for key, value in zipmap(result.names, result.values) : element(reverse(split("/", key)), 0) => value } } + container_aliases = { for name, settings in var.containers : + settings["name"] => name if local.enabled + } + + container_s3 = { for item in lookup(local.task_definition_s3, "containerDefinitions", []) : + local.container_aliases[item.name] => { container_definition = item } + } + containers = { for name, settings in var.containers : - name => merge(settings, local.container_chamber[name]) + name => merge(settings, local.container_chamber[name], lookup(local.container_s3, name, {})) if local.enabled } } @@ -85,7 +124,7 @@ data "template_file" "envs" { namespace = module.this.namespace name = module.this.name full_domain = local.full_domain - vanity_domain = local.vanity_domain + vanity_domain = var.vanity_domain # `service_domain` uses whatever the current service is (public/private) service_domain = local.domain_no_service_name service_domain_public = local.public_domain_no_service_name @@ -102,11 +141,11 @@ locals { module "container_definition" { source = "cloudposse/ecs-container-definition/aws" - version = "0.58.1" + version = "0.60.0" for_each = { for k, v in local.containers : k => v if local.enabled } - container_name = lookup(each.value, "name") + container_name = each.value["name"] container_image = lookup(each.value, "ecr_image", null) != null ? format( "%s.dkr.ecr.%s.amazonaws.com/%s", @@ -115,12 +154,12 @@ module "container_definition" { lookup(each.value, "ecr_image", null) ) : lookup(each.value, "image") - container_memory = lookup(each.value, "memory", null) - container_memory_reservation = lookup(each.value, "memory_reservation", null) - container_cpu = lookup(each.value, "cpu", null) - essential = lookup(each.value, "essential", true) - readonly_root_filesystem = lookup(each.value, "readonly_root_filesystem", null) - mount_points = lookup(each.value, "mount_points", []) + container_memory = each.value["memory"] + container_memory_reservation = each.value["memory_reservation"] + container_cpu = each.value["cpu"] + essential = each.value["essential"] + readonly_root_filesystem = each.value["readonly_root_filesystem"] + mount_points = each.value["mount_points"] map_environment = lookup(each.value, "map_environment", null) != null ? merge( { for k, v in local.env_map_subst : split(",", k)[1] => v if split(",", k)[0] == each.key }, @@ -141,14 +180,15 @@ module "container_definition" { formatlist("%s/%s", format("arn:aws:ssm:%s:%s:parameter", var.region, module.roles_to_principals.full_account_map[format("%s-%s", var.tenant, var.stage)]), values(lookup(each.value, "map_secrets", null))) ) : null - port_mappings = lookup(each.value, "port_mappings", []) - command = lookup(each.value, "command", null) - entrypoint = lookup(each.value, "entrypoint", null) - healthcheck = lookup(each.value, "healthcheck", null) - ulimits = lookup(each.value, "ulimits", null) - volumes_from = lookup(each.value, "volumes_from", null) - docker_labels = lookup(each.value, "docker_labels", null) - container_depends_on = lookup(each.value, "container_depends_on", []) + port_mappings = each.value["port_mappings"] + command = each.value["command"] + entrypoint = each.value["entrypoint"] + healthcheck = each.value["healthcheck"] + ulimits = each.value["ulimits"] + volumes_from = each.value["volumes_from"] + docker_labels = each.value["docker_labels"] + container_depends_on = each.value["container_depends_on"] + privileged = each.value["privileged"] log_configuration = lookup(lookup(each.value, "log_configuration", {}), "logDriver", {}) == "awslogs" ? merge(lookup(each.value, "log_configuration", {}), { logDriver = "awslogs" @@ -168,12 +208,13 @@ module "container_definition" { } locals { - awslogs_group = var.datadog_log_method_is_firelens ? "" : join("", module.logs[*].log_group_name) + awslogs_group = var.datadog_log_method_is_firelens ? "" : join("", module.logs[*].log_group_name) + external_security_group = try(module.security_group[*].outputs.security_group_id, []) } module "ecs_alb_service_task" { source = "cloudposse/ecs-alb-service-task/aws" - version = "0.66.4" + version = "0.71.0" count = local.enabled ? 1 : 0 @@ -184,42 +225,49 @@ module "ecs_alb_service_task" { container_definition_json = jsonencode(local.container_definition) # This is set to true to allow ingress from the ALB sg - use_alb_security_group = lookup(var.task, "use_alb_security_group", true) + use_alb_security_group = local.use_alb_security_group container_port = local.container_port alb_security_group = local.lb_sg_id - security_group_ids = compact([local.vpc_sg_id, local.rds_sg_id]) + security_group_ids = compact(concat([local.vpc_sg_id, local.rds_sg_id], local.external_security_group)) + + nlb_cidr_blocks = local.is_nlb ? [module.vpc.outputs.vpc_cidr] : [] + nlb_container_port = local.is_nlb ? local.container_port : 80 + use_nlb_cidr_blocks = local.is_nlb # See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#load_balancer - ecs_load_balancers = var.use_lb ? [ + ecs_load_balancers = local.use_lb ? [ { container_name = lookup(local.service_container, "name"), container_port = local.container_port, - target_group_arn = module.alb_ingress[0].target_group_arn + target_group_arn = local.is_alb ? module.alb_ingress[0].target_group_arn : local.nlb.default_target_group_arn # not required since elb is unused but must be set to null elb_name = null }, ] : [] assign_public_ip = local.assign_public_ip - ignore_changes_task_definition = lookup(var.task, "ignore_changes_task_definition", false) - ignore_changes_desired_count = lookup(var.task, "ignore_changes_desired_count", true) - launch_type = lookup(var.task, "launch_type", "FARGATE") - network_mode = lookup(var.task, "network_mode", "awsvpc") - propagate_tags = lookup(var.task, "propagate_tags", "SERVICE") - deployment_minimum_healthy_percent = lookup(var.task, "deployment_minimum_healthy_percent", null) - deployment_maximum_percent = lookup(var.task, "deployment_maximum_percent", null) - deployment_controller_type = lookup(var.task, "deployment_controller_type", null) - desired_count = lookup(var.task, "desired_count", 0) - task_memory = lookup(var.task, "task_memory", null) - task_cpu = lookup(var.task, "task_cpu", null) - wait_for_steady_state = lookup(var.task, "wait_for_steady_state", true) - circuit_breaker_deployment_enabled = lookup(var.task, "circuit_breaker_deployment_enabled", true) - circuit_breaker_rollback_enabled = lookup(var.task, "circuit_breaker_rollback_enabled ", true) - task_policy_arns = var.iam_policy_enabled ? concat(var.task_policy_arns, formatlist(aws_iam_policy.default[0].arn)) : var.task_policy_arns - ecs_service_enabled = lookup(var.task, "ecs_service_enabled", true) - bind_mount_volumes = lookup(var.task, "bind_mount_volumes", []) - task_role_arn = lookup(var.task, "task_role_arn", []) - capacity_provider_strategies = lookup(var.task, "capacity_provider_strategies", []) + ignore_changes_task_definition = lookup(local.task, "ignore_changes_task_definition", false) + ignore_changes_desired_count = lookup(local.task, "ignore_changes_desired_count", true) + launch_type = lookup(local.task, "launch_type", "FARGATE") + scheduling_strategy = lookup(local.task, "scheduling_strategy", "REPLICA") + network_mode = lookup(local.task, "network_mode", "awsvpc") + pid_mode = local.task["pid_mode"] + ipc_mode = local.task["ipc_mode"] + propagate_tags = lookup(local.task, "propagate_tags", "SERVICE") + deployment_minimum_healthy_percent = lookup(local.task, "deployment_minimum_healthy_percent", null) + deployment_maximum_percent = lookup(local.task, "deployment_maximum_percent", null) + deployment_controller_type = lookup(local.task, "deployment_controller_type", null) + desired_count = lookup(local.task, "desired_count", 0) + task_memory = lookup(local.task, "task_memory", null) + task_cpu = lookup(local.task, "task_cpu", null) + wait_for_steady_state = lookup(local.task, "wait_for_steady_state", true) + circuit_breaker_deployment_enabled = lookup(local.task, "circuit_breaker_deployment_enabled", true) + circuit_breaker_rollback_enabled = lookup(local.task, "circuit_breaker_rollback_enabled", true) + task_policy_arns = var.iam_policy_enabled ? concat(var.task_policy_arns, aws_iam_policy.default[*].arn) : var.task_policy_arns + ecs_service_enabled = lookup(local.task, "ecs_service_enabled", true) + bind_mount_volumes = lookup(local.task, "bind_mount_volumes", []) + task_role_arn = lookup(local.task, "task_role_arn", one(module.iam_role[*]["outputs"]["role"]["arn"])) + capacity_provider_strategies = lookup(local.task, "capacity_provider_strategies") depends_on = [ module.alb_ingress @@ -228,35 +276,27 @@ module "ecs_alb_service_task" { context = module.this.context } -module "alb_ecs_label" { - source = "cloudposse/label/null" - version = "0.25.0" # requires Terraform >= 0.13.0 - - namespace = "" - environment = "" - tenant = "" - stage = "" - - context = module.this.context -} - module "alb_ingress" { source = "cloudposse/alb-ingress/aws" - version = "0.24.3" - - count = local.enabled && var.use_lb ? 1 : 0 + version = "0.28.0" - target_group_name = module.alb_ecs_label.id + count = local.is_alb ? 1 : 0 vpc_id = local.vpc_id unauthenticated_listener_arns = [local.lb_listener_https_arn] - unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", local.vanity_domain), local.full_domain] : [local.full_domain] + unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", var.vanity_domain), local.full_domain] : [local.full_domain] + unauthenticated_paths = flatten(var.unauthenticated_paths) # When set to catch-all, make priority super high to make sure last to match - unauthenticated_priority = var.lb_catch_all ? 99 : 0 + unauthenticated_priority = var.lb_catch_all ? 99 : var.unauthenticated_priority default_target_group_enabled = true - health_check_matcher = "200-404" - health_check_path = var.health_check_path - health_check_port = var.health_check_port + + health_check_matcher = var.health_check_matcher + health_check_path = var.health_check_path + health_check_port = var.health_check_port + health_check_healthy_threshold = var.health_check_healthy_threshold + health_check_unhealthy_threshold = var.health_check_unhealthy_threshold + health_check_interval = var.health_check_interval + health_check_timeout = var.health_check_timeout stickiness_enabled = var.stickiness_enabled stickiness_type = var.stickiness_type @@ -314,8 +354,10 @@ data "aws_iam_policy_document" "this" { } resource "aws_iam_policy" "default" { - count = local.enabled && var.iam_policy_enabled ? 1 : 0 - policy = join("", data.aws_iam_policy_document.this[*].json) + count = local.enabled && var.iam_policy_enabled ? 1 : 0 + + name = format("%s-task-access", module.this.id) + policy = join("", data.aws_iam_policy_document.this[*]["json"]) tags_all = module.this.tags } @@ -337,12 +379,12 @@ module "ecs_cloudwatch_autoscaling" { source = "cloudposse/ecs-cloudwatch-autoscaling/aws" version = "0.7.3" - count = local.enabled && var.task_enabled ? 1 : 0 + count = local.enabled && var.task_enabled && var.autoscaling_enabled ? 1 : 0 service_name = module.ecs_alb_service_task[0].service_name cluster_name = module.ecs_cluster.outputs.cluster_name - min_capacity = lookup(var.task, "min_capacity", 1) - max_capacity = lookup(var.task, "max_capacity", 2) + min_capacity = lookup(local.task, "min_capacity", 1) + max_capacity = lookup(local.task, "max_capacity", 2) scale_up_adjustment = 1 scale_up_cooldown = 60 scale_down_adjustment = -1 @@ -356,15 +398,19 @@ module "ecs_cloudwatch_autoscaling" { } locals { - cpu_utilization_high_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "cpu" ? module.ecs_cloudwatch_autoscaling[0].scale_up_policy_arn : "" - cpu_utilization_low_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "cpu" ? module.ecs_cloudwatch_autoscaling[0].scale_down_policy_arn : "" - memory_utilization_high_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "memory" ? module.ecs_cloudwatch_autoscaling[0].scale_up_policy_arn : "" - memory_utilization_low_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "memory" ? module.ecs_cloudwatch_autoscaling[0].scale_down_policy_arn : "" + scale_up_policy_arn = try(module.ecs_cloudwatch_autoscaling[0].scale_up_policy_arn, "") + scale_down_policy_arn = try(module.ecs_cloudwatch_autoscaling[0].scale_down_policy_arn, "") + + cpu_utilization_high_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "cpu" ? local.scale_up_policy_arn : "" + cpu_utilization_low_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "cpu" ? local.scale_down_policy_arn : "" + memory_utilization_high_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "memory" ? local.scale_up_policy_arn : "" + memory_utilization_low_alarm_actions = var.autoscaling_enabled && var.autoscaling_dimension == "memory" ? local.scale_down_policy_arn : "" } module "ecs_cloudwatch_sns_alarms" { source = "cloudposse/ecs-cloudwatch-sns-alarms/aws" version = "0.12.3" + count = local.enabled && var.autoscaling_enabled ? 1 : 0 cluster_name = module.ecs_cluster.outputs.cluster_name service_name = module.ecs_alb_service_task[0].service_name diff --git a/modules/ecs-service/remote-state.tf b/modules/ecs-service/remote-state.tf index 47f69a80f..3c11291c1 100644 --- a/modules/ecs-service/remote-state.tf +++ b/modules/ecs-service/remote-state.tf @@ -5,22 +5,36 @@ locals { subnet_ids = lookup(module.vpc.outputs.subnets, local.assign_public_ip ? "public" : "private", { ids = [] }).ids ecs_cluster_arn = module.ecs_cluster.outputs.cluster_arn - lb_arn = try(module.ecs_cluster.outputs.alb[var.alb_configuration].alb_arn, null) - lb_name = try(module.ecs_cluster.outputs.alb[var.alb_configuration].alb_name, null) - lb_listener_https_arn = try(module.ecs_cluster.outputs.alb[var.alb_configuration].https_listener_arn, null) - lb_sg_id = try(module.ecs_cluster.outputs.alb[var.alb_configuration].security_group_id, null) - lb_zone_id = try(module.ecs_cluster.outputs.alb[var.alb_configuration].alb_zone_id, null) + use_external_lb = local.use_lb && (try(length(var.alb_name) > 0, false) || try(length(var.nlb_name) > 0, false)) + + is_alb = local.use_lb && !try(length(var.nlb_name) > 0, false) + alb = local.use_lb ? (local.use_external_lb ? try(module.alb[0].outputs, null) : module.ecs_cluster.outputs.alb[var.alb_configuration]) : null + + is_nlb = local.use_lb && try(length(var.nlb_name) > 0, false) + nlb = try(module.nlb[0].outputs, null) + + use_lb = local.enabled && var.use_lb + + requested_protocol = local.use_lb && !local.lb_listener_http_is_redirect ? var.http_protocol : null + lb_protocol = local.lb_listener_http_is_redirect || try(local.is_nlb && local.nlb.is_443_enabled, false) ? "https" : "http" + http_protocol = coalesce(local.requested_protocol, local.lb_protocol) + + lb_arn = try(coalesce(local.nlb.nlb_arn, ""), coalesce(local.alb.alb_arn, ""), null) + lb_name = try(coalesce(local.nlb.nlb_name, ""), coalesce(local.alb.alb_name, ""), null) + lb_listener_http_is_redirect = try(length(local.is_nlb ? "" : local.alb.http_redirect_listener_arn) > 0, false) + lb_listener_https_arn = try(coalesce(local.nlb.default_listener_arn, ""), coalesce(local.alb.https_listener_arn, ""), null) + lb_sg_id = try(local.is_nlb ? null : local.alb.security_group_id, null) + lb_zone_id = try(coalesce(local.nlb.nlb_zone_id, ""), coalesce(local.alb.alb_zone_id, ""), null) + lb_fqdn = try(coalesce(local.nlb.route53_record.fqdn, ""), coalesce(local.alb.route53_record.fqdn, ""), local.full_domain) + } ## Company specific locals for domain convention locals { - domain_name = { - tenantexample = "example.net", - } - zone_domain = format("%s.%s.%s", var.stage, var.tenant, coalesce(var.domain_name, local.domain_name[var.tenant])) - domain_type = var.alb_configuration - cluster_type = var.cluster_attributes[0] + cluster_type = try(var.cluster_attributes[0], "platform") + + zone_domain = jsondecode(data.jq_query.service_domain_query.result) # e.g. example.public-platform.{environment}.{zone_domain} full_domain = format("%s.%s-%s.%s.%s", join("-", concat([ @@ -30,47 +44,113 @@ locals { public_domain_no_service_name = format("%s-%s.%s.%s", "public", local.cluster_type, var.environment, local.zone_domain) private_domain_no_service_name = format("%s-%s.%s.%s", "private", local.cluster_type, var.environment, local.zone_domain) - # tenant to domain mapping - vanity_domain_names = { - tenantexample = { - "dev" = "example-dev.com", - "staging" = "example-staging.com", - "prod" = "example-prod.com", - }, - } + vanity_domain_zone_id = one(data.aws_route53_zone.selected_vanity[*].zone_id) + + unauthenticated_paths = local.is_nlb ? ["/"] : var.unauthenticated_paths + + # NOTE: this is the rare _not_ in the ternary purely for readability + full_urls = !local.use_lb ? [] : [for path in local.unauthenticated_paths : format("%s://%s%s", local.http_protocol, local.lb_fqdn, trimsuffix(trimsuffix(path, "*"), "/"))] - vanity_domain = local.vanity_domain_names[var.tenant][var.stage] - vanity_domain_zone_id = try(one(data.aws_route53_zone.selected_vanity[*].zone_id), null) } module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" context = module.this.context } +module "security_group" { + count = local.enabled && var.task_security_group_component != null ? 1 : 0 + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.task_security_group_component + + context = module.this.context +} + module "rds" { - count = local.enabled && var.use_rds_client_sg ? 1 : 0 + count = local.enabled && var.use_rds_client_sg && try(length(var.rds_name), 0) > 0 ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" - component = "rds" + component = var.rds_name context = module.this.context } module "ecs_cluster" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" + + component = coalesce(var.ecs_cluster_name, "ecs-cluster") + + context = module.this.context +} + +module "alb" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.is_alb && local.use_external_lb ? 1 : 0 + + component = var.alb_name + + context = module.this.context +} + +module "nlb" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" - component = "ecs" + count = local.is_nlb ? 1 : 0 + + component = var.nlb_name + + context = module.this.context +} + +module "s3" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.s3_mirroring_enabled ? 1 : 0 + + component = var.s3_mirror_name context = module.this.context } + +module "service_domain" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.zone_component + + context = module.this.context + environment = "gbl" +} + +data "jq_query" "service_domain_query" { + data = jsonencode(one(module.service_domain[*].outputs)) + query = var.zone_component_output +} + +module "datadog_configuration" { + count = var.datadog_agent_sidecar_enabled ? 1 : 0 + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "datadog_keys" + + context = module.this.context +} + + # This is purely a check to ensure this zone exists # tflint-ignore: terraform_unused_declarations data "aws_route53_zone" "selected" { @@ -81,9 +161,9 @@ data "aws_route53_zone" "selected" { } data "aws_route53_zone" "selected_vanity" { - count = local.enabled && var.vanity_domain_enabled ? 1 : 0 + count = local.enabled && var.vanity_domain != null ? 1 : 0 - name = local.vanity_domain + name = var.vanity_domain private_zone = false } @@ -91,3 +171,13 @@ data "aws_kms_alias" "selected" { count = local.enabled && var.kinesis_enabled ? 1 : 0 name = format("alias/%s", coalesce(var.kms_key_alias, var.name)) } + +module "iam_role" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + count = local.enabled && var.task_iam_role_component != null ? 1 : 0 + + component = var.task_iam_role_component + + context = module.this.context +} diff --git a/modules/ecs-service/systems-manager.tf b/modules/ecs-service/systems-manager.tf new file mode 100644 index 000000000..5a40afa07 --- /dev/null +++ b/modules/ecs-service/systems-manager.tf @@ -0,0 +1,70 @@ +# AWS KMS alias used for encryption/decryption of SSM secure strings +variable "kms_alias_name_ssm" { + type = string + default = "alias/aws/ssm" + description = "KMS alias name for SSM" +} + +variable "ssm_enabled" { + type = bool + default = false + description = "If `true` create SSM keys for the database user and password." +} + +variable "ssm_key_format" { + type = string + default = "/%v/%v/%v" + description = "SSM path format. The values will will be used in the following order: `var.ssm_key_prefix`, `var.name`, `var.ssm_key_*`" +} + +variable "ssm_key_prefix" { + type = string + default = "ecs-service" + description = "SSM path prefix. Omit the leading forward slash `/`." +} + +locals { + ssm_enabled = module.this.enabled && var.ssm_enabled + + url_params = { for i, url in local.full_urls : format(var.ssm_key_format, var.ssm_key_prefix, var.name, "url/${i}") => { + description = "ECS Service URL for ${var.name}" + type = "String", + value = url + } + } + + params = merge({}, local.url_params) + + # Use the format for any other params we need to create + # params = { + # "${format(var.ssm_key_format, var.ssm_key_prefix, var.name, "name")}" = { + # description = "ECS Service [name here] for ${var.name}" + # type = "String", + # value = "some value" + # }, + # } +} + +resource "aws_ssm_parameter" "full_urls" { + for_each = local.ssm_enabled ? local.params : {} + + name = each.key + description = each.value.description + type = each.value.type + key_id = var.kms_alias_name_ssm + value = each.value.value + overwrite = true + + tags = module.this.tags +} + + +output "ssm_key_prefix" { + value = local.ssm_enabled ? format(var.ssm_key_format, var.ssm_key_prefix, var.name, "") : null + description = "SSM prefix" +} + +output "ssm_parameters" { + description = "SSM parameters for the ECS Service" + value = local.ssm_enabled ? keys(local.params) : [] +} diff --git a/modules/ecs-service/variables.tf b/modules/ecs-service/variables.tf index 874016bd8..95e1ed903 100644 --- a/modules/ecs-service/variables.tf +++ b/modules/ecs-service/variables.tf @@ -15,14 +15,126 @@ variable "logs" { default = {} } -variable "containers" { +variable "ecs_cluster_name" { type = any + description = "The name of the ECS Cluster this belongs to" + default = "ecs" +} + +variable "alb_name" { + type = string + description = "The name of the ALB this service should attach to" + default = null +} + +variable "nlb_name" { + type = string + description = "The name of the NLB this service should attach to" + default = null +} + +variable "s3_mirror_name" { + type = string + description = "The name of the S3 mirror component" + default = null +} + +variable "rds_name" { + type = any + description = "The name of the RDS database this service should allow access to" + default = null +} + +variable "containers" { + type = map(object({ + name = string + ecr_image = optional(string) + image = optional(string) + memory = optional(number) + memory_reservation = optional(number) + cpu = optional(number) + essential = optional(bool, true) + readonly_root_filesystem = optional(bool, null) + privileged = optional(bool, null) + container_depends_on = optional(list(object({ + containerName = string + condition = string # START, COMPLETE, SUCCESS, HEALTHY + })), null) + + port_mappings = optional(list(object({ + containerPort = number + hostPort = number + protocol = string + })), []) + command = optional(list(string), null) + entrypoint = optional(list(string), null) + healthcheck = optional(object({ + command = list(string) + interval = number + retries = number + startPeriod = number + timeout = number + }), null) + ulimits = optional(list(object({ + name = string + softLimit = number + hardLimit = number + })), null) + log_configuration = optional(object({ + logDriver = string + options = optional(map(string), {}) + })) + docker_labels = optional(map(string), null) + map_environment = optional(map(string), {}) + map_secrets = optional(map(string), {}) + volumes_from = optional(list(object({ + sourceContainer = string + readOnly = bool + })), null) + mount_points = optional(list(object({ + sourceVolume = string + containerPath = string + readOnly = bool + })), []) + })) description = "Feed inputs into container definition module" default = {} } variable "task" { - type = any + type = object({ + task_cpu = optional(number) + task_memory = optional(number) + task_role_arn = optional(string, "") + pid_mode = optional(string, null) + ipc_mode = optional(string, null) + network_mode = optional(string) + propagate_tags = optional(string) + assign_public_ip = optional(bool, false) + use_alb_security_groups = optional(bool, true) + launch_type = optional(string, "FARGATE") + scheduling_strategy = optional(string, "REPLICA") + capacity_provider_strategies = optional(list(object({ + capacity_provider = string + weight = number + base = number + })), []) + + deployment_minimum_healthy_percent = optional(number, null) + deployment_maximum_percent = optional(number, null) + desired_count = optional(number, 0) + min_capacity = optional(number, 1) + max_capacity = optional(number, 2) + wait_for_steady_state = optional(bool, true) + circuit_breaker_deployment_enabled = optional(bool, true) + circuit_breaker_rollback_enabled = optional(bool, true) + + ecs_service_enabled = optional(bool, true) + bind_mount_volumes = optional(list(object({ + name = string + host_path = string + })), []) + }) description = "Feed inputs into ecs_alb_service_task module" default = {} } @@ -36,10 +148,16 @@ variable "task_policy_arns" { ] } -variable "domain_name" { +variable "unauthenticated_paths" { + type = list(string) + description = "Unauthenticated path pattern to match" + default = [] +} + +variable "unauthenticated_priority" { type = string - description = "The domain name to use as the host header suffix" - default = "" + description = "The priority for the rules without authentication, between 1 and 50000 (1 being highest priority). Must be different from `authenticated_priority` since a listener can't have multiple rules with the same priority " + default = 0 } variable "task_enabled" { @@ -122,6 +240,17 @@ variable "use_lb" { type = bool } +variable "http_protocol" { + description = "Which http protocol to use in outputs and SSM url params. This value is ignored if a load balancer is not used. If it is `null`, the redirect value from the ALB determines the protocol." + default = null + type = string + + validation { + condition = anytrue([var.http_protocol == null, try(contains(["https", "http"], var.http_protocol), false)]) + error_message = "Allowed values: `http`, `https`, and `null`." + } +} + variable "stream_mode" { description = "Stream mode details for the Kinesis stream" default = "PROVISIONED" @@ -140,9 +269,21 @@ variable "chamber_service" { description = "SSM parameter service name for use with chamber. This is used in chamber_format where /$chamber_service/$name/$container_name/$parameter would be the default." } -variable "vanity_domain_enabled" { - default = false - type = bool +variable "zone_component" { + type = string + description = "The component name to look up service domain remote-state on" + default = "dns-delegated" +} + +variable "zone_component_output" { + type = string + description = "A json query to use to get the zone domain from the remote state. See " + default = ".default_domain_name" +} + +variable "vanity_domain" { + default = null + type = string description = "Whether to use the vanity domain alias for the service" } @@ -164,6 +305,36 @@ variable "health_check_port" { description = "The port to use to connect with the target. Valid values are either ports 1-65536, or `traffic-port`. Defaults to `traffic-port`" } +variable "health_check_timeout" { + type = number + default = 10 + description = "The amount of time to wait in seconds before failing a health check request" +} + +variable "health_check_healthy_threshold" { + type = number + default = 2 + description = "The number of consecutive health checks successes required before healthy" +} + +variable "health_check_unhealthy_threshold" { + type = number + default = 2 + description = "The number of consecutive health check failures required before unhealthy" +} + +variable "health_check_interval" { + type = number + default = 15 + description = "The duration in seconds in between health checks" +} + +variable "health_check_matcher" { + type = string + default = "200-404" + description = "The HTTP response codes to indicate a healthy check" +} + variable "lb_catch_all" { type = bool description = "Should this service act as catch all for all subdomain hosts of the vanity domain" @@ -324,3 +495,15 @@ variable "memory_utilization_low_ok_actions" { description = "A list of ARNs (i.e. SNS Topic ARN) to notify on Memory Utilization Low OK action" default = [] } + +variable "task_security_group_component" { + type = string + description = "A component that outputs security_group_id for adding to the service as a whole." + default = null +} + +variable "task_iam_role_component" { + type = string + description = "A component that outputs an iam_role module as 'role' for adding to the service as a whole." + default = null +} diff --git a/modules/ecs-service/versions.tf b/modules/ecs-service/versions.tf index 697261ff1..c06bc308b 100644 --- a/modules/ecs-service/versions.tf +++ b/modules/ecs-service/versions.tf @@ -18,5 +18,9 @@ terraform { source = "cloudposse/template" version = ">= 2.2" } + jq = { + source = "massdriver-cloud/jq" + version = ">=0.2.0" + } } } From f7eb20ec54f38975d67535a03f64049d33583142 Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 15 Aug 2023 14:47:40 -0700 Subject: [PATCH 223/501] Karpenter bugfix, EKS add-ons to mangaed node group (#816) --- modules/eks/cluster/README.md | 11 ++++++++--- modules/eks/cluster/variables.tf | 10 ++++++---- modules/eks/karpenter/main.tf | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index ce875e97e..de01f3463 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -130,6 +130,8 @@ components: # All Fargate Profiles will use the same IAM Role when `legacy_fargate_1_role_per_profile_enabled` is set to false. # Recommended for all new clusters, but will damage existing clusters provisioned with the legacy component. legacy_fargate_1_role_per_profile_enabled: false + # While it is possible to deploy add-ons to Fargate Profiles, it is not recommended. Use a managed node group instead. + deploy_addons_to_fargate: false # EKS addons # https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html @@ -147,7 +149,10 @@ components: # https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html # https://github.com/kubernetes-sigs/aws-ebs-csi-driver aws-ebs-csi-driver: - addon_version: "v1.20.0-eksbuild.1" # set `addon_version` to `null` to use the latest version # Only install the EFS driver if you are using EFS and have already created an EFS file system. + addon_version: "v1.20.0-eksbuild.1" # set `addon_version` to `null` to use the latest version + # Only install the EFS driver if you are using EFS. + # Create an EFS file system with the `efs` component. + # Create an EFS StorageClass with the `eks/storage-class` component. # https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html aws-efs-csi-driver: addon_version: "v1.5.8-eksbuild.1" @@ -471,7 +476,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
map(object({
addon_version = optional(string, null)
# configuration_values is a JSON string, such as '{"computeType": "Fargate"}'.
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | -| [addons\_depends\_on](#input\_addons\_depends\_on) | If set `true`, all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned.
See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. | `bool` | `false` | no | +| [addons\_depends\_on](#input\_addons\_depends\_on) | If set `true` (recommended), all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned.
See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. | `bool` | `true` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | @@ -499,7 +504,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [color](#input\_color) | The cluster stage represented by a color; e.g. blue, green | `string` | `""` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [deploy\_addons\_to\_fargate](#input\_deploy\_addons\_to\_fargate) | Set to `true` to deploy addons to Fargate instead of initial node pool | `bool` | `true` | no | +| [deploy\_addons\_to\_fargate](#input\_deploy\_addons\_to\_fargate) | Set to `true` (not recommended) to deploy addons to Fargate instead of initial node pool | `bool` | `false` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index f5609d8ec..4635db2ed 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -355,6 +355,8 @@ variable "node_group_defaults" { default = { desired_group_size = 1 + # t3.medium is kept as the default for backward compatibility. + # Recommendation as of 2023-08-08 is c6a.large to provide reserve HA capacity regardless of Karpenter behavoir. instance_types = ["t3.medium"] kubernetes_version = null # set to null to use cluster_kubernetes_version max_group_size = 100 @@ -544,8 +546,8 @@ variable "addons" { variable "deploy_addons_to_fargate" { type = bool - description = "Set to `true` to deploy addons to Fargate instead of initial node pool" - default = true + description = "Set to `true` (not recommended) to deploy addons to Fargate instead of initial node pool" + default = false nullable = false } @@ -553,11 +555,11 @@ variable "addons_depends_on" { type = bool description = <<-EOT - If set `true`, all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned. + If set `true` (recommended), all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned. See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. EOT - default = false + default = true nullable = false } diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index 5d42902df..b2c0f0c11 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -139,7 +139,7 @@ module "karpenter" { settings = { # This configuration of settings requires Karpenter chart v0.19.0 or later aws = { - defaultInstanceProfile = one(aws_iam_instance_profile.default[*].name) + defaultInstanceProfile = local.karpenter_iam_role_name # instance profile name === role name clusterName = local.eks_cluster_id # clusterEndpoint not needed as of v0.25.0 clusterEndpoint = local.eks_cluster_endpoint From e36f56713d81544f4d32df712c36512bf39ecc79 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Wed, 16 Aug 2023 09:36:21 +0300 Subject: [PATCH 224/501] Update storage-class efs component documentation (#817) --- modules/eks/storage-class/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/eks/storage-class/README.md b/modules/eks/storage-class/README.md index 1be842269..9ecb30238 100644 --- a/modules/eks/storage-class/README.md +++ b/modules/eks/storage-class/README.md @@ -15,7 +15,7 @@ do not propagate to existing PersistentVolumeClaims. This component can create storage classes backed by EBS or EFS, and is intended to be used with the corresponding EKS add-ons `aws-ebs-csi-driver` and `aws-efs-csi-driver` respectively. In the case of EFS, this component also requires that you have provisioned an EFS filesystem -in the same region as your cluster, and expects you have used the `eks/efs` component to do so. +in the same region as your cluster, and expects you have used the `efs` (previously `eks/efs`) component to do so. The EFS storage classes will get the file system ID from the EFS component's output. ### Note: Default Storage Class @@ -26,12 +26,15 @@ This default StorageClass is then used by PersistentVolumeClaims that do not spe Prior to Kubernetes 1.26, if more than one StorageClass is marked as default, a PersistentVolumeClaim without `storageClassName` explicitly specified cannot be created. In Kubernetes 1.26 and later, if more than one StorageClass is marked as default, -the last one created will be used. +the last one created will be used, which means you can get by with just ignoring +the default "gp2" StorageClass that EKS creates for you. EKS always creates a default storage class for the cluster, typically an EBS backed class named `gp2`. Find out what the default storage class is for your cluster by running this command: ```bash +# You only need to run `set-cluster` when you are changing target clusters +set-cluster admin # replace admin with other role name if desired kubectl get storageclass ``` @@ -41,6 +44,8 @@ If you want to change the default, you can unset the existing default manually, ```bash SC_NAME=gp2 # Replace with the name of the storage class you want to unset as default +# You only need to run `set-cluster` when you are changing target clusters +set-cluster admin # replace admin with other role name if desired kubectl patch storageclass $SC_NAME -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' ``` @@ -55,6 +60,8 @@ View the parameters of a storage class by running this command: ```bash SC_NAME=gp2 # Replace with the name of the storage class you want to view +# You only need to run `set-cluster` when you are changing target clusters +set-cluster admin # replace admin with other role name if desired kubectl get storageclass $SC_NAME -o yaml ``` @@ -95,6 +102,7 @@ Here's an example snippet for how to use this component. efs_storage_classes: efs-sc: make_default_storage_class: false + efs_component_name: "efs" # Replace with the name of the EFS component, previously "eks/efs" ``` From e995ecee6a9f23252a6100dc89b1be731ae21a8a Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Wed, 16 Aug 2023 12:23:18 -0500 Subject: [PATCH 225/501] Update EC2-Autoscale-Group Modules to 0.35.1 (#809) Co-authored-by: cloudpossebot Co-authored-by: Dan Miller --- modules/bastion/README.md | 2 +- modules/bastion/main.tf | 2 +- modules/github-runners/README.md | 2 +- modules/github-runners/main.tf | 2 +- modules/spacelift/worker-pool/README.md | 4 ++-- modules/spacelift/worker-pool/main.tf | 2 +- modules/spacelift/worker-pool/variables.tf | 8 +++++--- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/modules/bastion/README.md b/modules/bastion/README.md index 168062778..967c36b51 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -73,7 +73,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [bastion\_autoscale\_group](#module\_bastion\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.0 | +| [bastion\_autoscale\_group](#module\_bastion\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [sg](#module\_sg) | cloudposse/security-group/aws | 2.2.0 | | [ssm\_tls\_ssh\_key\_pair](#module\_ssm\_tls\_ssh\_key\_pair) | cloudposse/ssm-tls-ssh-key-pair/aws | 0.10.2 | diff --git a/modules/bastion/main.tf b/modules/bastion/main.tf index b91c990af..5b4bfa5f5 100644 --- a/modules/bastion/main.tf +++ b/modules/bastion/main.tf @@ -94,7 +94,7 @@ data "aws_ami" "bastion_image" { module "bastion_autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" - version = "0.35.0" + version = "0.35.1" image_id = join("", data.aws_ami.bastion_image[*].id) instance_type = var.instance_type diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 26b593f05..ce4d3a816 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -180,7 +180,7 @@ chamber write github token | Name | Source | Version | |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.0 | +| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.1 | | [graceful\_scale\_in](#module\_graceful\_scale\_in) | ./modules/graceful_scale_in | n/a | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [sg](#module\_sg) | cloudposse/security-group/aws | 1.0.1 | diff --git a/modules/github-runners/main.tf b/modules/github-runners/main.tf index 3bd1de0fd..d7659f151 100644 --- a/modules/github-runners/main.tf +++ b/modules/github-runners/main.tf @@ -107,7 +107,7 @@ module "sg" { module "autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" - version = "0.35.0" + version = "0.35.1" image_id = join("", data.aws_ami.runner.*.id) instance_type = var.instance_type diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index 751fd8d95..34dfe1acb 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -133,7 +133,7 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | Name | Source | Version | |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | -| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.34.2 | +| [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.1 | | [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | | [iam\_label](#module\_iam\_label) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | @@ -197,7 +197,7 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [infracost\_cli\_args](#input\_infracost\_cli\_args) | These are the CLI args passed to infracost | `string` | `""` | no | | [infracost\_enabled](#input\_infracost\_enabled) | Whether to enable infracost for Spacelift stacks | `bool` | `false` | no | | [infracost\_warn\_on\_failure](#input\_infracost\_warn\_on\_failure) | A failure executing Infracost, or a non-zero exit code being returned from the command will cause runs to fail. If this is true, this will only warn instead of failing the stack. | `bool` | `true` | no | -| [instance\_refresh](#input\_instance\_refresh) | The instance refresh definition. If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated |
object({
strategy = string
preferences = object({
instance_warmup = number
min_healthy_percentage = number
})
triggers = list(string)
})
| `null` | no | +| [instance\_refresh](#input\_instance\_refresh) | The instance refresh definition. If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated |
object({
strategy = string
preferences = object({
instance_warmup = optional(number, null)
min_healthy_percentage = optional(number, null)
skip_matching = optional(bool, null)
auto_rollback = optional(bool, null)
})
triggers = optional(list(string), [])
})
| `null` | no | | [instance\_type](#input\_instance\_type) | EC2 instance type to use for workers | `string` | `"r5n.large"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/spacelift/worker-pool/main.tf b/modules/spacelift/worker-pool/main.tf index 93dfa21b3..cb2e53feb 100644 --- a/modules/spacelift/worker-pool/main.tf +++ b/modules/spacelift/worker-pool/main.tf @@ -90,7 +90,7 @@ module "security_group" { module "autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" - version = "0.34.2" + version = "0.35.1" image_id = var.spacelift_ami_id == null ? join("", data.aws_ami.spacelift.*.image_id) : var.spacelift_ami_id instance_type = var.instance_type diff --git a/modules/spacelift/worker-pool/variables.tf b/modules/spacelift/worker-pool/variables.tf index d0d22c8ed..549e4700c 100644 --- a/modules/spacelift/worker-pool/variables.tf +++ b/modules/spacelift/worker-pool/variables.tf @@ -213,10 +213,12 @@ variable "instance_refresh" { type = object({ strategy = string preferences = object({ - instance_warmup = number - min_healthy_percentage = number + instance_warmup = optional(number, null) + min_healthy_percentage = optional(number, null) + skip_matching = optional(bool, null) + auto_rollback = optional(bool, null) }) - triggers = list(string) + triggers = optional(list(string), []) }) default = null From 4af9adfce7f152deb07626b9ae8ca1758afe0689 Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 16 Aug 2023 19:10:28 -0700 Subject: [PATCH 226/501] Datadog upgrades (#814) --- modules/datadog-configuration/README.md | 2 +- modules/datadog-configuration/main.tf | 4 +- .../modules/datadog_keys/README.md | 2 - .../modules/datadog_keys/providers.tf | 20 ++--- modules/datadog-configuration/ssm.tf | 2 +- modules/datadog-integration/CHANGELOG.md | 22 +++++ modules/datadog-integration/README.md | 8 +- modules/datadog-integration/main.tf | 9 +- modules/datadog-integration/variables.tf | 29 +++++++ modules/datadog-lambda-forwarder/CHANGELOG.md | 13 +++ modules/datadog-lambda-forwarder/README.md | 9 +- modules/datadog-lambda-forwarder/main.tf | 13 +-- .../datadog-lambda-forwarder/remote-state.tf | 4 +- modules/datadog-lambda-forwarder/variables.tf | 2 +- modules/datadog-monitor/CHANGELOG.md | 17 ++++ modules/datadog-monitor/README.md | 20 +---- modules/datadog-monitor/main.tf | 15 ++-- modules/datadog-monitor/variables.tf | 36 -------- .../CHANGELOG.md | 17 ++++ .../README.md | 7 +- .../main.tf | 30 +++---- .../outputs.tf | 2 +- .../remote-state.tf | 2 +- modules/datadog-synthetics/README.md | 2 +- modules/datadog-synthetics/remote-state.tf | 2 +- modules/eks/datadog-agent/CHANGELOG.md | 75 +++++++++++++++++ modules/eks/datadog-agent/README.md | 10 +-- modules/eks/datadog-agent/main.tf | 84 ++++--------------- modules/eks/datadog-agent/remote-state.tf | 2 +- modules/eks/datadog-agent/values.yaml | 63 +++++++++----- modules/eks/datadog-agent/variables.tf | 12 --- 31 files changed, 306 insertions(+), 229 deletions(-) create mode 100644 modules/datadog-integration/CHANGELOG.md create mode 100644 modules/datadog-lambda-forwarder/CHANGELOG.md create mode 100644 modules/datadog-monitor/CHANGELOG.md create mode 100644 modules/datadog-synthetics-private-location/CHANGELOG.md create mode 100644 modules/eks/datadog-agent/CHANGELOG.md diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index 0da762748..f97f16511 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -77,7 +77,7 @@ provider "datadog" { |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [iam\_roles\_datadog\_secrets](#module\_iam\_roles\_datadog\_secrets) | ../account-map/modules/iam-roles | n/a | -| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | +| [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/datadog-configuration/main.tf b/modules/datadog-configuration/main.tf index 416dc171c..e4dffa51a 100644 --- a/modules/datadog-configuration/main.tf +++ b/modules/datadog-configuration/main.tf @@ -4,8 +4,8 @@ locals { ssm_enabled = local.enabled && var.datadog_secrets_store_type == "SSM" # https://docs.datadoghq.com/account_management/api-app-keys/ - datadog_api_key = var.datadog_secrets_store_type == "ASM" ? data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string : data.aws_ssm_parameter.datadog_api_key[0].value - datadog_app_key = var.datadog_secrets_store_type == "ASM" ? data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string : data.aws_ssm_parameter.datadog_app_key[0].value + datadog_api_key = local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_api_key[0].secret_string : local.ssm_enabled ? data.aws_ssm_parameter.datadog_api_key[0].value : "" + datadog_app_key = local.asm_enabled ? data.aws_secretsmanager_secret_version.datadog_app_key[0].secret_string : local.ssm_enabled ? data.aws_ssm_parameter.datadog_app_key[0].value : "" datadog_site = coalesce(var.datadog_site_url, "datadoghq.com") datadog_api_url = format("https://api.%s", local.datadog_site) diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index 2a6780834..18f007977 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -63,8 +63,6 @@ provider "datadog" { | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [global\_environment\_name](#input\_global\_environment\_name) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/datadog-configuration/modules/datadog_keys/providers.tf b/modules/datadog-configuration/modules/datadog_keys/providers.tf index 068004cd4..f039376e1 100644 --- a/modules/datadog-configuration/modules/datadog_keys/providers.tf +++ b/modules/datadog-configuration/modules/datadog_keys/providers.tf @@ -2,12 +2,14 @@ provider "aws" { region = module.datadog_configuration.outputs.region alias = "dd_api_keys" - profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name dynamic "assume_role" { - for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + role_arn = assume_role.value } } } @@ -16,15 +18,3 @@ module "iam_roles" { source = "../../../account-map/modules/iam-roles" context = module.this.context } - -variable "import_profile_name" { - type = string - default = null - description = "AWS Profile name to use when importing a resource" -} - -variable "import_role_arn" { - type = string - default = null - description = "IAM Role ARN to use when importing a resource" -} diff --git a/modules/datadog-configuration/ssm.tf b/modules/datadog-configuration/ssm.tf index f4ada17b6..28e3ccc72 100644 --- a/modules/datadog-configuration/ssm.tf +++ b/modules/datadog-configuration/ssm.tf @@ -27,7 +27,7 @@ data "aws_ssm_parameter" "datadog_app_key" { module "store_write" { source = "cloudposse/ssm-parameter-store/aws" - version = "0.11.0" + version = "0.10.0" parameter_write = [ { diff --git a/modules/datadog-integration/CHANGELOG.md b/modules/datadog-integration/CHANGELOG.md new file mode 100644 index 000000000..bafd98f33 --- /dev/null +++ b/modules/datadog-integration/CHANGELOG.md @@ -0,0 +1,22 @@ +## PR [#814](https://github.com/cloudposse/terraform-aws-components/pull/814) + +### Possible Breaking Change + +The `module "datadog_integration"` and `module "store_write"` had been changed +in an earlier PR from a module without a `count` +to a module with a `count` of zero or one. This PR changes it back to a module +without a count. If you were using the module with a `count` of zero or one, +applying this new version will cause it be destroyed and recreated. This should only +cause a very brief outage in your Datadog monitoring. + +### New Integration Options + +This PR adds the following new integration options: + +- `cspm_resource_collection_enabled` - Enable Datadog Cloud Security Posture Management scanning of your AWS account. See [announcement](https://www.datadoghq.com/product/cloud-security-management/cloud-security-posture-management/) for details. +- `metrics_collection_enabled` - When enabled, a metric-by-metric crawl of the CloudWatch API pulls data and sends it +to Datadog. New metrics are pulled every ten minutes, on average. +- `resource_collection_enabled` - Some Datadog products leverage information about how your AWS resources ( +such as S3 Buckets, RDS snapshots, and CloudFront distributions) are configured. +When `resource_collection_enabled` is `true`, Datadog collects this information +by making read-only API calls into your AWS account. diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index fec94e7fc..ca0db46a3 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -1,6 +1,7 @@ # Component: `datadog-integration` -This component is responsible for provisioning Datadog AWS integrations. +This component is responsible for provisioning Datadog AWS integrations. It depends on +the `datadog-configuration` component to get the Datadog API keys. See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. @@ -41,7 +42,7 @@ components: | Name | Source | Version | |------|--------|---------| | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_integration](#module\_datadog\_integration) | cloudposse/datadog-integration/aws | 1.0.0 | +| [datadog\_integration](#module\_datadog\_integration) | cloudposse/datadog-integration/aws | 1.2.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -61,6 +62,7 @@ components: | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [context\_host\_and\_filter\_tags](#input\_context\_host\_and\_filter\_tags) | Automatically add host and filter tags for these context keys | `list(string)` |
[
"namespace",
"tenant",
"stage"
]
| no | +| [cspm\_resource\_collection\_enabled](#input\_cspm\_resource\_collection\_enabled) | Enable Datadog Cloud Security Posture Management scanning of your AWS account.
See [announcement](https://www.datadoghq.com/product/cloud-security-management/cloud-security-posture-management/) for details. | `bool` | `null` | no | | [datadog\_aws\_account\_id](#input\_datadog\_aws\_account\_id) | The AWS account ID Datadog's integration servers use for all integrations | `string` | `"464622532012"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | @@ -76,10 +78,12 @@ components: | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [metrics\_collection\_enabled](#input\_metrics\_collection\_enabled) | When enabled, a metric-by-metric crawl of the CloudWatch API pulls data and sends it
to Datadog. New metrics are pulled every ten minutes, on average. | `bool` | `null` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [resource\_collection\_enabled](#input\_resource\_collection\_enabled) | Some Datadog products leverage information about how your AWS resources
(such as S3 Buckets, RDS snapshots, and CloudFront distributions) are configured.
When `resource_collection_enabled` is `true`, Datadog collects this information
by making read-only API calls into your AWS account. | `bool` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/datadog-integration/main.tf b/modules/datadog-integration/main.tf index 7bd7054a7..18ed5e90a 100644 --- a/modules/datadog-integration/main.tf +++ b/modules/datadog-integration/main.tf @@ -10,9 +10,9 @@ data "aws_regions" "all" { module "datadog_integration" { source = "cloudposse/datadog-integration/aws" - version = "1.0.0" + version = "1.2.0" - count = module.this.enabled && length(var.integrations) > 0 ? 1 : 0 + enabled = module.this.enabled && length(var.integrations) > 0 datadog_aws_account_id = var.datadog_aws_account_id integrations = var.integrations @@ -20,6 +20,9 @@ module "datadog_integration" { host_tags = local.host_tags excluded_regions = concat(var.excluded_regions, tolist(local.excluded_list_by_include)) account_specific_namespace_rules = var.account_specific_namespace_rules + cspm_resource_collection_enabled = var.cspm_resource_collection_enabled + metrics_collection_enabled = var.metrics_collection_enabled + resource_collection_enabled = var.resource_collection_enabled context = module.this.context } @@ -38,9 +41,9 @@ locals { } module "store_write" { - count = local.enabled ? 1 : 0 source = "cloudposse/ssm-parameter-store/aws" version = "0.11.0" + parameter_write = [ { name = "/datadog/datadog_external_id" diff --git a/modules/datadog-integration/variables.tf b/modules/datadog-integration/variables.tf index 8d79434dd..8df990f95 100644 --- a/modules/datadog-integration/variables.tf +++ b/modules/datadog-integration/variables.tf @@ -49,3 +49,32 @@ variable "context_host_and_filter_tags" { description = "Automatically add host and filter tags for these context keys" default = ["namespace", "tenant", "stage"] } + +variable "cspm_resource_collection_enabled" { + type = bool + default = null + description = <<-EOT + Enable Datadog Cloud Security Posture Management scanning of your AWS account. + See [announcement](https://www.datadoghq.com/product/cloud-security-management/cloud-security-posture-management/) for details. + EOT +} + +variable "metrics_collection_enabled" { + type = bool + default = null + description = <<-EOT + When enabled, a metric-by-metric crawl of the CloudWatch API pulls data and sends it + to Datadog. New metrics are pulled every ten minutes, on average. + EOT +} + +variable "resource_collection_enabled" { + type = bool + default = null + description = <<-EOT + Some Datadog products leverage information about how your AWS resources + (such as S3 Buckets, RDS snapshots, and CloudFront distributions) are configured. + When `resource_collection_enabled` is `true`, Datadog collects this information + by making read-only API calls into your AWS account. + EOT +} diff --git a/modules/datadog-lambda-forwarder/CHANGELOG.md b/modules/datadog-lambda-forwarder/CHANGELOG.md new file mode 100644 index 000000000..9a1593e45 --- /dev/null +++ b/modules/datadog-lambda-forwarder/CHANGELOG.md @@ -0,0 +1,13 @@ +## PR [#814](https://github.com/cloudposse/terraform-aws-components/pull/814) + +### Fix for `enabled = false` or Destroy and Recreate + +Previously, when `enabled = false` was set, the component would not necessarily +function as desired (deleting any existing resources and not creating any new ones). +Also, previously, when deleting the component, there was a race condition where +the log group could be deleted before the lambda function was deleted, causing +the lambda function to trigger automatic recreation of the log group. This +would result in re-creation failing because Terraform would try to create the +log group but it already existed. + +These issues have been fixed in this PR. diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 220fb6891..4146326be 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -1,7 +1,8 @@ # Component: `datadog-lambda-forwarder` This component is responsible for provision all the necessary infrastructure to -deploy [Datadog Lambda forwarders](https://github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring). +deploy [Datadog Lambda forwarders](https://github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring). It depends on +the `datadog-configuration` component to get the Datadog API keys. ## Usage @@ -62,9 +63,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [datadog-integration](#module\_datadog-integration) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.3.1 | +| [datadog\_lambda\_forwarder](#module\_datadog\_lambda\_forwarder) | cloudposse/datadog-lambda-forwarder/aws | 1.5.3 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [log\_group\_prefix](#module\_log\_group\_prefix) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -92,7 +93,7 @@ components: | [datadog\_forwarder\_lambda\_environment\_variables](#input\_datadog\_forwarder\_lambda\_environment\_variables) | Map of environment variables to pass to the Lambda Function | `map(string)` | `{}` | no | | [dd\_api\_key\_kms\_ciphertext\_blob](#input\_dd\_api\_key\_kms\_ciphertext\_blob) | CiphertextBlob stored in environment variable DD\_KMS\_API\_KEY used by the lambda function, along with the KMS key, to decrypt Datadog API key | `string` | `""` | no | | [dd\_artifact\_filename](#input\_dd\_artifact\_filename) | The Datadog artifact filename minus extension | `string` | `"aws-dd-forwarder"` | no | -| [dd\_forwarder\_version](#input\_dd\_forwarder\_version) | Version tag of Datadog lambdas to use. https://github.com/DataDog/datadog-serverless-functions/releases | `string` | `"3.61.0"` | no | +| [dd\_forwarder\_version](#input\_dd\_forwarder\_version) | Version tag of Datadog lambdas to use. https://github.com/DataDog/datadog-serverless-functions/releases | `string` | `"3.66.0"` | no | | [dd\_module\_name](#input\_dd\_module\_name) | The Datadog GitHub repository name | `string` | `"datadog-serverless-functions"` | no | | [dd\_tags\_map](#input\_dd\_tags\_map) | A map of Datadog tags to apply to all logs forwarded to Datadog | `map(string)` | `{}` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | diff --git a/modules/datadog-lambda-forwarder/main.tf b/modules/datadog-lambda-forwarder/main.tf index 8080204c8..62628c666 100644 --- a/modules/datadog-lambda-forwarder/main.tf +++ b/modules/datadog-lambda-forwarder/main.tf @@ -1,5 +1,6 @@ locals { - enabled = module.this.enabled + enabled = module.this.enabled + lambda_arn_enabled = local.enabled && var.lambda_arn_enabled # If any keys contain name_suffix, then use a null label to get the label prefix, and create # the appropriate input for the upstream module. @@ -40,7 +41,7 @@ module "log_group_prefix" { module "datadog_lambda_forwarder" { source = "cloudposse/datadog-lambda-forwarder/aws" - version = "1.3.1" + version = "1.5.3" cloudwatch_forwarder_log_groups = local.cloudwatch_forwarder_log_groups cloudwatch_forwarder_event_patterns = var.cloudwatch_forwarder_event_patterns @@ -88,28 +89,28 @@ module "datadog_lambda_forwarder" { # Create a new Datadog - Amazon Web Services integration Lambda ARN resource "datadog_integration_aws_lambda_arn" "rds_collector" { - count = var.lambda_arn_enabled && var.forwarder_rds_enabled ? 1 : 0 + count = local.lambda_arn_enabled && var.forwarder_rds_enabled ? 1 : 0 account_id = module.datadog-integration.outputs.aws_account_id lambda_arn = module.datadog_lambda_forwarder.lambda_forwarder_rds_function_arn } resource "datadog_integration_aws_lambda_arn" "vpc_logs_collector" { - count = var.lambda_arn_enabled && var.forwarder_vpc_logs_enabled ? 1 : 0 + count = local.lambda_arn_enabled && var.forwarder_vpc_logs_enabled ? 1 : 0 account_id = module.datadog-integration.outputs.aws_account_id lambda_arn = module.datadog_lambda_forwarder.lambda_forwarder_vpc_log_function_arn } resource "datadog_integration_aws_lambda_arn" "log_collector" { - count = var.lambda_arn_enabled && var.forwarder_log_enabled ? 1 : 0 + count = local.lambda_arn_enabled && var.forwarder_log_enabled ? 1 : 0 account_id = module.datadog-integration.outputs.aws_account_id lambda_arn = module.datadog_lambda_forwarder.lambda_forwarder_log_function_arn } resource "datadog_integration_aws_log_collection" "main" { - count = var.lambda_arn_enabled ? 1 : 0 + count = local.lambda_arn_enabled ? 1 : 0 account_id = module.datadog-integration.outputs.aws_account_id services = var.log_collection_services diff --git a/modules/datadog-lambda-forwarder/remote-state.tf b/modules/datadog-lambda-forwarder/remote-state.tf index 157792473..da85c90da 100644 --- a/modules/datadog-lambda-forwarder/remote-state.tf +++ b/modules/datadog-lambda-forwarder/remote-state.tf @@ -1,9 +1,9 @@ module "datadog-integration" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "datadog-integration" - environment = "gbl" + environment = module.iam_roles.global_environment_name context = module.this.context } diff --git a/modules/datadog-lambda-forwarder/variables.tf b/modules/datadog-lambda-forwarder/variables.tf index fbb83bd6c..0ef5ecc55 100644 --- a/modules/datadog-lambda-forwarder/variables.tf +++ b/modules/datadog-lambda-forwarder/variables.tf @@ -55,7 +55,7 @@ variable "dd_module_name" { variable "dd_forwarder_version" { type = string description = "Version tag of Datadog lambdas to use. https://github.com/DataDog/datadog-serverless-functions/releases" - default = "3.61.0" + default = "3.66.0" } variable "forwarder_log_enabled" { diff --git a/modules/datadog-monitor/CHANGELOG.md b/modules/datadog-monitor/CHANGELOG.md new file mode 100644 index 000000000..ca3260084 --- /dev/null +++ b/modules/datadog-monitor/CHANGELOG.md @@ -0,0 +1,17 @@ +## PR [#814](https://github.com/cloudposse/terraform-aws-components/pull/814) + +### Removed Dead Code, Possible Breaking Change + +The following inputs were removed because they no longer have any effect: + +- datadog_api_secret_key +- datadog_app_secret_key +- datadog_secrets_source_store_account +- monitors_roles_map +- role_paths +- secrets_store_type + +Except for `monitors_roles_map` and `role_paths`, these inputs were deprecated +in an earlier PR, and replaced with outputs from `datadog-configuration`. + +The implementation of `monitors_roles_map` and `role_paths` has been lost. diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index f853bb04e..cd4312cba 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -2,8 +2,7 @@ This component is responsible for provisioning Datadog monitors and assigning Datadog roles to the monitors. -It's required that the DataDog API and APP secret keys are available in the consuming account at the `var.datadog_api_secret_key` -and `var.datadog_app_secret_key` paths in the AWS SSM Parameter Store. +It depends on the `datadog-configuration` component to get the Datadog API keys. ## Usage @@ -20,19 +19,8 @@ components: workspace_enabled: true vars: enabled: true - secrets_store_type: SSM local_datadog_monitors_config_paths: - "catalog/monitors/dev/*.yaml" - # Assign roles to monitors to allow/restrict access - monitors_roles_map: - aurora-replica-lag-dev: - - "corporate-it-dev" - - "development-dev" - - "site-reliability-dev" - ec2-failed-status-check-dev: - - "corporate-it-dev" - - "development-dev" - - "site-reliability-dev" ``` ## Conventions @@ -210,13 +198,10 @@ No resources. | [alert\_tags\_separator](#input\_alert\_tags\_separator) | Separator for the alert tags. All strings from the `alert_tags` variable will be joined into one string using the separator and then added to the alert message | `string` | `"\n"` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [datadog\_api\_secret\_key](#input\_datadog\_api\_secret\_key) | The key of the Datadog API secret | `string` | `"datadog/datadog_api_key"` | no | -| [datadog\_app\_secret\_key](#input\_datadog\_app\_secret\_key) | The key of the Datadog Application secret | `string` | `"datadog/datadog_app_key"` | no | | [datadog\_monitor\_context\_tags](#input\_datadog\_monitor\_context\_tags) | List of context tags to add to each monitor | `set(string)` |
[
"namespace",
"tenant",
"environment",
"stage"
]
| no | | [datadog\_monitor\_context\_tags\_enabled](#input\_datadog\_monitor\_context\_tags\_enabled) | Whether to add context tags to each monitor | `bool` | `true` | no | | [datadog\_monitor\_globals](#input\_datadog\_monitor\_globals) | Global parameters to add to each monitor | `any` | `{}` | no | | [datadog\_monitors\_config\_parameters](#input\_datadog\_monitors\_config\_parameters) | Map of parameters to Datadog monitor configurations | `map(any)` | `{}` | no | -| [datadog\_secrets\_source\_store\_account](#input\_datadog\_secrets\_source\_store\_account) | Account (stage) holding Secret Store for Datadog API and app keys. | `string` | `"corp"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -229,15 +214,12 @@ No resources. | [local\_datadog\_monitors\_config\_paths](#input\_local\_datadog\_monitors\_config\_paths) | List of paths to local Datadog monitor configurations | `list(string)` | `[]` | no | | [message\_postfix](#input\_message\_postfix) | Additional information to put after each monitor message | `string` | `""` | no | | [message\_prefix](#input\_message\_prefix) | Additional information to put before each monitor message | `string` | `""` | no | -| [monitors\_roles\_map](#input\_monitors\_roles\_map) | Map of Datadog monitor names to a set of Datadog role names to restrict access to the monitors | `map(set(string))` | `{}` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [remote\_datadog\_monitors\_base\_path](#input\_remote\_datadog\_monitors\_base\_path) | Base path to remote Datadog monitor configurations | `string` | `""` | no | | [remote\_datadog\_monitors\_config\_paths](#input\_remote\_datadog\_monitors\_config\_paths) | List of paths to remote Datadog monitor configurations | `list(string)` | `[]` | no | -| [role\_paths](#input\_role\_paths) | List of paths to Datadog role configurations | `list(string)` | `[]` | no | -| [secrets\_store\_type](#input\_secrets\_store\_type) | Secret store type for Datadog API and app keys. Valid values: `SSM`, `ASM` | `string` | `"SSM"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/datadog-monitor/main.tf b/modules/datadog-monitor/main.tf index d47548f08..65581bd5f 100644 --- a/modules/datadog-monitor/main.tf +++ b/modules/datadog-monitor/main.tf @@ -24,8 +24,8 @@ locals { } : {} context_dd_tags = { context_dd_tags = join(",", [ - for k, v in local.context_tags : - v != null ? format("%s:%s", k, v) : k + for k, v in local.context_tags : ( + v != null ? format("%s:%s", k, v) : k) ]) } @@ -41,7 +41,6 @@ locals { message = format("%s%s%s", var.message_prefix, lookup(v.merged, "message", ""), var.message_postfix) }) } - } # Convert all Datadog Monitors from YAML config to Terraform map with token replacement using `parameters` @@ -82,10 +81,12 @@ module "datadog_monitors_merge" { version = "1.0.2" # for_each = { for k, v in local.datadog_monitors_yaml_config_map_configs : k => v if local.datadog_monitors_enabled } - for_each = { for k, v in merge( - module.local_datadog_monitors_yaml_config.map_configs, - module.remote_datadog_monitors_yaml_config.map_configs - ) : k => v if local.datadog_monitors_enabled } + for_each = { + for k, v in merge( + module.local_datadog_monitors_yaml_config.map_configs, + module.remote_datadog_monitors_yaml_config.map_configs + ) : k => v if local.datadog_monitors_enabled + } # Merge in order: datadog monitor, datadog monitor globals, context tags maps = [ diff --git a/modules/datadog-monitor/variables.tf b/modules/datadog-monitor/variables.tf index afbdad7cc..58153a3e2 100644 --- a/modules/datadog-monitor/variables.tf +++ b/modules/datadog-monitor/variables.tf @@ -39,42 +39,6 @@ variable "datadog_monitors_config_parameters" { default = {} } -variable "secrets_store_type" { - type = string - description = "Secret store type for Datadog API and app keys. Valid values: `SSM`, `ASM`" - default = "SSM" -} - -variable "datadog_api_secret_key" { - type = string - description = "The key of the Datadog API secret" - default = "datadog/datadog_api_key" -} - -variable "datadog_app_secret_key" { - type = string - description = "The key of the Datadog Application secret" - default = "datadog/datadog_app_key" -} - -variable "role_paths" { - type = list(string) - description = "List of paths to Datadog role configurations" - default = [] -} - -variable "monitors_roles_map" { - type = map(set(string)) - description = "Map of Datadog monitor names to a set of Datadog role names to restrict access to the monitors" - default = {} -} - -variable "datadog_secrets_source_store_account" { - type = string - description = "Account (stage) holding Secret Store for Datadog API and app keys." - default = "corp" -} - variable "datadog_monitor_globals" { type = any description = "Global parameters to add to each monitor" diff --git a/modules/datadog-synthetics-private-location/CHANGELOG.md b/modules/datadog-synthetics-private-location/CHANGELOG.md new file mode 100644 index 000000000..aa19dd6d1 --- /dev/null +++ b/modules/datadog-synthetics-private-location/CHANGELOG.md @@ -0,0 +1,17 @@ +## PR [#814](https://github.com/cloudposse/terraform-aws-components/pull/814) + +### Possible Breaking Change + +Previously this component directly created the Kubernetes namespace for +the agent when `create_namespace` was set to `true`. Now this component +delegates that responsibility to the `helm-release` module, which +better coordinates the destruction of resources at destruction time +(for example, ensuring that the Helm release is completely destroyed +and finalizers run before deleting the namespace). + +Generally the simplest upgrade path is to destroy the Helm release, +then destroy the namespace, then apply the new configuration. Alternatively, +you can use `terraform state mv` to move the existing namespace to the new +Terraform "address", which will preserve the existing deployment and reduce +the possibility of the destroy failing and leaving the Kubernetes cluster +in a bad state. diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index 6a570ebdc..6a2bfaf50 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -23,9 +23,10 @@ components: description: "Datadog Synthetics Private Location Agent" kubernetes_namespace: "monitoring" create_namespace: true + # https://github.com/DataDog/helm-charts/tree/main/charts/synthetics-private-location repository: "https://helm.datadoghq.com" chart: "synthetics-private-location" - chart_version: "0.15.6" + chart_version: "0.15.15" timeout: 180 wait: true atomic: true @@ -145,8 +146,8 @@ Environment variables: | Name | Source | Version | |------|--------|---------| | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/helm-release/aws | 0.7.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/helm-release/aws | 0.9.3 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/datadog-synthetics-private-location/main.tf b/modules/datadog-synthetics-private-location/main.tf index 6cd94dd64..076319374 100644 --- a/modules/datadog-synthetics-private-location/main.tf +++ b/modules/datadog-synthetics-private-location/main.tf @@ -16,20 +16,22 @@ resource "datadog_synthetics_private_location" "this" { module "datadog_synthetics_private_location" { source = "cloudposse/helm-release/aws" - version = "0.7.0" - - name = module.this.name - chart = var.chart - description = var.description - repository = var.repository - chart_version = var.chart_version - namespace = var.kubernetes_namespace - create_namespace = var.create_namespace - verify = var.verify - wait = var.wait - atomic = var.atomic - cleanup_on_fail = var.cleanup_on_fail - timeout = var.timeout + version = "0.9.3" + + name = module.this.name + chart = var.chart + description = var.description + repository = var.repository + chart_version = var.chart_version + + namespace = var.kubernetes_namespace + create_namespace_with_kubernetes = var.create_namespace + + verify = var.verify + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer diff --git a/modules/datadog-synthetics-private-location/outputs.tf b/modules/datadog-synthetics-private-location/outputs.tf index 004d37a52..1958fd6de 100644 --- a/modules/datadog-synthetics-private-location/outputs.tf +++ b/modules/datadog-synthetics-private-location/outputs.tf @@ -1,5 +1,5 @@ output "synthetics_private_location_id" { - value = join("", datadog_synthetics_private_location.this.*.id) + value = one(datadog_synthetics_private_location.this[*].id) description = "Synthetics private location ID" } diff --git a/modules/datadog-synthetics-private-location/remote-state.tf b/modules/datadog-synthetics-private-location/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/datadog-synthetics-private-location/remote-state.tf +++ b/modules/datadog-synthetics-private-location/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/datadog-synthetics/README.md b/modules/datadog-synthetics/README.md index 534d2a5c4..64cccbda8 100644 --- a/modules/datadog-synthetics/README.md +++ b/modules/datadog-synthetics/README.md @@ -147,7 +147,7 @@ No providers. | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [datadog\_synthetics](#module\_datadog\_synthetics) | cloudposse/platform/datadog//modules/synthetics | 1.0.1 | | [datadog\_synthetics\_merge](#module\_datadog\_synthetics\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | -| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [datadog\_synthetics\_yaml\_config](#module\_datadog\_synthetics\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/datadog-synthetics/remote-state.tf b/modules/datadog-synthetics/remote-state.tf index 34a34a447..2abaaedc8 100644 --- a/modules/datadog-synthetics/remote-state.tf +++ b/modules/datadog-synthetics/remote-state.tf @@ -1,6 +1,6 @@ module "datadog_synthetics_private_location" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.synthetics_private_location_component_name diff --git a/modules/eks/datadog-agent/CHANGELOG.md b/modules/eks/datadog-agent/CHANGELOG.md new file mode 100644 index 000000000..1b1c8e8d3 --- /dev/null +++ b/modules/eks/datadog-agent/CHANGELOG.md @@ -0,0 +1,75 @@ +## PR [#814](https://github.com/cloudposse/terraform-aws-components/pull/814) + +### Possible Breaking Change + +Removed inputs `iam_role_enabled` and `iam_policy_statements` because +the Datadog agent does not need an IAM (IRSA) role or any special AWS +permissions because it works solely within the Kubernetes environment. +(Datadog has AWS integrations to handle monitoring that requires AWS permissions.) + +This only a breaking change if you were setting these inputs. If you were, +simply remove them from your configuration. + +### Possible Breaking Change + +Previously this component directly created the Kubernetes namespace for +the agent when `create_namespace` was set to `true`. Now this component +delegates that responsibility to the `helm-release` module, which +better coordinates the destruction of resources at destruction time +(for example, ensuring that the Helm release is completely destroyed +and finalizers run before deleting the namespace). + +Generally the simplest upgrade path is to destroy the Helm release, +then destroy the namespace, then apply the new configuration. Alternatively, +you can use `terraform state mv` to move the existing namespace to the new +Terraform "address", which will preserve the existing deployment and reduce +the possibility of the destroy failing and leaving the Kubernetes cluster +in a bad state. + +### Cluster Agent Redundancy + +In this PR we have defaulted the number of Cluster Agents to 2. This is +because when there are no Cluster Agents, all cluster metrics are lost. +Having 2 agents makes it possible to keep 1 agent running at all times, even +when the other is on a node being drained. + +### DNS Resolution Enhancement + +If Datadog processes are looking for where to send data and are configured +to look up `datadog.monitoring.svc.cluster.local`, by default the cluster +will make a DNS query for each of the following: + +1. `datadog.monitoring.svc.cluster.local.monitoring.svc.cluster.local` +2. `datadog.monitoring.svc.cluster.local.svc.cluster.local` +3. `datadog.monitoring.svc.cluster.local.cluster.local` +4. `datadog.monitoring.svc.cluster.local.ec2.internal` +5. `datadog.monitoring.svc.cluster.local` + +due to the DNS resolver's [search path](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#namespaces-of-services). +Because this lookup happens so frequently +(several times a second in a production environment), it can cause a lot of +unnecessary work, even if the DNS query is cached. + +In this PR we have set `ndots: 2` in the agent and cluster agent configuration +so that only the 5th query is made. (In Kubernetes, the default value for +`ndots` is 5. DNS queries having fewer than `ndots` dots in them will be attempted +using each component of the search path in turn until a match is +found, while those with more dots, or with a final dot, are looked up as is.) + +Alternately, where you are setting the host name to be resolved, you can add a final dot at the end so that the +search path is not used, e.g. `datadog.monitoring.svc.cluster.local.` + + +### Note for Bottlerocket users + +If you are using Bottlerocket, you will want to uncomment the following from +`vaules.yaml` or add it to your `values` input: + +```yaml +criSocketPath: /run/dockershim.sock # Bottlerocket Only +env: # Bottlerocket Only + - name: DD_AUTOCONFIG_INCLUDE_FEATURES # Bottlerocket Only + value: "containerd" # Bottlerocket Only +``` + +See the [Datadog documentation](https://docs.datadoghq.com/containers/kubernetes/distributions/?tab=helm#EKS) for details. diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index b7102d4eb..b277baaad 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -177,16 +177,15 @@ https-checks: | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.7.0 | +| [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.9.1 | | [datadog\_cluster\_check\_yaml\_config](#module\_datadog\_cluster\_check\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | | [datadog\_configuration](#module\_datadog\_configuration) | ../../datadog-configuration/modules/datadog_keys | n/a | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [values\_merge](#module\_values\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | @@ -195,10 +194,7 @@ https-checks: | Name | Type | |------|------| -| [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | -| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | ## Inputs @@ -224,8 +220,6 @@ https-checks: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | -| [iam\_policy\_statements](#input\_iam\_policy\_statements) | IAM policy for the service account. Required if `var.iam_role_enabled` is `true`. This will not do variable replacements. Please see `var.iam_policy_statements_template_path`. | `any` | `{}` | no | -| [iam\_role\_enabled](#input\_iam\_role\_enabled) | Whether to create an IAM role. Setting this to `true` will also replace any occurrences of `{service_account_role_arn}` in `var.values_template_path` with the ARN of the IAM role created by this module. | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | diff --git a/modules/eks/datadog-agent/main.tf b/modules/eks/datadog-agent/main.tf index 8527e6011..0adc069c8 100644 --- a/modules/eks/datadog-agent/main.tf +++ b/modules/eks/datadog-agent/main.tf @@ -44,27 +44,6 @@ locals { value = yamlencode(cluster_check_value) } ] - - # This will match both datadog and datadog-cluster-agent service account names - service_account_name = "datadog*" - - partition = join("", data.aws_partition.current[*].partition) - account_id = join("", data.aws_caller_identity.current[*].account_id) - - iam_role_arn = "arn:${local.partition}:iam::${local.account_id}:role/${module.this.id}-${trimsuffix(local.service_account_name, "*")}@${var.kubernetes_namespace}" - - set_datadog_irsa = var.iam_role_enabled ? [ - { - name = "clusterAgent.rbac.serviceAccountAnnotations.eks\\.amazonaws\\.com/role-arn" - type = "string" - value = local.iam_role_arn - }, - { - name = "agents.rbac.serviceAccountAnnotations.eks\\.amazonaws\\.com/role-arn" - type = "string" - value = local.iam_role_arn - }, - ] : [] } module "datadog_configuration" { @@ -72,14 +51,6 @@ module "datadog_configuration" { context = module.this.context } -data "aws_caller_identity" "current" { - count = local.enabled ? 1 : 0 -} - -data "aws_partition" "current" { - count = local.enabled ? 1 : 0 -} - module "datadog_cluster_check_yaml_config" { count = local.cluster_checks_enabled ? 1 : 0 @@ -112,32 +83,25 @@ module "values_merge" { ] } -resource "kubernetes_namespace" "default" { - count = local.enabled && var.create_namespace ? 1 : 0 - - metadata { - name = var.kubernetes_namespace - - labels = local.tags - } -} module "datadog_agent" { source = "cloudposse/helm-release/aws" - version = "0.7.0" - - name = module.this.name - chart = var.chart - description = var.description - repository = var.repository - chart_version = var.chart_version - kubernetes_namespace = var.kubernetes_namespace # join("", kubernetes_namespace.default[*].id) - create_namespace = false - verify = var.verify - wait = var.wait - atomic = var.atomic - cleanup_on_fail = var.cleanup_on_fail - timeout = var.timeout + version = "0.9.1" + + name = module.this.name + chart = var.chart + description = var.description + repository = var.repository + chart_version = var.chart_version + + kubernetes_namespace = var.kubernetes_namespace + create_namespace_with_kubernetes = var.create_namespace + + verify = var.verify + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer @@ -169,21 +133,9 @@ module "datadog_agent" { type = "string" value = module.eks.outputs.eks_cluster_id }, - ], local.set_datadog_cluster_checks, local.set_datadog_irsa) - - iam_role_enabled = var.iam_role_enabled - - # Add the IAM role using set() - service_account_role_arn_annotation_enabled = false - - # Dictates which ServiceAccounts are allowed to assume the IAM Role. - service_account_name = local.service_account_name - service_account_namespace = var.kubernetes_namespace - - # IAM policy statements to add to the IAM role - iam_policy_statements = var.iam_policy_statements + ], local.set_datadog_cluster_checks) - depends_on = [kubernetes_namespace.default] + iam_role_enabled = false context = module.this.context } diff --git a/modules/eks/datadog-agent/remote-state.tf b/modules/eks/datadog-agent/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/datadog-agent/remote-state.tf +++ b/modules/eks/datadog-agent/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/datadog-agent/values.yaml b/modules/eks/datadog-agent/values.yaml index 757554c24..d07c5f943 100644 --- a/modules/eks/datadog-agent/values.yaml +++ b/modules/eks/datadog-agent/values.yaml @@ -1,16 +1,32 @@ +registry: public.ecr.aws/datadog datadog: logLevel: INFO + # If running on Bottlerocket OS, uncomment the following lines. + # See https://docs.datadoghq.com/containers/kubernetes/distributions/?tab=helm#EKS + # criSocketPath: /run/dockershim.sock # Bottlerocket Only + # env: # Bottlerocket Only + # - name: DD_AUTOCONFIG_INCLUDE_FEATURES # Bottlerocket Only + # value: "containerd" # Bottlerocket Only + + # kubeStateMetricsEnabled is false because the feature is obsolete (replaced by kubeStateMetricsCore). + # See https://github.com/DataDog/helm-charts/issues/415#issuecomment-943117608 + # https://docs.datadoghq.com/integrations/kubernetes_state_core/?tab=helm + # https://www.datadoghq.com/blog/kube-state-metrics-v2-monitoring-datadog/ kubeStateMetricsEnabled: false kubeStateMetricsCore: enabled: true + collectVpaMetrics: true + collectCrdMetrics: true collectEvents: true leaderElection: true + remoteConfiguration: + enabled: true logs: enabled: true containerCollectAll: true containerCollectUsingFiles: true apm: - enabled: false + enabled: true processAgent: enabled: true processCollection: true @@ -20,8 +36,10 @@ datadog: collectDNSStats: true enableConntrack: true bpfDebug: false - networkMonitoring: + orchestratorExplorer: enabled: true + networkMonitoring: + enabled: false clusterChecksRunner: enabled: false clusterChecks: @@ -31,32 +49,25 @@ datadog: nonLocalTraffic: true securityAgent: runtime: - enabled: true + enabled: false compliance: enabled: true helmCheck: enabled: true collectEvents: true -agents: - enabled: true - image: - repository: "public.ecr.aws/datadog/agent" - tag: 7 - tolerations: - - effect: NoSchedule - operator: Exists - - effect: NoExecute - operator: Exists - # Per https://github.com/DataDog/helm-charts/blob/main/charts/datadog/README.md#configuration-required-for-amazon-linux-2-based-nodes - podSecurity: - apparmor: - enabled: false clusterAgent: enabled: true + # Maintain 2 cluster agents so that there is no interruption in metrics collection + # when the cluster agents' node is being deprovisioned. + replicas: 2 + ## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config + ## without ndots: 2, DNS will try to resolve a DNS lookup 5 different ways + dnsConfig: + options: + - name: ndots + value: "2" image: - repository: "public.ecr.aws/datadog/cluster-agent" - tag: "7.43.1" - replicas: 1 + pullPolicy: IfNotPresent metricsProvider: enabled: false resources: @@ -66,3 +77,15 @@ clusterAgent: limits: cpu: 300m memory: 512Mi +agents: + priorityClassName: "system-node-critical" + ## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config + ## without ndots: 2, DNS will try to resolve a DNS lookup 5 different ways + dnsConfig: + options: + - name: ndots + value: "2" + # Per https://github.com/DataDog/helm-charts/blob/main/charts/datadog/README.md#configuration-required-for-amazon-linux-2-based-nodes + podSecurity: + apparmor: + enabled: false diff --git a/modules/eks/datadog-agent/variables.tf b/modules/eks/datadog-agent/variables.tf index f0a6720b3..89d16a61b 100644 --- a/modules/eks/datadog-agent/variables.tf +++ b/modules/eks/datadog-agent/variables.tf @@ -44,15 +44,3 @@ variable "values" { description = "Additional values to yamlencode as `helm_release` values." default = {} } - -variable "iam_role_enabled" { - type = bool - description = "Whether to create an IAM role. Setting this to `true` will also replace any occurrences of `{service_account_role_arn}` in `var.values_template_path` with the ARN of the IAM role created by this module." - default = false -} - -variable "iam_policy_statements" { - type = any - description = "IAM policy for the service account. Required if `var.iam_role_enabled` is `true`. This will not do variable replacements. Please see `var.iam_policy_statements_template_path`." - default = {} -} From 7ce51d76fb1c3e1e1ef9f707044544b994d9d58e Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Thu, 17 Aug 2023 00:48:51 -0500 Subject: [PATCH 227/501] Update api-gateway-account-settings README.md (#819) --- modules/api-gateway-rest-api/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/api-gateway-rest-api/README.md b/modules/api-gateway-rest-api/README.md index 5e475a297..192897ace 100644 --- a/modules/api-gateway-rest-api/README.md +++ b/modules/api-gateway-rest-api/README.md @@ -1,4 +1,4 @@ -# Component: `api-gateway-account-settings` +# Component: `api-gateway-rest-api` This component is responsible for deploying an API Gateway REST API. ## Usage From 2d1f200023f04ba078b76e5527ed6c79c3b97b14 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Thu, 17 Aug 2023 00:49:26 -0500 Subject: [PATCH 228/501] Update cloudposse/utils/aws to 1.3.0 (#815) Co-authored-by: cloudpossebot Co-authored-by: Dan Miller Co-authored-by: Dan Miller --- modules/aws-config/README.md | 2 +- modules/aws-config/main.tf | 2 +- modules/datadog-configuration/modules/datadog_keys/README.md | 2 +- modules/datadog-configuration/modules/datadog_keys/main.tf | 2 +- modules/dns-delegated/README.md | 2 +- modules/dns-delegated/main.tf | 2 +- modules/spa-s3-cloudfront/README.md | 2 +- modules/spa-s3-cloudfront/failover.tf | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index f5578c185..1d6f5eb0c 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -138,7 +138,7 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} | [global\_collector\_region](#module\_global\_collector\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [utils](#module\_utils) | cloudposse/utils/aws | 1.1.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | ## Resources diff --git a/modules/aws-config/main.tf b/modules/aws-config/main.tf index 8728f91ba..8b58c0077 100644 --- a/modules/aws-config/main.tf +++ b/modules/aws-config/main.tf @@ -35,7 +35,7 @@ module "aws_config_label" { module "utils" { source = "cloudposse/utils/aws" - version = "1.1.0" + version = "1.3.0" context = module.this.context } diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index 18f007977..aef4a29bb 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -41,7 +41,7 @@ provider "datadog" { | [datadog\_configuration](#module\_datadog\_configuration) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [utils\_example\_complete](#module\_utils\_example\_complete) | cloudposse/utils/aws | 1.1.0 | +| [utils\_example\_complete](#module\_utils\_example\_complete) | cloudposse/utils/aws | 1.3.0 | ## Resources diff --git a/modules/datadog-configuration/modules/datadog_keys/main.tf b/modules/datadog-configuration/modules/datadog_keys/main.tf index edf24e52e..d75ab3b26 100644 --- a/modules/datadog-configuration/modules/datadog_keys/main.tf +++ b/modules/datadog-configuration/modules/datadog_keys/main.tf @@ -11,7 +11,7 @@ module "always" { module "utils_example_complete" { source = "cloudposse/utils/aws" - version = "1.1.0" + version = "1.3.0" } locals { diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index 91118b2ef..12da2bd0f 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -143,7 +143,7 @@ Takeaway | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [utils](#module\_utils) | cloudposse/utils/aws | 1.1.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | ## Resources diff --git a/modules/dns-delegated/main.tf b/modules/dns-delegated/main.tf index 511bfc2ea..57df4037f 100644 --- a/modules/dns-delegated/main.tf +++ b/modules/dns-delegated/main.tf @@ -55,7 +55,7 @@ resource "aws_route53_zone" "private" { module "utils" { source = "cloudposse/utils/aws" - version = "1.1.0" + version = "1.3.0" } resource "aws_route53_zone_association" "secondary" { diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index d67935e36..006e4f714 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -122,7 +122,7 @@ an extensive explanation for how these preview environments work. | [lambda\_edge\_redirect\_404](#module\_lambda\_edge\_redirect\_404) | ./modules/lambda_edge_redirect_404 | n/a | | [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.92.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [utils](#module\_utils) | cloudposse/utils/aws | 0.8.1 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | | [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/spa-s3-cloudfront/failover.tf b/modules/spa-s3-cloudfront/failover.tf index 090a95d64..51b84205c 100644 --- a/modules/spa-s3-cloudfront/failover.tf +++ b/modules/spa-s3-cloudfront/failover.tf @@ -6,7 +6,7 @@ locals { module "utils" { source = "cloudposse/utils/aws" - version = "0.8.1" + version = "1.3.0" } data "aws_s3_bucket" "failover_bucket" { From 9b41939cfdfee8d28bb8e62bbcf104c4e95d3b83 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Thu, 17 Aug 2023 11:52:35 -0400 Subject: [PATCH 229/501] Update `remote-states` modules to the latest version (#820) Co-authored-by: cloudpossebot --- Dockerfile | 4 ++-- LICENSE | 2 +- modules/account-map/README.md | 6 +++--- modules/account-map/modules/iam-roles/main.tf | 2 +- .../account-map/modules/roles-to-principals/main.tf | 2 +- .../modules/team-assume-role-policy/README.md | 2 +- .../github-assume-role-policy.mixin.tf | 2 +- modules/account-map/remote-state.tf | 2 +- modules/account-map/versions.tf | 2 +- modules/acm/README.md | 4 ++-- modules/acm/remote-state.tf | 4 ++-- modules/alb/README.md | 6 +++--- modules/alb/remote-state.tf | 6 +++--- modules/amplify/README.md | 2 +- modules/amplify/remote-state.tf | 2 +- modules/api-gateway-rest-api/README.md | 6 +++--- modules/api-gateway-rest-api/remote-state.tf | 6 +++--- modules/athena/README.md | 4 ++-- modules/athena/remote-state.tf | 4 ++-- modules/aurora-mysql-resources/README.md | 2 +- modules/aurora-mysql-resources/remote-state.tf | 2 +- modules/aurora-mysql/README.md | 10 +++++----- modules/aurora-mysql/remote-state.tf | 10 +++++----- modules/aurora-postgres-resources/README.md | 2 +- modules/aurora-postgres-resources/remote-state.tf | 2 +- modules/aurora-postgres/README.md | 8 ++++---- modules/aurora-postgres/remote-state.tf | 8 ++++---- modules/aws-config/README.md | 8 ++++---- modules/aws-config/remote-state.tf | 8 ++++---- modules/aws-sso/README.md | 4 ++-- modules/aws-sso/policy-TerraformUpdateAccess.tf | 2 +- modules/aws-sso/remote-state.tf | 2 +- modules/aws-team-roles/README.md | 2 +- modules/aws-team-roles/remote-state.tf | 2 +- modules/aws-teams/README.md | 4 ++-- modules/aws-teams/remote-state.tf | 4 ++-- modules/bastion/README.md | 2 +- modules/bastion/remote-state.tf | 2 +- modules/cloudtrail/README.md | 4 ++-- modules/cloudtrail/remote-state.tf | 4 ++-- .../modules/datadog_keys/README.md | 2 +- .../datadog-configuration/modules/datadog_keys/main.tf | 2 +- modules/datadog-private-location-ecs/README.md | 4 ++-- modules/datadog-private-location-ecs/remote-state.tf | 4 ++-- modules/dms/replication-instance/README.md | 2 +- modules/dms/replication-instance/remote-state.tf | 2 +- modules/dms/replication-task/README.md | 6 +++--- modules/dms/replication-task/remote-state.tf | 6 +++--- modules/dns-delegated/README.md | 4 ++-- modules/dns-delegated/remote-state.tf | 4 ++-- modules/documentdb/README.md | 8 ++++---- modules/documentdb/remote-state.tf | 8 ++++---- modules/ec2-client-vpn/README.md | 2 +- modules/ec2-client-vpn/remote-state.tf | 2 +- modules/ecs/README.md | 4 ++-- modules/ecs/remote-state.tf | 4 ++-- modules/efs/README.md | 6 +++--- modules/efs/remote-state.tf | 6 +++--- modules/eks/actions-runner-controller/README.md | 2 +- modules/eks/actions-runner-controller/remote-state.tf | 2 +- modules/eks/alb-controller-ingress-class/README.md | 2 +- .../eks/alb-controller-ingress-class/remote-state.tf | 2 +- modules/eks/alb-controller-ingress-group/README.md | 8 ++++---- .../eks/alb-controller-ingress-group/remote-state.tf | 8 ++++---- modules/eks/alb-controller/README.md | 2 +- modules/eks/alb-controller/remote-state.tf | 2 +- modules/eks/argocd/README.md | 8 ++++---- modules/eks/argocd/remote-state.tf | 8 ++++---- modules/eks/aws-node-termination-handler/README.md | 2 +- .../eks/aws-node-termination-handler/remote-state.tf | 2 +- modules/eks/cert-manager/README.md | 4 ++-- modules/eks/cert-manager/remote-state.tf | 4 ++-- modules/eks/datadog-agent/README.md | 2 +- modules/eks/datadog-agent/versions.tf | 2 +- modules/eks/echo-server/README.md | 4 ++-- modules/eks/echo-server/remote-state.tf | 4 ++-- modules/eks/external-dns/README.md | 6 +++--- modules/eks/external-dns/remote-state.tf | 6 +++--- modules/eks/external-secrets-operator/README.md | 4 ++-- modules/eks/external-secrets-operator/remote-state.tf | 4 ++-- modules/eks/idp-roles/README.md | 2 +- modules/eks/idp-roles/remote-state.tf | 2 +- modules/eks/karpenter-provisioner/README.md | 4 ++-- modules/eks/karpenter-provisioner/remote-state.tf | 4 ++-- modules/eks/karpenter/README.md | 2 +- modules/eks/karpenter/remote-state.tf | 2 +- modules/eks/metrics-server/README.md | 2 +- modules/eks/metrics-server/remote-state.tf | 2 +- modules/eks/platform/README.md | 4 ++-- modules/eks/platform/remote-state.tf | 4 ++-- modules/eks/redis-operator/README.md | 2 +- modules/eks/redis-operator/remote-state.tf | 2 +- modules/eks/redis/README.md | 2 +- modules/eks/redis/remote-state.tf | 2 +- modules/eks/reloader/README.md | 2 +- modules/eks/reloader/remote-state.tf | 2 +- modules/elasticache-redis/README.md | 8 ++++---- modules/elasticache-redis/remote-state.tf | 8 ++++---- modules/elasticsearch/README.md | 4 ++-- modules/elasticsearch/remote-state.tf | 4 ++-- modules/github-runners/README.md | 4 ++-- modules/github-runners/remote-state.tf | 4 ++-- modules/gitops/README.md | 4 ++-- modules/gitops/remote-state.tf | 4 ++-- modules/global-accelerator-endpoint-group/README.md | 2 +- .../global-accelerator-endpoint-group/remote-state.tf | 2 +- modules/global-accelerator/README.md | 2 +- modules/global-accelerator/remote-state.tf | 2 +- modules/guardduty/README.md | 4 ++-- modules/guardduty/remote-state.tf | 4 ++-- modules/mq-broker/README.md | 6 +++--- modules/mq-broker/remote-state.tf | 4 ++-- modules/mq-broker/versions.tf | 2 +- modules/mwaa/README.md | 4 ++-- modules/mwaa/remote-state.tf | 4 ++-- modules/network-firewall/README.md | 6 +++--- modules/network-firewall/remote-state.tf | 6 +++--- modules/rds/README.md | 6 +++--- modules/rds/remote-state.tf | 6 +++--- modules/redshift/README.md | 2 +- modules/redshift/remote-state.tf | 2 +- modules/route53-resolver-dns-firewall/README.md | 4 ++-- modules/route53-resolver-dns-firewall/remote-state.tf | 4 ++-- modules/s3-bucket/README.md | 2 +- modules/s3-bucket/remote-state.tf | 2 +- modules/security-hub/README.md | 2 +- modules/security-hub/remote-state.tf | 2 +- modules/ses/README.md | 2 +- modules/ses/remote-state.tf | 2 +- modules/sftp/README.md | 2 +- modules/sftp/remote-state.tf | 2 +- modules/snowflake-account/README.md | 2 +- modules/snowflake-account/remote-state.tf | 2 +- modules/snowflake-database/README.md | 2 +- modules/snowflake-database/remote-state.tf | 2 +- modules/spacelift/admin-stack/README.md | 2 +- modules/spacelift/admin-stack/remote-state.tf | 2 +- modules/spacelift/worker-pool/README.md | 8 ++++---- modules/spacelift/worker-pool/remote-state.tf | 8 ++++---- modules/tgw/cross-region-hub-connector/README.md | 2 +- modules/tgw/cross-region-hub-connector/versions.tf | 2 +- modules/vpc-peering/README.md | 2 +- modules/vpc-peering/remote-state.tf | 2 +- modules/vpc/README.md | 2 +- modules/vpc/remote-state.tf | 2 +- modules/waf/README.md | 2 +- modules/waf/remote-state.tf | 2 +- modules/zscaler/README.md | 2 +- modules/zscaler/versions.tf | 2 +- 149 files changed, 270 insertions(+), 270 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8f7e1a98f..9063ba7f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,3 @@ FROM scratch -COPY aws/ /aws -WORKDIR /aws +COPY modules/ /modules +WORKDIR /modules diff --git a/LICENSE b/LICENSE index 4bd1946f1..7afefb95c 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2018-2022 Cloud Posse, LLC + Copyright 2018-2023 Cloud Posse, 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/modules/account-map/README.md b/modules/account-map/README.md index be6dfcba2..9b619c21d 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -55,7 +55,7 @@ components: | [terraform](#requirement\_terraform) | >= 1.2.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [local](#requirement\_local) | >= 1.3 | -| [utils](#requirement\_utils) | >= 1.8.0 | +| [utils](#requirement\_utils) | >= 1.10.0 | ## Providers @@ -63,13 +63,13 @@ components: |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | | [local](#provider\_local) | >= 1.3 | -| [utils](#provider\_utils) | >= 1.8.0 | +| [utils](#provider\_utils) | >= 1.10.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [accounts](#module\_accounts) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [accounts](#module\_accounts) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [atmos](#module\_atmos) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/account-map/modules/iam-roles/main.tf b/modules/account-map/modules/iam-roles/main.tf index fbf93b3d7..0e17d4f18 100644 --- a/modules/account-map/modules/iam-roles/main.tf +++ b/modules/account-map/modules/iam-roles/main.tf @@ -17,7 +17,7 @@ module "always" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "account-map" privileged = var.privileged diff --git a/modules/account-map/modules/roles-to-principals/main.tf b/modules/account-map/modules/roles-to-principals/main.tf index 51386707d..94aba013a 100644 --- a/modules/account-map/modules/roles-to-principals/main.tf +++ b/modules/account-map/modules/roles-to-principals/main.tf @@ -10,7 +10,7 @@ module "always" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "account-map" privileged = var.privileged diff --git a/modules/account-map/modules/team-assume-role-policy/README.md b/modules/account-map/modules/team-assume-role-policy/README.md index b2644e981..7a22501fc 100644 --- a/modules/account-map/modules/team-assume-role-policy/README.md +++ b/modules/account-map/modules/team-assume-role-policy/README.md @@ -47,7 +47,7 @@ No requirements. |------|--------|---------| | [allowed\_role\_map](#module\_allowed\_role\_map) | ../../../account-map/modules/roles-to-principals | n/a | | [denied\_role\_map](#module\_denied\_role\_map) | ../../../account-map/modules/roles-to-principals | n/a | -| [github\_oidc\_provider](#module\_github\_oidc\_provider) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [github\_oidc\_provider](#module\_github\_oidc\_provider) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf b/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf index 4449e5a37..18004e16f 100644 --- a/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf +++ b/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf @@ -60,7 +60,7 @@ module "github_oidc_provider" { count = local.github_oidc_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "github-oidc-provider" environment = var.global_environment_name diff --git a/modules/account-map/remote-state.tf b/modules/account-map/remote-state.tf index 61856c5ec..c9dfaa884 100644 --- a/modules/account-map/remote-state.tf +++ b/modules/account-map/remote-state.tf @@ -1,6 +1,6 @@ module "accounts" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "account" privileged = true diff --git a/modules/account-map/versions.tf b/modules/account-map/versions.tf index cf42bf06e..98fe82089 100644 --- a/modules/account-map/versions.tf +++ b/modules/account-map/versions.tf @@ -12,7 +12,7 @@ terraform { } utils = { source = "cloudposse/utils" - version = ">= 1.8.0" + version = ">= 1.10.0" } } } diff --git a/modules/acm/README.md b/modules/acm/README.md index 88c72972e..24d15500d 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -69,9 +69,9 @@ components: | Name | Source | Version | |------|--------|---------| | [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.16.0 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/acm/remote-state.tf b/modules/acm/remote-state.tf index 888007f58..937168330 100644 --- a/modules/acm/remote-state.tf +++ b/modules/acm/remote-state.tf @@ -1,6 +1,6 @@ module "private_ca" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" count = local.private_ca_enabled ? 1 : 0 @@ -13,7 +13,7 @@ module "private_ca" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.dns_delegated_component_name stage = var.dns_delegated_stage_name diff --git a/modules/alb/README.md b/modules/alb/README.md index 1389f912f..a6f2651ad 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -34,12 +34,12 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [acm](#module\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [acm](#module\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [alb](#module\_alb) | cloudposse/alb/aws | 1.10.0 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/alb/remote-state.tf b/modules/alb/remote-state.tf index a6a9ab35c..d884a3f42 100644 --- a/modules/alb/remote-state.tf +++ b/modules/alb/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.vpc_component_name @@ -9,7 +9,7 @@ module "vpc" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.dns_delegated_component_name environment = coalesce(var.dns_delegated_environment_name, module.iam_roles.global_environment_name) @@ -29,7 +29,7 @@ module "dns_delegated" { module "acm" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.acm_component_name diff --git a/modules/amplify/README.md b/modules/amplify/README.md index 03b7b729f..50e5b12f5 100644 --- a/modules/amplify/README.md +++ b/modules/amplify/README.md @@ -152,7 +152,7 @@ atmos terraform apply amplify/example -s |------|--------|---------| | [amplify\_app](#module\_amplify\_app) | cloudposse/amplify-app/aws | 0.2.1 | | [certificate\_verification\_dns\_record](#module\_certificate\_verification\_dns\_record) | cloudposse/route53-cluster-hostname/aws | 0.12.3 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [subdomains\_dns\_record](#module\_subdomains\_dns\_record) | cloudposse/route53-cluster-hostname/aws | 0.12.3 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/amplify/remote-state.tf b/modules/amplify/remote-state.tf index 83eb0c6e8..55f410fb5 100644 --- a/modules/amplify/remote-state.tf +++ b/modules/amplify/remote-state.tf @@ -1,6 +1,6 @@ module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.dns_delegated_component_name environment = var.dns_delegated_environment_name diff --git a/modules/api-gateway-rest-api/README.md b/modules/api-gateway-rest-api/README.md index 192897ace..d2e496445 100644 --- a/modules/api-gateway-rest-api/README.md +++ b/modules/api-gateway-rest-api/README.md @@ -54,13 +54,13 @@ components: | Name | Source | Version | |------|--------|---------| -| [acm](#module\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [acm](#module\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [api\_gateway\_rest\_api](#module\_api\_gateway\_rest\_api) | cloudposse/api-gateway/aws | 0.3.1 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [nlb](#module\_nlb) | cloudposse/nlb/aws | 0.12.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/api-gateway-rest-api/remote-state.tf b/modules/api-gateway-rest-api/remote-state.tf index fde544bb2..5f5b11668 100644 --- a/modules/api-gateway-rest-api/remote-state.tf +++ b/modules/api-gateway-rest-api/remote-state.tf @@ -1,6 +1,6 @@ module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = module.iam_roles.global_environment_name @@ -10,7 +10,7 @@ module "dns_delegated" { module "acm" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "acm" ignore_errors = true @@ -20,7 +20,7 @@ module "acm" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" diff --git a/modules/athena/README.md b/modules/athena/README.md index e8e234230..8b2e27ad6 100644 --- a/modules/athena/README.md +++ b/modules/athena/README.md @@ -129,9 +129,9 @@ component | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [athena](#module\_athena) | cloudposse/athena/aws | 0.1.1 | -| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/athena/remote-state.tf b/modules/athena/remote-state.tf index 7b286e18a..fd8ea7824 100644 --- a/modules/athena/remote-state.tf +++ b/modules/athena/remote-state.tf @@ -2,7 +2,7 @@ module "cloudtrail_bucket" { count = local.cloudtrail_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.cloudtrail_bucket_component_name @@ -11,7 +11,7 @@ module "cloudtrail_bucket" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "account-map" tenant = module.iam_roles.global_tenant_name diff --git a/modules/aurora-mysql-resources/README.md b/modules/aurora-mysql-resources/README.md index bb13ac3cd..3f5a8318b 100644 --- a/modules/aurora-mysql-resources/README.md +++ b/modules/aurora-mysql-resources/README.md @@ -71,7 +71,7 @@ components: |------|--------|---------| | [additional\_grants](#module\_additional\_grants) | ./modules/mysql-user | n/a | | [additional\_users](#module\_additional\_users) | ./modules/mysql-user | n/a | -| [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aurora-mysql-resources/remote-state.tf b/modules/aurora-mysql-resources/remote-state.tf index 921a71b2e..33f457aca 100644 --- a/modules/aurora-mysql-resources/remote-state.tf +++ b/modules/aurora-mysql-resources/remote-state.tf @@ -1,6 +1,6 @@ module "aurora_mysql" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.aurora_mysql_component_name diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index e4f5d17d0..dcf250a28 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -171,15 +171,15 @@ Reploying the component should show no changes. For example, `atmos terraform ap |------|--------|---------| | [aurora\_mysql](#module\_aurora\_mysql) | cloudposse/rds-cluster/aws | 1.3.1 | | [cluster](#module\_cluster) | cloudposse/label/null | 0.25.0 | -| [dns-delegated](#module\_dns-delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns-delegated](#module\_dns-delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | | [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | -| [primary\_cluster](#module\_primary\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [primary\_cluster](#module\_primary\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/aurora-mysql/remote-state.tf b/modules/aurora-mysql/remote-state.tf index c48a82448..f910e288c 100644 --- a/modules/aurora-mysql/remote-state.tf +++ b/modules/aurora-mysql/remote-state.tf @@ -4,7 +4,7 @@ locals { module "dns-delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = "gbl" @@ -14,7 +14,7 @@ module "dns-delegated" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = var.eks_component_names @@ -25,7 +25,7 @@ module "eks" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -34,7 +34,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = local.accounts_with_vpc @@ -49,7 +49,7 @@ module "vpc_ingress" { module "primary_cluster" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" count = local.remote_read_replica_enabled ? 1 : 0 diff --git a/modules/aurora-postgres-resources/README.md b/modules/aurora-postgres-resources/README.md index 5e265ee77..8c4aa09e1 100644 --- a/modules/aurora-postgres-resources/README.md +++ b/modules/aurora-postgres-resources/README.md @@ -47,7 +47,7 @@ components: |------|--------|---------| | [additional\_grants](#module\_additional\_grants) | ./modules/postgresql-user | n/a | | [additional\_users](#module\_additional\_users) | ./modules/postgresql-user | n/a | -| [aurora\_postgres](#module\_aurora\_postgres) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [aurora\_postgres](#module\_aurora\_postgres) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aurora-postgres-resources/remote-state.tf b/modules/aurora-postgres-resources/remote-state.tf index 2f18e4fcf..f4cebbc63 100644 --- a/modules/aurora-postgres-resources/remote-state.tf +++ b/modules/aurora-postgres-resources/remote-state.tf @@ -1,6 +1,6 @@ module "aurora_postgres" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.aurora_postgres_component_name diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 6f83bca04..d45a6c416 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -103,14 +103,14 @@ components: |------|--------|---------| | [aurora\_postgres\_cluster](#module\_aurora\_postgres\_cluster) | cloudposse/rds-cluster/aws | 1.3.2 | | [cluster](#module\_cluster) | cloudposse/label/null | 0.25.0 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | | [parameter\_store\_write](#module\_parameter\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/aurora-postgres/remote-state.tf b/modules/aurora-postgres/remote-state.tf index 1ee16d716..74c0a1cac 100644 --- a/modules/aurora-postgres/remote-state.tf +++ b/modules/aurora-postgres/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = { for i, account in var.allow_ingress_from_vpc_accounts : @@ -27,7 +27,7 @@ module "vpc_ingress" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = local.eks_security_group_enabled ? var.eks_component_names : toset([]) component = each.value @@ -38,7 +38,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = var.dns_gbl_delegated_environment_name diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index 1d6f5eb0c..8f342705e 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -129,13 +129,13 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [aws\_config](#module\_aws\_config) | cloudposse/config/aws | 0.17.0 | | [aws\_config\_label](#module\_aws\_config\_label) | cloudposse/label/null | 0.25.0 | -| [aws\_team\_roles](#module\_aws\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | -| [config\_bucket](#module\_config\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [aws\_team\_roles](#module\_aws\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [config\_bucket](#module\_config\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [conformance\_pack](#module\_conformance\_pack) | cloudposse/config/aws//modules/conformance-pack | 0.17.0 | -| [global\_collector\_region](#module\_global\_collector\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [global\_collector\_region](#module\_global\_collector\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | diff --git a/modules/aws-config/remote-state.tf b/modules/aws-config/remote-state.tf index 9155fec51..8dad91122 100644 --- a/modules/aws-config/remote-state.tf +++ b/modules/aws-config/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "account-map" tenant = (var.account_map_tenant != "") ? var.account_map_tenant : module.this.tenant @@ -13,7 +13,7 @@ module "account_map" { module "config_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "config-bucket" tenant = (var.config_bucket_tenant != "") ? var.config_bucket_tenant : module.this.tenant @@ -26,7 +26,7 @@ module "config_bucket" { module "global_collector_region" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" count = !local.enabled || local.is_global_collector_region ? 0 : 1 @@ -40,7 +40,7 @@ module "global_collector_region" { module "aws_team_roles" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "aws-team-roles" environment = var.iam_roles_environment_name diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index 51351f100..2d5e26ad1 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -164,14 +164,14 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [iam\_roles\_root](#module\_iam\_roles\_root) | ../account-map/modules/iam-roles | n/a | | [permission\_sets](#module\_permission\_sets) | cloudposse/sso/aws//modules/permission-sets | 1.1.1 | | [role\_map](#module\_role\_map) | ../account-map/modules/roles-to-principals | n/a | | [sso\_account\_assignments](#module\_sso\_account\_assignments) | cloudposse/sso/aws//modules/account-assignments | 1.1.1 | | [sso\_account\_assignments\_root](#module\_sso\_account\_assignments\_root) | cloudposse/sso/aws//modules/account-assignments | 1.1.1 | -| [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [tfstate](#module\_tfstate) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/aws-sso/policy-TerraformUpdateAccess.tf b/modules/aws-sso/policy-TerraformUpdateAccess.tf index c9ad00d9f..095d64d5e 100644 --- a/modules/aws-sso/policy-TerraformUpdateAccess.tf +++ b/modules/aws-sso/policy-TerraformUpdateAccess.tf @@ -10,7 +10,7 @@ locals { module "tfstate" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" bypass = !local.tf_update_access_enabled diff --git a/modules/aws-sso/remote-state.tf b/modules/aws-sso/remote-state.tf index b6edc1678..3e818ad3f 100644 --- a/modules/aws-sso/remote-state.tf +++ b/modules/aws-sso/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = "account-map" environment = module.iam_roles.global_environment_name diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index 789e4ced0..67d6908cc 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -177,7 +177,7 @@ components: | Name | Source | Version | |------|--------|---------| | [assume\_role](#module\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | -| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aws-team-roles/remote-state.tf b/modules/aws-team-roles/remote-state.tf index 5e795c6e1..87a5d8af2 100644 --- a/modules/aws-team-roles/remote-state.tf +++ b/modules/aws-team-roles/remote-state.tf @@ -1,6 +1,6 @@ module "aws_saml" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "aws-saml" privileged = true diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index 90b360deb..9eeb06754 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -148,9 +148,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [assume\_role](#module\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | -| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [aws\_saml](#module\_aws\_saml) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aws-teams/remote-state.tf b/modules/aws-teams/remote-state.tf index ec9dc956e..bb8c8f1b3 100644 --- a/modules/aws-teams/remote-state.tf +++ b/modules/aws-teams/remote-state.tf @@ -1,6 +1,6 @@ module "aws_saml" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "aws-saml" privileged = true @@ -16,7 +16,7 @@ module "aws_saml" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "account-map" tenant = module.iam_roles.global_tenant_name diff --git a/modules/bastion/README.md b/modules/bastion/README.md index 967c36b51..b0c991c36 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -78,7 +78,7 @@ components: | [sg](#module\_sg) | cloudposse/security-group/aws | 2.2.0 | | [ssm\_tls\_ssh\_key\_pair](#module\_ssm\_tls\_ssh\_key\_pair) | cloudposse/ssm-tls-ssh-key-pair/aws | 0.10.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/bastion/remote-state.tf b/modules/bastion/remote-state.tf index 3e0ccd51e..757ef9067 100644 --- a/modules/bastion/remote-state.tf +++ b/modules/bastion/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" diff --git a/modules/cloudtrail/README.md b/modules/cloudtrail/README.md index 80761fee8..e4b702562 100644 --- a/modules/cloudtrail/README.md +++ b/modules/cloudtrail/README.md @@ -45,9 +45,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [cloudtrail](#module\_cloudtrail) | cloudposse/cloudtrail/aws | 0.21.0 | -| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [cloudtrail\_bucket](#module\_cloudtrail\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_cloudtrail](#module\_kms\_key\_cloudtrail) | cloudposse/kms-key/aws | 0.12.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/cloudtrail/remote-state.tf b/modules/cloudtrail/remote-state.tf index d09bf7925..78a54796b 100644 --- a/modules/cloudtrail/remote-state.tf +++ b/modules/cloudtrail/remote-state.tf @@ -1,6 +1,6 @@ module "cloudtrail_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = var.cloudtrail_bucket_component_name environment = var.cloudtrail_bucket_environment_name @@ -11,7 +11,7 @@ module "cloudtrail_bucket" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "account-map" tenant = module.iam_roles.global_tenant_name diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index aef4a29bb..cc930cb2f 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -38,7 +38,7 @@ provider "datadog" { | Name | Source | Version | |------|--------|---------| | [always](#module\_always) | cloudposse/label/null | 0.25.0 | -| [datadog\_configuration](#module\_datadog\_configuration) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [datadog\_configuration](#module\_datadog\_configuration) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils\_example\_complete](#module\_utils\_example\_complete) | cloudposse/utils/aws | 1.3.0 | diff --git a/modules/datadog-configuration/modules/datadog_keys/main.tf b/modules/datadog-configuration/modules/datadog_keys/main.tf index d75ab3b26..7fcfcd54a 100644 --- a/modules/datadog-configuration/modules/datadog_keys/main.tf +++ b/modules/datadog-configuration/modules/datadog_keys/main.tf @@ -27,7 +27,7 @@ locals { module "datadog_configuration" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "datadog-configuration" diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index e8d9031a5..5e50c68c4 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -80,11 +80,11 @@ components: | [container\_definition](#module\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | | [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.66.2 | -| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [roles\_to\_principals](#module\_roles\_to\_principals) | ../account-map/modules/roles-to-principals | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/datadog-private-location-ecs/remote-state.tf b/modules/datadog-private-location-ecs/remote-state.tf index d732e096c..131679f57 100644 --- a/modules/datadog-private-location-ecs/remote-state.tf +++ b/modules/datadog-private-location-ecs/remote-state.tf @@ -11,7 +11,7 @@ locals { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -20,7 +20,7 @@ module "vpc" { module "ecs_cluster" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "ecs" diff --git a/modules/dms/replication-instance/README.md b/modules/dms/replication-instance/README.md index 6e041774a..8eeb64017 100644 --- a/modules/dms/replication-instance/README.md +++ b/modules/dms/replication-instance/README.md @@ -62,7 +62,7 @@ No providers. | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 1.0.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/dms/replication-instance/remote-state.tf b/modules/dms/replication-instance/remote-state.tf index 3e0ccd51e..757ef9067 100644 --- a/modules/dms/replication-instance/remote-state.tf +++ b/modules/dms/replication-instance/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" diff --git a/modules/dms/replication-task/README.md b/modules/dms/replication-task/README.md index b67f30c36..7b7c4dbbc 100644 --- a/modules/dms/replication-task/README.md +++ b/modules/dms/replication-task/README.md @@ -53,9 +53,9 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [dms\_endpoint\_source](#module\_dms\_endpoint\_source) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [dms\_endpoint\_target](#module\_dms\_endpoint\_target) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [dms\_replication\_instance](#module\_dms\_replication\_instance) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dms\_endpoint\_source](#module\_dms\_endpoint\_source) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [dms\_endpoint\_target](#module\_dms\_endpoint\_target) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [dms\_replication\_instance](#module\_dms\_replication\_instance) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [dms\_replication\_task](#module\_dms\_replication\_task) | cloudposse/dms/aws//modules/dms-replication-task | 0.1.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/dms/replication-task/remote-state.tf b/modules/dms/replication-task/remote-state.tf index 72b46e4bf..fdbd10b07 100644 --- a/modules/dms/replication-task/remote-state.tf +++ b/modules/dms/replication-task/remote-state.tf @@ -1,6 +1,6 @@ module "dms_replication_instance" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.replication_instance_component_name @@ -9,7 +9,7 @@ module "dms_replication_instance" { module "dms_endpoint_source" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.source_endpoint_component_name @@ -18,7 +18,7 @@ module "dms_endpoint_source" { module "dms_endpoint_target" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.target_endpoint_component_name diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index 12da2bd0f..d88cc4231 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -141,10 +141,10 @@ Takeaway |------|--------|---------| | [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.17.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/dns-delegated/remote-state.tf b/modules/dns-delegated/remote-state.tf index 02dc50a8d..e920505e8 100644 --- a/modules/dns-delegated/remote-state.tf +++ b/modules/dns-delegated/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = local.private_enabled ? local.vpc_environment_names : toset([]) @@ -12,7 +12,7 @@ module "vpc" { module "private_ca" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" count = local.private_ca_enabled && local.certificate_enabled ? 1 : 0 diff --git a/modules/documentdb/README.md b/modules/documentdb/README.md index 1af510394..5dee96a71 100644 --- a/modules/documentdb/README.md +++ b/modules/documentdb/README.md @@ -44,13 +44,13 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [documentdb\_cluster](#module\_documentdb\_cluster) | cloudposse/documentdb-cluster/aws | 0.14.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/documentdb/remote-state.tf b/modules/documentdb/remote-state.tf index e4fe005b9..68a715244 100644 --- a/modules/documentdb/remote-state.tf +++ b/modules/documentdb/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "eks" @@ -24,7 +24,7 @@ module "eks" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" @@ -33,7 +33,7 @@ module "dns_delegated" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" stack_config_local_path = "../../../stacks" component = "dns-delegated" diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index b56e5561a..e71633a2c 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -103,7 +103,7 @@ No providers. | [ec2\_client\_vpn](#module\_ec2\_client\_vpn) | cloudposse/ec2-client-vpn/aws | 0.14.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/ec2-client-vpn/remote-state.tf b/modules/ec2-client-vpn/remote-state.tf index 3e0ccd51e..757ef9067 100644 --- a/modules/ec2-client-vpn/remote-state.tf +++ b/modules/ec2-client-vpn/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" diff --git a/modules/ecs/README.md b/modules/ecs/README.md index dba55187a..f0997eb34 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -57,11 +57,11 @@ components: |------|--------|---------| | [alb](#module\_alb) | cloudposse/alb/aws | 1.5.0 | | [cluster](#module\_cluster) | cloudposse/ecs-cluster/aws | 0.4.1 | -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [target\_group\_label](#module\_target\_group\_label) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/ecs/remote-state.tf b/modules/ecs/remote-state.tf index 1323cd151..db002a31c 100644 --- a/modules/ecs/remote-state.tf +++ b/modules/ecs/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.dns_delegated_component_name stage = var.dns_delegated_stage_name diff --git a/modules/efs/README.md b/modules/efs/README.md index afb1522c2..f43e3ec2e 100644 --- a/modules/efs/README.md +++ b/modules/efs/README.md @@ -38,12 +38,12 @@ components: | Name | Source | Version | |------|--------|---------| | [efs](#module\_efs) | cloudposse/efs/aws | 0.32.7 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [gbl\_dns\_delegated](#module\_gbl\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [gbl\_dns\_delegated](#module\_gbl\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_efs](#module\_kms\_key\_efs) | cloudposse/kms-key/aws | 0.12.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/efs/remote-state.tf b/modules/efs/remote-state.tf index 1c7fa8b1f..57f2055b5 100644 --- a/modules/efs/remote-state.tf +++ b/modules/efs/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = local.eks_security_group_enabled ? var.eks_component_names : toset([]) @@ -20,7 +20,7 @@ module "eks" { module "gbl_dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = "gbl" diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 020254e63..af8be190b 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -416,7 +416,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller |------|--------|---------| | [actions\_runner](#module\_actions\_runner) | cloudposse/helm-release/aws | 0.7.0 | | [actions\_runner\_controller](#module\_actions\_runner\_controller) | cloudposse/helm-release/aws | 0.7.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/actions-runner-controller/remote-state.tf b/modules/eks/actions-runner-controller/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/actions-runner-controller/remote-state.tf +++ b/modules/eks/actions-runner-controller/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/alb-controller-ingress-class/README.md b/modules/eks/alb-controller-ingress-class/README.md index 955d1794a..8df9c1549 100644 --- a/modules/eks/alb-controller-ingress-class/README.md +++ b/modules/eks/alb-controller-ingress-class/README.md @@ -42,7 +42,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/alb-controller-ingress-class/remote-state.tf b/modules/eks/alb-controller-ingress-class/remote-state.tf index fdf68ab92..c1ec8226d 100644 --- a/modules/eks/alb-controller-ingress-class/remote-state.tf +++ b/modules/eks/alb-controller-ingress-class/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index 05c3dd919..d1df6874d 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -53,13 +53,13 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | -| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [load\_balancer\_name](#module\_load\_balancer\_name) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/eks/alb-controller-ingress-group/remote-state.tf b/modules/eks/alb-controller-ingress-group/remote-state.tf index 4cf760134..138570d00 100644 --- a/modules/eks/alb-controller-ingress-group/remote-state.tf +++ b/modules/eks/alb-controller-ingress-group/remote-state.tf @@ -1,6 +1,6 @@ module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.dns_delegated_component_name environment = var.dns_delegated_environment_name @@ -10,7 +10,7 @@ module "dns_delegated" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.eks_component_name @@ -19,7 +19,7 @@ module "eks" { module "global_accelerator" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" for_each = local.global_accelerator_enabled ? toset(["true"]) : [] @@ -31,7 +31,7 @@ module "global_accelerator" { module "waf" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" for_each = local.waf_enabled ? toset(["true"]) : [] diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 301b6243b..6d6a9cebc 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -68,7 +68,7 @@ components: | Name | Source | Version | |------|--------|---------| | [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.9.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/alb-controller/remote-state.tf b/modules/eks/alb-controller/remote-state.tf index fdf68ab92..c1ec8226d 100644 --- a/modules/eks/alb-controller/remote-state.tf +++ b/modules/eks/alb-controller/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index d89c6201c..052b1c949 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -395,12 +395,12 @@ components: |------|--------|---------| | [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.9.1 | | [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.9.1 | -| [argocd\_repo](#module\_argocd\_repo) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [argocd\_repo](#module\_argocd\_repo) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles\_config\_secrets](#module\_iam\_roles\_config\_secrets) | ../../account-map/modules/iam-roles | n/a | -| [saml\_sso\_providers](#module\_saml\_sso\_providers) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [saml\_sso\_providers](#module\_saml\_sso\_providers) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/argocd/remote-state.tf b/modules/eks/argocd/remote-state.tf index fc6d9abf5..dcb414db9 100644 --- a/modules/eks/argocd/remote-state.tf +++ b/modules/eks/argocd/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" environment = "gbl" component = "dns-delegated" @@ -20,7 +20,7 @@ module "dns_gbl_delegated" { module "saml_sso_providers" { for_each = local.enabled ? var.saml_sso_providers : {} source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = each.value.component @@ -31,7 +31,7 @@ module "argocd_repo" { for_each = local.enabled ? var.argocd_repositories : {} source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = each.key environment = each.value.environment diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 26eec9263..066b6dd37 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -60,7 +60,7 @@ components: | Name | Source | Version | |------|--------|---------| | [aws\_node\_termination\_handler](#module\_aws\_node\_termination\_handler) | cloudposse/helm-release/aws | 0.5.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/aws-node-termination-handler/remote-state.tf b/modules/eks/aws-node-termination-handler/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/aws-node-termination-handler/remote-state.tf +++ b/modules/eks/aws-node-termination-handler/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index 63eea3b6f..acdab7eaf 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -71,8 +71,8 @@ The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` |------|--------|---------| | [cert\_manager](#module\_cert\_manager) | cloudposse/helm-release/aws | 0.7.0 | | [cert\_manager\_issuer](#module\_cert\_manager\_issuer) | cloudposse/helm-release/aws | 0.7.0 | -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/cert-manager/remote-state.tf b/modules/eks/cert-manager/remote-state.tf index 3cd649b37..1e6842bfa 100644 --- a/modules/eks/cert-manager/remote-state.tf +++ b/modules/eks/cert-manager/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = "gbl" diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index b277baaad..f93f86f6e 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -170,7 +170,7 @@ https-checks: | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.7 | | [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 | -| [utils](#requirement\_utils) | >= 0.3.0 | +| [utils](#requirement\_utils) | >= 1.10.0 | ## Providers diff --git a/modules/eks/datadog-agent/versions.tf b/modules/eks/datadog-agent/versions.tf index e656d97c0..b104e91ca 100644 --- a/modules/eks/datadog-agent/versions.tf +++ b/modules/eks/datadog-agent/versions.tf @@ -12,7 +12,7 @@ terraform { } utils = { source = "cloudposse/utils" - version = ">= 0.3.0" + version = ">= 1.10.0" } kubernetes = { source = "hashicorp/kubernetes" diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index 3cc6be112..af29bb013 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -88,9 +88,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.9.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/echo-server/remote-state.tf b/modules/eks/echo-server/remote-state.tf index f990c3bcb..a31ae6bb6 100644 --- a/modules/eks/echo-server/remote-state.tf +++ b/modules/eks/echo-server/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "alb" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.alb_controller_ingress_group_component_name diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index ae455d68b..b58e78417 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -65,9 +65,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [dns\_gbl\_primary](#module\_dns\_gbl\_primary) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [dns\_gbl\_primary](#module\_dns\_gbl\_primary) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [external\_dns](#module\_external\_dns) | cloudposse/helm-release/aws | 0.7.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/external-dns/remote-state.tf b/modules/eks/external-dns/remote-state.tf index 3110659b3..d499f78c7 100644 --- a/modules/eks/external-dns/remote-state.tf +++ b/modules/eks/external-dns/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = var.dns_gbl_delegated_environment_name @@ -23,7 +23,7 @@ module "dns_gbl_delegated" { module "dns_gbl_primary" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-primary" environment = var.dns_gbl_primary_environment_name diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index bc79aad1f..14fd861ef 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -109,8 +109,8 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.8.1 | | [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.8.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | diff --git a/modules/eks/external-secrets-operator/remote-state.tf b/modules/eks/external-secrets-operator/remote-state.tf index 9503744ff..37c866a99 100644 --- a/modules/eks/external-secrets-operator/remote-state.tf +++ b/modules/eks/external-secrets-operator/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "account-map" tenant = module.iam_roles.global_tenant_name environment = module.iam_roles.global_environment_name diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index 965b0c313..df66dd3ee 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -41,7 +41,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [idp\_roles](#module\_idp\_roles) | cloudposse/helm-release/aws | 0.6.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/idp-roles/remote-state.tf b/modules/eks/idp-roles/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/idp-roles/remote-state.tf +++ b/modules/eks/idp-roles/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/karpenter-provisioner/README.md b/modules/eks/karpenter-provisioner/README.md index 33ab50b81..eb2323119 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/modules/eks/karpenter-provisioner/README.md @@ -120,10 +120,10 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/eks/karpenter-provisioner/remote-state.tf b/modules/eks/karpenter-provisioner/remote-state.tf index b217d4bca..cf8ed5c1c 100644 --- a/modules/eks/karpenter-provisioner/remote-state.tf +++ b/modules/eks/karpenter-provisioner/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name @@ -9,7 +9,7 @@ module "eks" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 2e6c8c3a5..62725cd38 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -317,7 +317,7 @@ For more details, refer to: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.7.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/karpenter/remote-state.tf b/modules/eks/karpenter/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/karpenter/remote-state.tf +++ b/modules/eks/karpenter/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 6abba6e3a..07d2349e2 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -58,7 +58,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [metrics\_server](#module\_metrics\_server) | cloudposse/helm-release/aws | 0.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/metrics-server/remote-state.tf b/modules/eks/metrics-server/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/metrics-server/remote-state.tf +++ b/modules/eks/metrics-server/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/platform/README.md b/modules/eks/platform/README.md index 3a9fdad37..a089db808 100644 --- a/modules/eks/platform/README.md +++ b/modules/eks/platform/README.md @@ -66,9 +66,9 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [remote](#module\_remote) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [remote](#module\_remote) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [store\_write](#module\_store\_write) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/platform/remote-state.tf b/modules/eks/platform/remote-state.tf index 6ee221982..9c7a97c11 100644 --- a/modules/eks/platform/remote-state.tf +++ b/modules/eks/platform/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name @@ -11,7 +11,7 @@ module "eks" { module "remote" { for_each = merge(var.references, local.metadata) source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = each.value["component"] privileged = coalesce(try(each.value["privileged"], null), false) diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 96f3ce473..0e815149d 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -86,7 +86,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [redis\_operator](#module\_redis\_operator) | cloudposse/helm-release/aws | 0.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/redis-operator/remote-state.tf b/modules/eks/redis-operator/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/redis-operator/remote-state.tf +++ b/modules/eks/redis-operator/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index 118d18120..e7cdbda55 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -92,7 +92,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [redis](#module\_redis) | cloudposse/helm-release/aws | 0.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/redis/remote-state.tf b/modules/eks/redis/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/redis/remote-state.tf +++ b/modules/eks/redis/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/eks/reloader/README.md b/modules/eks/reloader/README.md index 8eda55a17..f84ccc99c 100644 --- a/modules/eks/reloader/README.md +++ b/modules/eks/reloader/README.md @@ -51,7 +51,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/reloader/remote-state.tf b/modules/eks/reloader/remote-state.tf index ac55ba94c..c1ec8226d 100644 --- a/modules/eks/reloader/remote-state.tf +++ b/modules/eks/reloader/remote-state.tf @@ -1,6 +1,6 @@ module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.eks_component_name diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md index 2f591fbb9..b8bae75c0 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -77,13 +77,13 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [redis\_clusters](#module\_redis\_clusters) | ./modules/redis_cluster | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/elasticache-redis/remote-state.tf b/modules/elasticache-redis/remote-state.tf index d9d2833f0..fa1eb2ece 100644 --- a/modules/elasticache-redis/remote-state.tf +++ b/modules/elasticache-redis/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = local.eks_security_group_enabled ? var.eks_component_names : toset([]) @@ -20,7 +20,7 @@ module "eks" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = "gbl" @@ -30,7 +30,7 @@ module "dns_delegated" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = toset(var.allow_ingress_from_vpc_stages) diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index 461324c17..e67d17af8 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -47,12 +47,12 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [elasticsearch](#module\_elasticsearch) | cloudposse/elasticsearch/aws | 0.42.0 | | [elasticsearch\_log\_cleanup](#module\_elasticsearch\_log\_cleanup) | cloudposse/lambda-elasticsearch-cleanup/aws | 0.14.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/elasticsearch/remote-state.tf b/modules/elasticsearch/remote-state.tf index 73a773ce2..950d6d996 100644 --- a/modules/elasticsearch/remote-state.tf +++ b/modules/elasticsearch/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "dns_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = var.dns_delegated_environment_name diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index ce4d3a816..e95e8219a 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -179,13 +179,13 @@ chamber write github token | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.1 | | [graceful\_scale\_in](#module\_graceful\_scale\_in) | ./modules/graceful_scale_in | n/a | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [sg](#module\_sg) | cloudposse/security-group/aws | 1.0.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/github-runners/remote-state.tf b/modules/github-runners/remote-state.tf index 48c302f75..e96cf2bec 100644 --- a/modules/github-runners/remote-state.tf +++ b/modules/github-runners/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "account-map" environment = var.account_map_environment_name diff --git a/modules/gitops/README.md b/modules/gitops/README.md index 749d288df..266c8c16f 100644 --- a/modules/gitops/README.md +++ b/modules/gitops/README.md @@ -70,11 +70,11 @@ components: | Name | Source | Version | |------|--------|---------| -| [dynamodb](#module\_dynamodb) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [dynamodb](#module\_dynamodb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | | [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [s3\_bucket](#module\_s3\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [s3\_bucket](#module\_s3\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/gitops/remote-state.tf b/modules/gitops/remote-state.tf index 948097924..57954c886 100644 --- a/modules/gitops/remote-state.tf +++ b/modules/gitops/remote-state.tf @@ -1,6 +1,6 @@ module "s3_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.s3_bucket_component_name environment = try(var.s3_bucket_environment_name, module.this.environment) @@ -10,7 +10,7 @@ module "s3_bucket" { module "dynamodb" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.dynamodb_component_name environment = try(var.dynamodb_environment_name, module.this.environment) diff --git a/modules/global-accelerator-endpoint-group/README.md b/modules/global-accelerator-endpoint-group/README.md index c8cf2c108..a20a6ca10 100644 --- a/modules/global-accelerator-endpoint-group/README.md +++ b/modules/global-accelerator-endpoint-group/README.md @@ -38,7 +38,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [endpoint\_group](#module\_endpoint\_group) | cloudposse/global-accelerator/aws//modules/endpoint-group | 0.5.0 | -| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [global\_accelerator](#module\_global\_accelerator) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/global-accelerator-endpoint-group/remote-state.tf b/modules/global-accelerator-endpoint-group/remote-state.tf index 78317b80d..2ce29033d 100644 --- a/modules/global-accelerator-endpoint-group/remote-state.tf +++ b/modules/global-accelerator-endpoint-group/remote-state.tf @@ -1,6 +1,6 @@ module "global_accelerator" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "global-accelerator" environment = var.global_accelerator_environment_name diff --git a/modules/global-accelerator/README.md b/modules/global-accelerator/README.md index da42d6522..2001b8ff5 100644 --- a/modules/global-accelerator/README.md +++ b/modules/global-accelerator/README.md @@ -41,7 +41,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [flow\_logs\_bucket](#module\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [flow\_logs\_bucket](#module\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [global\_accelerator](#module\_global\_accelerator) | cloudposse/global-accelerator/aws | 0.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/global-accelerator/remote-state.tf b/modules/global-accelerator/remote-state.tf index 1e30a00b2..110ebf383 100644 --- a/modules/global-accelerator/remote-state.tf +++ b/modules/global-accelerator/remote-state.tf @@ -2,7 +2,7 @@ module "flow_logs_bucket" { count = var.flow_logs_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.flow_logs_s3_bucket_component tenant = var.flow_logs_s3_bucket_tenant diff --git a/modules/guardduty/README.md b/modules/guardduty/README.md index 378483051..1cfaf66c0 100644 --- a/modules/guardduty/README.md +++ b/modules/guardduty/README.md @@ -161,9 +161,9 @@ atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [guardduty](#module\_guardduty) | cloudposse/guardduty/aws | 0.5.0 | -| [guardduty\_delegated\_detector](#module\_guardduty\_delegated\_detector) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [guardduty\_delegated\_detector](#module\_guardduty\_delegated\_detector) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/guardduty/remote-state.tf b/modules/guardduty/remote-state.tf index 5da185e0f..a79b03d04 100644 --- a/modules/guardduty/remote-state.tf +++ b/modules/guardduty/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = "account-map" tenant = var.account_map_tenant != "" ? var.account_map_tenant : module.this.tenant @@ -13,7 +13,7 @@ module "account_map" { module "guardduty_delegated_detector" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" # If we are creating the delegated detector (because we are in the delegated admin account), then don't try to lookup # the delegated detector ID from remote state diff --git a/modules/mq-broker/README.md b/modules/mq-broker/README.md index 70229992e..134413ff0 100644 --- a/modules/mq-broker/README.md +++ b/modules/mq-broker/README.md @@ -36,7 +36,7 @@ components: | [aws](#requirement\_aws) | >= 3.0 | | [local](#requirement\_local) | >= 1.3 | | [template](#requirement\_template) | >= 2.2 | -| [utils](#requirement\_utils) | >= 0.3.0 | +| [utils](#requirement\_utils) | >= 1.10.0 | ## Providers @@ -46,11 +46,11 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [mq\_broker](#module\_mq\_broker) | cloudposse/mq-broker/aws | 0.14.0 | | [this](#module\_this) | cloudposse/label/null | 0.24.1 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/mq-broker/remote-state.tf b/modules/mq-broker/remote-state.tf index 7227b9927..cca23e913 100644 --- a/modules/mq-broker/remote-state.tf +++ b/modules/mq-broker/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" stack_config_local_path = "../../../stacks" component = "vpc" @@ -10,7 +10,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" stack_config_local_path = "../../../stacks" component = "eks" diff --git a/modules/mq-broker/versions.tf b/modules/mq-broker/versions.tf index 1527b5e19..23548707e 100644 --- a/modules/mq-broker/versions.tf +++ b/modules/mq-broker/versions.tf @@ -16,7 +16,7 @@ terraform { } utils = { source = "cloudposse/utils" - version = ">= 0.3.0" + version = ">= 1.10.0" } } } diff --git a/modules/mwaa/README.md b/modules/mwaa/README.md index dd9fa0db3..8b8cb1006 100644 --- a/modules/mwaa/README.md +++ b/modules/mwaa/README.md @@ -63,8 +63,8 @@ components: | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [mwaa\_environment](#module\_mwaa\_environment) | cloudposse/mwaa/aws | 0.4.8 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/mwaa/remote-state.tf b/modules/mwaa/remote-state.tf index 61064e9e1..f02894659 100644 --- a/modules/mwaa/remote-state.tf +++ b/modules/mwaa/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "vpc_ingress" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" for_each = toset(var.allow_ingress_from_vpc_stages) diff --git a/modules/network-firewall/README.md b/modules/network-firewall/README.md index d13bed98c..2dedb4d13 100644 --- a/modules/network-firewall/README.md +++ b/modules/network-firewall/README.md @@ -249,12 +249,12 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [alert\_logs\_bucket](#module\_alert\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [flow\_logs\_bucket](#module\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [alert\_logs\_bucket](#module\_alert\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [flow\_logs\_bucket](#module\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [network\_firewall](#module\_network\_firewall) | cloudposse/network-firewall/aws | 0.3.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/network-firewall/remote-state.tf b/modules/network-firewall/remote-state.tf index 59f6d7cbf..da533f575 100644 --- a/modules/network-firewall/remote-state.tf +++ b/modules/network-firewall/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.vpc_component_name @@ -9,7 +9,7 @@ module "vpc" { module "flow_logs_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.flow_logs_bucket_component_name @@ -25,7 +25,7 @@ module "flow_logs_bucket" { module "alert_logs_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.alert_logs_bucket_component_name diff --git a/modules/rds/README.md b/modules/rds/README.md index 66c6c291a..a4a3ccd17 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -107,15 +107,15 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | Name | Source | Version | |------|--------|---------| -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | | [rds\_client\_sg](#module\_rds\_client\_sg) | cloudposse/security-group/aws | 2.2.0 | | [rds\_instance](#module\_rds\_instance) | cloudposse/rds/aws | 0.38.5 | | [rds\_monitoring\_role](#module\_rds\_monitoring\_role) | cloudposse/iam-role/aws | 0.17.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/rds/remote-state.tf b/modules/rds/remote-state.tf index 3cea5bb29..863e6daef 100644 --- a/modules/rds/remote-state.tf +++ b/modules/rds/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" @@ -9,7 +9,7 @@ module "vpc" { module "eks" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" count = var.use_eks_security_group ? 1 : 0 @@ -20,7 +20,7 @@ module "eks" { module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" count = var.use_dns_delegated ? 1 : 0 diff --git a/modules/redshift/README.md b/modules/redshift/README.md index 1bd016c93..755a3c1f9 100644 --- a/modules/redshift/README.md +++ b/modules/redshift/README.md @@ -63,7 +63,7 @@ components: | [redshift\_cluster](#module\_redshift\_cluster) | cloudposse/redshift-cluster/aws | 1.0.0 | | [redshift\_sg](#module\_redshift\_sg) | cloudposse/security-group/aws | 2.2.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/redshift/remote-state.tf b/modules/redshift/remote-state.tf index 3e0ccd51e..757ef9067 100644 --- a/modules/redshift/remote-state.tf +++ b/modules/redshift/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" diff --git a/modules/route53-resolver-dns-firewall/README.md b/modules/route53-resolver-dns-firewall/README.md index e467d0b4d..bab5662f6 100644 --- a/modules/route53-resolver-dns-firewall/README.md +++ b/modules/route53-resolver-dns-firewall/README.md @@ -93,10 +93,10 @@ No providers. | Name | Source | Version | |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [logs\_bucket](#module\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [logs\_bucket](#module\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [route53\_resolver\_dns\_firewall](#module\_route53\_resolver\_dns\_firewall) | cloudposse/route53-resolver-dns-firewall/aws | 0.2.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/route53-resolver-dns-firewall/remote-state.tf b/modules/route53-resolver-dns-firewall/remote-state.tf index d99ec71df..e80e04f1e 100644 --- a/modules/route53-resolver-dns-firewall/remote-state.tf +++ b/modules/route53-resolver-dns-firewall/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.vpc_component_name @@ -9,7 +9,7 @@ module "vpc" { module "logs_bucket" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.logs_bucket_component_name diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index 55270021b..7c4182500 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -94,7 +94,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [bucket\_policy](#module\_bucket\_policy) | cloudposse/iam-policy/aws | 0.4.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [s3\_bucket](#module\_s3\_bucket) | cloudposse/s3-bucket/aws | 3.1.1 | diff --git a/modules/s3-bucket/remote-state.tf b/modules/s3-bucket/remote-state.tf index db6a74ff4..69f657564 100644 --- a/modules/s3-bucket/remote-state.tf +++ b/modules/s3-bucket/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "account-map" environment = var.account_map_environment_name diff --git a/modules/security-hub/README.md b/modules/security-hub/README.md index 7cb155b0f..f2611672c 100644 --- a/modules/security-hub/README.md +++ b/modules/security-hub/README.md @@ -168,7 +168,7 @@ atmos terraform apply security-hub/org-settings/uw1 -s core-uw1-security | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [security\_hub](#module\_security\_hub) | cloudposse/security-hub/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/security-hub/remote-state.tf b/modules/security-hub/remote-state.tf index da115834a..d9c31bca2 100644 --- a/modules/security-hub/remote-state.tf +++ b/modules/security-hub/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = "account-map" tenant = var.account_map_tenant != "" ? var.account_map_tenant : module.this.tenant diff --git a/modules/ses/README.md b/modules/ses/README.md index 8e55ac222..046ad6b89 100644 --- a/modules/ses/README.md +++ b/modules/ses/README.md @@ -45,7 +45,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_ses](#module\_kms\_key\_ses) | cloudposse/kms-key/aws | 0.12.1 | | [ses](#module\_ses) | cloudposse/ses/aws | 0.22.3 | diff --git a/modules/ses/remote-state.tf b/modules/ses/remote-state.tf index 3026a74ec..2e0d4da3c 100644 --- a/modules/ses/remote-state.tf +++ b/modules/ses/remote-state.tf @@ -1,6 +1,6 @@ module "dns_gbl_delegated" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "dns-delegated" environment = "gbl" diff --git a/modules/sftp/README.md b/modules/sftp/README.md index f97eeb362..a3b6d662f 100644 --- a/modules/sftp/README.md +++ b/modules/sftp/README.md @@ -44,7 +44,7 @@ components: | [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 1.0.1 | | [sftp](#module\_sftp) | cloudposse/transfer-sftp/aws | 1.2.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/sftp/remote-state.tf b/modules/sftp/remote-state.tf index 3e0ccd51e..757ef9067 100644 --- a/modules/sftp/remote-state.tf +++ b/modules/sftp/remote-state.tf @@ -1,6 +1,6 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "vpc" diff --git a/modules/snowflake-account/README.md b/modules/snowflake-account/README.md index 0fce6874f..89b8932ff 100644 --- a/modules/snowflake-account/README.md +++ b/modules/snowflake-account/README.md @@ -79,7 +79,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [account](#module\_account) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [account](#module\_account) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [introspection](#module\_introspection) | cloudposse/label/null | 0.25.0 | | [snowflake\_account](#module\_snowflake\_account) | cloudposse/label/null | 0.25.0 | diff --git a/modules/snowflake-account/remote-state.tf b/modules/snowflake-account/remote-state.tf index 5f0597fa0..db5f163ef 100644 --- a/modules/snowflake-account/remote-state.tf +++ b/modules/snowflake-account/remote-state.tf @@ -1,6 +1,6 @@ module "account" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "account" stage = var.root_account_stage_name diff --git a/modules/snowflake-database/README.md b/modules/snowflake-database/README.md index 277c74120..50f28e74d 100644 --- a/modules/snowflake-database/README.md +++ b/modules/snowflake-database/README.md @@ -61,7 +61,7 @@ components: |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [introspection](#module\_introspection) | cloudposse/label/null | 0.25.0 | -| [snowflake\_account](#module\_snowflake\_account) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [snowflake\_account](#module\_snowflake\_account) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [snowflake\_database](#module\_snowflake\_database) | cloudposse/label/null | 0.25.0 | | [snowflake\_label](#module\_snowflake\_label) | cloudposse/label/null | 0.25.0 | | [snowflake\_schema](#module\_snowflake\_schema) | cloudposse/label/null | 0.25.0 | diff --git a/modules/snowflake-database/remote-state.tf b/modules/snowflake-database/remote-state.tf index bd00a12f5..a39a2fa4a 100644 --- a/modules/snowflake-database/remote-state.tf +++ b/modules/snowflake-database/remote-state.tf @@ -1,6 +1,6 @@ module "snowflake_account" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "snowflake-account" diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index 5efb16bb5..bf8f352e9 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -159,7 +159,7 @@ components: | [child\_stacks\_config](#module\_child\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.0.0 | | [root\_admin\_stack](#module\_root\_admin\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.0.0 | | [root\_admin\_stack\_config](#module\_root\_admin\_stack\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.0.0 | -| [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/spacelift/admin-stack/remote-state.tf b/modules/spacelift/admin-stack/remote-state.tf index 397a1c65c..ae42e4713 100644 --- a/modules/spacelift/admin-stack/remote-state.tf +++ b/modules/spacelift/admin-stack/remote-state.tf @@ -1,6 +1,6 @@ module "spaces" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = "spacelift/spaces" environment = try(var.spacelift_spaces_environment_name, module.this.environment) diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index 34dfe1acb..164e4421e 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -132,15 +132,15 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | Name | Source | Version | |------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.1 | -| [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_label](#module\_iam\_label) | cloudposse/label/null | 0.25.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.2.0 | -| [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/spacelift/worker-pool/remote-state.tf b/modules/spacelift/worker-pool/remote-state.tf index f11ff88f7..4179f36ff 100644 --- a/modules/spacelift/worker-pool/remote-state.tf +++ b/modules/spacelift/worker-pool/remote-state.tf @@ -1,6 +1,6 @@ module "account_map" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = "account-map" environment = coalesce(var.account_map_environment_name, module.this.environment) @@ -12,7 +12,7 @@ module "account_map" { module "ecr" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = "ecr" environment = coalesce(var.ecr_environment_name, module.this.environment) @@ -24,7 +24,7 @@ module "ecr" { module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = "vpc" @@ -33,7 +33,7 @@ module "vpc" { module "spaces" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" component = var.spacelift_spaces_component_name environment = try(var.spacelift_spaces_environment_name, module.this.environment) diff --git a/modules/tgw/cross-region-hub-connector/README.md b/modules/tgw/cross-region-hub-connector/README.md index 558b6a4d5..f627badd4 100644 --- a/modules/tgw/cross-region-hub-connector/README.md +++ b/modules/tgw/cross-region-hub-connector/README.md @@ -61,7 +61,7 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.1 | -| [utils](#requirement\_utils) | >= 1.8.0 | +| [utils](#requirement\_utils) | >= 1.10.0 | ## Providers diff --git a/modules/tgw/cross-region-hub-connector/versions.tf b/modules/tgw/cross-region-hub-connector/versions.tf index 289af5570..d2cc89dba 100644 --- a/modules/tgw/cross-region-hub-connector/versions.tf +++ b/modules/tgw/cross-region-hub-connector/versions.tf @@ -8,7 +8,7 @@ terraform { } utils = { source = "cloudposse/utils" - version = ">= 1.8.0" + version = ">= 1.10.0" } } } diff --git a/modules/vpc-peering/README.md b/modules/vpc-peering/README.md index fa5f39764..670fdc5bd 100644 --- a/modules/vpc-peering/README.md +++ b/modules/vpc-peering/README.md @@ -201,7 +201,7 @@ atmos terraform apply vpc-peering -s ue1-prod | Name | Source | Version | |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [requester\_vpc](#module\_requester\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [requester\_vpc](#module\_requester\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc\_peering](#module\_vpc\_peering) | cloudposse/vpc-peering-multi-account/aws | 0.19.1 | diff --git a/modules/vpc-peering/remote-state.tf b/modules/vpc-peering/remote-state.tf index 108ec8737..17a9d24ec 100644 --- a/modules/vpc-peering/remote-state.tf +++ b/modules/vpc-peering/remote-state.tf @@ -1,6 +1,6 @@ module "requester_vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.1" + version = "1.5.0" component = var.requester_vpc_component_name diff --git a/modules/vpc/README.md b/modules/vpc/README.md index e5b0ec080..19dadd33c 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -77,7 +77,7 @@ components: | [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | | [vpc](#module\_vpc) | cloudposse/vpc/aws | 2.1.0 | | [vpc\_endpoints](#module\_vpc\_endpoints) | cloudposse/vpc/aws//modules/vpc-endpoints | 2.1.0 | -| [vpc\_flow\_logs\_bucket](#module\_vpc\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.2 | +| [vpc\_flow\_logs\_bucket](#module\_vpc\_flow\_logs\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources diff --git a/modules/vpc/remote-state.tf b/modules/vpc/remote-state.tf index 3b0148dd0..b9db2205c 100644 --- a/modules/vpc/remote-state.tf +++ b/modules/vpc/remote-state.tf @@ -2,7 +2,7 @@ module "vpc_flow_logs_bucket" { count = var.vpc_flow_logs_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.2" + version = "1.5.0" component = "vpc-flow-logs-bucket" environment = var.vpc_flow_logs_bucket_environment_name diff --git a/modules/waf/README.md b/modules/waf/README.md index 6a3524685..9f472f5da 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -57,7 +57,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [association\_resource\_components](#module\_association\_resource\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [association\_resource\_components](#module\_association\_resource\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 1.0.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/waf/remote-state.tf b/modules/waf/remote-state.tf index 4e6dba912..ad6674f34 100644 --- a/modules/waf/remote-state.tf +++ b/modules/waf/remote-state.tf @@ -1,6 +1,6 @@ module "association_resource_components" { source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.4.3" + version = "1.5.0" count = local.enabled ? length(var.association_resource_component_selectors) : 0 diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index 9dd623537..ee3159d00 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -44,7 +44,7 @@ import: | [null](#requirement\_null) | >= 3.0 | | [random](#requirement\_random) | >= 3.0 | | [template](#requirement\_template) | >= 2.2 | -| [utils](#requirement\_utils) | >= 0.4.3 | +| [utils](#requirement\_utils) | >= 1.10.0 | ## Providers diff --git a/modules/zscaler/versions.tf b/modules/zscaler/versions.tf index b3adb7a76..a35488bbe 100644 --- a/modules/zscaler/versions.tf +++ b/modules/zscaler/versions.tf @@ -20,7 +20,7 @@ terraform { } utils = { source = "cloudposse/utils" - version = ">= 0.4.3" + version = ">= 1.10.0" } } } From 9d9255d2f1fc15fa7fb211cef0a58c800b6bd716 Mon Sep 17 00:00:00 2001 From: Maksym Vlasov Date: Thu, 17 Aug 2023 19:41:05 +0300 Subject: [PATCH 230/501] chore: Remove unused (#818) Co-authored-by: cloudpossebot Co-authored-by: Andriy Knysh --- modules/eks/cluster/README.md | 1 - modules/eks/cluster/main.tf | 3 +-- modules/eks/cluster/variables.tf | 16 ---------------- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index de01f3463..336546ee4 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -489,7 +489,6 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [aws\_ssm\_agent\_enabled](#input\_aws\_ssm\_agent\_enabled) | Set true to attach the required IAM policy for AWS SSM agent to each EC2 instance's IAM Role | `bool` | `false` | no | | [aws\_sso\_permission\_sets\_rbac](#input\_aws\_sso\_permission\_sets\_rbac) | (Not Recommended): AWS SSO (IAM Identity Center) permission sets in the EKS deployment account to add to `aws-auth` ConfigMap.
Unfortunately, `aws-auth` ConfigMap does not support SSO permission sets, so we map the generated
IAM Role ARN corresponding to the permission set at the time Terraform runs. This is subject to change
when any changes are made to the AWS SSO configuration, invalidating the mapping, and requiring a
`terraform apply` in this project to update the `aws-auth` ConfigMap and restore access. |
list(object({
aws_sso_permission_set = string
groups = list(string)
}))
| `[]` | no | | [aws\_team\_roles\_rbac](#input\_aws\_team\_roles\_rbac) | List of `aws-team-roles` (in the target AWS account) to map to Kubernetes RBAC groups. |
list(object({
aws_team_role = string
groups = list(string)
}))
| `[]` | no | -| [aws\_teams\_rbac](#input\_aws\_teams\_rbac) | OBSOLETE: This feature never worked as intended, and this input is now ignored.
List of `aws-teams` to map to Kubernetes RBAC groups.
This gives teams direct access to Kubernetes without having to assume a team-role. |
list(object({
aws_team = string
groups = list(string)
}))
| `[]` | no | | [cluster\_encryption\_config\_enabled](#input\_cluster\_encryption\_config\_enabled) | Set to `true` to enable Cluster Encryption Configuration | `bool` | `true` | no | | [cluster\_encryption\_config\_kms\_key\_deletion\_window\_in\_days](#input\_cluster\_encryption\_config\_kms\_key\_deletion\_window\_in\_days) | Cluster Encryption Config KMS Key Resource argument - key deletion windows in days post destruction | `number` | `10` | no | | [cluster\_encryption\_config\_kms\_key\_enable\_key\_rotation](#input\_cluster\_encryption\_config\_kms\_key\_enable\_key\_rotation) | Cluster Encryption Config KMS Key Resource argument - enable kms key rotation | `bool` | `true` | no | diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 9c3fa7197..205f904f9 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -5,8 +5,7 @@ locals { attributes = flatten(concat(module.this.attributes, [var.color])) - this_account_name = module.iam_roles.current_account_account_name - identity_account_name = module.iam_roles.identity_account_account_name + this_account_name = module.iam_roles.current_account_account_name role_map = { (local.this_account_name) = var.aws_team_roles_rbac[*].aws_team_role } diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 4635db2ed..fb1f27f12 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -114,22 +114,6 @@ variable "map_additional_worker_roles" { nullable = false } -variable "aws_teams_rbac" { - type = list(object({ - aws_team = string - groups = list(string) - })) - - description = <<-EOT - OBSOLETE: This feature never worked as intended, and this input is now ignored. - List of `aws-teams` to map to Kubernetes RBAC groups. - This gives teams direct access to Kubernetes without having to assume a team-role. - EOT - - default = [] - nullable = false -} - variable "aws_team_roles_rbac" { type = list(object({ aws_team_role = string From c951f3babfdc2f7c33e79e31eefc1070d2232a4b Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Fri, 18 Aug 2023 10:42:24 -0400 Subject: [PATCH 231/501] update guardduty boolean logic (#822) --- modules/guardduty/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/guardduty/main.tf b/modules/guardduty/main.tf index 0d966067e..9026384d0 100644 --- a/modules/guardduty/main.tf +++ b/modules/guardduty/main.tf @@ -10,7 +10,7 @@ locals { is_org_management_account = local.current_account_id == local.org_management_account_id create_sns_topic = local.enabled && var.create_sns_topic - create_guardduty_collector = local.enabled && local.is_org_delegated_administrator_account && !var.admin_delegated + create_guardduty_collector = local.enabled && ((local.is_org_delegated_administrator_account && !var.admin_delegated) || local.is_org_management_account) create_org_delegation = local.enabled && local.is_org_management_account create_org_configuration = local.enabled && local.is_org_delegated_administrator_account && var.admin_delegated } From 9756cb1ec9e0bdd30c7bb08f32ffd52b931fd745 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 18 Aug 2023 08:11:50 -0700 Subject: [PATCH 232/501] Placeholder for `upgrade-guide.md` (#823) Co-authored-by: Zinovii Dmytriv --- docs/upgrade-guide.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/upgrade-guide.md diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md new file mode 100644 index 000000000..f7f12afb1 --- /dev/null +++ b/docs/upgrade-guide.md @@ -0,0 +1,9 @@ +# Upgrade Guide + + +:::info + +Upgrade steps are included with any given component if required. We will be adding overarching upgrades to this file in the future, +but those changes are in progress now (August 2023). Expect to see this page and component pages updated soon. + +::: From d9b79a651a83ee4e3eb591627e1f4556c85dbc89 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 18 Aug 2023 13:17:46 -0700 Subject: [PATCH 233/501] Spacelift Alternate git Providers (#825) --- modules/spacelift/admin-stack/child-stacks.tf | 8 ++++---- .../spacelift/admin-stack/root-admin-stack.tf | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index d7f976ed0..37cb17887 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -121,10 +121,10 @@ module "child_stack" { bitbucket_cloud = try(each.value.bitbucket_cloud, null) bitbucket_datacenter = try(each.value.bitbucket_datacenter, null) cloudformation = try(each.value.cloudformation, null) - github_enterprise = try(local.root_admin_stack_config.github_enterprise, null) - gitlab = try(local.root_admin_stack_config.gitlab, null) - pulumi = try(local.root_admin_stack_config.pulumi, null) - showcase = try(local.root_admin_stack_config.showcase, null) + github_enterprise = try(local.root_admin_stack_config.settings.spacelift.github_enterprise, null) + gitlab = try(local.root_admin_stack_config.settings.spacelift.gitlab, null) + pulumi = try(local.root_admin_stack_config.settings.spacelift.pulumi, null) + showcase = try(local.root_admin_stack_config.settings.spacelift.showcase, null) context = module.this.context } diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf index 3258dde17..622977914 100644 --- a/modules/spacelift/admin-stack/root-admin-stack.tf +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -78,14 +78,14 @@ module "root_admin_stack" { webhook_secret = try(local.root_admin_stack_config.webhook_secret, var.webhook_secret) worker_pool_id = local.worker_pools[var.worker_pool_name] - azure_devops = try(local.root_admin_stack_config.azure_devops, null) - bitbucket_cloud = try(local.root_admin_stack_config.bitbucket_cloud, null) - bitbucket_datacenter = try(local.root_admin_stack_config.bitbucket_datacenter, null) - cloudformation = try(local.root_admin_stack_config.cloudformation, null) - github_enterprise = try(local.root_admin_stack_config.github_enterprise, null) - gitlab = try(local.root_admin_stack_config.gitlab, null) - pulumi = try(local.root_admin_stack_config.pulumi, null) - showcase = try(local.root_admin_stack_config.showcase, null) + azure_devops = try(local.root_admin_stack_config.settings.spacelift.azure_devops, null) + bitbucket_cloud = try(local.root_admin_stack_config.settings.spacelift.bitbucket_cloud, null) + bitbucket_datacenter = try(local.root_admin_stack_config.settings.spacelift.bitbucket_datacenter, null) + cloudformation = try(local.root_admin_stack_config.settings.spacelift.cloudformation, null) + github_enterprise = try(local.root_admin_stack_config.settings.spacelift.github_enterprise, null) + gitlab = try(local.root_admin_stack_config.settings.spacelift.gitlab, null) + pulumi = try(local.root_admin_stack_config.settings.spacelift.pulumi, null) + showcase = try(local.root_admin_stack_config.settings.spacelift.showcase, null) } resource "spacelift_policy_attachment" "root" { From 9da133f96b6a8dd622999af55fb9e0af5e7d3679 Mon Sep 17 00:00:00 2001 From: Nuru Date: Fri, 18 Aug 2023 22:20:05 -0700 Subject: [PATCH 234/501] [eks/alb-controller] Update ALB controller IAM policy (#821) --- modules/eks/alb-controller/CHANGELOG.md | 24 ++ modules/eks/alb-controller/README.md | 14 +- .../alb-controller/distributed-iam-policy.tf | 263 +++++++++++++++ modules/eks/alb-controller/main.tf | 307 +----------------- 4 files changed, 302 insertions(+), 306 deletions(-) create mode 100644 modules/eks/alb-controller/CHANGELOG.md create mode 100644 modules/eks/alb-controller/distributed-iam-policy.tf diff --git a/modules/eks/alb-controller/CHANGELOG.md b/modules/eks/alb-controller/CHANGELOG.md new file mode 100644 index 000000000..8c78808b1 --- /dev/null +++ b/modules/eks/alb-controller/CHANGELOG.md @@ -0,0 +1,24 @@ +## PR [#821](https://github.com/cloudposse/terraform-aws-components/pull/821) + +### Update IAM Policy and Change How it is Managed + +The ALB controller needs a lot of permissions and has a complex IAM policy. +For this reason, the project releases a complete JSON policy document that is +updated as needed. + +In this release: + +1. We have updated the policy to the one distributed with version 2.6.0 of the ALB controller. This fixes an issue + where the controller was not able to create the service-linked role for the Elastic Load Balancing service. +2. To ease maintenance, we have moved the policy document to a separate file, + `distributed-iam-policy.tf` and made it easy to update or override. + + +#### Gov Cloud and China Regions + +Actually, the project releases 3 policy documents, one for each of the +three AWS partitions: `aws`, `aws-cn`, and `aws-us-gov`. For simplicity, +this module only uses the `aws` partition policy. If you are in another +partition, you can create a `distributed-iam-policy_override.tf` file in your +directory and override the `distributed_iam_policy_overridable` local +variable with the policy document for your partition. diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 6d6a9cebc..8f0ca8cf3 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -6,6 +6,14 @@ This component creates a Helm release for [alb-controller](https://github.com/ku in the context of AWS, provisions and manages ALBs and NLBs based on Service and Ingress annotations. This module also can (and is recommended to) provision a default IngressClass. +### Special note about upgrading + +When upgrading the chart version, check to see if the IAM policy for the service account needs to be updated. +If it does, update the policy in the `distributed-iam-policy.tf` file. +Probably the easiest way to check if it needs updating is to simply download the policy from +https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json +and compare it to the policy in `distributed-iam-policy.tf`. + ## Usage **Stack Level**: Regional @@ -27,7 +35,9 @@ components: vars: chart: aws-load-balancer-controller chart_repository: https://aws.github.io/eks-charts - chart_version: "1.4.5" + # IMPORTANT: When updating the chart version, check to see if the IAM policy for the service account. + # needs to be updated, and if it does, update the policy in the `distributed-iam-policy.tf` file. + chart_version: "1.6.0" create_namespace: true kubernetes_namespace: alb-controller # this feature causes inconsistent final plans @@ -67,7 +77,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.9.1 | +| [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.9.3 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/alb-controller/distributed-iam-policy.tf b/modules/eks/alb-controller/distributed-iam-policy.tf new file mode 100644 index 000000000..eef46e135 --- /dev/null +++ b/modules/eks/alb-controller/distributed-iam-policy.tf @@ -0,0 +1,263 @@ + +# The kubernetes-sigs/aws-load-balancer-controller/ project distributes the +# AWS IAM policy that is required for the AWS Load Balancer Controller as a JSON +# download at https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v/docs/install/iam_policy.json +# See https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.6/deploy/installation/#option-a-recommended-iam-roles-for-service-accounts-irsa for details. + +# We could directly use the URL to download and install the policy at runtime, +# via the cloudposse/helm-release/aws module's ` iam_source_json_url` input, +# but that lacks transparency and auditability. It also does not give us a chance +# to make changes in response to bugs, such as +# https://github.com/kubernetes-sigs/aws-load-balancer-controller/issues/2692#issuecomment-1426242236 +# +# So we download the policy and insert it here as a local variable. + +locals { + # To update, just replace everything between the two "EOT"s with the contents of the downloaded JSON file. + # Below is the policy as of version 2.6.0, downloaded from + # https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.6.0/docs/install/iam_policy.json + # This policy is for the `aws` partition. Override distributed_iam_policy_overridable for other partitions. + distributed_iam_policy_overridable = < Date: Mon, 21 Aug 2023 01:52:47 -0700 Subject: [PATCH 235/501] Fix naming convention of overridable local variable (#826) --- modules/eks/alb-controller/CHANGELOG.md | 2 +- modules/eks/alb-controller/distributed-iam-policy.tf | 4 ++-- modules/eks/alb-controller/main.tf | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/eks/alb-controller/CHANGELOG.md b/modules/eks/alb-controller/CHANGELOG.md index 8c78808b1..a33945647 100644 --- a/modules/eks/alb-controller/CHANGELOG.md +++ b/modules/eks/alb-controller/CHANGELOG.md @@ -20,5 +20,5 @@ Actually, the project releases 3 policy documents, one for each of the three AWS partitions: `aws`, `aws-cn`, and `aws-us-gov`. For simplicity, this module only uses the `aws` partition policy. If you are in another partition, you can create a `distributed-iam-policy_override.tf` file in your -directory and override the `distributed_iam_policy_overridable` local +directory and override the `overridable_distributed_iam_policy` local variable with the policy document for your partition. diff --git a/modules/eks/alb-controller/distributed-iam-policy.tf b/modules/eks/alb-controller/distributed-iam-policy.tf index eef46e135..fcc655182 100644 --- a/modules/eks/alb-controller/distributed-iam-policy.tf +++ b/modules/eks/alb-controller/distributed-iam-policy.tf @@ -16,8 +16,8 @@ locals { # To update, just replace everything between the two "EOT"s with the contents of the downloaded JSON file. # Below is the policy as of version 2.6.0, downloaded from # https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.6.0/docs/install/iam_policy.json - # This policy is for the `aws` partition. Override distributed_iam_policy_overridable for other partitions. - distributed_iam_policy_overridable = < Date: Mon, 21 Aug 2023 13:57:21 -0500 Subject: [PATCH 236/501] Upgrade aws-config and conformance pack modules to 1.1.0 (#829) Co-authored-by: cloudpossebot --- modules/aws-config/README.md | 4 ++-- modules/aws-config/main.tf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index 8f342705e..920153566 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -130,11 +130,11 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} | Name | Source | Version | |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [aws\_config](#module\_aws\_config) | cloudposse/config/aws | 0.17.0 | +| [aws\_config](#module\_aws\_config) | cloudposse/config/aws | 1.1.0 | | [aws\_config\_label](#module\_aws\_config\_label) | cloudposse/label/null | 0.25.0 | | [aws\_team\_roles](#module\_aws\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [config\_bucket](#module\_config\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [conformance\_pack](#module\_conformance\_pack) | cloudposse/config/aws//modules/conformance-pack | 0.17.0 | +| [conformance\_pack](#module\_conformance\_pack) | cloudposse/config/aws//modules/conformance-pack | 1.1.0 | | [global\_collector\_region](#module\_global\_collector\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/aws-config/main.tf b/modules/aws-config/main.tf index 8b58c0077..28b444184 100644 --- a/modules/aws-config/main.tf +++ b/modules/aws-config/main.tf @@ -42,7 +42,7 @@ module "utils" { module "conformance_pack" { source = "cloudposse/config/aws//modules/conformance-pack" - version = "0.17.0" + version = "1.1.0" count = local.enabled ? length(var.conformance_packs) : 0 @@ -59,7 +59,7 @@ module "conformance_pack" { module "aws_config" { source = "cloudposse/config/aws" - version = "0.17.0" + version = "1.1.0" s3_bucket_id = local.s3_bucket.config_bucket_id s3_bucket_arn = local.s3_bucket.config_bucket_arn From f05c98e491712dde60e8f675f9c6e3facf2b5217 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 22 Aug 2023 13:07:59 -0400 Subject: [PATCH 237/501] chore: remove defaults from components (#831) --- modules/acm/default.auto.tfvars | 3 --- modules/argocd-repo/default.auto.tfvars | 3 --- .../modules/mysql-user/default.auto.tfvars | 1 - modules/aurora-mysql/default.auto.tfvars | 1 - modules/aws-sso/default.auto.tfvars | 3 --- modules/cloudwatch-logs/default.auto.tfvars | 1 - modules/documentdb/default.auto.tfvars | 5 ---- modules/ec2-client-vpn/default.auto.tfvars | 3 --- modules/ecr/default.auto.tfvars | 3 --- modules/ecs/default.auto.tfvars | 1 - .../aws-node-termination-handler/README.md | 8 +++---- .../default.auto.tfvars | 23 ------------------- .../aws-node-termination-handler/variables.tf | 14 ++++++++++- modules/eks/metrics-server/README.md | 12 +++++----- .../eks/metrics-server/default.auto.tfvars | 21 ----------------- modules/eks/metrics-server/variables.tf | 17 ++++++++++++-- .../eks/redis-operator/default.auto.tfvars | 3 --- modules/eks/redis/default.auto.tfvars | 3 --- modules/elasticache-redis/default.auto.tfvars | 3 --- .../default.auto.tfvars | 3 --- modules/github-runners/default.auto.tfvars | 3 --- .../default.auto.tfvars | 3 --- .../global-accelerator/default.auto.tfvars | 3 --- modules/kinesis-stream/default.auto.tfvars | 1 - modules/lakeformation/default.auto.tfvars | 1 - modules/mq-broker/default.auto.tfvars | 5 ---- modules/opsgenie-team/default.auto.tfvars | 5 ---- modules/ses/default.auto.tfvars | 3 --- modules/sftp/default.auto.tfvars | 1 - modules/snowflake-account/default.auto.tfvars | 3 --- modules/snowflake-database/README.md | 8 +++---- .../snowflake-database/default.auto.tfvars | 8 ------- modules/snowflake-database/variables.tf | 8 +++---- modules/sns-topic/default.auto.tfvars | 2 -- modules/sqs-queue/default.auto.tfvars | 2 -- modules/zscaler/README.md | 2 +- modules/zscaler/default.auto.tfvars | 6 ----- modules/zscaler/variables.tf | 7 ++++-- 38 files changed, 52 insertions(+), 150 deletions(-) delete mode 100644 modules/acm/default.auto.tfvars delete mode 100644 modules/argocd-repo/default.auto.tfvars delete mode 100644 modules/aurora-mysql-resources/modules/mysql-user/default.auto.tfvars delete mode 100644 modules/aurora-mysql/default.auto.tfvars delete mode 100644 modules/aws-sso/default.auto.tfvars delete mode 100644 modules/cloudwatch-logs/default.auto.tfvars delete mode 100644 modules/documentdb/default.auto.tfvars delete mode 100644 modules/ec2-client-vpn/default.auto.tfvars delete mode 100644 modules/ecr/default.auto.tfvars delete mode 100644 modules/ecs/default.auto.tfvars delete mode 100644 modules/eks/aws-node-termination-handler/default.auto.tfvars delete mode 100644 modules/eks/metrics-server/default.auto.tfvars delete mode 100644 modules/eks/redis-operator/default.auto.tfvars delete mode 100644 modules/eks/redis/default.auto.tfvars delete mode 100644 modules/elasticache-redis/default.auto.tfvars delete mode 100644 modules/github-action-token-rotator/default.auto.tfvars delete mode 100644 modules/github-runners/default.auto.tfvars delete mode 100644 modules/global-accelerator-endpoint-group/default.auto.tfvars delete mode 100644 modules/global-accelerator/default.auto.tfvars delete mode 100644 modules/kinesis-stream/default.auto.tfvars delete mode 100644 modules/lakeformation/default.auto.tfvars delete mode 100644 modules/mq-broker/default.auto.tfvars delete mode 100644 modules/opsgenie-team/default.auto.tfvars delete mode 100644 modules/ses/default.auto.tfvars delete mode 100644 modules/sftp/default.auto.tfvars delete mode 100644 modules/snowflake-account/default.auto.tfvars delete mode 100644 modules/snowflake-database/default.auto.tfvars delete mode 100644 modules/sns-topic/default.auto.tfvars delete mode 100644 modules/sqs-queue/default.auto.tfvars delete mode 100644 modules/zscaler/default.auto.tfvars diff --git a/modules/acm/default.auto.tfvars b/modules/acm/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/acm/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/argocd-repo/default.auto.tfvars b/modules/argocd-repo/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/argocd-repo/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/aurora-mysql-resources/modules/mysql-user/default.auto.tfvars b/modules/aurora-mysql-resources/modules/mysql-user/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/aurora-mysql-resources/modules/mysql-user/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/aurora-mysql/default.auto.tfvars b/modules/aurora-mysql/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/aurora-mysql/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/aws-sso/default.auto.tfvars b/modules/aws-sso/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/aws-sso/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/cloudwatch-logs/default.auto.tfvars b/modules/cloudwatch-logs/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/cloudwatch-logs/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/documentdb/default.auto.tfvars b/modules/documentdb/default.auto.tfvars deleted file mode 100644 index b6c99eb87..000000000 --- a/modules/documentdb/default.auto.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# This file is included by default in terraform plans - -enabled = true - -name = "documentdb" diff --git a/modules/ec2-client-vpn/default.auto.tfvars b/modules/ec2-client-vpn/default.auto.tfvars deleted file mode 100644 index 86813cc2f..000000000 --- a/modules/ec2-client-vpn/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -enabled = false - -name = "ec2-client-vpn" diff --git a/modules/ecr/default.auto.tfvars b/modules/ecr/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/ecr/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/ecs/default.auto.tfvars b/modules/ecs/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/ecs/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 066b6dd37..a459124b0 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -78,11 +78,11 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | n/a | yes | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"aws-node-termination-handler"` | no | | [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | -| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | n/a | yes | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://aws.github.io/eks-charts"` | no | | [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | -| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"0.15.3"` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `null` | no | @@ -113,7 +113,7 @@ components: | [rbac\_enabled](#input\_rbac\_enabled) | Service Account for pods. | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | -| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | +| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
|
{
"limits": {
"cpu": "100m",
"memory": "128Mi"
},
"requests": {
"cpu": "50m",
"memory": "64Mi"
}
}
| no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/eks/aws-node-termination-handler/default.auto.tfvars b/modules/eks/aws-node-termination-handler/default.auto.tfvars deleted file mode 100644 index 79fa04cf0..000000000 --- a/modules/eks/aws-node-termination-handler/default.auto.tfvars +++ /dev/null @@ -1,23 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - -name = "aws-node-termination-handler" - -chart = "aws-node-termination-handler" -chart_repository = "https://aws.github.io/eks-charts" -chart_version = "0.15.3" - -create_namespace = true -kubernetes_namespace = "aws-node-termination-handler" - -resources = { - limits = { - cpu = "100m" - memory = "128Mi" - }, - requests = { - cpu = "50m" - memory = "64Mi" - } -} diff --git a/modules/eks/aws-node-termination-handler/variables.tf b/modules/eks/aws-node-termination-handler/variables.tf index 29173515e..2faebfaf0 100644 --- a/modules/eks/aws-node-termination-handler/variables.tf +++ b/modules/eks/aws-node-termination-handler/variables.tf @@ -12,17 +12,19 @@ variable "chart_description" { variable "chart" { type = string description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." + default = "aws-node-termination-handler" } variable "chart_repository" { type = string description = "Repository URL where to locate the requested chart." + default = "https://aws.github.io/eks-charts" } variable "chart_version" { type = string description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." - default = null + default = "0.15.3" } variable "resources" { @@ -37,6 +39,16 @@ variable "resources" { }) }) description = "The cpu and memory of the deployment's limits and requests." + default = { + limits = { + cpu = "100m" + memory = "128Mi" + } + requests = { + cpu = "50m" + memory = "64Mi" + } + } } variable "create_namespace" { diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 07d2349e2..1acb73502 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -77,14 +77,14 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | n/a | yes | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"metrics-server"` | no | | [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | -| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | n/a | yes | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://charts.bitnami.com/bitnami"` | no | | [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | -| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"6.2.6"` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `null` | no | +| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | @@ -102,7 +102,7 @@ components: | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | -| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | n/a | yes | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | `"metrics-server"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | @@ -112,7 +112,7 @@ components: | [rbac\_enabled](#input\_rbac\_enabled) | Service Account for pods. | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | -| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | +| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
|
{
"limits": {
"cpu": "100m",
"memory": "300Mi"
},
"requests": {
"cpu": "20m",
"memory": "60Mi"
}
}
| no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/eks/metrics-server/default.auto.tfvars b/modules/eks/metrics-server/default.auto.tfvars deleted file mode 100644 index d0baeab36..000000000 --- a/modules/eks/metrics-server/default.auto.tfvars +++ /dev/null @@ -1,21 +0,0 @@ -enabled = false - -name = "metrics-server" - -chart = "metrics-server" -chart_repository = "https://charts.bitnami.com/bitnami" -chart_version = "6.2.6" - -create_namespace = true -kubernetes_namespace = "metrics-server" - -resources = { - limits = { - cpu = "100m" - memory = "300Mi" - }, - requests = { - cpu = "20m" - memory = "60Mi" - } -} diff --git a/modules/eks/metrics-server/variables.tf b/modules/eks/metrics-server/variables.tf index 29173515e..ef1678456 100644 --- a/modules/eks/metrics-server/variables.tf +++ b/modules/eks/metrics-server/variables.tf @@ -12,17 +12,19 @@ variable "chart_description" { variable "chart" { type = string description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." + default = "metrics-server" } variable "chart_repository" { type = string description = "Repository URL where to locate the requested chart." + default = "https://charts.bitnami.com/bitnami" } variable "chart_version" { type = string description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." - default = null + default = "6.2.6" } variable "resources" { @@ -37,17 +39,28 @@ variable "resources" { }) }) description = "The cpu and memory of the deployment's limits and requests." + default = { + limits = { + cpu = "100m" + memory = "300Mi" + } + requests = { + cpu = "20m" + memory = "60Mi" + } + } } variable "create_namespace" { type = bool description = "Create the namespace if it does not yet exist. Defaults to `false`." - default = null + default = true } variable "kubernetes_namespace" { type = string description = "The namespace to install the release into." + default = "metrics-server" } variable "timeout" { diff --git a/modules/eks/redis-operator/default.auto.tfvars b/modules/eks/redis-operator/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/eks/redis-operator/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/eks/redis/default.auto.tfvars b/modules/eks/redis/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/eks/redis/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/elasticache-redis/default.auto.tfvars b/modules/elasticache-redis/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/elasticache-redis/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/github-action-token-rotator/default.auto.tfvars b/modules/github-action-token-rotator/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/github-action-token-rotator/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/github-runners/default.auto.tfvars b/modules/github-runners/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/github-runners/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/global-accelerator-endpoint-group/default.auto.tfvars b/modules/global-accelerator-endpoint-group/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/global-accelerator-endpoint-group/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/global-accelerator/default.auto.tfvars b/modules/global-accelerator/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/global-accelerator/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/kinesis-stream/default.auto.tfvars b/modules/kinesis-stream/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/kinesis-stream/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/lakeformation/default.auto.tfvars b/modules/lakeformation/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/lakeformation/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/mq-broker/default.auto.tfvars b/modules/mq-broker/default.auto.tfvars deleted file mode 100644 index 153d814a7..000000000 --- a/modules/mq-broker/default.auto.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - -name = "mq-broker" diff --git a/modules/opsgenie-team/default.auto.tfvars b/modules/opsgenie-team/default.auto.tfvars deleted file mode 100644 index afca9e898..000000000 --- a/modules/opsgenie-team/default.auto.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - -name = "opsgenie-team" diff --git a/modules/ses/default.auto.tfvars b/modules/ses/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/ses/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/sftp/default.auto.tfvars b/modules/sftp/default.auto.tfvars deleted file mode 100644 index 47f94fb9b..000000000 --- a/modules/sftp/default.auto.tfvars +++ /dev/null @@ -1 +0,0 @@ -enabled = false diff --git a/modules/snowflake-account/default.auto.tfvars b/modules/snowflake-account/default.auto.tfvars deleted file mode 100644 index bccc95614..000000000 --- a/modules/snowflake-account/default.auto.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false diff --git a/modules/snowflake-database/README.md b/modules/snowflake-database/README.md index 50f28e74d..02ebd9beb 100644 --- a/modules/snowflake-database/README.md +++ b/modules/snowflake-database/README.md @@ -94,7 +94,7 @@ components: | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [data\_retention\_time\_in\_days](#input\_data\_retention\_time\_in\_days) | Time in days to retain data in Snowflake databases, schemas, and tables by default. | `string` | `1` | no | | [database\_comment](#input\_database\_comment) | The comment to give to the provisioned database. | `string` | `"A database created for managing programmatically created Snowflake schemas and tables."` | no | -| [database\_grants](#input\_database\_grants) | A list of Grants to give to the database created with component. | `list(string)` | `[]` | no | +| [database\_grants](#input\_database\_grants) | A list of Grants to give to the database created with component. | `list(string)` |
[
"MODIFY",
"MONITOR",
"USAGE"
]
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -109,13 +109,13 @@ components: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [required\_tags](#input\_required\_tags) | List of required tag names | `list(string)` | `[]` | no | -| [schema\_grants](#input\_schema\_grants) | A list of Grants to give to the schema created with component. | `list(string)` | `[]` | no | +| [schema\_grants](#input\_schema\_grants) | A list of Grants to give to the schema created with component. | `list(string)` |
[
"MODIFY",
"MONITOR",
"USAGE",
"CREATE TABLE",
"CREATE VIEW"
]
| no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [table\_grants](#input\_table\_grants) | A list of Grants to give to the tables created with component. | `list(string)` | `[]` | no | +| [table\_grants](#input\_table\_grants) | A list of Grants to give to the tables created with component. | `list(string)` |
[
"SELECT",
"INSERT",
"UPDATE",
"DELETE",
"TRUNCATE",
"REFERENCES"
]
| no | | [tables](#input\_tables) | A map of tables to create for Snowflake. A schema and database will be assigned for this group of tables. | `map(any)` | `{}` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [view\_grants](#input\_view\_grants) | A list of Grants to give to the views created with component. | `list(string)` | `[]` | no | +| [view\_grants](#input\_view\_grants) | A list of Grants to give to the views created with component. | `list(string)` |
[
"SELECT",
"REFERENCES"
]
| no | | [views](#input\_views) | A map of views to create for Snowflake. The same schema and database will be assigned as for tables. | `map(any)` | `{}` | no | ## Outputs diff --git a/modules/snowflake-database/default.auto.tfvars b/modules/snowflake-database/default.auto.tfvars deleted file mode 100644 index 5ab99c1dd..000000000 --- a/modules/snowflake-database/default.auto.tfvars +++ /dev/null @@ -1,8 +0,0 @@ -# This file is included by default in terraform plans - -enabled = false - -database_grants = ["MODIFY", "MONITOR", "USAGE"] -schema_grants = ["MODIFY", "MONITOR", "USAGE", "CREATE TABLE", "CREATE VIEW"] -table_grants = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES"] -view_grants = ["SELECT", "REFERENCES"] diff --git a/modules/snowflake-database/variables.tf b/modules/snowflake-database/variables.tf index cdd64d997..6d085cdce 100644 --- a/modules/snowflake-database/variables.tf +++ b/modules/snowflake-database/variables.tf @@ -24,25 +24,25 @@ variable "views" { variable "database_grants" { type = list(string) description = "A list of Grants to give to the database created with component." - default = [] + default = ["MODIFY", "MONITOR", "USAGE"] } variable "schema_grants" { type = list(string) description = "A list of Grants to give to the schema created with component." - default = [] + default = ["MODIFY", "MONITOR", "USAGE", "CREATE TABLE", "CREATE VIEW"] } variable "table_grants" { type = list(string) description = "A list of Grants to give to the tables created with component." - default = [] + default = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES"] } variable "view_grants" { type = list(string) description = "A list of Grants to give to the views created with component." - default = [] + default = ["SELECT", "REFERENCES"] } variable "database_comment" { diff --git a/modules/sns-topic/default.auto.tfvars b/modules/sns-topic/default.auto.tfvars deleted file mode 100644 index 31d32a5b7..000000000 --- a/modules/sns-topic/default.auto.tfvars +++ /dev/null @@ -1,2 +0,0 @@ -# This file is included by default in terraform plans -enabled = false diff --git a/modules/sqs-queue/default.auto.tfvars b/modules/sqs-queue/default.auto.tfvars deleted file mode 100644 index 31d32a5b7..000000000 --- a/modules/sqs-queue/default.auto.tfvars +++ /dev/null @@ -1,2 +0,0 @@ -# This file is included by default in terraform plans -enabled = false diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index ee3159d00..20de9c78e 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -84,7 +84,7 @@ import: | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for default, which is `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [instance\_type](#input\_instance\_type) | The instance family to use for the ZScaler EC2 instances. | `string` | `"r5n.medium"` | no | +| [instance\_type](#input\_instance\_type) | The instance family to use for the ZScaler EC2 instances. | `string` | `"m5n.large"` | no | | [label\_key\_case](#input\_label\_key\_case) | The letter case of label keys (`tag` names) (i.e. `name`, `namespace`, `environment`, `stage`, `attributes`) to use in `tags`.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The naming order of the id output and Name tag.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 5 elements, but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | The letter case of output label values (also used in `tags` and `id`).
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/zscaler/default.auto.tfvars b/modules/zscaler/default.auto.tfvars deleted file mode 100644 index 063bfdca6..000000000 --- a/modules/zscaler/default.auto.tfvars +++ /dev/null @@ -1,6 +0,0 @@ -enabled = false - -name = "zscaler" - -# Cheapest instance that satisfies DenyInstancesWithoutEncryptionInTransit SCP (see: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/data-protection.html#encryption-transit) -instance_type = "m5n.large" diff --git a/modules/zscaler/variables.tf b/modules/zscaler/variables.tf index d3492dd92..8b7d5bff1 100644 --- a/modules/zscaler/variables.tf +++ b/modules/zscaler/variables.tf @@ -22,8 +22,11 @@ variable "aws_ssm_enabled" { } variable "instance_type" { - type = string - default = "r5n.medium" + type = string + # We default to m5n.large because it is cheapest instance that satisfies + # DenyInstancesWithoutEncryptionInTransit SCP + # (see: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/data-protection.html#encryption-transit ) + default = "m5n.large" description = "The instance family to use for the ZScaler EC2 instances." } variable "secrets_store_type" { From 7fa80f1902f157ad8def6dd9f37d49c4c65b6c60 Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 22 Aug 2023 13:24:53 -0700 Subject: [PATCH 238/501] [aws-sso] Fix root provider, restore `SetSourceIdentity` permission (#830) --- modules/aws-sso/CHANGELOG.md | 10 ++++- modules/aws-sso/README.md | 15 +++++-- .../policy-Identity-role-TeamAccess.tf | 1 + modules/aws-sso/providers.tf | 44 +++++++++---------- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/modules/aws-sso/CHANGELOG.md b/modules/aws-sso/CHANGELOG.md index 6815a2f01..5173ebc9e 100644 --- a/modules/aws-sso/CHANGELOG.md +++ b/modules/aws-sso/CHANGELOG.md @@ -2,7 +2,15 @@ ***NOTE***: This file is manually generated and is a work-in-progress. -### PR 740 +### PR 830 + +- Fix `providers.tf` to properly assign roles for `root` account when deploying to `identity` account. +- Restore the `sts:SetSourceIdentity` permission for Identity-role-TeamAccess +permission sets added in PR 738 and inadvertently removed in PR 740. +- Update comments and documentation to reflect Cloud Posse's current + recommendation that SSO ***not*** be delegated to the `identity` account. + +### Version 1.240.1, PR 740 This PR restores compatibility with `account-map` prior to version 1.227.0 and fixes bugs that made versions 1.227.0 up to this release unusable. diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index 2d5e26ad1..aab18d348 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -12,17 +12,26 @@ This component assumes that AWS SSO has already been enabled via the AWS Console 1. Select primary region 1. Go to AWS SSO 1. Enable AWS SSO + +#### Delegation no longer recommended + +Previously, Cloud Posse recommended delegating SSO to the identity account by following the next 2 steps: 1. Click Settings > Management -1. Delegate Identity as an administrator +1. Delegate Identity as an administrator. This can take up to 30 minutes to take effect. + +However, this is no longer recommended. Because the delegated SSO administrator cannot make changes in the `root` account +and this component needs to be able to make changes in the `root` account, any purported security advantage achieved by +delegating SSO to the `identity` account is lost. -Once identity is delegated, it will take up to 20 to 30 minutes for the identity account to understand its delegation. +Nevertheless, it is also not worth the effort to remove the delegation. If you have already delegated SSO to the `identity`, +continue on, leaving the stack configuration in the `gbl-identity` stack rather than the currently recommended `gbl-root` stack. ### Atmos **Stack Level**: Global **Deployment**: Must be deployed by root-admin using `atmos` CLI -Add catalog to `gbl-identity` root stack. +Add catalog to `gbl-root` root stack. #### `account_assignments` The `account_assignments` setting configures access to permission sets for users and groups in accounts, in the following structure: diff --git a/modules/aws-sso/policy-Identity-role-TeamAccess.tf b/modules/aws-sso/policy-Identity-role-TeamAccess.tf index 7c02b2ffd..371b293a3 100644 --- a/modules/aws-sso/policy-Identity-role-TeamAccess.tf +++ b/modules/aws-sso/policy-Identity-role-TeamAccess.tf @@ -12,6 +12,7 @@ data "aws_iam_policy_document" "assume_aws_team" { effect = "Allow" actions = [ "sts:AssumeRole", + "sts:SetSourceIdentity", "sts:TagSession", ] diff --git a/modules/aws-sso/providers.tf b/modules/aws-sso/providers.tf index 09347de4a..fb0b204f5 100644 --- a/modules/aws-sso/providers.tf +++ b/modules/aws-sso/providers.tf @@ -1,29 +1,29 @@ -# This is a special provider configuration that allows us to use many different -# versions of the Cloud Posse reference architecture to deploy this component -# in any account, including the identity and root accounts. +# This component is unusual in that part of it must be deployed to the `root` +# account. You have the option of where to deploy the remaining part, and +# Cloud Posse recommends you deploy it also to the `root` account, however +# it can be deployed to the `identity` account instead. In the discussion +# below, when we talk about where this module is being deployed, we are +# referring to the part of the module that is not deployed to the `root` +# account and is configured by setting `stage` etc.. -# If you have dynamic Terraform roles enabled and an `aws-team` (such as `managers`) -# empowered to make changes in the identity and root accounts. Then you can -# use those roles to deploy this component in the identity and root accounts, -# just like almost any other component. Leave `privileged: false` and leave the -# backend `role_arn` at its default value. +# If you have Dynamic Terraform Roles enabled, leave the backend `role_arn` at +# its default value. If deploying only to the `root` account, leave `privileged: false` +# and use either SuperAdmin or an appropriate `aws-team` (such as `managers`). +# If deploying to the `identity` account, set `privileged: true` +# and use SuperAdmin or any other role in the `root` account with Admin access. # # For those not using dynamic Terraform roles: # -# If you are deploying this to the "identity" account and are restricted to using -# the SuperAdmin role to deploy components to "identity", then you will need to -# set the stack configuration for this component to set `privileged: true` -# and backend `role_arn` to `null`. +# Set the stack configuration for this component to set `privileged: true` +# and backend `role_arn` to `null`, and deploy it using either the SuperAdmin +# role or any other role in the `root` account with Admin access. # # If you are deploying this to the "identity" account and have a team empowered -# to deploy components to "identity", then you will need to set the stack -# configuration for this component to set `privileged: false` and leave the -# backend `role_arn` at its default value. +# to deploy to both the "identity" and "root" accounts, then you have the option to set +# `privileged: false` and leave the backend `role_arn` at its default value, but +# then SuperAdmin will not be able to deploy this component, +# only the team with access to both accounts will be able to deploy it. # -# If you are deploying this to the "root" account, then you will need to -# set the stack configuration for this component to set `privileged: true` -# and backend `role_arn` to `null`, and deploy it using either the SuperAdmin -# role or any other role in the `root` account with Admin access. provider "aws" { region = var.region @@ -51,10 +51,10 @@ provider "aws" { alias = "root" region = var.region - profile = !var.privileged && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null + profile = !var.privileged && module.iam_roles_root.profiles_enabled ? module.iam_roles_root.terraform_profile_name : null dynamic "assume_role" { - for_each = !var.privileged && module.iam_roles.profiles_enabled ? [] : ( - var.privileged ? compact([module.iam_roles.org_role_arn]) : compact([module.iam_roles.terraform_role_arn]) + for_each = !var.privileged && module.iam_roles_root.profiles_enabled ? [] : ( + var.privileged ? compact([module.iam_roles_root.org_role_arn]) : compact([module.iam_roles_root.terraform_role_arn]) ) content { role_arn = assume_role.value From c0403bbe983a14219e867536cb8496970a4ce01d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 22 Aug 2023 14:32:58 -0700 Subject: [PATCH 239/501] Aurora Optional `vpc` Component Names (#832) --- modules/aurora-mysql/README.md | 3 ++- modules/aurora-mysql/remote-state.tf | 4 ++-- modules/aurora-mysql/variables.tf | 16 ++++++++++++++-- modules/aurora-postgres/README.md | 3 ++- modules/aurora-postgres/remote-state.tf | 4 ++-- modules/aurora-postgres/variables.tf | 9 +++++++++ 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index dcf250a28..c65a00fb9 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -198,7 +198,7 @@ Reploying the component should show no changes. For example, `atmos terraform ap | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | +| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
}

Defaults to the "vpc" component in the given account |
list(object({
vpc = optional(string)
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the RDS cluster | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [aurora\_mysql\_cluster\_family](#input\_aurora\_mysql\_cluster\_family) | DBParameterGroupFamily (e.g. `aurora5.6`, `aurora-mysql5.7` for Aurora MySQL databases). See https://stackoverflow.com/a/55819394 for help finding the right one to use. | `string` | n/a | yes | @@ -247,6 +247,7 @@ Reploying the component should show no changes. For example, `atmos terraform ap | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | The name of the VPC component | `string` | `"vpc"` | no | ## Outputs diff --git a/modules/aurora-mysql/remote-state.tf b/modules/aurora-mysql/remote-state.tf index f910e288c..97e3d2dc4 100644 --- a/modules/aurora-mysql/remote-state.tf +++ b/modules/aurora-mysql/remote-state.tf @@ -27,7 +27,7 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" - component = "vpc" + component = var.vpc_component_name context = module.this.context } @@ -38,7 +38,7 @@ module "vpc_ingress" { for_each = local.accounts_with_vpc - component = "vpc" + component = try(each.value.vpc, "vpc") environment = try(each.value.environment, module.this.environment) stage = try(each.value.stage, module.this.environment) tenant = try(each.value.tenant, module.this.tenant) diff --git a/modules/aurora-mysql/variables.tf b/modules/aurora-mysql/variables.tf index 462468951..b8397eb35 100644 --- a/modules/aurora-mysql/variables.tf +++ b/modules/aurora-mysql/variables.tf @@ -199,17 +199,29 @@ variable "primary_cluster_component" { } variable "allow_ingress_from_vpc_accounts" { - type = any + type = list(object({ + vpc = optional(string) + environment = optional(string) + stage = optional(string) + tenant = optional(string) + })) default = [] description = <<-EOF List of account contexts to pull VPC ingress CIDR and add to cluster security group. e.g. - { environment = "ue2", stage = "auto", tenant = "core" } + + Defaults to the "vpc" component in the given account EOF } + +variable "vpc_component_name" { + type = string + default = "vpc" + description = "The name of the VPC component" +} diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index d45a6c416..502795aa5 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -132,7 +132,7 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [admin\_password](#input\_admin\_password) | Postgres password for the admin user | `string` | `""` | no | | [admin\_user](#input\_admin\_user) | Postgres admin user name | `string` | `""` | no | -| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.
e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
} |
list(object({
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | +| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.
e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
}

Defaults to the "vpc" component in the given account |
list(object({
vpc = optional(string)
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDRs allowed to access the database (in addition to security groups and subnets) | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [autoscaling\_enabled](#input\_autoscaling\_enabled) | Whether to enable cluster autoscaling | `bool` | `false` | no | @@ -189,6 +189,7 @@ components: | [storage\_encrypted](#input\_storage\_encrypted) | Specifies whether the DB cluster is encrypted | `bool` | `true` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | The name of the VPC component | `string` | `"vpc"` | no | ## Outputs diff --git a/modules/aurora-postgres/remote-state.tf b/modules/aurora-postgres/remote-state.tf index 74c0a1cac..2db5dc539 100644 --- a/modules/aurora-postgres/remote-state.tf +++ b/modules/aurora-postgres/remote-state.tf @@ -2,7 +2,7 @@ module "vpc" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" - component = "vpc" + component = var.vpc_component_name context = module.cluster.context } @@ -17,7 +17,7 @@ module "vpc_ingress" { format("%s-%s", account.tenant, account.stage) : account.stage => account } - component = "vpc" + component = try(each.value.vpc, "vpc") tenant = try(each.value.tenant, module.this.tenant) environment = try(each.value.environment, module.this.environment) stage = try(each.value.stage, module.this.stage) diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 5d001f805..22de0d78c 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -274,6 +274,7 @@ variable "eks_component_names" { variable "allow_ingress_from_vpc_accounts" { type = list(object({ + vpc = optional(string) environment = optional(string) stage = optional(string) tenant = optional(string) @@ -287,6 +288,8 @@ variable "allow_ingress_from_vpc_accounts" { stage = "auto", tenant = "core" } + + Defaults to the "vpc" component in the given account EOF } @@ -299,3 +302,9 @@ variable "ssm_password_source" { a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. EOT } + +variable "vpc_component_name" { + type = string + default = "vpc" + description = "The name of the VPC component" +} From 1284d8cb2ccf19c547a2b97f6d4802806a6e6153 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 22 Aug 2023 18:18:29 -0700 Subject: [PATCH 240/501] Add visibility to default VPC component name (#833) --- modules/aurora-mysql/README.md | 2 +- modules/aurora-mysql/remote-state.tf | 2 +- modules/aurora-mysql/variables.tf | 2 +- modules/aurora-postgres/README.md | 2 +- modules/aurora-postgres/remote-state.tf | 2 +- modules/aurora-postgres/variables.tf | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index c65a00fb9..8520f852d 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -198,7 +198,7 @@ Reploying the component should show no changes. For example, `atmos terraform ap | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
}

Defaults to the "vpc" component in the given account |
list(object({
vpc = optional(string)
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | +| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
}

Defaults to the "vpc" component in the given account |
list(object({
vpc = optional(string, "vpc")
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the RDS cluster | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [aurora\_mysql\_cluster\_family](#input\_aurora\_mysql\_cluster\_family) | DBParameterGroupFamily (e.g. `aurora5.6`, `aurora-mysql5.7` for Aurora MySQL databases). See https://stackoverflow.com/a/55819394 for help finding the right one to use. | `string` | n/a | yes | diff --git a/modules/aurora-mysql/remote-state.tf b/modules/aurora-mysql/remote-state.tf index 97e3d2dc4..5f8201069 100644 --- a/modules/aurora-mysql/remote-state.tf +++ b/modules/aurora-mysql/remote-state.tf @@ -38,7 +38,7 @@ module "vpc_ingress" { for_each = local.accounts_with_vpc - component = try(each.value.vpc, "vpc") + component = each.value.vpc environment = try(each.value.environment, module.this.environment) stage = try(each.value.stage, module.this.environment) tenant = try(each.value.tenant, module.this.tenant) diff --git a/modules/aurora-mysql/variables.tf b/modules/aurora-mysql/variables.tf index b8397eb35..3297da7d5 100644 --- a/modules/aurora-mysql/variables.tf +++ b/modules/aurora-mysql/variables.tf @@ -200,7 +200,7 @@ variable "primary_cluster_component" { variable "allow_ingress_from_vpc_accounts" { type = list(object({ - vpc = optional(string) + vpc = optional(string, "vpc") environment = optional(string) stage = optional(string) tenant = optional(string) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 502795aa5..0d48399df 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -132,7 +132,7 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [admin\_password](#input\_admin\_password) | Postgres password for the admin user | `string` | `""` | no | | [admin\_user](#input\_admin\_user) | Postgres admin user name | `string` | `""` | no | -| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.
e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
}

Defaults to the "vpc" component in the given account |
list(object({
vpc = optional(string)
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | +| [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.
e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
}

Defaults to the "vpc" component in the given account |
list(object({
vpc = optional(string, "vpc")
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDRs allowed to access the database (in addition to security groups and subnets) | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [autoscaling\_enabled](#input\_autoscaling\_enabled) | Whether to enable cluster autoscaling | `bool` | `false` | no | diff --git a/modules/aurora-postgres/remote-state.tf b/modules/aurora-postgres/remote-state.tf index 2db5dc539..00b7a886e 100644 --- a/modules/aurora-postgres/remote-state.tf +++ b/modules/aurora-postgres/remote-state.tf @@ -17,7 +17,7 @@ module "vpc_ingress" { format("%s-%s", account.tenant, account.stage) : account.stage => account } - component = try(each.value.vpc, "vpc") + component = each.value.vpc tenant = try(each.value.tenant, module.this.tenant) environment = try(each.value.environment, module.this.environment) stage = try(each.value.stage, module.this.stage) diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 22de0d78c..131013aa1 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -274,7 +274,7 @@ variable "eks_component_names" { variable "allow_ingress_from_vpc_accounts" { type = list(object({ - vpc = optional(string) + vpc = optional(string, "vpc") environment = optional(string) stage = optional(string) tenant = optional(string) From 5c8597131b8222794203d8033996934d0f0fc7fa Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 24 Aug 2023 04:24:24 -0700 Subject: [PATCH 241/501] [spacelift/worker-pool] Update providers.tf nesting (#834) --- modules/spacelift/worker-pool/README.md | 2 +- modules/spacelift/worker-pool/providers.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index 164e4421e..f49162f5d 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -136,7 +136,7 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [autoscale\_group](#module\_autoscale\_group) | cloudposse/ec2-autoscale-group/aws | 0.35.1 | | [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_label](#module\_iam\_label) | cloudposse/label/null | 0.25.0 | -| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.2.0 | | [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/spacelift/worker-pool/providers.tf b/modules/spacelift/worker-pool/providers.tf index ef923e10a..89ed50a98 100644 --- a/modules/spacelift/worker-pool/providers.tf +++ b/modules/spacelift/worker-pool/providers.tf @@ -14,6 +14,6 @@ provider "aws" { } module "iam_roles" { - source = "../account-map/modules/iam-roles" + source = "../../account-map/modules/iam-roles" context = module.this.context } From f432fe53991ae27470f4272a856fa7674f1b1351 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Thu, 24 Aug 2023 11:50:29 -0400 Subject: [PATCH 242/501] Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` (#837) Co-authored-by: cloudpossebot --- .github/ISSUE_TEMPLATE/bug_report.yml | 72 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 71 +++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 23 ++++--- modules/spacelift/admin-stack/outputs.tf | 2 +- 4 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..f40fe2f56 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,72 @@ +--- +name: Bug report +description: Create a report to help us improve +labels: ["bug"] +assignees: [""] +body: + - type: markdown + attributes: + value: | + Found a bug? + + Please checkout our [Slack Community](https://slack.cloudposse.com) + or visit our [Slack Archive](https://archive.sweetops.com/). + + [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) + + - type: textarea + id: concise-description + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + placeholder: What is the bug about? + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected. + placeholder: What happened? + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: Steps to Reproduce + description: Steps to reproduce the behavior. + placeholder: How do we reproduce it? + validations: + required: true + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots or logs to help explain. + validations: + required: false + + - type: textarea + id: environment + attributes: + label: Environment + description: Anything that will help us triage the bug. + placeholder: | + - OS: [e.g. Linux, OSX, WSL, etc] + - Version [e.g. 10.15] + - Module version + - Terraform version + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: | + Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..44047f02e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,71 @@ +--- +name: Feature Request +description: Suggest an idea for this project +labels: ["feature request"] +assignees: [""] +body: + - type: markdown + attributes: + value: | + Have a question? + + Please checkout our [Slack Community](https://slack.cloudposse.com) + or visit our [Slack Archive](https://archive.sweetops.com/). + + [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) + + - type: textarea + id: concise-description + attributes: + label: Describe the Feature + description: A clear and concise description of what the feature is. + placeholder: What is the feature about? + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected. + placeholder: What happened? + validations: + required: true + + - type: textarea + id: use-case + attributes: + label: Use Case + description: | + Is your feature request related to a problem/challenge you are trying + to solve? + + Please provide some additional context of why this feature or + capability will be valuable. + validations: + required: true + + - type: textarea + id: ideal-solution + attributes: + label: Describe Ideal Solution + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives-considered + attributes: + label: Alternatives Considered + description: Explain alternative solutions or features considered. + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: | + Add any other context about the problem here. + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d443ce673..f5fb7d435 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,21 @@ ## what -* Describe high-level what changed as a result of these commits (i.e. in plain-english, what do these changes mean?) -* Use bullet points to be concise and to the point. + + ## why -* Provide the justifications for the changes (e.g. business case). -* Describe why these changes were made (e.g. why do these commits fix the problem?) -* Use bullet points to be concise and to the point. + + ## references -* Link to any supporting github issues or helpful documentation to add some context (e.g. stackoverflow). -* Use `closes #123`, if this PR closes a GitHub issue `#123` + + diff --git a/modules/spacelift/admin-stack/outputs.tf b/modules/spacelift/admin-stack/outputs.tf index 16b4a09d7..446281515 100644 --- a/modules/spacelift/admin-stack/outputs.tf +++ b/modules/spacelift/admin-stack/outputs.tf @@ -5,7 +5,7 @@ output "root_stack_id" { output "root_stack" { description = "The root stack, if enabled and created by this component" - value = local.enabled && local.create_root_admin_stack ? module.root_admin_stack : "" + value = local.enabled && local.create_root_admin_stack ? module.root_admin_stack : null sensitive = true } From d049f2c21df5b7989e956e4712aa86354976d8dd Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 25 Aug 2023 17:07:16 -0700 Subject: [PATCH 243/501] Aurora Upstream: Serverless, Tags, Enabled: False (#841) --- .../aurora-mysql-resources/provider-mysql.tf | 4 ++-- modules/aurora-mysql/ssm.tf | 20 ++++++++++--------- .../provider-postgres.tf | 4 ++-- modules/aurora-postgres/ssm.tf | 20 ++++++++++--------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/modules/aurora-mysql-resources/provider-mysql.tf b/modules/aurora-mysql-resources/provider-mysql.tf index 43186eed9..c3af1b0a9 100644 --- a/modules/aurora-mysql-resources/provider-mysql.tf +++ b/modules/aurora-mysql-resources/provider-mysql.tf @@ -9,11 +9,11 @@ locals { mysql_admin_user = module.aurora_mysql.outputs.aurora_mysql_master_username mysql_admin_password_key = module.aurora_mysql.outputs.aurora_mysql_master_password_ssm_key - mysql_admin_password = length(var.mysql_admin_password) > 0 ? var.mysql_admin_password : data.aws_ssm_parameter.admin_password[0].value + mysql_admin_password = local.enabled ? (length(var.mysql_admin_password) > 0 ? var.mysql_admin_password : data.aws_ssm_parameter.mysql_admin_password[0].value) : "" } data "aws_ssm_parameter" "admin_password" { - count = length(var.mysql_admin_password) > 0 ? 0 : 1 + count = local.enabled && !(length(var.mysql_admin_password) > 0) ? 1 : 0 name = local.mysql_admin_password_key diff --git a/modules/aurora-mysql/ssm.tf b/modules/aurora-mysql/ssm.tf index 40f04b20a..621c579bc 100644 --- a/modules/aurora-mysql/ssm.tf +++ b/modules/aurora-mysql/ssm.tf @@ -26,13 +26,6 @@ locals { type = "String" overwrite = true }, - { - name = format("%s/%s", local.ssm_path_prefix, "replicas_hostname") - value = module.aurora_mysql.replicas_host - description = "Aurora MySQL DB Replicas hostname" - type = "String" - overwrite = true - }, { name = format("%s/%s", local.ssm_path_prefix, "cluster_name") value = module.aurora_mysql.cluster_identifier @@ -41,6 +34,15 @@ locals { overwrite = true } ] + cluster_parameters = var.cluster_size > 0 ? [ + { + name = format("%s/%s", local.ssm_path_prefix, "replicas_hostname") + value = module.aurora_mysql.replicas_host + description = "Aurora MySQL DB Replicas hostname" + type = "String" + overwrite = true + }, + ] : [] admin_user_parameters = [ { name = local.mysql_admin_user_key @@ -58,7 +60,7 @@ locals { } ] - parameter_write = local.mysql_db_enabled ? concat(local.default_parameters, local.admin_user_parameters) : local.default_parameters + parameter_write = local.mysql_db_enabled ? concat(local.default_parameters, local.cluster_parameters, local.admin_user_parameters) : concat(local.default_parameters, local.cluster_parameters) } data "aws_ssm_parameter" "password" { @@ -78,5 +80,5 @@ module "parameter_store_write" { parameter_write = local.parameter_write - context = module.this.context + context = module.cluster.context } diff --git a/modules/aurora-postgres-resources/provider-postgres.tf b/modules/aurora-postgres-resources/provider-postgres.tf index f2bf51e34..f07512af9 100644 --- a/modules/aurora-postgres-resources/provider-postgres.tf +++ b/modules/aurora-postgres-resources/provider-postgres.tf @@ -3,11 +3,11 @@ locals { admin_user = module.aurora_postgres.outputs.config_map.username admin_password_key = module.aurora_postgres.outputs.config_map.password_ssm_key - admin_password = length(var.admin_password) > 0 ? var.admin_password : data.aws_ssm_parameter.admin_password[0].value + admin_password = local.enabled ? (length(var.admin_password) > 0 ? var.admin_password : data.aws_ssm_parameter.admin_password[0].value) : "" } data "aws_ssm_parameter" "admin_password" { - count = length(var.admin_password) > 0 ? 0 : 1 + count = local.enabled && !(length(var.admin_password) > 0) ? 1 : 0 name = local.admin_password_key diff --git a/modules/aurora-postgres/ssm.tf b/modules/aurora-postgres/ssm.tf index 322233e36..20619ddb6 100644 --- a/modules/aurora-postgres/ssm.tf +++ b/modules/aurora-postgres/ssm.tf @@ -30,13 +30,6 @@ locals { type = "String" overwrite = true }, - { - name = format("%s/%s", local.ssm_path_prefix, "replicas_hostname") - value = module.aurora_postgres_cluster.replicas_host - description = "Aurora Postgres DB Replicas hostname" - type = "String" - overwrite = true - }, { name = format("%s/%s", local.ssm_path_prefix, "cluster_name") value = module.aurora_postgres_cluster.cluster_identifier @@ -45,6 +38,15 @@ locals { overwrite = true } ] + cluster_parameters = var.cluster_size > 0 ? [ + { + name = format("%s/%s", local.ssm_path_prefix, "replicas_hostname") + value = module.aurora_postgres_cluster.replicas_host + description = "Aurora Postgres DB Replicas hostname" + type = "String" + overwrite = true + }, + ] : [] admin_user_parameters = [ { name = local.admin_user_key @@ -62,7 +64,7 @@ locals { } ] - parameter_write = concat(local.default_parameters, local.admin_user_parameters) + parameter_write = concat(local.default_parameters, local.cluster_parameters, local.admin_user_parameters) } data "aws_ssm_parameter" "password" { @@ -82,5 +84,5 @@ module "parameter_store_write" { parameter_write = local.parameter_write - context = module.this.context + context = module.cluster.context } From 5046af480286e5faa1b322175fcfe7d56ef6def0 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 25 Aug 2023 17:50:50 -0700 Subject: [PATCH 244/501] TGW FAQ and Spoke Alternate VPC Support (#840) --- modules/tgw/README.md | 56 ++++++++++++++++++++++++++++++++++ modules/tgw/spoke/README.md | 1 + modules/tgw/spoke/main.tf | 11 ++++--- modules/tgw/spoke/variables.tf | 6 ++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/modules/tgw/README.md b/modules/tgw/README.md index 0947462fe..68103f870 100644 --- a/modules/tgw/README.md +++ b/modules/tgw/README.md @@ -340,3 +340,59 @@ tgw/spoke: stage: prod environment: usw2 ``` + +## Destruction + +When destroying Transit Gateway components, order of operations matters. Always destroy any removed `tgw/spoke` components before removing a connection from the `tgw/hub` component. + +The `tgw/hub` component creates map of VPC resources that each `tgw/spoke` component references. If the required reference is removed before the `tgw/spoke` is destroyed, Terraform will fail to destroy the given `tgw/spoke` component. + +:::info Pro Tip! + +[Atmos Workflows](https://atmos.tools/core-concepts/workflows/) make applying and destroying Transit Gateway much easier! For example, to destroy components in the correct order, use a workflow similiar to the following: + +```yaml +# stacks/workflows/network.yaml +workflows: + destroy/tgw: + description: Destroy the Transit Gateway "hub" and "spokes" for connecting VPCs. + steps: + - command: echo 'Destroying platform spokes for Transit Gateway' + type: shell + name: plat-spokes + - command: terraform destroy tgw/spoke -s plat-use1-sandbox --auto-approve + - command: terraform destroy tgw/spoke -s plat-use1-dev --auto-approve + - command: terraform destroy tgw/spoke -s plat-use1-staging --auto-approve + - command: terraform destroy tgw/spoke -s plat-use1-prod --auto-approve + - command: echo 'Destroying core spokes for Transit Gateway' + type: shell + name: core-spokes + - command: terraform destroy tgw/spoke -s core-use1-auto --auto-approve + - command: terraform destroy tgw/spoke -s core-use1-network --auto-approve + - command: echo 'Destroying Transit Gateway Hub' + type: shell + name: hub + - command: terraform destroy tgw/hub -s core-use1-network --auto-approve +``` + +::: + +# FAQ + +## `tgw/spoke` Fails to Recreate VPC Attachment with `DuplicateTransitGatewayAttachment` Error + +```bash +╷ +│ Error: creating EC2 Transit Gateway VPC Attachment: DuplicateTransitGatewayAttachment: tgw-0xxxxxxxxxxxxxxxx has non-deleted Transit Gateway Attachments with same VPC ID. +│ status code: 400, request id: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee +│ +│ with module.tgw_spoke_vpc_attachment.module.standard_vpc_attachment.aws_ec2_transit_gateway_vpc_attachment.default["core-use2-network"], +│ on .terraform/modules/tgw_spoke_vpc_attachment.standard_vpc_attachment/main.tf line 43, in resource "aws_ec2_transit_gateway_vpc_attachment" "default": +│ 43: resource "aws_ec2_transit_gateway_vpc_attachment" "default" { +│ +╵ +Releasing state lock. This may take a few moments... +exit status 1 +``` + +This is caused by Terraform attempting to create the replacement VPC attachment before the original is completely destroyed. Retry the apply. Now you should see only "create" actions. diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index 2afd19e68..08e7e7e60 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -139,6 +139,7 @@ No resources. | [own\_eks\_component\_names](#input\_own\_eks\_component\_names) | The name of the eks components in the owning account. | `list(string)` | `[]` | no | | [own\_vpc\_component\_name](#input\_own\_vpc\_component\_name) | The name of the vpc component in the owning account. Defaults to "vpc" | `string` | `"vpc"` | no | | [peered\_region](#input\_peered\_region) | Set `true` if this region is not the primary region | `bool` | `false` | no | +| [primary\_vpc](#input\_primary\_vpc) | Set `false` if this region is not the primary VPC in this region | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/tgw/spoke/main.tf b/modules/tgw/spoke/main.tf index 32ef7792e..3f19b124b 100644 --- a/modules/tgw/spoke/main.tf +++ b/modules/tgw/spoke/main.tf @@ -21,10 +21,13 @@ module "tgw_hub_routes" { ram_resource_share_enabled = false route_keys_enabled = false - create_transit_gateway = false - create_transit_gateway_route_table = false - create_transit_gateway_vpc_attachment = false - create_transit_gateway_route_table_association_and_propagation = true + create_transit_gateway = false + create_transit_gateway_route_table = false + create_transit_gateway_vpc_attachment = false + + # Only create transit gateway route table association and propogation + # if this is the primary VPC in this given region + create_transit_gateway_route_table_association_and_propagation = var.primary_vpc config = { (local.spoke_account) = module.tgw_spoke_vpc_attachment.tg_config, diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index a9b66e42f..01c6d56ff 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -66,3 +66,9 @@ variable "peered_region" { description = "Set `true` if this region is not the primary region" default = false } + +variable "primary_vpc" { + type = bool + description = "Set `false` if this region is not the primary VPC in this region" + default = true +} From 10b5db5cc135fbcfb0fed1e3cf7b2eb77e891cba Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 28 Aug 2023 12:23:39 -0400 Subject: [PATCH 245/501] datadog agent update defaults (#839) --- modules/eks/datadog-agent/values.yaml | 38 ++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/modules/eks/datadog-agent/values.yaml b/modules/eks/datadog-agent/values.yaml index d07c5f943..b8215b2ab 100644 --- a/modules/eks/datadog-agent/values.yaml +++ b/modules/eks/datadog-agent/values.yaml @@ -1,17 +1,17 @@ registry: public.ecr.aws/datadog datadog: logLevel: INFO - # If running on Bottlerocket OS, uncomment the following lines. - # See https://docs.datadoghq.com/containers/kubernetes/distributions/?tab=helm#EKS + ## If running on Bottlerocket OS, uncomment the following lines. + ## See https://docs.datadoghq.com/containers/kubernetes/distributions/?tab=helm#EKS # criSocketPath: /run/dockershim.sock # Bottlerocket Only # env: # Bottlerocket Only # - name: DD_AUTOCONFIG_INCLUDE_FEATURES # Bottlerocket Only # value: "containerd" # Bottlerocket Only - # kubeStateMetricsEnabled is false because the feature is obsolete (replaced by kubeStateMetricsCore). - # See https://github.com/DataDog/helm-charts/issues/415#issuecomment-943117608 - # https://docs.datadoghq.com/integrations/kubernetes_state_core/?tab=helm - # https://www.datadoghq.com/blog/kube-state-metrics-v2-monitoring-datadog/ + ## kubeStateMetricsEnabled is false because the feature is obsolete (replaced by kubeStateMetricsCore). + ## See https://github.com/DataDog/helm-charts/issues/415#issuecomment-943117608 + ## https://docs.datadoghq.com/integrations/kubernetes_state_core/?tab=helm + ## https://www.datadoghq.com/blog/kube-state-metrics-v2-monitoring-datadog/ kubeStateMetricsEnabled: false kubeStateMetricsCore: enabled: true @@ -27,6 +27,10 @@ datadog: containerCollectUsingFiles: true apm: enabled: true + socketEnabled: true + useSocketVolume: true + serviceMonitoring: + enabled: true processAgent: enabled: true processCollection: true @@ -40,6 +44,8 @@ datadog: enabled: true networkMonitoring: enabled: false + clusterTagger: + collectKubernetesTags: true clusterChecksRunner: enabled: false clusterChecks: @@ -56,6 +62,11 @@ datadog: enabled: true collectEvents: true clusterAgent: + admissionController: + enabled: true + mutateUnlabelled: false + configMode: "hostip" + enabled: true # Maintain 2 cluster agents so that there is no interruption in metrics collection # when the cluster agents' node is being deprovisioned. @@ -78,6 +89,7 @@ clusterAgent: cpu: 300m memory: 512Mi agents: + enabled: true priorityClassName: "system-node-critical" ## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-dns-config ## without ndots: 2, DNS will try to resolve a DNS lookup 5 different ways @@ -89,3 +101,17 @@ agents: podSecurity: apparmor: enabled: false + tolerations: + - effect: NoSchedule + operator: Exists + - effect: NoExecute + operator: Exists + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: eks.amazonaws.com/compute-type + operator: NotIn + values: + - fargate From b93155edaa0c07c8730a1d6e35542986e6d0477f Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Mon, 28 Aug 2023 21:05:45 +0300 Subject: [PATCH 246/501] AWS provider V5 dependency updates (#729) Co-authored-by: cloudpossebot --- deprecated/eks/efs-controller/README.md | 2 +- deprecated/eks/efs-controller/main.tf | 2 +- modules/cloudtrail-bucket/README.md | 2 +- modules/cloudtrail-bucket/main.tf | 2 +- modules/config-bucket/README.md | 2 +- modules/config-bucket/main.tf | 2 +- modules/datadog-logs-archive/main.tf | 6 +++--- modules/eks/actions-runner-controller/README.md | 4 ++-- modules/eks/actions-runner-controller/main.tf | 4 ++-- modules/eks/argocd/README.md | 2 +- modules/eks/argocd/main.tf | 2 +- modules/eks/aws-node-termination-handler/README.md | 2 +- modules/eks/aws-node-termination-handler/main.tf | 2 +- modules/eks/cert-manager/README.md | 4 ++-- modules/eks/cert-manager/main.tf | 4 ++-- modules/eks/external-dns/README.md | 2 +- modules/eks/external-dns/main.tf | 2 +- modules/eks/external-secrets-operator/README.md | 4 ++-- modules/eks/external-secrets-operator/main.tf | 4 ++-- modules/eks/idp-roles/README.md | 2 +- modules/eks/idp-roles/main.tf | 2 +- modules/eks/karpenter/README.md | 2 +- modules/eks/karpenter/main.tf | 2 +- modules/eks/metrics-server/README.md | 2 +- modules/eks/metrics-server/main.tf | 2 +- modules/eks/redis-operator/README.md | 2 +- modules/eks/redis-operator/main.tf | 2 +- modules/eks/redis/README.md | 2 +- modules/eks/redis/main.tf | 2 +- 29 files changed, 37 insertions(+), 37 deletions(-) diff --git a/deprecated/eks/efs-controller/README.md b/deprecated/eks/efs-controller/README.md index e92ee66c2..c6c495c7a 100644 --- a/deprecated/eks/efs-controller/README.md +++ b/deprecated/eks/efs-controller/README.md @@ -60,7 +60,7 @@ components: | Name | Source | Version | |------|--------|---------| | [efs](#module\_efs) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | -| [efs\_controller](#module\_efs\_controller) | cloudposse/helm-release/aws | 0.5.0 | +| [efs\_controller](#module\_efs\_controller) | cloudposse/helm-release/aws | 0.9.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/deprecated/eks/efs-controller/main.tf b/deprecated/eks/efs-controller/main.tf index 9a32566c0..c09a9d4a2 100644 --- a/deprecated/eks/efs-controller/main.tf +++ b/deprecated/eks/efs-controller/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "efs_controller" { source = "cloudposse/helm-release/aws" - version = "0.5.0" + version = "0.9.1" name = var.name chart = var.chart diff --git a/modules/cloudtrail-bucket/README.md b/modules/cloudtrail-bucket/README.md index ef943c2df..723114132 100644 --- a/modules/cloudtrail-bucket/README.md +++ b/modules/cloudtrail-bucket/README.md @@ -38,7 +38,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [cloudtrail\_s3\_bucket](#module\_cloudtrail\_s3\_bucket) | cloudposse/cloudtrail-s3-bucket/aws | 0.25.0 | +| [cloudtrail\_s3\_bucket](#module\_cloudtrail\_s3\_bucket) | cloudposse/cloudtrail-s3-bucket/aws | 0.26.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/cloudtrail-bucket/main.tf b/modules/cloudtrail-bucket/main.tf index 0ad5770fc..1f7f89450 100644 --- a/modules/cloudtrail-bucket/main.tf +++ b/modules/cloudtrail-bucket/main.tf @@ -1,6 +1,6 @@ module "cloudtrail_s3_bucket" { source = "cloudposse/cloudtrail-s3-bucket/aws" - version = "0.25.0" + version = "0.26.1" acl = var.acl expiration_days = var.expiration_days diff --git a/modules/config-bucket/README.md b/modules/config-bucket/README.md index 25d1dfaf6..aba560b5a 100644 --- a/modules/config-bucket/README.md +++ b/modules/config-bucket/README.md @@ -47,7 +47,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [config\_bucket](#module\_config\_bucket) | cloudposse/config-storage/aws | 0.8.1 | +| [config\_bucket](#module\_config\_bucket) | cloudposse/config-storage/aws | 1.0.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/config-bucket/main.tf b/modules/config-bucket/main.tf index 8e8148aed..c029fe72b 100644 --- a/modules/config-bucket/main.tf +++ b/modules/config-bucket/main.tf @@ -1,6 +1,6 @@ module "config_bucket" { source = "cloudposse/config-storage/aws" - version = "0.8.1" + version = "1.0.0" expiration_days = var.expiration_days force_destroy = false diff --git a/modules/datadog-logs-archive/main.tf b/modules/datadog-logs-archive/main.tf index 66e6d593e..dfc9726de 100644 --- a/modules/datadog-logs-archive/main.tf +++ b/modules/datadog-logs-archive/main.tf @@ -138,7 +138,7 @@ data "aws_iam_policy_document" "default" { module "bucket_policy" { source = "cloudposse/iam-policy/aws" - version = "0.3.0" + version = "1.0.1" iam_policy_statements = try(lookup(local.policy, "Statement"), null) @@ -159,7 +159,7 @@ data "aws_partition" "current" { module "archive_bucket" { source = "cloudposse/s3-bucket/aws" - version = "2.0.1" + version = "3.1.2" count = local.enabled ? 1 : 0 @@ -218,7 +218,7 @@ module "archive_bucket" { module "cloudtrail_s3_bucket" { source = "cloudposse/s3-bucket/aws" - version = "2.0.1" + version = "3.1.2" depends_on = [data.aws_iam_policy_document.default] diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index af8be190b..27ecc0ce7 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -414,8 +414,8 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | Name | Source | Version | |------|--------|---------| -| [actions\_runner](#module\_actions\_runner) | cloudposse/helm-release/aws | 0.7.0 | -| [actions\_runner\_controller](#module\_actions\_runner\_controller) | cloudposse/helm-release/aws | 0.7.0 | +| [actions\_runner](#module\_actions\_runner) | cloudposse/helm-release/aws | 0.9.1 | +| [actions\_runner\_controller](#module\_actions\_runner\_controller) | cloudposse/helm-release/aws | 0.9.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index 30d05f596..c1b406949 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -110,7 +110,7 @@ data "aws_ssm_parameter" "docker_config_json" { module "actions_runner_controller" { source = "cloudposse/helm-release/aws" - version = "0.7.0" + version = "0.9.1" name = "" # avoids hitting length restrictions on IAM Role names chart = var.chart @@ -200,7 +200,7 @@ module "actions_runner" { for_each = local.enabled ? var.runners : {} source = "cloudposse/helm-release/aws" - version = "0.7.0" + version = "0.9.1" name = each.key chart = "${path.module}/charts/actions-runner" diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 052b1c949..62aec6073 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -393,7 +393,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.9.1 | +| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.11.0 | | [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.9.1 | | [argocd\_repo](#module\_argocd\_repo) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 8b879f867..e79b9666a 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -139,7 +139,7 @@ locals { module "argocd" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.11.0" name = "argocd" # avoids hitting length restrictions on IAM Role names chart = var.chart diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index a459124b0..e27878188 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -59,7 +59,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [aws\_node\_termination\_handler](#module\_aws\_node\_termination\_handler) | cloudposse/helm-release/aws | 0.5.0 | +| [aws\_node\_termination\_handler](#module\_aws\_node\_termination\_handler) | cloudposse/helm-release/aws | 0.9.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/aws-node-termination-handler/main.tf b/modules/eks/aws-node-termination-handler/main.tf index e4e2c1561..9405d2e79 100644 --- a/modules/eks/aws-node-termination-handler/main.tf +++ b/modules/eks/aws-node-termination-handler/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "aws_node_termination_handler" { source = "cloudposse/helm-release/aws" - version = "0.5.0" + version = "0.9.1" name = "" # avoids hitting length restrictions on IAM Role names chart = var.chart diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index acdab7eaf..58ad825c3 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -69,8 +69,8 @@ The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` | Name | Source | Version | |------|--------|---------| -| [cert\_manager](#module\_cert\_manager) | cloudposse/helm-release/aws | 0.7.0 | -| [cert\_manager\_issuer](#module\_cert\_manager\_issuer) | cloudposse/helm-release/aws | 0.7.0 | +| [cert\_manager](#module\_cert\_manager) | cloudposse/helm-release/aws | 0.9.1 | +| [cert\_manager\_issuer](#module\_cert\_manager\_issuer) | cloudposse/helm-release/aws | 0.9.1 | | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | diff --git a/modules/eks/cert-manager/main.tf b/modules/eks/cert-manager/main.tf index f87e21c49..bc02675ae 100644 --- a/modules/eks/cert-manager/main.tf +++ b/modules/eks/cert-manager/main.tf @@ -9,7 +9,7 @@ data "aws_partition" "current" { module "cert_manager" { source = "cloudposse/helm-release/aws" - version = "0.7.0" + version = "0.9.1" name = "" # avoids hitting length restrictions on IAM Role names chart = var.cert_manager_chart @@ -108,7 +108,7 @@ module "cert_manager" { module "cert_manager_issuer" { source = "cloudposse/helm-release/aws" - version = "0.7.0" + version = "0.9.1" # Only install the issuer if either letsencrypt_installed or selfsigned_installed is true enabled = local.enabled && (var.letsencrypt_enabled || var.cert_manager_issuer_selfsigned_enabled) diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index b58e78417..7f3f0ecf8 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -68,7 +68,7 @@ components: | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [dns\_gbl\_primary](#module\_dns\_gbl\_primary) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [external\_dns](#module\_external\_dns) | cloudposse/helm-release/aws | 0.7.0 | +| [external\_dns](#module\_external\_dns) | cloudposse/helm-release/aws | 0.9.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/external-dns/main.tf b/modules/eks/external-dns/main.tf index 7a0d67ec9..0126b2952 100644 --- a/modules/eks/external-dns/main.tf +++ b/modules/eks/external-dns/main.tf @@ -19,7 +19,7 @@ data "aws_partition" "current" { module "external_dns" { source = "cloudposse/helm-release/aws" - version = "0.7.0" + version = "0.9.1" name = module.this.name chart = var.chart diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 14fd861ef..7c93427c7 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -111,8 +111,8 @@ components: |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.8.1 | -| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.8.1 | +| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.9.1 | +| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.9.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf index 93cea7a6f..e4f155044 100644 --- a/modules/eks/external-secrets-operator/main.tf +++ b/modules/eks/external-secrets-operator/main.tf @@ -18,7 +18,7 @@ resource "kubernetes_namespace" "default" { # https://external-secrets.io/v0.5.9/guides-getting-started/ module "external_secrets_operator" { source = "cloudposse/helm-release/aws" - version = "0.8.1" + version = "0.9.1" name = "" # avoid redundant release name in IAM role: ...-ekc-cluster-external-secrets-operator-external-secrets-operator@secrets description = var.chart_description @@ -78,7 +78,7 @@ module "external_secrets_operator" { module "external_ssm_secrets" { source = "cloudposse/helm-release/aws" - version = "0.8.1" + version = "0.9.1" name = "ssm" # distinguish from external_secrets_operator description = "This Chart uses creates a SecretStore and ExternalSecret to pull variables (under a given path) from AWS SSM Parameter Store into a Kubernetes secret." diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index df66dd3ee..0832af1f3 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -43,7 +43,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [idp\_roles](#module\_idp\_roles) | cloudposse/helm-release/aws | 0.6.0 | +| [idp\_roles](#module\_idp\_roles) | cloudposse/helm-release/aws | 0.9.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/idp-roles/main.tf b/modules/eks/idp-roles/main.tf index 7103ea5af..d0e25886b 100644 --- a/modules/eks/idp-roles/main.tf +++ b/modules/eks/idp-roles/main.tf @@ -4,7 +4,7 @@ locals { module "idp_roles" { source = "cloudposse/helm-release/aws" - version = "0.6.0" + version = "0.9.1" # Required arguments name = module.this.name diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 62725cd38..146427322 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -319,7 +319,7 @@ For more details, refer to: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.7.0 | +| [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.9.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index b2c0f0c11..fd7151417 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -28,7 +28,7 @@ resource "aws_iam_instance_profile" "default" { # Deploy Karpenter helm chart module "karpenter" { source = "cloudposse/helm-release/aws" - version = "0.7.0" + version = "0.9.1" chart = var.chart repository = var.chart_repository diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 1acb73502..710fce7ae 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -60,7 +60,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [metrics\_server](#module\_metrics\_server) | cloudposse/helm-release/aws | 0.5.0 | +| [metrics\_server](#module\_metrics\_server) | cloudposse/helm-release/aws | 0.9.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/metrics-server/main.tf b/modules/eks/metrics-server/main.tf index 620599d70..038156404 100644 --- a/modules/eks/metrics-server/main.tf +++ b/modules/eks/metrics-server/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "metrics_server" { source = "cloudposse/helm-release/aws" - version = "0.5.0" + version = "0.9.1" name = "" # avoids hitting length restrictions on IAM Role names chart = var.chart diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 0e815149d..85f77c835 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -88,7 +88,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [redis\_operator](#module\_redis\_operator) | cloudposse/helm-release/aws | 0.5.0 | +| [redis\_operator](#module\_redis\_operator) | cloudposse/helm-release/aws | 0.9.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/redis-operator/main.tf b/modules/eks/redis-operator/main.tf index a91e0e8f8..75a788013 100644 --- a/modules/eks/redis-operator/main.tf +++ b/modules/eks/redis-operator/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "redis_operator" { source = "cloudposse/helm-release/aws" - version = "0.5.0" + version = "0.9.1" chart = var.chart repository = var.chart_repository diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index e7cdbda55..25319c0be 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -94,7 +94,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [redis](#module\_redis) | cloudposse/helm-release/aws | 0.5.0 | +| [redis](#module\_redis) | cloudposse/helm-release/aws | 0.9.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/redis/main.tf b/modules/eks/redis/main.tf index f612b45a8..fc7c46518 100644 --- a/modules/eks/redis/main.tf +++ b/modules/eks/redis/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "redis" { source = "cloudposse/helm-release/aws" - version = "0.5.0" + version = "0.9.1" chart = var.chart repository = var.chart_repository From e76f0c7aa708ceed8e0996aaa996e1b3cc660cf3 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 28 Aug 2023 13:56:01 -0700 Subject: [PATCH 247/501] Aurora Postgres Engine Options (#845) --- modules/aurora-postgres/README.md | 178 ++++++++++++++++++-- modules/aurora-postgres/cluster-regional.tf | 2 + modules/aurora-postgres/variables.tf | 21 +++ 3 files changed, 188 insertions(+), 13 deletions(-) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 0d48399df..dd7dee3da 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -27,25 +27,20 @@ components: deletion_protection: false storage_encrypted: true engine: aurora-postgresql + + # Provisioned configuration engine_mode: provisioned - # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Updates.20180305.html - # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.PostgreSQL.html - # aws rds describe-db-engine-versions --engine aurora-postgresql --query 'DBEngineVersions[].EngineVersion' - engine_version: "13.4" - # engine and cluster family are notoriously hard to find. - # If you know the engine version (example here is "12.4"), use Engine and DBParameterGroupFamily from: - # aws rds describe-db-engine-versions --engine aurora-postgresql --query "DBEngineVersions[]" | \ - # jq '.[] | select(.EngineVersion == "12.4") | - # { Engine: .Engine, EngineVersion: .EngineVersion, DBParameterGroupFamily: .DBParameterGroupFamily }' - cluster_family: aurora-postgresql13 + engine_version: "15.3" + cluster_family: aurora-postgresql15 # 1 writer, 1 reader - cluster_size: 1 + cluster_size: 2 + # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.DBInstanceClass.html + instance_type: db.t3.medium + admin_user: postgres admin_password: "" # generate random password database_name: postgres database_port: 5432 - # https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.DBInstanceClass.html - instance_type: db.t3.medium skip_final_snapshot: false # Enhanced Monitoring # A boolean flag to enable/disable the creation of the enhanced monitoring IAM role. @@ -80,6 +75,161 @@ components: ``` +### Finding Aurora Engine Version + +Use the following to query the AWS API by `engine-mode`. Both provisioned and Serverless v2 use the `privisoned` engine mode, whereas only Serverless v1 uses the `serverless` engine mode. + +```bash +aws rds describe-db-engine-versions \ + --engine aurora-postgresql \ + --query 'DBEngineVersions[].EngineVersion' \ + --filters 'Name=engine-mode,Values=serverless' +``` + +Use the following to query AWS API by `db-instance-class`. Use this query to find supported versions for a specific instance class, such as `db.serverless` with Serverless v2. + +```bash +aws rds describe-orderable-db-instance-options \ + --engine aurora-postgresql \ + --db-instance-class db.serverless \ + --query 'OrderableDBInstanceOptions[].[EngineVersion]' +``` + +Once a version has been selected, use the following to find the cluster family. + +```bash +aws rds describe-db-engine-versions --engine aurora-postgresql --query "DBEngineVersions[]" | \ +jq '.[] | select(.EngineVersion == "15.3") | + { Engine: .Engine, EngineVersion: .EngineVersion, DBParameterGroupFamily: .DBParameterGroupFamily }' +``` + +## Examples + +Generally there are three different engine configurations for Aurora: provisioned, Serverless v1, and Serverless v2. + +### Provisioned Aurora Postgres + +[See the default usage example above](#Usage) + +### Serverless v1 Aurora Postgres + +Serverless v1 requires `engine-mode` set to `serverless` uses `scaling_configuration` to configure scaling options. + +For valid values, see [ModifyCurrentDBClusterCapacity](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyCurrentDBClusterCapacity.html). + +```yaml +components: + terraform: + aurora-postgres: + vars: + enabled: true + name: aurora-postgres + eks_component_names: + - eks/cluster + allow_ingress_from_vpc_accounts: + # Allows Spacelift + - tenant: core + stage: auto + environment: use2 + # Allows VPN + - tenant: core + stage: network + environment: use2 + cluster_name: shared + engine: aurora-postgresql + + # Serverless v1 configuration + engine_mode: serverless + instance_type: "" # serverless engine_mode ignores `var.instance_type` + engine_version: "13.9" # Latest supported version as of 08/28/2023 + cluster_family: aurora-postgresql13 + cluster_size: 0 # serverless + scaling_configuration: + - auto_pause: true + max_capacity: 5 + min_capacity: 2 + seconds_until_auto_pause: 300 + timeout_action: null + + admin_user: postgres + admin_password: "" # generate random password + database_name: postgres + database_port: 5432 + storage_encrypted: true + deletion_protection: true + skip_final_snapshot: false + # Creating read-only users or additional databases requires Spacelift + read_only_users_enabled: false + # Enhanced Monitoring + # A boolean flag to enable/disable the creation of the enhanced monitoring IAM role. + # If set to false, the module will not create a new role and will use rds_monitoring_role_arn for enhanced monitoring + enhanced_monitoring_role_enabled: true + enhanced_monitoring_attributes: ["monitoring"] + # The interval, in seconds, between points when enhanced monitoring metrics are collected for the DB instance. + # To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. Valid Values: 0, 1, 5, 10, 15, 30, 60 + rds_monitoring_interval: 15 + iam_database_authentication_enabled: false + additional_users: {} +``` + +### Serverless v2 Aurora Postgres + +Aurora Postgres Serverless v2 uses the `provisioned` engine mode with `db.serverless` instances. In order to configure scaling with Serverless v2, use `var.serverlessv2_scaling_configuration`. + +For more on valid scaling configurations, see [Performance and scaling for Aurora Serverless v2](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.setting-capacity.html). + +```yaml +components: + terraform: + aurora-postgres: + vars: + enabled: true + name: aurora-postgres + eks_component_names: + - eks/cluster + allow_ingress_from_vpc_accounts: + # Allows Spacelift + - tenant: core + stage: auto + environment: use2 + # Allows VPN + - tenant: core + stage: network + environment: use2 + cluster_name: shared + engine: aurora-postgresql + + # Serverless v2 configuration + engine_mode: provisioned + instance_type: "db.serverless" + engine_version: "15.3" + cluster_family: aurora-postgresql15 + cluster_size: 2 + serverlessv2_scaling_configuration: + min_capacity: 2 + max_capacity: 64 + + admin_user: postgres + admin_password: "" # generate random password + database_name: postgres + database_port: 5432 + storage_encrypted: true + deletion_protection: true + skip_final_snapshot: false + # Creating read-only users or additional databases requires Spacelift + read_only_users_enabled: false + # Enhanced Monitoring + # A boolean flag to enable/disable the creation of the enhanced monitoring IAM role. + # If set to false, the module will not create a new role and will use rds_monitoring_role_arn for enhanced monitoring + enhanced_monitoring_role_enabled: true + enhanced_monitoring_attributes: ["monitoring"] + # The interval, in seconds, between points when enhanced monitoring metrics are collected for the DB instance. + # To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. Valid Values: 0, 1, 5, 10, 15, 30, 60 + rds_monitoring_interval: 15 + iam_database_authentication_enabled: false + additional_users: {} +``` + ## Requirements @@ -181,6 +331,8 @@ components: | [reader\_dns\_name\_part](#input\_reader\_dns\_name\_part) | Part of DNS name added to module and cluster name for DNS for cluster reader | `string` | `"reader"` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [scaling\_configuration](#input\_scaling\_configuration) | List of nested attributes with scaling properties. Only valid when `engine_mode` is set to `serverless`. This is required for Serverless v1 |
list(object({
auto_pause = bool
max_capacity = number
min_capacity = number
seconds_until_auto_pause = number
timeout_action = string
}))
| `[]` | no | +| [serverlessv2\_scaling\_configuration](#input\_serverlessv2\_scaling\_configuration) | Nested attribute with scaling properties for ServerlessV2. Only valid when `engine_mode` is set to `provisioned.` This is required for Serverless v2 |
object({
min_capacity = number
max_capacity = number
})
| `null` | no | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Normally AWS makes a snapshot of the database before deleting it. Set this to `true` in order to skip this.
NOTE: The final snapshot has a name derived from the cluster name. If you delete a cluster, get a final snapshot,
then create a cluster of the same name, its final snapshot will fail with a name collision unless you delete
the previous final snapshot first. | `bool` | `false` | no | | [snapshot\_identifier](#input\_snapshot\_identifier) | Specifies whether or not to create this cluster from a snapshot | `string` | `null` | no | | [ssm\_password\_source](#input\_ssm\_password\_source) | If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using
`var.ssm_password_source` and the database username. If this value is not set,
a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. | `string` | `""` | no | diff --git a/modules/aurora-postgres/cluster-regional.tf b/modules/aurora-postgres/cluster-regional.tf index 2400f7f35..ad825ca40 100644 --- a/modules/aurora-postgres/cluster-regional.tf +++ b/modules/aurora-postgres/cluster-regional.tf @@ -45,6 +45,8 @@ module "aurora_postgres_cluster" { autoscaling_scale_out_cooldown = var.autoscaling_scale_out_cooldown autoscaling_min_capacity = var.autoscaling_min_capacity autoscaling_max_capacity = var.autoscaling_max_capacity + scaling_configuration = var.scaling_configuration + serverlessv2_scaling_configuration = var.serverlessv2_scaling_configuration skip_final_snapshot = var.skip_final_snapshot deletion_protection = var.deletion_protection snapshot_identifier = var.snapshot_identifier diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 131013aa1..d852a49f9 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -308,3 +308,24 @@ variable "vpc_component_name" { default = "vpc" description = "The name of the VPC component" } + +variable "scaling_configuration" { + type = list(object({ + auto_pause = bool + max_capacity = number + min_capacity = number + seconds_until_auto_pause = number + timeout_action = string + })) + default = [] + description = "List of nested attributes with scaling properties. Only valid when `engine_mode` is set to `serverless`. This is required for Serverless v1" +} + +variable "serverlessv2_scaling_configuration" { + type = object({ + min_capacity = number + max_capacity = number + }) + default = null + description = "Nested attribute with scaling properties for ServerlessV2. Only valid when `engine_mode` is set to `provisioned.` This is required for Serverless v2" +} From c724f319523ee0387a843c8a09f73655e28a39fe Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 30 Aug 2023 10:24:42 -0400 Subject: [PATCH 248/501] docs: add notifications to account-settings readme (#846) --- modules/account-settings/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index 3825fc5b3..132ca39a5 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -34,6 +34,27 @@ components: limit_amount: "3" limit_unit: GB time_unit: MONTHLY + notification: + - comparison_operator: GREATER_THAN + notification_type: FORECASTED + threshold_type: PERCENTAGE + threshold: 80 + subscribers: + - slack + - comparison_operator: GREATER_THAN + notification_type: FORECASTED + threshold_type: PERCENTAGE + # We generate two forecast notifications. This makes sure that notice is taken, + # and hopefully action can be taken to prevent going over budget. + threshold: 100 + subscribers: + - slack + - comparison_operator: GREATER_THAN + notification_type: ACTUAL + threshold_type: PERCENTAGE + threshold: 100 + subscribers: + - slack service_quotas_enabled: true service_quotas: - quota_name: Subnets per VPC From b35541f8d572bb7f9cd17f8759c446dfd1bd893b Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Wed, 30 Aug 2023 22:26:48 +0300 Subject: [PATCH 249/501] GHA workflow to update Changelog (#827) --- .github/workflows/update-changelog.yml | 176 + CHANGELOG.md | 5027 ++++++++++++++++++++++++ docs/upgrade-guide.md | 9 - 3 files changed, 5203 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/update-changelog.yml create mode 100644 CHANGELOG.md delete mode 100644 docs/upgrade-guide.md diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml new file mode 100644 index 000000000..a06764b92 --- /dev/null +++ b/.github/workflows/update-changelog.yml @@ -0,0 +1,176 @@ +name: "Update Changelog" + +on: + release: + types: + - published + +permissions: + id-token: write + contents: write + pull-requests: write + +jobs: + update-changelog: + runs-on: + - "self-hosted" + + steps: + - name: Current Release + id: current-release + uses: actions/github-script@v6 + with: + script: | + const event = ${{ toJSON(github.event) }}; + + const tag = event.release.tag_name; + const body = event.release.body; + + console.log(`Current release tag: ${tag}`); + console.log(`Current release body: ${body}`); + + core.setOutput('tag', tag); + core.setOutput('body', Buffer.from(body).toString('base64')); + + - name: Previous Release + id: previous-release + uses: actions/github-script@v6 + with: + script: | + const releases = await github.rest.repos.listReleases({ + ...context.repo + }); + + const currentReleaseIndex = releases.data.findIndex(release => "${{ steps.current-release.outputs.tag }}" === context.payload.release.tag_name); + + const previousRelease = releases.data[currentReleaseIndex + 1]; + const tag = previousRelease.tag_name; + + console.log(`Previous release tag: ${tag}`); + + core.setOutput('tag', tag); + + - name: Checkout Current Release + uses: actions/checkout@v3 + with: + ref: "${{ steps.current-release.outputs.tag }}" + path: current + + - name: Checkout Previous Release + uses: actions/checkout@v3 + with: + ref: "${{ steps.previous-release.outputs.tag }}" + path: previous + + - name: Find Updated CHANGELOG.md files + id: updated + uses: actions/github-script@v6 + with: + script: | + const path = require('path'); + const fs = require('fs'); + const crypto = require('crypto'); + + function findChangelogs(dir, fileList = []) { + const files = fs.readdirSync(dir); + + files.forEach(file => { + if (fs.statSync(path.join(dir, file)).isDirectory()) { + fileList = findChangelogs(path.join(dir, file), fileList); + } else if (file === 'CHANGELOG.md') { + fileList.push(path.join(dir, file)); + } + }); + + return fileList; + } + + function calculateHash(file) { + const hash = crypto.createHash('md5'); + const fileContent = fs.readFileSync(file); + + hash.update(fileContent); + + return hash.digest('hex'); + } + + function trimPath(relativePath) { + return relativePath + .replace('components/terraform/', '') + .replace('/CHANGELOG.md', ''); + } + + const currentChangeLogFiles = findChangelogs('./current/components'); + const components = []; + + for (let i = 0; i < currentChangeLogFiles.length; i++) { + const currentReleaseFile = currentChangeLogFiles[i]; + const relativePath = currentReleaseFile.replace(/^current\//, ''); + const previousReleaseFile = `previous/${relativePath}` + + if (!fs.existsSync(previousReleaseFile)) { + console.log(`New CHANGELOG.md found: ${relativePath}`); + components.push(trimPath(relativePath)); + continue; + } + + if (calculateHash(currentReleaseFile) !== calculateHash(previousReleaseFile)) { + console.log(`CHANGELOG.md changed: ${relativePath}`); + components.push(trimPath(relativePath)); + } else { + console.log(`${relativePath} didn't change. Skipping ...`); + } + } + + core.setOutput('components', JSON.stringify(components)); + + - name: Checkout + uses: actions/checkout@v3 + + - name: Generate Changelog + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + + const tag = "${{ steps.current-release.outputs.tag }}"; + let body = Buffer.from("${{ steps.current-release.outputs.body }}", 'base64').toString('utf-8'); + const updatedComponents = JSON.parse(`${{ steps.updated.outputs.components }}`); + + let affectedComponents = ""; + if (updatedComponents.length > 0) { + affectedComponents += "\n\n"; + affectedComponents += "## Affected Components\n"; + + for (let i = 0; i < updatedComponents.length; i++) { + const relativePath = updatedComponents[i]; + affectedComponents += `- [${relativePath}](https://docs.cloudposse.com/components/library/aws/${relativePath}#changelog)\n` + } + + affectedComponents += "\n\n"; + } + + const content = `\n## ${tag}\n\n${affectedComponents}\n\n${body}\n` + + console.log(content); + + const filePath = 'CHANGELOG.md' + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const lines = fileContent.split('\n'); + + lines.splice(1, 0, content); + + const updatedContent = lines.join('\n'); + fs.writeFileSync(filePath, updatedContent, 'utf-8'); + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + title: 'Update Changelog for `${{ steps.current-release.outputs.tag }}`' + body: 'Update Changelog for [`${{ steps.current-release.outputs.tag }}`](${{ github.event.release.html_url }})' + base: main + branch: "changelog/${{ steps.current-release.outputs.tag }}" + delete-branch: "true" + commit-message: "Update Changelog for ${{ steps.current-release.outputs.tag }}" + labels: | + no-release diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..2f1a70dc6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5027 @@ +# CHANGELOG + +## 1.298.0 (2023-08-28T20:56:25Z) + +
+ Aurora Postgres Engine Options @milldr (#845) + +### what +- Add scaling configuration variables for both Serverless and Serverless v2 to `aurora-postgres` +- Update `aurora-postgres` README + +### why +- Support both serverless options +- Add an explanation for how to configure each, and where to find valid engine options + +### references +- n/a + +
+ + +## 1.297.0 (2023-08-28T18:06:11Z) + +
+ AWS provider V5 dependency updates @max-lobur (#729) + +### what +* Update component dependencies for the AWS provider V5 + +Requested components: +- cloudtrail-bucket +- config-bucket +- datadog-logs-archive +- eks/argocd +- eks/efs-controller +- eks/metric-server +- spacelift-worker-pool +- eks/external-secrets-operator + +### why +* Maintenance + + + +
+ + +## 1.296.0 (2023-08-28T16:24:05Z) + +
+ datadog agent update defaults @Benbentwo (#839) + +### what + +- prevent fargate agents +- use sockets instead of ports for APM +- enable other services + +### why + +- Default Datadog APM enabled over k8s + +### references + + +
+ + +## 1.295.0 (2023-08-26T00:51:10Z) + +
+ TGW FAQ and Spoke Alternate VPC Support @milldr (#840) + +### what +- Added FAQ to the TGW upgrade guide for replacing attachments +- Added note about destroying TGW components +- Added option to not create TGW propagation and association when connecting an alternate VPC + +### why +- When connecting an alternate VPC in the same region as the primary VPC, we do not want to create a duplicate TGW propagation and association + +### references +- n/a + + +
+ + +## 1.294.0 (2023-08-26T00:07:42Z) + +
+ Aurora Upstream: Serverless, Tags, Enabled: False @milldr (#841) + +### what +- Set `module.context` to `module.cluster` across all resources +- Only set parameter for replica if cluster size is > 0 +- `enabled: false` support + +### why +- Missing tags for SSM parameters for cluster attributes +- Serverless clusters set `cluster_size: 0`, which will break the SSM parameter for replica hostname (since it does not exist) +- Support enabled false for `aurora-*-resources` components + +### references +- n/a + +
+ + +## 1.293.2 (2023-08-24T15:50:53Z) + +### 🚀 Enhancements + +
+ Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` @aknysh (#837) + +### what +* Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` + +### why +* Fix the issue described in https://github.com/cloudposse/terraform-aws-components/issues/771 + +### related +* Closes https://github.com/cloudposse/terraform-aws-components/issues/771 + + +
+ + +## 1.293.1 (2023-08-24T11:24:46Z) + +### 🐛 Bug Fixes + +
+ [spacelift/worker-pool] Update providers.tf nesting @Nuru (#834) + +### what + +- Update relative path to `account-map` in `spacelift/worker-pool/providers.tf` + +### why + +- Fixes #828 + + +
+ + +## 1.293.0 (2023-08-23T01:18:53Z) + +
+ Add visibility to default VPC component name @milldr (#833) + +### what +- Set the default component name for `vpc` in variables, not remote-state + +### why +- Bring visibility to where the default is set + +### references +- Follow up on comments on #832 + + +
+ + +## 1.292.0 (2023-08-22T21:33:18Z) + +
+ Aurora Optional `vpc` Component Names @milldr (#832) + +### what +- Allow optional VPC component names in the aurora components + +### why +- Support deploying the clusters for other VPC components than `"vpc"` + +### references +- n/a + + +
+ + +## 1.291.1 (2023-08-22T20:25:17Z) + +### 🐛 Bug Fixes + +
+ [aws-sso] Fix root provider, restore `SetSourceIdentity` permission @Nuru (#830) + +### what + +For `aws-sso`: +- Fix root provider, improperly restored in #740 +- Restore `SetSourceIdentity` permission inadvertently removed in #740 + +### why + +- When deploying to `identity`, `root` provider did not reference `root` account +- Likely unintentional removal due to merge error + +### references + +- #740 +- #738 + + +
+ + +## 1.291.0 (2023-08-22T17:08:27Z) + +
+ chore: remove defaults from components @dudymas (#831) + +### what +* remove `defaults.auto.tfvars` from component modules + +### why +* in favor of drying up configuration using atmos + +### Notes +* Some defaults may not be captured yet. Regressions might occur. + + +
+ + +## 1.290.0 (2023-08-21T18:57:43Z) + +
+ Upgrade aws-config and conformance pack modules to 1.1.0 @johncblandii (#829) + +### what +* Upgrade aws-config and conformance pack modules to 1.1.0 + +### why +* They're outdated. + +### references + +- #771 + + +
+ + +## 1.289.2 (2023-08-21T08:53:08Z) + +### 🐛 Bug Fixes + +
+ [eks/alb-controller] Fix naming convention of overridable local variable @Nuru (#826) + +### what + +- [eks/alb-controller] Change name of local variable from `distributed_iam_policy_overridable` to `overridable_distributed_iam_policy` + +### why + +- Cloud Posse style guide requires `overridable` as prefix, not suffix. + +
+ + +## 1.289.1 (2023-08-19T05:20:26Z) + +### 🐛 Bug Fixes + +
+ [eks/alb-controller] Update ALB controller IAM policy @Nuru (#821) + +### what + +- [eks/alb-controller] Update ALB controller IAM policy + +### why + +- Previous policy had error preventing the creation of the ELB service-linked role + + + +
+ + +## 1.289.0 (2023-08-18T20:18:12Z) + +
+ Spacelift Alternate git Providers @milldr (#825) + +### what +- set alternate git provider blocks to filter under `settings.spacelift` + +### why +- Debugging GitLab support specifically +- These settings should be defined under `settings.spacelift`, not as a top-level configuration + +### references +- n/a + + +
+ + +## 1.288.0 (2023-08-18T15:12:16Z) + +
+ Placeholder for `upgrade-guide.md` @milldr (#823) + +### what +- Added a placeholder file for `docs/upgrade-guide.md` with a basic explanation of what is to come + +### why +- With #811 we moved the contents of this upgrade-guide file to the individual component. We plan to continue adding upgrade guides for individual components, and in addition, create a higher-level upgrade guide here +- However, the build steps for refarch-scaffold expect `docs/upgrade-guide.md` to exist and are failing without it. We need a placeholder until the `account-map`, etc changes are added to this file + +### references +- Example of failing release: https://github.com/cloudposse/refarch-scaffold/actions/runs/5885022872 + + +
+ + +## 1.287.2 (2023-08-18T14:42:49Z) + +### 🚀 Enhancements + +
+ update boolean logic @mcalhoun (#822) + +### what +* Update the GuardDuty component to enable GuardDuty on the root account + +### why + +The API call to designate organization members now fails with the following if GuardDuty was not already enabled in the organization management (root) account : + +``` +Error: error designating guardduty administrator account members: [{ +│ AccountId: "111111111111, +│ Result: "Operation failed because your organization master must first enable GuardDuty to be added as a member" +│ }] +``` + + +
+ + +## 1.287.1 (2023-08-17T16:41:24Z) + +### 🚀 Enhancements + +
+ chore: Remove unused + @MaxymVlasov (#818) + + # why + +``` +TFLint in components/terraform/eks/cluster/: +2 issue(s) found: + +Warning: [Fixable] local.identity_account_name is declared but not used (terraform_unused_declarations) + + on main.tf line 9: + 9: identity_account_name = module.iam_roles.identity_account_account_name + +Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md + +Warning: [Fixable] variable "aws_teams_rbac" is declared but not used (terraform_unused_declarations) + + on variables.tf line 117: + 117: variable "aws_teams_rbac" { + +Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md +``` + + +
+ + +## 1.287.0 (2023-08-17T15:52:57Z) + +
+ Update `remote-states` modules to the latest version @aknysh (#820) + +### what +* Update `remote-states` modules to the latest version + +### why +* `remote-state` version `1.5.0` uses the latest version of `terraform-provider-utils` which uses the latest version of Atmos with many new features and improvements + + + +
+ + +## 1.286.0 (2023-08-17T05:49:45Z) + +
+ Update cloudposse/utils/aws to 1.3.0 @RoseSecurity (#815) + +### What: + +- Updated the following to utilize the newest version of `cloudposse/utils/aws`: + +``` +0.8.1 modules/spa-s3-cloudfront +1.1.0 modules/aws-config +1.1.0 modules/datadog-configuration/modules/datadog_keys +1.1.0 modules/dns-delegated +``` + +### Why: + +- `cloudposse/utils/aws` components were not updated to `1.3.0` + +### References: + +- [AWS Utils](https://github.com/cloudposse/terraform-aws-utils/releases/tag/1.3.0) + +
+ + +## 1.285.0 (2023-08-17T05:49:09Z) + +
+ Update api-gateway-account-settings README.md @johncblandii (#819) + +### what +* Updated the title + +### why +* It was an extra helping of copy/pasta + +### references + + +
+ + +## 1.284.0 (2023-08-17T02:10:47Z) + +
+ Datadog upgrades @Nuru (#814) + +### what + +- Update Datadog components: + - `eks/datadog-agent` see `eks/datadog-agent/CHANGELOG.md` + - `datadog-configuration` better handling of `enabled = false` + - `datadog-integration` move "module count" back to "module" for better compatibility and maintainability, see `datadog-integration/CHANGELOG.md` + - `datadog-lambda-forwared` fix issues around `enable = false` and incomplete destruction of resources (particularly log groups) see `datadog-lambda-forwarder/CHANGELOG.md` + - Cleanup `datadog-monitor` see `datadog-monitor/CHANGELOG.md` for details. Possible breaking change in that several inputs have been removed, but they were previously ignored anyway, so no infrastructure change should result from you simply removing any inputs you had for the removed inputs. + - Update `datadog-sythetics` dependency `remote-state` version + - `datadog-synthetics-private-location` migrate control of namespace to `helm-release` module. Possible destruction and recreation of component on upgrade. See CHANGELOG.md + +### why + +- More reliable deployments, especially when destroying or disabling them +- Bug fixes and new features + + + +
+ + +## 1.283.0 (2023-08-16T17:23:39Z) + +
+ Update EC2-Autoscale-Group Modules to 0.35.1 @RoseSecurity (#809) + +### What: + +- Updated `modules/spacelift/worker-pool` from 0.34.2 to 0.35.1 and adapted new variable features +- Updated `modules/bastion` from 0.35.0 to 0.35.1 +- Updated `modules/github-runners` from 0.35.0 to 0.35.1 + +### Why: + +- Modules were utilizing previous `ec2-autoscale-group` versions + +### References: + +- [terraform-aws-ec2-autoscale-group](https://github.com/cloudposse/terraform-aws-ec2-autoscale-group/blob/main/variables.tf) +- [Terraform Registry](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#instance_refresh) + +
+ +
+ Update storage-class efs component documentation @max-lobur (#817) + +### what +* Update storage-class efs component defaults + +### why +* Follow component move outside of eks dir + + + +
+ + +## 1.282.1 (2023-08-15T21:48:02Z) + +### 🐛 Bug Fixes + +
+ Karpenter bugfix, EKS add-ons to mangaed node group @Nuru (#816) + +### what + +- [eks/karpenter] use Instance Profile name from EKS output +- Clarify recommendation and fix defaults regarding deploying add-ons to managed node group + +### why + +- Bug fix: Karpenter did not work when legacy mode disabled +- Originally we expected to use Karpenter-only clusters and the documentation and defaults aligned with this. Now we recommend all Add-Ons be deployed to a managed node group, but the defaults and documentation did not reflect this. + + + +
+ + +## 1.282.0 (2023-08-14T16:05:08Z) + +
+ Upstream the latest ecs-service module @goruha (#810) + +### what +* Upsteam the latest `ecs-service` component + +### why +* Support ecspresso deployments +* Support s3 task definition mirroring +* Support external ALB/NLN components + +
+ + +## 1.281.0 (2023-08-14T09:10:42Z) + +
+ Refactor Changelog @milldr (#811) + +### what +- moved changelog for individual components +- changed title + +### why +- Title changelogs consistently by components version +- Separate changes by affected components + +### references +- https://github.com/cloudposse/knowledge-base/discussions/132 + + +
+ + +## 1.280.1 (2023-08-14T08:06:42Z) + +### 🚀 Enhancements + +
+ Fix eks/cluster default values @Nuru (#813) + +### what + +- Fix eks/cluster `node_group_defaults` to default to legal (empty) values for `kubernetes_labels` and `kubernetes_taints` +- Increase eks/cluster managed node group default disk size from 20 to 50 GB + +### why + +- Default values should be legal values or else they are not really defaults +- Nodes were running out of disk space just hosting daemon set pods at 20 GB + +
+ + +## 1.280.0 (2023-08-11T20:13:45Z) + +
+ Updated ssm parameter versions @RoseSecurity (#812) + +### Why: + +- `cloudposse/ssm-parameter-store/aws` was out of date +- There are no new [changes](https://github.com/cloudposse/terraform-aws-ssm-parameter-store/releases/tag/0.11.0) incorporated but just wanted to standardize new modules to updated version + +### What: + +- Updated the following to `v0.11.0`: + +``` +0.10.0 modules/argocd-repo +0.10.0 modules/aurora-mysql +0.10.0 modules/aurora-postgres +0.10.0 modules/datadog-configuration +0.10.0 modules/eks/platform +0.10.0 modules/opsgenie-team/modules/integration +0.10.0 modules/ses +0.9.1 modules/datadog-integration +``` + +
+ + +## 1.279.0 (2023-08-11T16:39:01Z) + +
+ fix: restore argocd notification ssm lookups @dudymas (#764) + +### what +* revert some changes to `argocd` component +* connect argocd notifications with ssm secrets +* remove `deployment_id` from `argocd-repo` component +* correct `app_hostname` since gha usually adds protocol + +### why +* regressions with argocd notifications caused github actions to timeout +* `deployment_id` no longer needed for fascilitating communication between gha +and ArgoCD +* application urls were incorrect and problematic during troubleshooting + + +
+ + +## 1.278.0 (2023-08-09T21:54:09Z) + +
+ Upstream `eks/keda` @milldr (#808) + +### what +- Added the component `eks/keda` + +### why +- We've deployed KEDA for a few customers now and the component should be upstreamed + +### references +- n/a + + +
+ + +## 1.277.0 (2023-08-09T20:39:21Z) + +
+ Added Inputs for `elasticsearch` and `cognito` @milldr (#786) + +### what +- Added `deletion_protection` for `cognito` +- Added options for dedicated master for `elasticsearch` + +### why +- Allow the default options to be customized + +### references +- Customer requested additions + + +
+ + +## 1.276.1 (2023-08-09T20:30:36Z) + +
+ Update upgrade-guide.md Version @milldr (#807) + +### what +- Set the version to the correct updated release + +### why +- Needs to match correct version + +### references +#804 + +
+ + +### 🚀 Enhancements + +
+ feat: allow email to be configured at account level @sgtoj (#799) + +### what +* allow email to be configured at account level + +### why +* to allow importing existing accounts with email address that does not met the organization standard naming format + +### references +* n/a + + +
+ + +## 1.276.0 (2023-08-09T16:38:40Z) + +
+ Transit Gateway Cross-Region Support @milldr (#804) + +### what +- Upgraded `tgw` components to support cross region connections +- Added back `tgw/cross-region-hub-connector` with overhaul to support updated `tgw/hub` component + +### why +- Deploy `tgw/cross-region-hub-connector` to create peered TGW hubs +- Use `tgw/hub` both for in region and intra region connections + +### references +- n/a + + +
+ + +## 1.275.0 (2023-08-09T02:53:39Z) + +
+ [eks/cluster] Proper handling of cold start and enabled=false @Nuru (#806) + +### what + +- Proper handling of cold start and `enabled=false` + +### why + +- Fixes #797 +- Supersedes and closes #798 +- Cloud Posse standard requires error-free operation and no resources created when `enabled` is `false`, but previously this component had several errors + + + +
+ + +## 1.274.2 (2023-08-09T00:13:36Z) + +### 🚀 Enhancements + +
+ Added Enabled Parameter to aws-saml/okta-user and datadog-synthetics-private-location @RoseSecurity (#805) + +### What: + +- Added `enabled` parameter for `modules/aws-saml/modules/okta-user/main.tf` and `modules/datadog-private-location-ecs/main.tf` + +### Why: + +- No support for disabling the creation of the resources + + + +
+ + +## 1.274.1 (2023-08-09T00:11:55Z) + +### 🚀 Enhancements + +
+ Updated Security Group Component to 2.2.0 @RoseSecurity (#803) + +### What: + +- Updated `bastion`, `redshift`, `rds`, `spacelift`, and `vpc` to utilize the newest version of `cloudposse/security-group/aws` + +### Why: + +- `cloudposse/security-group/aws` components were not updated to `2.2.0` + +### References: + +- [AWS Security Group Component](https://github.com/cloudposse/terraform-aws-security-group/compare/2.0.0-rc1...2.2.0) + + +
+ + +## 1.274.0 (2023-08-08T17:03:41Z) + +
+ bug: update descriptions *_account_account_name variables @sgtoj (#801) + +### what +* update descriptions `*_account_account_name` variables + - I replaced `stage` with `short` because that is the description used for the respective `outputs` entries + +### why +* to help future implementors of CloudPosse's architectures + +### references +* n/a + +
+ + +## 1.273.0 (2023-08-08T17:01:23Z) + +
+ docs: fix issue with eks/cluster usage snippet @sgtoj (#796) + +### what +- update usage snippet in readme for `eks/cluster` component + +### why +- fix incorrect shape for one of the items in `aws_team_roles_rbac` +- improve consistency +- remove variables that are not appliable for the component + +### references +- n/a + + +
+ + +## 1.272.0 (2023-08-08T17:00:32Z) + +
+ feat: filter out “SUSPENDED” accounts for account-map @sgtoj (#800) + +### what +* filter out “SUSPENDED” accounts (aka accounts in waiting period for termination) for `account-map` component + +### why +* suspended account cannot be used, so therefore it should not exist in the account-map +* allows for new _active_ accounts with same exact name of suspended account to exists and work with `account-map` + +### references +* n/a + + +
+ + +## 1.271.0 (2023-08-08T16:44:18Z) + +
+ `eks/karpenter` Readme.md update @Benbentwo (#792) + +### what +* Adding Karpenter troubleshooting to readme +* Adding https://endoflife.date/amazon-eks to `EKS/Cluster` + +### references +* https://karpenter.sh/docs/troubleshooting/ +* https://endoflife.date/amazon-eks + +
+ + +## 1.270.0 (2023-08-07T21:54:49Z) + +
+ [eks/cluster] Add support for BottleRocket and EFS add-on @Nuru (#795) + +### what + +- Add support for EKS EFS add-on +- Better support for Managed Node Group's Block Device Storage +- Deprecate and ignore `aws_teams_rbac` and remove `identity` roles from `aws-auth` +- Support `eks/cluster` provisioning EC2 Instance Profile for Karpenter nodes (disabled by default via legacy flags) +- More options for specifying Availability Zones +- Deprecate `eks/ebs-controller` and `eks/efs-controller` +- Deprecate `eks/eks-without-spotinst` + +### why + +- Support EKS add-ons, follow-up to #723 +- Support BottleRocket, `gp3` storage, and provisioned iops and throughput +- Feature never worked +- Avoid specific failure mode when deleting and recreating an EKS cluster +- Maintain feature parity with `vpc` component +- Replace with add-ons +- Was not being maintained or used + + + + + +
+ +
+ [eks/storage-class] Initial implementation @Nuru (#794) + +### what + +- Initial implementation of `eks/storage-class` + +### why + +- Until now, we provisioned StorageClasses as a part of deploying [eks/ebs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/ebs-controller/main.tf#L20-L56) and [eks/efs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/efs-controller/main.tf#L48-L60). However, with the switch from deploying "self-managed" controllers to EKS add-ons, we no longer deploy `eks/ebs-controller` or `eks/efs-controller`. Therefore, we need a new component to manage StorageClasses independently of controllers. + + +### references + +- #723 + + +
+ +
+ [eks/karpenter] Script to update Karpenter CRDs @Nuru (#793) + +### what + +- [eks/karpenter] Script to update Karpenter CRDs + +### why + +- Upgrading Karpenter to v0.28.0 requires updating CRDs, which is not handled by current Helm chart. This script updates them by modifying the existing CRDs to be labeled as being managed by Helm, then installing the `karpenter-crd` Helm chart. + +### references + +- Karpenter [CRD Upgrades](https://karpenter.sh/docs/upgrade-guide/#custom-resource-definition-crd-upgrades) + + + + +
+ + +## 1.269.0 (2023-08-03T20:47:56Z) + +
+ upstream `api-gateway` and `api-gateway-settings` @Benbentwo (#788) + +### what +* Upstream api-gateway and it's corresponding settings component + + +
+ + +## 1.268.0 (2023-08-01T05:04:37Z) + +
+ Added new variable into `argocd-repo` component to configure ArgoCD's `ignore-differences` @zdmytriv (#785) + +### what +* Added new variable into `argocd-repo` component to configure ArcoCD `ignore-differences` + +### why +* There are cases when application and/or third-party operators might want to change k8s API objects. For example, change the number of replicas in deployment. This will conflict with ArgoCD application because the ArgoCD controller will spot drift and will try to make an application in sync with the codebase. + +### references +* https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs + + +
+ + +## 1.267.0 (2023-07-31T19:41:43Z) + +
+ Spacelift `admin-stack` `var.description` @milldr (#787) + +### what +- added missing description option + +### why +- Variable is defined, but never passed to the modules + +### references +n/a + + +
+ + +## 1.266.0 (2023-07-29T18:00:25Z) + +
+ Use s3_object_ownership variable @sjmiller609 (#779) + +### what +* Pass s3_object_ownership variable into s3 module + +### why +* I think it was accidentally not included +* Make possible to disable ACL from stack config + +### references + +* https://github.com/cloudposse/terraform-aws-s3-bucket/releases/tag/3.1.0 + + +
+ + +## 1.265.0 (2023-07-28T21:35:14Z) + +
+ `bastion` support for `availability_zones` and public IP and subnets @milldr (#783) + +### what +- Add support for `availability_zones` +- Fix issue with public IP and subnets +- `tflint` requirements -- removed all unused locals, variables, formatting + +### why +- All instance types are not available in all AZs in a region +- Bug fix + +### references +- [Internal Slack reference](https://cloudposse.slack.com/archives/C048LCN8LKT/p1689085395494969) + + +
+ + +## 1.264.0 (2023-07-28T18:57:28Z) + +
+ Aurora Resource Submodule Requirements @milldr (#775) + +### what +- Removed unnecessary requirement for aurora resources for the service name not to equal the user name for submodules of both aurora resource components + +### why +- This conditional doesn't add any value besides creating an unnecessary restriction. We should be able to create a user name as the service name if we want + +### references +- n/a + + +
+ + +## 1.263.0 (2023-07-28T18:12:30Z) + +
+ fix: restore notifications config in argocd @dudymas (#782) + +### what +* Restore ssm configuration options for argocd notifications + +### why +* notifications were not firing and tasks time out in some installations + + +
+ + +## 1.262.0 (2023-07-27T17:05:37Z) + +
+ Upstream `spa-s3-cloudfront` @milldr (#780) + +### what +- Update module +- Add Cloudfront Invalidation permission to GitHub policy + +### why +- Corrected bug in the module +- Allow GitHub Actions to run invalidations + +### references +- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/288 + + +
+ + +## 1.261.0 (2023-07-26T16:20:37Z) + +
+ Upstream `spa-s3-cloudfront` @milldr (#778) + +### what +- Upstream changes to `spa-s3-cloudfront` + +### why +- Updated the included modules to support Terraform v5 +- Handle disabled WAF from remote-state + +### references +- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/284 + + +
+ + +## 1.260.1 (2023-07-25T05:10:20Z) + +### 🚀 Enhancements + +
+ [vpc] bugfix, [aurora-postgres] & [cloudtrail-bucket] Tflint fixes @Nuru (#776) + +### what + +- [vpc]: disable vpc_endpoints when enabled = false +- [aurora-postgres]: ensure variables have explicit types +- [cloudtrail-bucket]: ensure variables have explicit types + +### why + +- bugfix +- tflint fix +- tflint fix + + + +
+ + +### 🐛 Bug Fixes + +
+ [vpc] bugfix, [aurora-postgres] & [cloudtrail-bucket] Tflint fixes @Nuru (#776) + +### what + +- [vpc]: disable vpc_endpoints when enabled = false +- [aurora-postgres]: ensure variables have explicit types +- [cloudtrail-bucket]: ensure variables have explicit types + +### why + +- bugfix +- tflint fix +- tflint fix + + + +
+ + +## 1.260.0 (2023-07-23T23:08:53Z) + +
+ Update `alb` component @aknysh (#773) + +### what +* Update `alb` component + +### why +* Fixes after provisioning and testing on AWS + + +
+ + +## 1.259.0 (2023-07-20T04:32:13Z) + +
+ `elasticsearch` DNS Component Lookup @milldr (#769) + +### what +- add environment for `dns-delegated` component lookup + +### why +- `elasticsearch` is deployed in a regional environment, but `dns-delegated` is deployed to `gbl` + +### references +- n/a + + +
+ + +## 1.258.0 (2023-07-20T04:17:31Z) + +
+ Bump `lambda-elasticsearch-cleanup` module @milldr (#768) + +### what +- bump version of `lambda-elasticsearch-cleanup` module + +### why +- Support Terraform provider v5 + +### references +- https://github.com/cloudposse/terraform-aws-lambda-elasticsearch-cleanup/pull/48 + +
+ + +## 1.257.0 (2023-07-20T03:04:51Z) + +
+ Bump ECS cluster module @max-lobur (#752) + +### what +* Update ECS cluster module + +### why +* Maintenance + + + + +
+ + +## 1.256.0 (2023-07-18T23:57:44Z) + +
+ Bump `elasticache-redis` Module @milldr (#767) + +### what +- Bump `elasticache-redis` module + +### why +- Resolve issues with terraform provider v5 + +### references +- https://github.com/cloudposse/terraform-aws-elasticache-redis/issues/199 + + +
+ + +## 1.255.0 (2023-07-18T22:53:51Z) + +
+ Aurora Postgres Enhanced Monitoring Input @milldr (#766) + +### what +- Added `enhanced_monitoring_attributes` as option +- Set default `aurora-mysql` component name + +### why +- Set this var with a custom value to avoid IAM role length restrictions (default unchanged) +- Set common value as default + +### references +- n/a + + +
+ + +## 1.254.0 (2023-07-18T21:00:30Z) + +
+ feat: acm no longer requires zone @dudymas (#765) + +### what +* `acm` only looks up zones if `process_domain_validation_options` is true + +### why +* Allow external validation of acm certs + + +
+ + +## 1.253.0 (2023-07-18T17:45:16Z) + +
+ `alb` and `ssm-parameters` Upstream for Basic Use @milldr (#763) + +### what +- `alb` component can get the ACM cert from either `dns-delegated` or `acm` +- Support deploying `ssm-parameters` without SOPS +- `waf` requires a value for `visibility_config` in the stack catalog + +### why +- resolving bugs while deploying example components + +### references +- https://cloudposse.atlassian.net/browse/JUMPSTART-1185 + +
+ + +## 1.252.0 (2023-07-18T16:14:23Z) + +
+ fix: argocd flags, versions, and expressions @dudymas (#753) + +### what +* adjust expressions in argocd +* update helmchart module +* tidy up variables + +### why +* component wouldn't run + + +
+ + +## 1.251.0 (2023-07-15T03:47:29Z) + +
+ fix: ecs capacity provider typing @dudymas (#762) + +### what +* Adjust typing of `capacity_providers_ec2` + +### why +* Component doesn't work without these fixes + + +
+ + +## 1.250.3 (2023-07-15T00:31:40Z) + +### 🚀 Enhancements + +
+ Update `alb` and `eks/alb-controller` components @aknysh (#760) + +### what +* Update `alb` and `eks/alb-controller` components + +### why +* Remove unused variables and locals +* Apply variables that are defined in `variables.tf` but were not used + + +
+ + +## 1.250.2 (2023-07-14T23:34:14Z) + +### 🚀 Enhancements + +
+ [aws-teams] Remove obsolete restriction on assuming roles in identity account @Nuru (#761) + +### what + +- [aws-teams] Remove obsolete restriction on assuming roles in the `identity` account + +### why + +Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy against assuming roles in the `identity` account. + +AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the previous restriction is both not needed and actually hinders desired operation. + + + + +
+ + +### 🐛 Bug Fixes + +
+ [aws-teams] Remove obsolete restriction on assuming roles in identity account @Nuru (#761) + +### what + +- [aws-teams] Remove obsolete restriction on assuming roles in the `identity` account + +### why + +Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy against assuming roles in the `identity` account. + +AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the previous restriction is both not needed and actually hinders desired operation. + + + + +
+ + +## 1.250.1 (2023-07-14T02:14:46Z) + +### 🚀 Enhancements + +
+ [eks/karpenter-provisioner] minor improvements @Nuru (#759) + +### what + +- [eks/karpenter-provisioner]: + - Implement `metadata_options` + - Avoid Terraform errors by marking Provisoner `spec.requirements` a computed field + - Add explicit error message about Consolidation and TTL Seconds After Empty being mutually exclusive + - Add `instance-category` and `instance-generation` to example in README + - Make many inputs optional +- [eks/karpenter] Update README to indicate that version 0.19 or later of Karpenter is required to work with this code. + +### why + +- Bug Fix: Input was there, but was being ignored, leading to unexpected behavior +- If a requirement that had a default value was not supplied, Terraform would fail with an error about inconsistent plans because Karpenter would fill in the default +- Show some default values and how to override them +- Reduce the burden of supplying empty fields + + +
+ + +## 1.250.0 (2023-07-14T02:10:46Z) + +
+ Add EKS addons and the required IRSA to the `eks` component @aknysh (#723) + +### what +* Deprecate the `eks-iam` component +* Add EKS addons and the required IRSA for the addons to the `eks` component +* Add ability to specify configuration values and timeouts for addons +* Add ability to deploy addons to Fargate when necessary +* Add ability to omit specifying Availability Zones and infer them from private subnets +* Add recommended but optional and requiring opt-in: use a single Fargate Pod Execution Role for all Fargate Profiles + +### why +* The `eks-iam` component is not in use (we now create the IAM roles for Kubernetes Service Accounts in the https://github.com/cloudposse/terraform-aws-helm-release module), and has very old and outdated code + +* AWS recommends to provision the required EKS addons and not to rely on the managed addons (some of which are automatically provisioned by EKS on a cluster) + +* Some EKS addons (e.g. `vpc-cni` and `aws-ebs-csi-driver`) require an IAM Role for Kubernetes Service Account (IRSA) with specific permissions. Since these addons are critical for cluster functionality, we create the IRSA roles for the addons in the `eks` component and provide the role ARNs to the addons + +* Some EKS addons can be configured. In particular, `coredns` requires configuration to enable it to be deployed to Fargate. + +* Users relying on Karpenter to deploy all nodes and wanting to deploy `coredns` or `aws-ebs-csi-driver` addons need to deploy them to Fargate or else the EKS deployment will fail. + +* Enable DRY specification of Availability Zones, and use of AZ IDs, by reading the VPCs AZs. + +* A cluster needs only one Fargate Pod Execution Role, and it was a mistake to provision one for every profile. However, making the change would break existing clusters, so it is optional and requires opt-in. + +### references +- https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html +- https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on +- https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html +- https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html +- https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role +- https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/#deploy-vpc-cni-managed-add-on +- https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html +- https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons +- https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role +- https://github.com/kubernetes-sigs/aws-ebs-csi-driver + + +
+ + +## 1.249.0 (2023-07-14T01:23:37Z) + +
+ Make alb-controller default Ingress actually the default Ingress @Nuru (#758) + +### what + +- Make the `alb-controller` default Ingress actually the default Ingress + +### why + +- When setting `default_ingress_enabled = true` it is a reasonable expectation that the deployed Ingress be marked as the Default Ingress. The previous code suggests this was the intended behavior, but does not work with the current Helm chart and may have never worked. + + + +
+ + +## 1.248.0 (2023-07-13T00:21:29Z) + +
+ Upstream `gitops` Policy Update @milldr (#757) + +### what +- allow actions on table resources + +### why +- required to be able to query using a global secondary index + +### references +- https://github.com/cloudposse/github-action-terraform-plan-storage/pull/16 + + +
+ + +## 1.247.0 (2023-07-12T19:32:33Z) + +
+ Update `waf` and `alb` components @aknysh (#755) + +### what +* Update `waf` component +* Update `alb` component + +### why +* For `waf` component, add missing features supported by the following resources: + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration + +* For `waf` component, remove deprecated features not supported by Terraform `aws` provider v5: + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade#resourceaws_wafv2_web_acl + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade#resourceaws_wafv2_web_acl_logging_configuration + +* For `waf` component, allow specifying a list of Atmos components to read from the remote state and associate their ARNs with the web ACL + +* For `alb` component, update the modules to the latest versions and allow specifying Atmos component names for the remote state in the variables (for the cases where the Atmos component names are not standard) + +### references +* https://github.com/cloudposse/terraform-aws-waf/pull/45 + + +
+ + +## 1.246.0 (2023-07-12T18:57:58Z) + +
+ `acm` Upstream @Benbentwo (#756) + +### what +* Upstream ACM + +### why +* New Variables + * `subject_alternative_names_prefixes` + * `domain_name_prefix` + + + +
+ + +## 1.245.0 (2023-07-11T19:36:11Z) + +
+ Bump `spaces` module versions @milldr (#754) + +### what +- bumped module version for `terraform-spacelift-cloud-infrastructure-automation` + +### why +- New policy added to `spaces` + +### references +- https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/1.1.0 + + +
+ + +## 1.244.0 (2023-07-11T17:50:19Z) + +
+ Upstream Spacelift and Documentation @milldr (#732) + +### what +- Minor corrections to spacelift components +- Documentation + +### why +- Deployed this at a customer and resolved the changed errors +- Adding documentation for updated Spacelift design + +### references +- n/a + + +
+ + +## 1.243.0 (2023-07-06T20:04:08Z) + +
+ Upstream `gitops` @milldr (#735) + +### what +- Upstream new component, `gitops` + +### why +- This component is used to create a role for GitHub to assume. This role is used to assume the `gitops` team and is required for enabling GitHub Action Terraform workflows + +### references +- JUMPSTART-904 + + +
+ + +## 1.242.1 (2023-07-05T19:46:08Z) + +### 🚀 Enhancements + +
+ Use the new subnets data source @max-lobur (#737) + +### what +* Use the new subnets data source + +### why +* Planned migration according to https://github.com/hashicorp/terraform-provider-aws/pull/18803 + + + +
+ + +## 1.242.0 (2023-07-05T17:05:57Z) + +
+ Restore backwards compatibility of account-map output @Nuru (#748) + +### what + +- Restore backwards compatibility of `account-map` output + +### why + +- PR #715 removed outputs from `account-map` that `iam-roles` relied on. Although it removed the references in `iam-roles`, this imposed an ordering on the upgrade: the `iam-roles` code had to be deployed before the module could be applied. That proved to be inconvenient. Furthermore, if a future `account-map` upgrade added outputs that iam-roles` required, neither order of operations would go smoothly. With this update, the standard practice of applying `account-map` before deploying code will work again. + + + +
+ + +## 1.241.0 (2023-07-05T16:52:58Z) + +
+ Fixed broken links in READMEs @zdmytriv (#749) + +### what +* Fixed broken links in READMEs + +### why +* Fixed broken links in READMEs + +### references +* https://github.com/cloudposse/terraform-aws-components/issues/747 + +
+ + +## 1.240.1 (2023-07-04T04:54:28Z) + +### Upgrade notes + +This fixes issues with `aws-sso` and `github-oidc-provider`. Versions from v1.227 through v1.240 should not be used. + +After installing this version of `aws-sso`, you may need to change the configuration in your stacks. See [modules/aws-sso/changelog](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/aws-sso/CHANGELOG.md) for more information. Note: this release is from PR #740 + + +After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. See the release notes for v1.238.1 for more information. + +### 🐛 Bug Fixes + +
+ bugfix `aws-sso`, `github-oidc-provider` @Benbentwo (#740) + +### what +* Bugfixes `filter` depreciation issue via module update to `1.1.1` +* Bugfixes missing `aws.root` provider +* Bugfixes `github-oidc-provider` v1.238.1 + +### why +* Bugfixes + +### references +* https://github.com/cloudposse/terraform-aws-sso/pull/44 +* closes #744 + +
+ + +## 1.240.0 (2023-07-03T18:14:14Z) + +
+ Fix TFLint violations in account-map @MaxymVlasov (#745) + +### Why + +I'm too lazy to fix it each time when we get module updates via `atmos vendor` GHA + +### References +* https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_deprecated_index.md +* https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_comment_syntax.md +* https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md + + +
+ + +## 1.239.0 (2023-06-29T23:34:53Z) + +
+ Bump `cloudposse/ec2-autoscale-group/aws` to `0.35.0` @milldr (#734) + +### what +- bumped ASG module version, `cloudposse/ec2-autoscale-group/aws` to `0.35.0` + +### why +- Recent versions of this module resolve errors for these components + +### references +- https://github.com/cloudposse/terraform-aws-ec2-autoscale-group + + +
+ + +## 1.238.1 (2023-06-29T21:15:50Z) + +### Upgrade notes: + +There is a bug in this version of `github-oidc-provider`. Upgrade to version v1.240.1 or later instead. + +After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. + +- If you have dynamic Terraform roles enabled, then this should be configured like a normal component. The previous component may have required you to set + + ```yaml + backend: + s3: + role_arn: null + ```` +and **that configuration should be removed** everywhere. +- If you only use SuperAdmin to deploy things to the `identity` account, then for the `identity` (and `root`, if applicable) account ***only***, set + + ```yaml + backend: + s3: + role_arn: null + vars: + superadmin: true + ```` +**Deployments to other accounts should not have any of those settings**. + +### 🚀 Enhancements + +
+ [github-oidc-provider] extra-compatible provider @Nuru (#742) + +### what && why + +- This updates `provider.tf` to provide compatibility with various legacy configurations as well as the current reference architecture +- This update does NOT require updating `account-map` + + + +
+ + +## 1.238.0 (2023-06-29T19:39:15Z) + +
+ IAM upgrades: SSO Permission Sets as Teams, SourceIdentity support, region independence @Nuru (#738) + +### what + +- Enable SSO Permission Sets to function as teams +- Allow SAML sign on via any regional endpoint, not only us-east-1 +- Allow use of AWS "Source Identity" for SAML and SSO users (not enabled for OIDC) + +### why + +- Reduce the friction between SSO permission sets and SAML roles by allowing people to use either interchangeably. (Almost. SSO permission sets do not yet have the same permissions as SAML roles in the `identity` account itself.) +- Enable continued access in the event of a regional outage in us-east-1 as happened recently +- Enable auditing of who is using assumed roles + +### References + +- [Monitor and control actions taken with assumed roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_monitor.html) +- [How to integrate AWS STS SourceIdentity with your identity provider](https://aws.amazon.com/blogs/security/how-to-integrate-aws-sts-sourceidentity-with-your-identity-provider/) +- [AWS Sign-In endpoints](https://docs.aws.amazon.com/general/latest/gr/signin-service.html) +- [Available keys for SAML-based AWS STS federation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-saml) + +### Upgrade notes + +The regional endpoints and Source Identity support are non-controversial and cannot be disabled. They do, however, require running `terraform apply` against `aws-saml`, `aws-teams`, and `aws-team-roles` in all accounts. + +#### AWS SSO updates + +To enable SSO Permission Sets to function as teams, you need to update `account-map` and `aws-sso`, then apply changes to +- `tfstate-backend` +- `aws-teams` +- `aws-team-roles` +- `aws-sso` + +This is all enabled by default. If you do not want it, you only need to update `account-map`, and add `account-map/modules/roles-to-principles/variables_override.tf` in which you set +`overridable_team_permission_sets_enabled` to default to `false` + +Under the old `iam-primary-roles` component, corresponding permission sets were named `IdentityRoleAccess`. Under the current `aws-teams` component, they are named `IdentityTeamAccess`. The current `account-map` defaults to the latter convention. To use the earlier convention, add `account-map/modules/roles-to-principles/variables_override.tf` in which you set `overridable_team_permission_set_name_pattern` to default to `"Identity%sRoleAccess"` + +There is a chance the resulting trust policies will be too big, especially for `tfstate-backend`. If you get an error like + +``` +Cannot exceed quota for ACLSizePerRole: 2048 +``` + +You need to request a quota increase (Quota Code L-C07B4B0D), which will be automatically granted, usually in about 5 minutes. The max quota is 4096, but we recommend increasing it to 3072 first, so you retain some breathing room for the future. + + +
+ + +## 1.237.0 (2023-06-27T22:27:49Z) + +
+ Add Missing `github-oidc-provider` Thumbprint @milldr (#736) + +### what +- include both thumbprints for GitHub OIDC + +### why +- There are two possible intermediary certificates for the Actions SSL certificate and either can be returned by Github's servers, requiring customers to trust both. This is a known behavior when the intermediary certificates are cross-signed by the CA. + +### references +- https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/ + + +
+ + +## 1.236.0 (2023-06-26T18:14:29Z) + +
+ Update `eks/echo-server` and `eks/alb-controller-ingress-group` components @aknysh (#733) + +### what +* Update `eks/echo-server` and `eks/alb-controller-ingress-group` components +* Allow specifying [alb.ingress.kubernetes.io/scheme](https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/#scheme) (`internal` or `internet-facing`) + +### why +* Allow the echo server to work with internal load balancers + +### references +* https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/ + + +
+ + +## 1.235.0 (2023-06-22T21:06:18Z) + +
+ [account-map] Backwards compatibility for terraform profile users and eks/cluster @Nuru (#731) + +### what + +- [account-map/modules/iam-roles] Add `profiles_enabled` input to override global value +- [eks/cluster] Use `iam-roles` `profiles_enabled` input to force getting a role ARN even when profiles are in use +- [guardduty] Make providers compatible with static and dynamic TF roles + +### why + +- Previously, when the global `account-map` `profiles_enabled` flag was `true`, `iam_roles.terraform_role_arn` would be null. However, `eks/cluster` requires `terraform_role_arn` regardless. +- Changes made in #728 work in environments that have not adopted dynamic Terraform roles but would fail in environments that have (when using SuperAdmin) + + + +
+ + +## 1.234.0 (2023-06-21T22:44:55Z) + +
+ [account-map] Feature flag to enable legacy Terraform role mapping @Nuru (#730) + +### what + +- [account-map] Add `legacy_terraform_uses_admin` feature flag to retain backwards compatibility + +### why + +- Historically, the `terraform` roles in `root` and `identity` were not used for Terraform plan/apply, but for other things, and so the `terraform_roles` map output selected the `admin` roles for those accounts. This "wart" has been remove in current `aws-team-roles` and `tfstate-backend` configurations, but for people who do not want to migrate to the new conventions, this feature flag enables them to maintain the status quo with respect to role usage while taking advantage of other updates to `account-map` and other components. + +### references + +This update is recommended for all customers wanting to use ***any*** component version 1.227 or later. + +- #715 +- + +
+ + +## 1.233.0 (2023-06-21T20:03:36Z) + +
+ [lambda] feat: allows to use YAML instead of JSON for IAM policy @gberenice (#692) + +### what +* BREAKING CHANGE: Actually use variable `function_name` to set the lambda function name. +* Make the variable `function_name` optional. When not set, the old null-lable-derived name will be use. +* Allow IAM policy to be specified in a custom terraform object as an alternative to JSON. + +### why +* `function_name` was required to set, but it wasn't actually passed to `module "lambda"` inputs. +* Allow callers to stop providing `function_name` and preserve old behavior of using automatically generated name. +* When using [Atmos](https://atmos.tools/) to generate inputs from "stack" YAML files, having the ability to pass the statements in as a custom object means specifying them via YAML, which makes the policy declaration in stack more readable compared to embedding a JSON string in the YAML. + + + + + +
+ + +## 1.232.0 (2023-06-21T15:49:06Z) + +
+ refactor securityhub component @mcalhoun (#728) + +### what +* Refactor the Security Hub components into a single component + +### why +* To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. + +
+ + +## 1.231.0 (2023-06-21T14:54:50Z) + +
+ roll guard duty back to previous providers logic @mcalhoun (#727) + +### what +* Roll the Guard Duty component back to using the previous logic for role assumption. + +### why +* The newer method is causing the provider to try to assume the role twice. We get the error: + +``` +AWS Error: operation error STS: AssumeRole, https response error StatusCode: 403, RequestID: 00000000-0000-0000-0000-00000000, api error AccessDenied: User: arn:aws:sts::000000000000:assumed-role/acme-core-gbl-security-terraform/aws-go-sdk-1687312396297825294 is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::000000000000:role/acme-core-gbl-security-terraform +``` + +
+ + +## 1.230.0 (2023-06-21T01:49:52Z) + +
+ refactor guardduty module @mcalhoun (#725) + +### what +* Refactor the GuardDuty components into a single component + +### why +* To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. + +
+ + +## 1.229.0 (2023-06-20T19:37:35Z) + +
+ upstream `github-action-runners` dockerhub authentication @Benbentwo (#726) + +### what +* Adds support for dockerhub authentication + +### why +* Dockerhub limits are unrealistically low for actually using dockerhub as an image registry for automated builds + + + +
+ + +## 1.228.0 (2023-06-15T20:57:45Z) + +
+ alb: use the https_ssl_policy @johncblandii (#722) + +### what + +* Apply the HTTPS policy + +### why + +* The policy was unused so it was defaulting to an old policy + +### references + + +
+ + +## 1.227.0 (2023-06-12T23:41:45Z) + + +Possibly breaking change: + +In this update, `account-map/modules/iam-roles` acquired a provider, making it no longer able to be used with `count`. If you have code like + +```hcl +module "optional_role" { + count = local.optional_role_enabled ? 1 : 0 + + source = "../account-map/modules/iam-roles" + stage = var.optional_role_stage + context = module.this.context +} +``` + +You will need to rewrite it, removing the `count` parameter. It will be fine to always instantiate the module. If there are problems with ensuring appropriate settings with the module is disabled, you can always replace them with the component's inputs: + +```hcl +module "optional_role" { + source = "../account-map/modules/iam-roles" + stage = local.optional_role_enabled ? var.optional_role_stage : var.stage + context = module.this.context +} +``` + + +The update to components 1.227.0 is huge, and you have options. + +- Enable, or not, dynamic Terraform IAM roles, which allow you to give some people (and Spacelift) the ability to run Terraform plan in some accounts without allowing apply. Note that these users will still have read/write access to Terraform state, but will not have IAM permissions to make changes in accounts. [terraform_dynamic_role_enabled](https://github.com/cloudposse/terraform-aws-components/blob/1b338fe664e5debc5bbac30cfe42003f7458575a/modules/account-map/variables.tf#L96-L100) +- Update to new `aws-teams` team names. The new names are (except for support) distinct from team-roles, making it easier to keep track. Also, the new managers team can run Terraform for identity and root in most (but not all) cases. +- Update to new `aws-team-roles`, including new permissions. The custom policies that have been removed are replaced in the `aws-team-roles` configuration with AWS managed policy ARNs. This is required to add the `planner` role and support the `terraform plan` restriction. +- Update the `providers.tf for` all components. Or some of them now, some later. Most components do not require updates, but all of them have updates. The new `providers.tf`, when used with dynamic Terraform roles, allows users directly logged into target accounts (rather than having roles in the `identity` account) to use Terraform in that account, and also allows SuperAdmin to run Terraform in more cases (almost everywhere). + +**If you do not want any new features**, you only need to update `account-map` to v1.235 or later, to be compatible with future components. Note that when updating `account-map` this way, you should update the code everywhere (all open PRs and branches) before applying the Terraform changes, because the applied changes break the old code. + +If you want all the new features, we recommend updating all of the following to the current release in 1 PR: + +- account-map +- aws-teams +- aws-team-roles +- tfstate-backend + +
+ Enable `terraform plan` access via dynamic Terraform roles @Nuru (#715) + +### Reviewers, please note: + +The PR changes a lot of files. In particular, the `providers.tf` and therefore the `README.md` for nearly every component. Therefore it will likely be easier to review this PR one commit at a time. + +`import_role_arn` and `import_profile_name` have been removed as they are no longer needed. Current versions of Terraform (probably beginning with v1.1.0, but maybe as late as 1.3.0, I have not found authoritative information) can read data sources during plan and so no longer need a role to be explicitly specified while importing. Feel free to perform your own tests to make yourself more comfortable that this is correct. + +### what + +* Updates to allow Terraform to dynamically assume a role based on the user, to allow some users to run `terraform plan` but not `terraform apply` + * Deploy standard `providers.tf` to all components that need an `aws` provider + * Move extra provider configurations to separate file, so that `providers.tf` can + remain consistent/identical among components and thus be easily updated + * Create `provider-awsutils.mixin.tf` to provide consistent, maintainable implementation +* Make `aws-sso` vendor safe +* Deprecate `sso` module in favor of `aws-saml` + + +### why + +- Allow users to try new code or updated configurations by running `terraform plan` without giving them permission to make changes with Terraform +- Make it easier for people directly logged into target accounts to still run Terraform +- Follow-up to #697, which updated `aws-teams` and `aws-team-roles`, to make `aws-sso` consistent +- Reduce confusion by moving deprecated code to `deprecated/` + + + + + +
+ + +## 1.226.0 (2023-06-12T17:42:51Z) + +
+ chore: Update and add more basic pre-commit hooks @MaxymVlasov (#714) + +### what + +Fix common issues in the repo + +### why + +It violates our basic checks, which adds a headache to using https://github.com/cloudposse/github-action-atmos-component-updater as is + +![image](https://github.com/cloudposse/terraform-aws-components/assets/11096782/248febbe-b65f-4080-8078-376ef576b457) + +> **Note**: It is much simpler to review PR if [hide whitespace changes](https://github.com/cloudposse/terraform-aws-components/pull/714/files?w=1) + +
+ + +## 1.225.0 (2023-06-12T14:57:20Z) + +
+ Removed list of components from main README.md @zdmytriv (#721) + +### what +* Removed list of components from main README.md + +### why +* That list is outdated + +### references + + + + +
+ + +## 1.224.0 (2023-06-09T19:52:51Z) + +
+ upstream argocd @Benbentwo (#634) + +### what +* Upstream fixes that allow for Google OIDC + +
+ + +## 1.223.0 (2023-06-09T14:28:08Z) + +
+ add new spacelift components @mcalhoun (#717) + +### what +* Add the newly developed spacelift components +* Deprecate the previous components + +### why +* We undertook a process of decomposing a monolithic module and broke it into smaller, composable pieces for a better developer experience + +### references +* Corresponding [Upstream Module PR](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/143) + + +
+ + +## 1.222.0 (2023-06-08T23:28:34Z) + +
+ Karpenter Node Interruption Handler @milldr (#713) + +### what +- Added Karpenter Interruption Handler to existing component + +### why +- Interruption is supported by karpenter, but we need to deploy sqs queue and event bridge rules to enable + +### references +- https://github.com/cloudposse/knowledge-base/discussions/127 + + + +
+ + +## 1.221.0 (2023-06-07T18:11:23Z) + +
+ feat: New Component `aws-ssosync` @dudymas (#625) + +### what +* adds a fork of [aws-ssosync](https://github.com/awslabs/ssosync) as a lambda on a 15m cronjob + +### Why +Google is one of those identity providers that doesn't have good integration with AWS SSO. In order to sync groups and users across we need to use some API calls, luckily AWS Built [aws-ssosync](https://github.com/awslabs/ssosync) to handle that. + +Unfortunately, it required ASM so we use [Benbentwo/ssosync](https://github.com/Benbentwo/ssosync) as it removes that requirement. + +
+ + +## 1.220.0 (2023-06-05T22:31:10Z) + +
+ Disable helm experiments by default, block Kubernetes provider 2.21.0 @Nuru (#712) + +### what + +* Set `helm_manifest_experiment_enabled` to `false` by default +* Block Kubernetes provider 2.21.0 + +### why + +* The `helm_manifest_experiment_enabled` reliably breaks when a Helm chart installs CRDs. The initial reason for enabling it was for better drift detection, but the provider seems to have fixed most if not all of the drift detection issues since then. +* Kubernetes provider 2.21.0 had breaking changes which were reverted in 2.21.1. + +### references + +* https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084#issuecomment-1576711378 + + + +
+ + +## 1.219.0 (2023-06-05T20:23:17Z) + +
+ Expand ECR GH OIDC Default Policy @milldr (#711) + +### what +- updated default ECR GH OIDC policy + +### why +- This policy should grant GH OIDC access both public and private ECR repos + +### references +- https://cloudposse.slack.com/archives/CA4TC65HS/p1685993698149499?thread_ts=1685990234.560589&cid=CA4TC65HS + + +
+ + +## 1.218.0 (2023-06-05T01:59:49Z) + +
+ Move `profiles_enabled` logic out of `providers.tf` and into `iam-roles` @Nuru (#702) + +### what + +- For Terraform roles and profiles used in `providers.tf`, return `null` for unused option +- Rename variables to `overridable_*` and update documentation to recommend `variables_override.tf` for customization + +### why + +- Prepare for `providers.tf` updates to support dynamic Terraform roles +- ARB decision on customization compatible with vendoring + + + + + +
+ + +## 1.217.0 (2023-06-04T23:11:44Z) + +
+ [eks/external-secrets-operator] Normalize variables, update dependencies @Nuru (#708) + +### what + +For `eks/external-secrets-operator`: + +* Normalize variables, update dependencies +* Exclude Kubernetes provider v2.21.0 + +### why + +* Bring in line with other Helm-based modules +* Take advantage of improvements in dependencies + +### references + +* [Breaking change in Kubernetes provider v2.21.0](https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084) + + + +
+ + +## 1.216.2 (2023-06-04T23:08:39Z) + +### 🚀 Enhancements + +
+ Update modules for Terraform AWS provider v5 @Nuru (#707) + +### what + +- Update modules for Terraform AWS provider v5 + +### why + +- Provider version 5.0.0 was released with breaking changes. This fixes the breakage. + +### references + +- [v5 upgrade guide](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade) +- [v5.0.0 Release Notes](https://github.com/hashicorp/terraform-provider-aws/releases/tag/v5.0.0) + + +
+ + +## 1.216.1 (2023-06-04T01:18:31Z) + +### 🚀 Enhancements + +
+ Preserve custom roles when vendoring in updates @Nuru (#697) + +### what + +- Add `additional-policy-map.tf` as glue meant to be replaced by customers with map of their custom policies. + +### why + +- Currently, custom polices have to be manually added to the map in `main.tf`, but that gets overwritten with every vendor update. Putting that map in a separate, optional file allows for the custom code to survive vendoring. + + + + + +
+ + +## 1.216.0 (2023-06-02T18:02:01Z) + +
+ ssm-parameters: support tiers @johncblandii (#705) + +### what + +* Added support for ssm param tiers +* Updated the minimum version to `>= 1.3.0` to support `optional` parameters + +### why + +* `Standard` tier only supports 4096 characters. This allows Advanced and Intelligent Tiering support. + +### references + + +
+ + +## 1.215.0 (2023-06-02T14:28:29Z) + +
+ `.editorconfig` Typo @milldr (#704) + +### what +fixed intent typo + +### why +should be spelled "indent" + +### references +https://cloudposse.slack.com/archives/C01EY65H1PA/p1685638634845009 + + + +
+ + +## 1.214.0 (2023-05-31T17:46:35Z) + +
+ Transit Gateway `var.connections` Redesign @milldr (#685) + +### what +- Updated how the connection variables for `tgw/hub` and `tgw/spoke` are defined +- Moved the old versions of `tgw` to `deprecated/tgw` + +### why +- We want to be able to define multiple or alternately named `vpc` or `eks/cluster` components for both hub and spoke +- The cross-region components are not updated yet with this new design, since the current customers requesting these updates do not need cross-region access at this time. But we want to still support the old design s.t. customers using cross-region components can access the old components. We will need to update the cross-region components with follow up effort + +### references +- https://github.com/cloudposse/knowledge-base/discussions/112 + + + +
+ + +## 1.213.0 (2023-05-31T14:50:16Z) + +
+ Introducing Security Hub @zdmytriv (#683) + +### what +* Introducing Security Hub component + +### why + +Amazon Security Hub enables users to centrally manage and monitor the security and compliance of their AWS accounts and resources. It aggregates, organizes, and prioritizes security findings from various AWS services, third-party tools, and integrated partner solutions. + +Here are the key features and capabilities of Amazon Security Hub: + +- Centralized security management: Security Hub provides a centralized dashboard where users can view and manage security findings from multiple AWS accounts and regions. This allows for a unified view of the security posture across the entire AWS environment. + +- Automated security checks: Security Hub automatically performs continuous security checks on AWS resources, configurations, and security best practices. It leverages industry standards and compliance frameworks, such as AWS CIS Foundations Benchmark, to identify potential security issues. + +- Integrated partner solutions: Security Hub integrates with a wide range of AWS native services, as well as third-party security products and solutions. This integration enables the ingestion and analysis of security findings from diverse sources, offering a comprehensive security view. + +- Security standards and compliance: Security Hub provides compliance checks against industry standards and regulatory frameworks, such as PCI DSS, HIPAA, and GDPR. It identifies non-compliant resources and provides guidance on remediation actions to ensure adherence to security best practices. + +- Prioritized security findings: Security Hub analyzes and prioritizes security findings based on severity, enabling users to focus on the most critical issues. It assigns severity levels and generates a consolidated view of security alerts, allowing for efficient threat response and remediation. + +- Custom insights and event aggregation: Security Hub supports custom insights, allowing users to create their own rules and filters to focus on specific security criteria or requirements. It also provides event aggregation and correlation capabilities to identify related security findings and potential attack patterns. + +- Integration with other AWS services: Security Hub seamlessly integrates with other AWS services, such as AWS CloudTrail, Amazon GuardDuty, AWS Config, and AWS IAM Access Analyzer. This integration allows for enhanced visibility, automated remediation, and streamlined security operations. + +- Alert notifications and automation: Security Hub supports alert notifications through Amazon SNS, enabling users to receive real-time notifications of security findings. It also facilitates automation and response through integration with AWS Lambda, allowing for automated remediation actions. + +By utilizing Amazon Security Hub, organizations can improve their security posture, gain insights into security risks, and effectively manage security compliance across their AWS accounts and resources. + +### references +- https://aws.amazon.com/security-hub/ +- https://github.com/cloudposse/terraform-aws-security-hub/ + +
+ + +## 1.212.0 (2023-05-31T14:45:30Z) + +
+ Introducing GuardDuty @zdmytriv (#682) + +### what + +* Introducing GuardDuty component + +### why + +AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by continuously monitoring for malicious activities and unauthorized behaviors. GuardDuty analyzes various data sources within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS logs, to detect potential security threats. + +Key features and components of AWS GuardDuty include: + +- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event logs and network traffic data to detect patterns, anomalies, and known attack techniques. + +- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, domains, and other indicators of compromise. + +- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS Lambda for immediate action or custom response workflows. + +- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security policies and practices. + +- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of security incidents and reduces the need for manual intervention. + +- Security findings and reports: GuardDuty provides detailed security findings and reports that include information about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed through the AWS Management Console or retrieved via APIs for further analysis and reporting. + +GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations with an additional layer of security to proactively identify and respond to potential security risks. + +### references +- https://aws.amazon.com/guardduty/ +- https://github.com/cloudposse/terraform-aws-guardduty + + + +
+ + +## 1.211.0 (2023-05-30T16:30:47Z) + +
+ Upstream `aws-inspector` @milldr (#700) + +### what +Upstream `aws-inspector` from past engagement + +### why +* This component was never upstreamed and now were want to use it again +* AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security vulnerabilities and deviations from security best practices. Here are some key features and functionalities of AWS Inspector: + - Security Assessments: AWS Inspector performs security assessments by analyzing the behavior of your resources and identifying potential security vulnerabilities. It examines the network configuration, operating system settings, and installed software to detect common security issues. + + - Vulnerability Detection: AWS Inspector uses a predefined set of rules to identify common vulnerabilities, misconfigurations, and security exposures. It leverages industry-standard security best practices and continuously updates its knowledge base to stay current with emerging threats. + + - Agent-Based Architecture: AWS Inspector utilizes an agent-based approach, where you install an Inspector agent on your EC2 instances. The agent collects data about the system and its configuration, securely sends it to AWS Inspector, and allows for more accurate and detailed assessments. + + - Security Findings: After performing an assessment, AWS Inspector generates detailed findings that highlight security vulnerabilities, including their severity level, impact, and remediation steps. These findings can help you prioritize and address security issues within your AWS environment. + + - Integration with AWS Services: AWS Inspector seamlessly integrates with other AWS services, such as AWS CloudFormation, AWS Systems Manager, and AWS Security Hub. This allows you to automate security assessments, manage findings, and centralize security information across your AWS infrastructure. + +### references +DEV-942 + + + +
+ + +## 1.210.1 (2023-05-27T18:52:11Z) + +### 🚀 Enhancements + +
+ Fix tags @aknysh (#701) + +### what +* Fix tags + +### why +* Typo + + +
+ + +### 🐛 Bug Fixes + +
+ Fix tags @aknysh (#701) + +### what +* Fix tags + +### why +* Typo + + +
+ + +## 1.210.0 (2023-05-25T22:06:24Z) + +
+ EKS FAQ for Addons @milldr (#699) + +### what +Added docs for EKS Cluster Addons + +### why +FAQ, requested for documentation + +### references +DEV-846 + + + +
+ + +## 1.209.0 (2023-05-25T19:05:53Z) + +
+ Update ALB controller IAM policy @Nuru (#696) + +### what + +* Update `eks/alb-controller` controller IAM policy + + +### why + +* Email from AWS: +> On June 1, 2023, we will be adding an additional layer of security to ELB ‘Create*' API calls where API callers must have explicit access to add tags in their Identity and Access Management (IAM) policy. Currently, access to attach tags was implicitly granted with access to 'Create*' APIs. + +### references +* [Updated IAM policy](https://github.com/kubernetes-sigs/aws-load-balancer-controller/pull/3068) + +
+ + +## 1.208.0 (2023-05-24T11:12:15Z) + +
+ Managed rules for AWS Config @zdmytriv (#690) + +### what +* Added option to specify Managed Rules for AWS Config in addition to Conformance Packs + +### why +* Managed rules will allows to add and tune AWS predefined rules in addition to Conformance Packs + +### references +* [About AWS Config Manager Rules](https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config_use-managed-rules.html) +* [List of AWS Config Managed Rules](https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html) + + +
+ + +## 1.207.0 (2023-05-22T18:40:06Z) + +
+ Corrections to `dms` components @milldr (#658) + +### what +- Corrections to `dms` components + +### why +- outputs were incorrect +- set pass and username with ssm + +### references +- n/a + + + +
+ + +## 1.206.0 (2023-05-20T19:41:35Z) + +
+ Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL @zdmytriv (#688) + +### what +* Upgraded S3 Bucket module version + +### why +* Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL + +### references +* https://github.com/cloudposse/terraform-aws-s3-bucket/pull/178 + + +
+ + +## 1.205.0 (2023-05-19T23:55:14Z) + +
+ feat: add lambda monitors to datadog-monitor @dudymas (#686) + +### what +* add lambda error monitor +* add datadog lambda log forwarder config monitor + +### why +* Observability + + + +
+ + +## 1.204.1 (2023-05-19T19:54:05Z) + +### 🚀 Enhancements + +
+ Update `module "datadog_configuration"` modules @aknysh (#684) + +### what +* Update `module "datadog_configuration"` modules + +### why +* The module does not accept the `region` variable +* The module must be always enabled to be able to read the Datadog API keys even if the component is disabled + + + +
+ + +## 1.204.0 (2023-05-18T20:31:49Z) + +
+ `datadog-agent` bugfixes @Benbentwo (#681) + +### what +* update datadog agent to latest +* remove variable in datadog configuration + +
+ + +## 1.203.0 (2023-05-18T19:44:08Z) + +
+ Update `vpc` and `eks/cluster` components @aknysh (#677) + +### what +* Update `vpc` and `eks/cluster` components + +### why +* Use latest module versions + +* Take into account `var.availability_zones` for the EKS cluster itself. Only the `node-group` module was using `var.availability_zones` to use the subnets from the provided AZs. The EKS cluster (control plane) was using all the subnets provisioned in a VPC. This caused issues because EKS is not available in all AZs in a region, e.g. it's not available in `us-east-1e` b/c of a limited capacity, and when using all AZs from `us-east-1`, the deployment fails + +* The latest version of the `vpc` component (which was updated in this PR as well) has the outputs to get a map of AZs to the subnet IDs in each AZ + +``` + # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` + # `az_public_subnets_map` is a map of AZ names to list of public subnet IDs in the AZs + public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k)]) + + # Get only the private subnets that correspond to the AZs provided in `var.availability_zones` + # `az_private_subnets_map` is a map of AZ names to list of private subnet IDs in the AZs + private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k)]) +``` + + + +
+ + +## 1.202.0 (2023-05-18T16:15:12Z) + +
+ feat: adds ability to list principals of Lambdas allowed to access ECR @gberenice (#680) + +### what +* This change allows listing IDs of the accounts allowed to consume ECR. + +### why +* This is supported by [terraform-aws-ecr](https://github.com/cloudposse/terraform-aws-ecr/tree/main), but not the component. + +### references +* N/A + + +
+ + +## 1.201.0 (2023-05-18T15:08:54Z) + +
+ Introducing AWS Config component @zdmytriv (#675) + +### what +* Added AWS Config and related `config-bucket` components + +### why +* Added AWS Config and related `config-bucket` components + +### references + + + +
+ + +## 1.200.1 (2023-05-18T14:52:10Z) + +### 🚀 Enhancements + +
+ Fix `datadog` components @aknysh (#679) + +### what +* Fix all `datadog` components + +### why +* Variable `region` is not supported by the `datadog-configuration/modules/datadog_keys` submodule + + +
+ + +## 1.200.0 (2023-05-17T09:19:40Z) + +* No changes + + +## 1.199.0 (2023-05-16T15:01:56Z) + +
+ `eks/alb-controller-ingress-group`: Corrected Tags to pull LB Data Resource @milldr (#676) + +### what +- corrected tag reference for pull lb data resource + +### why +- the tags that are used to pull the ALB that's created should be filtering using the same group_name that is given when the LB is created + +### references +- n/a + + + +
+ + +## 1.198.3 (2023-05-15T20:01:18Z) + +### 🐛 Bug Fixes + +
+ Correct `cloudtrail` Account-Map Reference @milldr (#673) + +### what +- Correctly pull Audit account from `account-map` for `cloudtrail` +- Remove `SessionName` from EKS RBAC user name wrongly added in #668 + +### why +- account-map remote state was missing from the `cloudtrail` component +- Account names should be pulled from account-map, not using a variable +- Session Name automatically logged in `user.extra.sessionName.0` starting at Kubernetes 1.20, plus addition had a typo and was only on Teams, not Team Roles + +### references +- Resolves change requests https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193297727 and https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193298107 +- Closes #672 +- [Internal Slack thread](https://cloudposse.slack.com/archives/CA4TC65HS/p1684122388801769) + + + +
+ + +## 1.198.2 (2023-05-15T19:47:39Z) + +### 🚀 Enhancements + +
+ bump config yaml dependency on account component as it still depends on hashicorp template provider @lantier (#671) + +### what +* Bump [cloudposse/config/yaml](https://github.com/cloudposse/terraform-yaml-config) module dependency from version 1.0.1 to 1.0.2 + +### why +* 1.0.1 still uses hashicorp/template provider, which has no M1 binary equivalent, 1.0.2 already uses the cloudposse version which has the binary + +### references +* (https://github.com/cloudposse/terraform-yaml-config/releases/tag/1.0.2) + + + +
+ + +## 1.198.1 (2023-05-15T18:55:09Z) + +### 🐛 Bug Fixes + +
+ Fixed `route53-resolver-dns-firewall` for the case when logging is disabled @zdmytriv (#669) + +### what +* Fixed `route53-resolver-dns-firewall` for the case when logging is disabled + +### why +* Component still required bucket when logging disabled + +### references + + +
+ + +## 1.198.0 (2023-05-15T17:37:47Z) + +
+ Add `aws-shield` component @aknysh (#670) + +### what +* Add `aws-shield` component + +### why +* The component is responsible for enabling AWS Shield Advanced Protection for the following resources: + + * Application Load Balancers (ALBs) + * CloudFront Distributions + * Elastic IPs + * Route53 Hosted Zones + +This component also requires that the account where the component is being provisioned to has +been [subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/enable-ddos-prem.html). + + +
+ + +## 1.197.2 (2023-05-15T15:25:39Z) + +### 🚀 Enhancements + +
+ EKS terraform module variable type fix @PiotrPalkaSpotOn (#674) + +### what + +- use `bool` rather than `string` type for a variable that's designed to hold `true`/`false` value + +### why + +- using `string` makes the [if .Values.pvc_enabled](https://github.com/SpotOnInc/cloudposse-actions-runner-controller-tf-module-bugfix/blob/f224c7a4ee8b2ab4baf6929710d6668bd8fc5e8c/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml#L1) condition always true and creates persistent volumes even if they're not intended to use + + + +
+ + +## 1.197.1 (2023-05-11T20:39:03Z) + +### 🐛 Bug Fixes + +
+ Remove (broken) root access to EKS clusters @Nuru (#668) + +### what + +- Remove (broken) root access to EKS clusters +- Include session name in audit trail of users accessing EKS + +### why + +- Test code granting access to all `root` users and roles was accidentally left in #645 and breaks when Tenants are part of account names +- There is no reason to allow `root` users to access EKS clusters, so even when this code worked it was wrong +- Audit trail can keep track of who is performing actions + +### references + +- https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-iam-roles-when-multiple-users-need-identical-access-to-the-cluster + + +
+ + +## 1.197.0 (2023-05-11T17:59:40Z) + +
+ `rds` Component readme update @Benbentwo (#667) + +### what +* Updating default example from mssql to postgres + +
+ + +## 1.196.0 (2023-05-11T17:56:41Z) + +
+ Update `vpc-flow-logs` @milldr (#649) + +### what +- Modernized `vpc-flow-logs` with latest conventions + +### why +- Old version of the component was significantly out of date +- #498 + +### references +- DEV-880 + + + +
+ + +## 1.195.0 (2023-05-11T07:27:29Z) + +
+ Add `iam-policy` to `ecs-service` @milldr (#663) + +### what +Add an option to attach the `iam-policy` resource to `ecs-service` + +### why +This policy is already created, but is missing its attachment. We should attach this to the resource when enabled + +### references +https://cloudposse.slack.com/archives/CA4TC65HS/p1683729972134479 + + + +
+ + +## 1.194.0 (2023-05-10T18:36:37Z) + +
+ upstream `acm` and `datadog-integration` @Benbentwo (#666) + +### what +* ACM allows disabling `*.my.domain` +* Datadog-Integration supports allow-list'ing regions + + +
+ + +## 1.193.0 (2023-05-09T16:00:08Z) + +
+ Add `route53-resolver-dns-firewall` and `network-firewall` components @aknysh (#651) + +### what +* Add `route53-resolver-dns-firewall` component +* Add `network-firewall` component + +### why +* The `route53-resolver-dns-firewall` component is responsible for provisioning [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging configuration + +* The `network-firewall` component is responsible for provisioning [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, including Network Firewall, firewall policy, rule groups, and logging configuration + + + + +
+ + +## 1.192.0 (2023-05-09T15:40:43Z) + +
+ [ecs-service] Added IAM policies for ecspresso deployments @goruha (#659) + +### what +* [ecs-service] Added IAM policies for [Ecspresso](https://github.com/kayac/ecspresso) deployments + + +
+ + +## 1.191.0 (2023-05-05T22:16:44Z) + +
+ `elasticsearch` Corrections @milldr (#662) + +### what +- Modernize Elasticsearch component + +### why +- `elasticsearch` was not deployable as is. Added up-to-date config + +### references +- n/a + + + +
+ + +## 1.190.0 (2023-05-05T18:46:26Z) + +
+ fix: remove stray component.yaml in lambda @dudymas (#661) + +### what +* Remove the `component.yaml` in the lambda component + +### why +* Vendoring would potentially cause conflicts + + + +
+ + +## 1.189.0 (2023-05-05T18:22:04Z) + +
+ fix: eks/efs-controller iam policy updates @dudymas (#660) + +### what +* Update the iam policy for eks/efs-controller + +### why +* Older permissions will not work with new versions of the controller + +### references +* [official iam policy +sample](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/iam-policy-example.json) + + + +
+ + +## 1.188.0 (2023-05-05T17:05:23Z) + +
+ Move `eks/efs` to `efs` @milldr (#653) + +### what +- Moved `eks/efs` to `efs` + +### why +- `efs` shouldn't be a submodule of `eks`. You can deploy EFS without EKS + +### references +- n/a + + + +
+ + +## 1.187.0 (2023-05-04T23:04:26Z) + +
+ ARC enhancement, aws-config bugfix, DNS documentation @Nuru (#655) + +### what + +- Fix bug in `aws-config` +- Enhance documentation to explain relationship of `dns-primary` and `dns-delegated` components and `dns` account +- [`eks/actions-runner-controller`] Add support for annotations and improve support for ephemeral storage + +### why + +- Bugfix +- Customer query, supersedes and closes #652 +- Better support for longer lived jobs + +### references + +- https://github.com/actions/actions-runner-controller/issues/2562 + + + +
+ + +## 1.186.0 (2023-05-04T18:15:31Z) + +
+ Update `RDS` @Benbentwo (#657) + +### what +* Update RDS Modules +* Allow disabling Monitoring Role + +### why +* Monitoring not always needed +* Context.tf Updates in modules + + + + +
+ + +## 1.185.0 (2023-04-26T21:30:24Z) + +
+ Add `amplify` component @aknysh (#650) + +### what +* Add `amplify` component + +### why +* Terraform component to provision AWS Amplify apps, backend environments, branches, domain associations, and webhooks + +### references +* https://aws.amazon.com/amplify + + +
+ + +## 1.184.0 (2023-04-25T14:29:29Z) + +
+ Upstream: `eks/ebs-controller` @milldr (#640) + +### what +- Added component for `eks/ebs-controller` + +### why +- Upstreaming this component for general use + +### references +- n/a + + +
+ + +## 1.183.0 (2023-04-24T23:21:17Z) + +
+ GitHub OIDC FAQ @milldr (#648) + +### what +Added common question for GHA + +### why +This is asked frequently + +### references +https://cloudposse.slack.com/archives/C04N39YPVAS/p1682355553255269 + + + +
+ + +## 1.182.1 (2023-04-24T19:37:31Z) + +### 🚀 Enhancements + +
+ [aws-config] Update usage info, add "help" and "teams" commands @Nuru (#647) + +### what + +Update `aws-config` command: +- Add `teams` command and suggest "aws-config-teams" file name instead of "aws-config-saml" because we want to use "aws-config-teams" for both SAML and SSO logins with Leapp handling the difference. +- Add `help` command +- Add more extensive help +- Do not rely on script generated by `account-map` for command `main()` function + +### why +- Reflect latest design pattern +- Improved user experience + + +
+ + +## 1.182.0 (2023-04-21T17:20:14Z) + +
+ Athena CloudTrail Queries @milldr (#638) + +### what +- added cloudtrail integration to athena +- conditionally allow audit account to decrypt kms key used for cloudtrail + +### why +- allow queries against cloudtrail logs from a centralized account (audit) + +### references +n/a + +
+ + +## 1.181.0 (2023-04-20T22:00:24Z) + +
+ Format Identity Team Access Permission Set Name @milldr (#646) + +### what +- format permission set roles with hyphens + +### why +- pretty Permission Set naming. We want `devops-super` to format to `IdentityDevopsSuperTeamAccess` + +### references +https://github.com/cloudposse/refarch-scaffold/pull/127 + + + +
+ + +## 1.180.0 (2023-04-20T21:12:28Z) + +
+ Fix `s3-bucket` `var.bucket_name` @milldr (#637) + +### what +changed default value for bucket name to empty string not null + +### why +default bucket name should be empty string not null. Module checks against name length + +### references +n/a + + + +
+ + +## 1.179.0 (2023-04-20T20:26:20Z) + +
+ ecs-service: fix lint issues @kevcube (#636) + + + +
+ + +## 1.178.0 (2023-04-20T20:23:10Z) + +
+ fix:aws-team-roles have stray locals @dudymas (#642) + +### what +* remove locals from modules/aws-team-roles + +### why +* breaks component when it tries to configure locals (the remote state for +account_map isn't around) + + + +
+ + +## 1.177.0 (2023-04-20T05:13:53Z) + +
+ Convert eks/cluster to aws-teams and aws-sso @Nuru (#645) + +### what + +- Convert `eks/cluster` to `aws-teams` +- Add `aws-sso` support to `eks/cluster` +- Undo automatic allowance of `identity` `aws-sso` permission sets into account roles added in #567 + +### why + +- Keep in sync with other modules +- #567 is a silent privilege escalation and not needed to accomplish desired goals + + + + + +
+ + +## 1.176.1 (2023-04-19T14:20:27Z) + +### 🚀 Enhancements + +
+ fix: Use `vpc` without tenant @MaxymVlasov (#644) + +### why + +```bash +│ Error: Error in function call +│ +│ on remote-state.tf line 10, in module "vpc_flow_logs_bucket": +│ 10: tenant = coalesce(var.vpc_flow_logs_bucket_tenant_name, module.this.tenant) +│ ├──────────────── +│ │ while calling coalesce(vals...) +│ │ module.this.tenant is "" +│ │ var.vpc_flow_logs_bucket_tenant_name is null +│ +│ Call to function "coalesce" failed: no non-null, non-empty-string +│ arguments. +``` + + +
+ + +## 1.176.0 (2023-04-18T18:46:38Z) + +
+ feat: cloudtrail-bucket can have acl configured @dudymas (#643) + +### what +* add `acl` var to `cloudtrail-bucket` component + +### why +* Creating new cloudtrail buckets will fail if the acl isn't set to private + +### references +* This is part of [a security update from AWS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-faq.html) + + + +
+ + +## 1.175.0 (2023-04-11T12:11:46Z) + +
+ [argocd-repo] Added ArgoCD git commit notifications @goruha (#633) + +### what +* [argocd-repo] Added ArgoCD git commit notifications + +### why +* ArgoCD sync deployment + +
+ + +## 1.174.0 (2023-04-11T08:53:06Z) + +
+ [argocd] Added github commit status notifications @goruha (#631) + +### what +* [argocd] Added github commit status notifications + +### why +* ArgoCD sync deployment fix concurrent issue + +
+ + +## 1.173.0 (2023-04-06T19:21:23Z) + +
+ Missing Version Pins for Bats @milldr (#629) + +### what +added missing provider version pins + +### why +missing provider versions, required for bats + +### references +#626 +#628, #627 + + +
+ + +## 1.172.0 (2023-04-06T18:32:04Z) + +
+ update datadog_lambda_forwarder ref for darwin_arm64 @kevcube (#626) + +### what +* update datadog-lambda-forwarder module for darwin_arm64 + +### why +* run on Darwin_arm64 hardware + +
+ + +## 1.171.0 (2023-04-06T18:11:40Z) + +
+ Version Pinning Requirements @milldr (#628) + +### what +- missing bats requirements resolved + +### why +- PR #627 missed a few bats requirements in submodules + +### references +- #627 +- #626 + + +
+ + +## 1.170.0 (2023-04-06T17:38:24Z) + +
+ Bats Version Pinning @milldr (#627) + +### what +- upgraded pattern for version pinning + +### why +- bats would fail for all of these components unless these versions are pinned as such + +### references +- https://github.com/cloudposse/terraform-aws-components/pull/626 + + + +
+ + +## 1.169.0 (2023-04-05T20:28:39Z) + +
+ [eks/actions-runner-controller]: support Runner Group, webhook queue size @Nuru (#621) + +### what +- `eks/actions-runner-controller` + - Support [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups) + - Enable configuration of the webhook queue size limit + - Change runner controller Docker image designation +- Add documentation on Runner Groups and Autoscaler configuration + +### why + +- Enable separate access control to self-hosted runners +- For users that launch a large number of jobs in a short period of time, allow bigger queues to avoid losing jobs +- Maintainers recommend new tag format. `ghcr.io` has better rate limits than `docker.io`. + +### references + +- https://github.com/actions/actions-runner-controller/issues/2056 + +
+ + +## 1.168.0 (2023-04-04T21:48:58Z) + +
+ s3-bucket: use cloudposse template provider for arm64 @kevcube (#618) + +### what +* use cloud posse's template provider + +### why +* arm64 +* also this provider was not pinned in versions.tf so that had to be fixed somehow + +### references +* closes #617 + +
+ + +## 1.167.0 (2023-04-04T18:14:45Z) + +
+ chore: aws-sso modules updated to 1.0.0 @dudymas (#623) + +### what +* upgrade aws-sso modules: permission_sets, sso_account_assignments, and +sso_account_assignments_root + +### why +* upstream updates + + + +
+ + +## 1.166.0 (2023-04-03T13:39:53Z) + +
+ Add `datadog-synthetics` component @aknysh (#619) + +### what +* Add `datadog-synthetics` component + +### why +* This component is responsible for provisioning Datadog synthetic tests + +* Supports Datadog synthetics private locations + - https://docs.datadoghq.com/getting_started/synthetics/private_location + - https://docs.datadoghq.com/synthetics/private_locations + +* Synthetic tests allow you to observe how your systems and applications are performing using simulated requests and actions from the AWS managed locations around the globe and to monitor internal endpoints from private locations + + + +
+ + +## 1.165.0 (2023-03-31T22:11:26Z) + +
+ Update `eks/cluster` README @milldr (#616) + +### what +- Updated the README with EKS cluster + +### why +The example stack is outdated. Add notes for Github OIDC and karpenter + +### references +https://cloudposse.atlassian.net/browse/DEV-835 + + +
+ + +## 1.164.1 (2023-03-30T20:03:15Z) + +### 🚀 Enhancements + +
+ spacelift: Update README.md example login policy @johncblandii (#597) + +### what +* Added support for allowing spaces read access to all members +* Added a reference for allowing spaces write access to the "Developers" group + +### why +* Spacelift moved to Spaces Access Control + +### references +* https://docs.spacelift.io/concepts/spaces/access-control + + + +
+ + +## 1.164.0 (2023-03-30T16:25:28Z) + +
+ Update several component Readmes @Benbentwo (#611) + +### what +* Update Readmes of many components from Refarch Docs + +
+ + +## 1.163.0 (2023-03-29T19:52:46Z) + +
+ add providers to `mixins` folder @Benbentwo (#613) + +### what +* Copies some common providers to the mixins folder + +### why +* Have a central place where our common providers are held. + + +
+ + +## 1.162.0 (2023-03-29T19:30:15Z) + +
+ Added ArgoCD GitHub notification subscription @goruha (#615) + +### what +* Added ArgoCD GitHub notification subscription + +### why +* To use synchronous deployment pattern + +
+ + +## 1.161.1 (2023-03-29T17:20:27Z) + +### 🚀 Enhancements + +
+ waf component, update dependency versions for aws provider and waf terraform module @arcaven (#612) + +### what +* updates to waf module: + * aws provider from ~> 4.0 to => 4.0 + * module cloudposse/waf/aws from 0.0.4 to 0.2.0 + * different recommended catalog entry + +### why +* @aknysh suggested some updates before we start using waf module + + +
+ + +## 1.161.0 (2023-03-28T19:51:27Z) + +
+ Quick fixes to EKS/ARC arm64 Support @Nuru (#610) + +### what +- While supporting EKS/ARC `arm64`, continue to deploy `amd64` by default +- Make `tolerations.value` optional + +### why +- Majority of echosystem support is currently `amd64` +- `tolerations.value` is option in Kubernetes spec + +### references +- Corrects issue which escaped review in #609 + + +
+ + +## 1.160.0 (2023-03-28T18:26:20Z) + +
+ Upstream EKS/ARC amd64 Support @milldr (#609) + +### what +Added arm64 support for eks/arc + +### why +when supporting both amd64 and arm64, we need to select the correct architecture + +### references +https://github.com/cloudposse/infra-live/pull/265 + + + +
+ + +## 1.159.0 (2023-03-27T16:19:29Z) + +
+ Update account-map to output account information for aws-config script @Nuru (#608) + +### what +* Update `account-map` to output account information for `aws-config` script +* Output AWS profile name for root of credential chain + +### why +* Enable `aws-config` to output account IDs and to generate configuration for "AWS Extend Switch Roles" browser plugin +* Support multiple namespaces in a single infrastructure repo + + + + + +
+ +
+ Update CODEOWNERS to remove contributors @Nuru (#607) + +### what +* Update CODEOWNERS to remove contributors + +### why +* Require approval from engineering team (or in some cases admins) for all changes, to keep better quality control on this repo + + +
+ + +## 1.158.0 (2023-03-27T03:41:43Z) + +
+ Upstream latest datadog-agent and datadog-configuration updates @nitrocode (#598) + +### what +* Upstream latest datadog-agent and datadog-configuration updates + +### why +* datadog irsa role +* removing unused input vars +* default to `public.ecr.aws` images +* ignore deprecated `default.auto.tfvars` +* move `datadog-agent` to `eks/` subfolder for consistency with other helm charts + +### references +N/A + + + +
+ + +## 1.157.0 (2023-03-24T19:12:17Z) + +
+ Remove `root_account_tenant_name` @milldr (#605) + +### what +- bumped ecr +- remove unnecssary variable + +### why +- ECR version update +- We shouldn't need to set `root_account_tenant_name` in providers +- Some Terraform docs are out-of-date + +### references +- n/a + + +
+ + +## 1.156.0 (2023-03-23T21:03:46Z) + +
+ exposing variables from 2.0.0 of `VPC` module @Benbentwo (#604) + +### what +* Adding vars for vpc module and sending them directly to module + +### references +* https://github.com/cloudposse/terraform-aws-vpc/blob/master/variables.tf#L10-L44 + + + +
+ + +## 1.155.0 (2023-03-23T02:01:29Z) + +
+ Add Privileged Option for GH OIDC @milldr (#603) + +### what +- allow gh oidc role to use privileged as option for reading tf backend + +### why +- If deploying GH OIDC with a component that needs to be applied with SuperAdmin (aws-teams) we need to set privileged here + +### references +- https://cloudposse.slack.com/archives/C04N39YPVAS/p1679409325357119 + + +
+ + +## 1.154.0 (2023-03-22T17:40:35Z) + +
+ update `opsgenie-team` to be delete-able via `enabled: false` @Benbentwo (#589) + +### what +* Uses Datdaog Configuration as it's source of datadog variables +* Now supports `enabled: false` on a team to destroy it. + +
+ + +## 1.153.0 (2023-03-21T19:22:03Z) + +
+ Upstream AWS Teams components @milldr (#600) + +### what +- added eks view only policy + +### why +- Provided updates from recent contracts + +### references +- https://github.com/cloudposse/refarch-scaffold/pull/99 + + + +
+ + +## 1.152.0 (2023-03-21T15:42:51Z) + +
+ upstream 'datadog-lambda-forwarder' @gberenice (#601) + +### what +* Upgrade 'datadog-lambda-forwarder' component to v1.3.0 + +### why +* Be able [to forward Cloudwatch Events](https://github.com/cloudposse/terraform-aws-datadog-lambda-forwarder/pull/48) via components. + +### references +* N/A + +
+ + +## 1.151.0 (2023-03-15T15:56:20Z) + +
+ Upstream `eks/external-secrets-operator` @milldr (#595) + +### what +- Adding new module for `eks/external-secrets-operator` + +### why +- Other customers want to use this module now, and it needs to be upstreamed + +### references +- n/a + + + +
+ + +## 1.150.0 (2023-03-14T20:20:41Z) + +
+ chore(spacelift): update with dependency resource @dudymas (#594) + +### what +* update spacelift component to 0.55.0 + +### why +* support feature flag for spacelift_stack_dependency resource + +### references +* [spacelift module 0.55.0](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/0.55.0) + + + +
+ + +## 1.149.0 (2023-03-13T15:25:25Z) + +
+ Fix SSO SAML provider fixes @goruha (#592) + +### what +* Fix SSO SAML provider fixes + + +
+ + +## 1.148.0 (2023-03-10T18:07:36Z) + +
+ ArgoCD SSO improvements @goruha (#590) + +### what +* ArgoCD SSO improvements + + +
+ + +## 1.147.0 (2023-03-10T17:52:18Z) + +
+ Upstream: `eks/echo-server` @milldr (#591) + +### what +- Adding the `ingress.alb.group_name` annotation to Echo Server + +### why +- Required to set the ALB specifically, rather than using the default + +### references +- n/a + + + +
+ + +## 1.146.0 (2023-03-08T23:13:13Z) + +
+ Improve platform and external-dns for release engineering @goruha (#588) + +### what +* `eks/external-dns` support `dns-primary` +* `eks/platform` support json query remote components outputs + +### why +* `vanity domain` pattern support by `eks/external-dns` +* Improve flexibility of `eks/platform` + + +
+ + +## 1.145.0 (2023-03-07T00:28:25Z) + +
+ `eks/actions-runner-controller`: use coalesce @Benbentwo (#586) + +### what +* use coalesce instead of try, as we need a value passed in here + +
+ + +## 1.144.0 (2023-03-05T20:24:09Z) + +
+ Upgrade Remote State to `1.4.1` @milldr (#585) + +### what +- Upgrade _all_ remote state modules (`cloudposse/stack-config/yaml//modules/remote-state`) to version `1.4.1` + +### why +- In order to use go templating with Atmos, we need to use the latest cloudposse/utils version. This version is specified by `1.4.1` + +### references +- https://github.com/cloudposse/terraform-yaml-stack-config/releases/tag/1.4.1 + + + +
+ + +## 1.143.0 (2023-03-02T18:07:53Z) + +
+ bugfix: rds anomalies monitor not sending team information @Benbentwo (#583) + +### what +* Update monitor to have default CP tags + +
+ + +## 1.142.0 (2023-03-02T17:49:40Z) + +
+ datadog-lambda-forwarder: if s3_buckets not set, module fails @kevcube (#581) + + This module attempts to do length() on the value for s3_buckets. + +We are not using s3_buckets, and it defaults to null, so length() fails. + +
+ + +## 1.141.0 (2023-03-01T19:10:07Z) + +
+ `datadog-monitors`: Team Grouping @Benbentwo (#580) + +### what +* grouping by team helps ensure the team tag is sent to Opsgenie + +### why +* ensures most data is fed to a valid team tag instead of `@opsgenie-` + + + +
+ + +## 1.140.0 (2023-02-28T18:47:44Z) + +
+ `spacelift` add missing `var.region` @johncblandii (#574) + +### what +* Added the missing `var.region` + +### why +* The AWS provider requires it and it was not available + +### references + + +
+ + +## 1.139.0 (2023-02-28T18:46:35Z) + +
+ datadog monitors improvements @Benbentwo (#579) + +### what +* Datadog monitor improvements + * Prepends `()` e.g. `(tenant-environment-stage)` + * Fixes some messages that had improper syntax - dd uses `{{ var.name }}` + +### why +* Datadog monitor improvements + + + +
+ + +## 1.138.0 (2023-02-28T18:45:48Z) + +
+ update `account` readme.md @Benbentwo (#570) + +### what +* Updated account readme + +
+ + +## 1.137.0 (2023-02-27T20:39:34Z) + +
+ Update `eks/cluster` @Benbentwo (#578) + +### what +* Update EKS Cluster Module to re-include addons + +
+ + +## 1.136.0 (2023-02-27T17:36:47Z) + +
+ Set spacelift-worker-pool ami explicitly to x86_64 @arcaven (#577) + +### why +- autoscaling group for spacelift-worker-pool will fail to launch when new arm64 images return first +- arm64 ami image is being returned first at the moment in us-east-1 + +### what +- set spacelift-worker-pool ami statically to return only x86_64 results + +### references +- Spacelift Worker Pool ASG may fail to scale due to ami/instance type mismatch #575 +- Note: this is an alternative to spacelift-worker-pool README update and AMI limits #573 which I read after, but I think this filter approach will be more easily be refactored into setting this as an attribute in variables.tf in the near future + +
+ + +## 1.135.0 (2023-02-27T13:56:48Z) + +
+ github-runners add support for runner groups @johncblandii (#569) + +### what +* Added optional support for separating runners by groups + +NOTE: I don't know if the default of `default` is valid or if it is `Default`. I'll confirm this soon. + +### why +* Groups are supported by GitHub and allow for Actions to target specific runners by group vs by label + +### references + +* https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups + +
+ + +## 1.134.0 (2023-02-24T20:59:40Z) + +
+ [account-map] Update remote config module version @goruha (#572) + +### what +* Update remote config module version `1.4.1` + +### why +* Solve terraform module version conflict + +
+ + +## 1.133.0 (2023-02-24T17:55:52Z) + +
+ Fix ArgoCD minor issues @goruha (#571) + +### what +* Fix slack notification annotations +* Fix CRD creation order + +### why +* Fix ArgoCD bootstrap + + +
+ + +## 1.132.0 (2023-02-23T04:33:29Z) + +
+ Add spacelift-policy component @nitrocode (#556) + +### what +* Add spacelift-policy component + +### why +- De-couple policy creation from admin and child stacks +- Auto attach policies to remove additional terraform management of resources + +### references +- Depends on PR https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/134 + + + +
+ + +## 1.131.0 (2023-02-23T01:13:58Z) + +
+ SSO upgrades and Support for Assume Role from Identity Users @johncblandii (#567) + +### what +* Upgraded `aws-sso` to use `0.7.1` modules +* Updated `account-map/modules/roles-to-principals` to support assume role from SSO users in the identity account +* Adjusted `aws-sso/policy-Identity-role-RoleAccess.tf` to use the identity account name vs the stage so it supports names like `core-identity` instead of just `identity` + +### why +* `aws-sso` users could not assume role to plan/apply terraform locally +* using `core-identity` as a name broke the `aws-sso` policy since account `identity` does not exist in `full_account_map` + +### references + + +
+ + +## 1.130.0 (2023-02-21T18:33:53Z) + +
+ Add Redshift component @max-lobur (#563) + +### what +* Add Redshift + +### why +* Fulfilling the AWS catalog + +### references +* https://github.com/cloudposse/terraform-aws-redshift-cluster + +
+ + +## 1.129.0 (2023-02-21T16:45:43Z) + +
+ update dd agent docs @Benbentwo (#565) + +### what +* Update Datadog Docs to be more clear on catalog entry + +
+ + +## 1.128.0 (2023-02-18T16:28:11Z) + +
+ feat: updates spacelift to support policies outside of the comp folder @Gowiem (#522) + +### what +* Adds back `policies_by_name_path` variable to spacelift component + +### why +* Allows specifying spacelift policies outside of the component folder + +### references +* N/A + + + +
+ + +## 1.127.0 (2023-02-16T17:53:31Z) + +
+ [sso-saml-provider] Upstream SSO SAML provider component @goruha (#562) + +### what +* [sso-saml-provider] Upstream SSO SAML provider component + +### why +* Required for ArgoCD + + + + +
+ + +## 1.126.0 (2023-02-14T23:01:00Z) + +
+ upstream `opsgenie-team` @Benbentwo (#561) + +### what +* Upstreams latest opsgenie-team component + +
+ + +## 1.125.0 (2023-02-14T21:45:32Z) + +
+ [eks/argocd] Upstream ArgoCD @goruha (#560) + +### what +* Upstream `eks/argocd` + + +
+ + +## 1.124.0 (2023-02-14T17:34:29Z) + +
+ `aws-backup` upstream @Benbentwo (#559) + +### what +* Update `aws-backup` to latest + +
+ + +## 1.123.0 (2023-02-13T22:42:56Z) + +
+ upstream lambda pt2 @Benbentwo (#558) + +### what +* Add archive zip +* Change to python (no compile) + + +
+ + +## 1.122.0 (2023-02-13T21:24:02Z) + +
+ upstream `lambda` @Benbentwo (#557) + +### what +* Upstream `lambda` component + +### why +* Quickly deploy serverless code + + +
+ + +## 1.121.0 (2023-02-13T16:59:16Z) + +
+ Upstream `ACM` and `eks/Platform` for release_engineering @Benbentwo (#555) + +### what +* ACM Component outputs it's acm url +* EKS/Platform will deploy many terraform outputs to SSM + +### why +* These components are required for CP Release Engineering Setup + + + +
+ + +## 1.120.0 (2023-02-08T16:34:25Z) + +
+ Upstream datadog logs archive @Benbentwo (#552) + +### what +* Upstream DD Logs Archive + + + + +
+ + +## 1.119.0 (2023-02-07T21:32:25Z) + +
+ Upstream `dynamodb` @milldr (#512) + +### what +- Updated the `dynamodb` component + +### why +- maintaining up-to-date upstream component + +### references +- N/A + + +
+ + +## 1.118.0 (2023-02-07T20:15:17Z) + +
+ fix dd-forwarder: datadog service config depends on lambda arn config @raybotha (#531) + + + +
+ + +## 1.117.0 (2023-02-07T19:44:32Z) + +
+ Upstream `spa-s3-cloudfront` @milldr (#500) + +### what +- Added missing component from upstream `spa-s3-cloudfront` + +### why +- We use this component to provision Cloudfront and related resources + +### references +- N/A + + +
+ + +## 1.116.0 (2023-02-07T00:52:27Z) + +
+ Upstream `aurora-mysql` @milldr (#517) + +### what +- Upstreaming both `aurora-mysql` and `aurora-mysql-resources` + +### why +- Added option for allowing ingress by account name, rather than requiring CIDR blocks copy and pasted +- Replaced the deprecated provider for MySQL +- Resolved issues with Terraform perma-drift for the resources component with granting "ALL" + +### references +- Old provider, archived: https://github.com/hashicorp/terraform-provider-mysql +- New provider: https://github.com/petoju/terraform-provider-mysql + + + +
+ + +## 1.115.0 (2023-02-07T00:49:59Z) + +
+ Upstream `aurora-postgres` @milldr (#518) + +### what +- Upstreaming `aurora-postgres` and `aurora-postgres-resources` + +### why +- TLC for these components +- Added options for adding ingress by account +- Cleaned up the submodule for the resources component +- Support creating schemas +- Support conditionally pulling passwords from SSM, similar to `aurora-mysql` + + + +
+ + +## 1.114.0 (2023-02-06T17:09:31Z) + +
+ `datadog-private-locations` update helm provider @Benbentwo (#549) + +### what +* Updates Helm Provider to the latest + +### why +* New API Version + + + + +
+ + +## 1.113.0 (2023-02-06T02:26:22Z) + +
+ Remove extra var from stack example @johncblandii (#550) + +### what + +* Stack example has an old variable defined + +### why + +* `The root module does not declare a variable named "eks_tags_enabled" but a value was found in file "uw2-automation-vpc.terraform.tfvars.json".` + +### references + + +
+ + +## 1.112.1 (2023-02-03T20:00:09Z) + +### 🚀 Enhancements + +
+ Fixed non-html tags that fails rendering on docusaurus @zdmytriv (#546) + +### what +* Fixed non-html tags + +### why +* Rendering has been failing on docusaurus mdx/jsx engine + + + +
+ + +## 1.112.0 (2023-02-03T19:02:57Z) + +
+ `datadog-agent` allow values var merged @Benbentwo (#548) + +### what +* Allows values to be passed in and merged to values file + +### why +* Need to be able to easily override values files + + + +
+ + +## 1.111.0 (2023-01-31T23:02:57Z) + +
+ Update echo and alb-controller-ingress-group @Benbentwo (#547) + +### what +* Allows target group to be targeted by echo server + +
+ + +## 1.110.0 (2023-01-26T00:25:13Z) + +
+ Chore/acme/bootcamp core tenant @dudymas (#543) + +### what +* upgrade the vpn module in the ec2-client-vpn component +* and protect outputs on ec2-client-vpn + +### why +* saml docs were broken in refarch-scaffold. module was trying to alter the cert provider + + +
+ + +## 1.109.0 (2023-01-24T20:01:56Z) + +
+ Chore/acme/bootcamp spacelift @dudymas (#545) + +### what +* adjust the type of context_filters in spacelift + +### why +* was getting errors trying to apply spacelift component + + + + +
+ + +## 1.108.0 (2023-01-20T22:36:54Z) + +
+ EC2 Client VPN Version Bump @Benbentwo (#544) + +### what +* Bump Versin of EC2 Client VPN + +### why +* Bugfixes issue with TLS provider + +### references +* https://github.com/cloudposse/terraform-aws-ec2-client-vpn/pull/58 +* https://github.com/cloudposse/terraform-aws-ssm-tls-self-signed-cert/pull/20 + + +
+ + +## 1.107.0 (2023-01-19T17:34:33Z) + +
+ Update pod security context schema in cert-manager @max-lobur (#538) + +### what +Pod security context `enabled` field has been deprecated. Now you just specify the options and that's it. +Update the options per recent schema. See references + +Tested on k8s 1.24 + +### why +* Otherwise it does not pass Deployment validation on newer clusters. + +### references +https://github.com/cert-manager/cert-manager/commit/c17b11fa01455eb1b83dce0c2c06be555e4d53eb + + + +
+ + +## 1.106.0 (2023-01-18T15:36:52Z) + +
+ Fix github actions runner controller default variables @max-lobur (#542) + +### what +Default value for string is null, not false + +### why +* Otherwise this does not pass schema when you deploy it without storage requests + + + + +
+ + +## 1.105.0 (2023-01-18T15:24:11Z) + +
+ Update k8s metrics-server to latest @max-lobur (#537) + + + +### what +Upgrade metrics-server +Tested on k8s 1.24 via `kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes"` + +### why +* The previous one was so old that bitnami has even removed the chart. + + + + +
+ + +## 1.104.0 (2023-01-18T14:52:58Z) + +
+ Pin kubernetes provider in metrics-server @max-lobur (#541) + +### what +* Pin the k8s provider version +* Update versions + +### why +* Fix CI + +### references +* https://github.com/cloudposse/terraform-aws-components/pull/537 + + + +
+ + +## 1.103.0 (2023-01-17T21:09:56Z) + +
+ fix(dns-primary/acm): include zone_name arg @dudymas (#540) + +### what +* in dns-primary, revert version of acm module 0.17.0 -> 0.16.2 (17 is a preview) + +### why +* primary zones must be specified now that names are trimmed before the dot (.) + + +
+ + +## 1.102.0 (2023-01-17T16:09:59Z) + +
+ Fix typo in karpenter-provisioner @max-lobur (#539) + +### what +I formatted it last moment and did not notice that actually changed the object. +Fixing that and reformatting all of it so it's more obvious for future maintainers. + +### why +* Fixing bug + +### references +https://github.com/cloudposse/terraform-aws-components/pull/536 + + +
+ + +## 1.101.0 (2023-01-17T07:47:30Z) + +
+ Support setting consolidation in karpenter-provisioner @max-lobur (#536) + +### what +This is an alternative way of deprovisioning - proactive one. +``` +There is another way to configure Karpenter to deprovision nodes called Consolidation. +This mode is preferred for workloads such as microservices and is imcompatible with setting +up the ttlSecondsAfterEmpty . When set in consolidation mode Karpenter works to actively +reduce cluster cost by identifying when nodes can be removed as their workloads will run +on other nodes in the cluster and when nodes can be replaced with cheaper variants due +to a change in the workloads +``` + +### why +* To let users set a more aggressive deprovisioning strategy + +### references +* https://ec2spotworkshops.com/karpenter/050_karpenter/consolidation.html + + +
+ + +## 1.100.0 (2023-01-17T07:41:58Z) + +
+ Sync karpenter chart values with the schema @max-lobur (#535) + +### what +Based on https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml#L129-L142 all these should go under settings.aws + +### why +Ensure compatibility with the new charts + +### references +Based on https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml + + + +
+ + +## 1.99.0 (2023-01-13T14:59:16Z) + +
+ fix(aws-sso): dont hardcode account name for root @dudymas (#534) + +### what +* remove hardcoding for root account moniker +* change default tenant from `gov` to `core` (now convention) + +### why +* tenant is not included in the account prefix. In this case, changed to be 'core' +* most accounts do not use `gov` as the root tenant + + +
+ + +## 1.98.0 (2023-01-12T00:12:36Z) + +
+ Bump spacelift to latest @nitrocode (#532) + +### what +- Bump spacelift to latest + +### why +- Latest + +### references +N/A + + +
+ + +## 1.97.0 (2023-01-11T01:16:33Z) + +
+ Upstream EKS Action Runner Controller @milldr (#528) + +### what +- Upstreaming the latest additions for the EKS actions runner controller component + +### why +- We've added additional features for the ARC runners, primarily adding options for ephemeral storage and persistent storage. Persistent storage can be used to add image caching with EFS +- Allow for setting a `webhook_startup_timeout` value different than `scale_down_delay_seconds`. Defaults to `scale_down_delay_seconds` + +### references +- N/A + + + +
+ + +## 1.96.0 (2023-01-05T21:19:22Z) + +
+ Datadog Upstreams and Account Settings @Benbentwo (#533) + +### what +* Datadog Upgrades (Bugfixes for Configuration on default datadog URL) +* Account Settings Fixes for emoji support and updated budgets + +### why +* Upstreams + + + + +
+ + +## 1.95.0 (2023-01-04T23:44:35Z) + +
+ fix(aws-sso): add missing tf update perms @dudymas (#530) + +### what +* Changes for supporting [Refarch Scaffold](github.com/cloudposse/refarch-scaffold) +* TerraformUpdateAccess permission set added + +### why +* Allow SSO users to update dynamodb/s3 for terraform backend + + +
+ + +## 1.94.0 (2022-12-21T18:38:15Z) + +
+ upstream `spacelift` @Benbentwo (#526) + +### what +* Updated Spacelift Component to latest +* Updated README with new example + +### why +* Upstreams + +
+ + +## 1.93.0 (2022-12-21T18:37:37Z) + +
+ upstream `ecs` & `ecs-service` @Benbentwo (#529) + +### what +* upstream + * `ecs` + * `ecs-service` + +### why +* `enabled` flag correctly destroys resources +* bugfixes and improvements +* datadog support for ecs services + + + +
+ + +## 1.92.0 (2022-12-21T18:36:35Z) + +
+ Upstream Datadog @Benbentwo (#525) + +### what +* Datadog updates +* New `datadog-configuration` component for setting up share functions and making codebase more dry + + +
+ + +## 1.91.0 (2022-11-29T17:17:58Z) + +
+ CPLIVE-320: Set VPC to use region-less AZs @nitrocode (#524) + +### what +* Set VPC to use region-less AZs + +### why +* Prevent having to set VPC AZs within global region defaults + +### references +* CPLIVE-320 + + +
+ + +## 1.90.2 (2022-11-20T05:41:14Z) + +### 🚀 Enhancements + +
+ Use cloudposse/template for arm support @nitrocode (#510) + +### what +* Use cloudposse/template for arm support + +### why +* The new cloudposse/template provider has a darwin arm binary for M1 laptops + +### references +* https://github.com/cloudposse/terraform-provider-template +* https://registry.terraform.io/providers/cloudposse/template/latest + + + +
+ + +## 1.90.1 (2022-10-31T13:27:37Z) + +### 🚀 Enhancements + +
+ Allow vpc-peering to peer v2 to v2 @nitrocode (#521) + +### what +* Allow vpc-peering to peer v2 to v2 + +### why +* Alternative to transit gateway + +### references +N/A + + + +
+ + +## 1.90.0 (2022-10-31T13:24:38Z) + +
+ Upstream iam-role component @nitrocode (#520) + +### what +- Upstream iam-role component + +### why +- Create simple IAM roles + +### references +- https://github.com/cloudposse/terraform-aws-iam-role + + + +
+ + +## 1.89.0 (2022-10-28T15:35:38Z) + +
+ [eks/actions-runner-controller] Auth via GitHub App, prefer webhook auto-scaling @Nuru (#519) + +### what + +- Support and prefer authentication via GitHub app +- Support and prefer webhook-based autoscaling + + +### why + +- GitHub app is much more restricted, plus has higher API rate limits +- Webhook-based autoscaling is proactive without being overly expensive + + + +
+ + +## 1.88.0 (2022-10-24T15:40:47Z) + +
+ Upstream iam-service-linked-roles @nitrocode (#516) + +### what +* Upstream iam-service-linked-roles (thanks to @aknysh for writing it) + +### why +* Centralized component to create IAM service linked roles + +### references +- N/A + + +
+ + +## 1.87.0 (2022-10-22T19:12:36Z) + +
+ Add account-quotas component @Nuru (#515) + +### what + +- Add `account-quotas` component to manage account service quota increase requests + +### why + +- Add service quotas to the infrastructure that can be represented in code + +### notes + +Cloud Posse has a [service quotas module](https://github.com/cloudposse/terraform-aws-service-quotas), but it has issues, such as not allowing the service to be specified by name, and not having well documented inputs. It also takes a list input, but Atmos does not merge lists, so a map input is more appropriate. Overall I like this component better, and if others do, too, I will replace the existing module (only at version 0.1.0) with this code. + +
+ + +## 1.86.0 (2022-10-19T07:28:11Z) + +
+ Update EKS basic components @Nuru (#509) + +### what && why + +Update EKS cluster and basic Kubernetes components for better behavior on initial deployment and on `terraform destroy`. + +- Update minimum Terraform version to 1.1.0 and use `one()` where applicable to manage resources that can be disabled with `count = 0` and for bug fixes regarding destroy behavior +- Update `terraform-aws-eks-cluster` to v2.5.0 for better destroy behavior +- Update all components' (plus `account-map/modules/`)`remote-state` to v1.2.0 for better destroy behavior +- Update all components' `helm-release` to v0.7.0 and move namespace creation via Kubernetes provider into it to avoid race conditions regarding creating IAM roles, Namespaces, and deployments, and to delete namespaces when destroyed +- Update `alb-controller` to deploy a default IngressClass for central, obvious configuration of shared default ingress for services that do not have special needs. +- Add `alb-controller-ingress-class` for the rare case when we want to deploy a non-default IngressClass outside of the component that will be using it +- Update `echo-server` to use the default IngressClass and not specify any configuration that affects other Ingresses, and remove dependence on `alb-controller-ingress-group` (which should be deprecated in favor of `alb-controller-ingress-class` and perhaps a specialized future `alb-controller-ingress`) +- Update `cert-manager` to remove `default.auto.tfvars` (which had a lot of settings) and add dependencies so that initial deployment succeeds in one `terraform apply` and destroy works in one `terraform destroy` +- Update `external-dns` to remove `default.auto.tfvars` (which had a lot of settings) +- Update `karpenter` to v0.18.0, fix/update IAM policy (README still needs work, but leaving that for another day) +- Update `karpenter-provisioner` to require Terraform 1.3 and make elements of the Provisioner configuration optional. Support block device mappings (previously broken). Avoid perpetual Terraform plan diff/drift caused by setting fields to `null`. +- Update `reloader` +- Update `mixins/provider-helm` to better support `terraform destroy` and to default the Kubernetes client authentication API version to `client.authentication.k8s.io/v1beta1` + +### references + +- https://github.com/cloudposse/terraform-aws-helm-release/pull/34 +- https://github.com/cloudposse/terraform-aws-eks-cluster/pull/169 +- https://github.com/cloudposse/terraform-yaml-stack-config/pull/56 +- https://github.com/hashicorp/terraform/issues/32023 + + + +
+ + +## 1.85.0 (2022-10-18T00:05:19Z) + +
+ Upstream `github-runners` @milldr (#508) + +### what +- Minor TLC updates for GitHub Runners ASG component + +### why +- Maintaining up-to-date upstream + + + +
+ + +## 1.84.0 (2022-10-12T22:49:28Z) + +
+ Fix feature allowing IAM users to assume team roles @Nuru (#507) + +### what +- Replace `deny_all_iam_users` input with `iam_users_enabled` +- Fix implementation +- Provide more context for `bats` test failures + +### why + +- Cloud Posse style guide dictates that boolean feature flags have names ending with `_enabled` +- Previous implementation only removed 1 of 2 policy provisions that blocked IAM users from assuming a role, and therefore IAM users were still not allowed to assume a role. Since the previous implementation did not work, a breaking change (changing the variable name) does not need major warnings or a major version bump. +- Indication of what was being tested was too far removed from `bats` test failure message to be able to easily identify what module had failed + +### notes + +Currently, any component provisioned by SuperAdmin needs to have a special provider configuration that requires SuperAdmin to provision the component. This feature is part of what is needed to enable SuperAdmin (an IAM User) to work with "normal" provider configurations. + +### references + +- Breaks change introduced in #495, but that didn't work anyway. + + +
diff --git a/docs/upgrade-guide.md b/docs/upgrade-guide.md deleted file mode 100644 index f7f12afb1..000000000 --- a/docs/upgrade-guide.md +++ /dev/null @@ -1,9 +0,0 @@ -# Upgrade Guide - - -:::info - -Upgrade steps are included with any given component if required. We will be adding overarching upgrades to this file in the future, -but those changes are in progress now (August 2023). Expect to see this page and component pages updated soon. - -::: From 4929c7f60cf8eb5a2195ada25e5e3d1492679f21 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 30 Aug 2023 14:29:49 -0700 Subject: [PATCH 250/501] TGW Spoke Association and Propagation (#847) --- modules/tgw/spoke/README.md | 1 - modules/tgw/spoke/main.tf | 11 ++++------- modules/tgw/spoke/variables.tf | 6 ------ 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index 08e7e7e60..2afd19e68 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -139,7 +139,6 @@ No resources. | [own\_eks\_component\_names](#input\_own\_eks\_component\_names) | The name of the eks components in the owning account. | `list(string)` | `[]` | no | | [own\_vpc\_component\_name](#input\_own\_vpc\_component\_name) | The name of the vpc component in the owning account. Defaults to "vpc" | `string` | `"vpc"` | no | | [peered\_region](#input\_peered\_region) | Set `true` if this region is not the primary region | `bool` | `false` | no | -| [primary\_vpc](#input\_primary\_vpc) | Set `false` if this region is not the primary VPC in this region | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/tgw/spoke/main.tf b/modules/tgw/spoke/main.tf index 3f19b124b..32ef7792e 100644 --- a/modules/tgw/spoke/main.tf +++ b/modules/tgw/spoke/main.tf @@ -21,13 +21,10 @@ module "tgw_hub_routes" { ram_resource_share_enabled = false route_keys_enabled = false - create_transit_gateway = false - create_transit_gateway_route_table = false - create_transit_gateway_vpc_attachment = false - - # Only create transit gateway route table association and propogation - # if this is the primary VPC in this given region - create_transit_gateway_route_table_association_and_propagation = var.primary_vpc + create_transit_gateway = false + create_transit_gateway_route_table = false + create_transit_gateway_vpc_attachment = false + create_transit_gateway_route_table_association_and_propagation = true config = { (local.spoke_account) = module.tgw_spoke_vpc_attachment.tg_config, diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index 01c6d56ff..a9b66e42f 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -66,9 +66,3 @@ variable "peered_region" { description = "Set `true` if this region is not the primary region" default = false } - -variable "primary_vpc" { - type = bool - description = "Set `false` if this region is not the primary VPC in this region" - default = true -} From f23e20293ebe32a66e7ee445aa65c826f259c266 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 30 Aug 2023 19:33:56 -0400 Subject: [PATCH 251/501] docs: opsgenie-team plan requirements (#789) --- modules/opsgenie-team/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index e6b28b70c..92989c135 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -9,6 +9,29 @@ You need an API Key stored in `/opsgenie/opsgenie_api_key` of SSM, this is confi Generate an API Key by going [here](https://id.atlassian.com/manage-profile/security/api-tokens) and Clicking **Create API Token**. +Once you have the key, you'll need to test it with a curl to verify that you are at least +on a Standard plan with OpsGenie: +``` +curl -X GET 'https://api.opsgenie.com/v2/account' + --header "Authorization: GenieKey $API_KEY" +``` + +The result should be something similar to below: +``` +{ + "data": { + "name": "opsgenie", + "plan": { + "maxUserCount": 1500, + "name": "Enterprise", + ... +} +``` + +If you see anything other than `Standard` or `Enterprise` in the plan, then you won't be able +to use this component. You can see more details here: +[OpsGenie pricing/features](https://www.atlassian.com/software/opsgenie/pricing#) + #### Getting Started **Stack Level**: Global From 2d23311f1ff0dbe22ffbedda1062e0b06e387531 Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 30 Aug 2023 17:22:38 -0700 Subject: [PATCH 252/501] Update helm-release to v0.10.0 (#848) --- modules/datadog-synthetics-private-location/README.md | 2 +- modules/datadog-synthetics-private-location/main.tf | 2 +- modules/eks/actions-runner-controller/README.md | 4 ++-- modules/eks/actions-runner-controller/main.tf | 4 ++-- modules/eks/alb-controller/README.md | 2 +- modules/eks/alb-controller/main.tf | 2 +- modules/eks/argocd/README.md | 4 ++-- modules/eks/argocd/main.tf | 4 ++-- modules/eks/aws-node-termination-handler/README.md | 2 +- modules/eks/aws-node-termination-handler/main.tf | 2 +- modules/eks/cert-manager/README.md | 4 ++-- modules/eks/cert-manager/main.tf | 4 ++-- modules/eks/datadog-agent/README.md | 2 +- modules/eks/datadog-agent/main.tf | 2 +- modules/eks/echo-server/README.md | 2 +- modules/eks/echo-server/main.tf | 2 +- modules/eks/external-dns/README.md | 2 +- modules/eks/external-dns/main.tf | 2 +- modules/eks/external-secrets-operator/README.md | 4 ++-- modules/eks/external-secrets-operator/main.tf | 4 ++-- modules/eks/idp-roles/README.md | 2 +- modules/eks/idp-roles/main.tf | 2 +- modules/eks/karpenter/README.md | 2 +- modules/eks/karpenter/main.tf | 2 +- modules/eks/keda/README.md | 2 +- modules/eks/keda/main.tf | 2 +- modules/eks/metrics-server/README.md | 2 +- modules/eks/metrics-server/main.tf | 2 +- modules/eks/redis-operator/README.md | 2 +- modules/eks/redis-operator/main.tf | 2 +- modules/eks/redis/README.md | 2 +- modules/eks/redis/main.tf | 2 +- 32 files changed, 40 insertions(+), 40 deletions(-) diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index 6a2bfaf50..0aa34dc2e 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -146,7 +146,7 @@ Environment variables: | Name | Source | Version | |------|--------|---------| | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/helm-release/aws | 0.9.3 | +| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/helm-release/aws | 0.10.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/datadog-synthetics-private-location/main.tf b/modules/datadog-synthetics-private-location/main.tf index 076319374..821ed9b05 100644 --- a/modules/datadog-synthetics-private-location/main.tf +++ b/modules/datadog-synthetics-private-location/main.tf @@ -16,7 +16,7 @@ resource "datadog_synthetics_private_location" "this" { module "datadog_synthetics_private_location" { source = "cloudposse/helm-release/aws" - version = "0.9.3" + version = "0.10.0" name = module.this.name chart = var.chart diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 27ecc0ce7..08a43b2bb 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -414,8 +414,8 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | Name | Source | Version | |------|--------|---------| -| [actions\_runner](#module\_actions\_runner) | cloudposse/helm-release/aws | 0.9.1 | -| [actions\_runner\_controller](#module\_actions\_runner\_controller) | cloudposse/helm-release/aws | 0.9.1 | +| [actions\_runner](#module\_actions\_runner) | cloudposse/helm-release/aws | 0.10.0 | +| [actions\_runner\_controller](#module\_actions\_runner\_controller) | cloudposse/helm-release/aws | 0.10.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index c1b406949..11b2c5a35 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -110,7 +110,7 @@ data "aws_ssm_parameter" "docker_config_json" { module "actions_runner_controller" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = "" # avoids hitting length restrictions on IAM Role names chart = var.chart @@ -200,7 +200,7 @@ module "actions_runner" { for_each = local.enabled ? var.runners : {} source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = each.key chart = "${path.module}/charts/actions-runner" diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 8f0ca8cf3..3b6ca1e98 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -77,7 +77,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.9.3 | +| [alb\_controller](#module\_alb\_controller) | cloudposse/helm-release/aws | 0.10.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/alb-controller/main.tf b/modules/eks/alb-controller/main.tf index b17ec41ae..bdb2b3f8a 100644 --- a/modules/eks/alb-controller/main.tf +++ b/modules/eks/alb-controller/main.tf @@ -1,6 +1,6 @@ module "alb_controller" { source = "cloudposse/helm-release/aws" - version = "0.9.3" + version = "0.10.0" chart = var.chart repository = var.chart_repository diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 62aec6073..0bbb37267 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -393,8 +393,8 @@ components: | Name | Source | Version | |------|--------|---------| -| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.11.0 | -| [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.9.1 | +| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.10.0 | +| [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.10.0 | | [argocd\_repo](#module\_argocd\_repo) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index e79b9666a..5d86365c1 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -139,7 +139,7 @@ locals { module "argocd" { source = "cloudposse/helm-release/aws" - version = "0.11.0" + version = "0.10.0" name = "argocd" # avoids hitting length restrictions on IAM Role names chart = var.chart @@ -253,7 +253,7 @@ data "kubernetes_resources" "crd" { module "argocd_apps" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = "" # avoids hitting length restrictions on IAM Role names chart = var.argocd_apps_chart diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index e27878188..59155bbf8 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -59,7 +59,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [aws\_node\_termination\_handler](#module\_aws\_node\_termination\_handler) | cloudposse/helm-release/aws | 0.9.1 | +| [aws\_node\_termination\_handler](#module\_aws\_node\_termination\_handler) | cloudposse/helm-release/aws | 0.10.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/aws-node-termination-handler/main.tf b/modules/eks/aws-node-termination-handler/main.tf index 9405d2e79..55b16d4ae 100644 --- a/modules/eks/aws-node-termination-handler/main.tf +++ b/modules/eks/aws-node-termination-handler/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "aws_node_termination_handler" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = "" # avoids hitting length restrictions on IAM Role names chart = var.chart diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index 58ad825c3..354008796 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -69,8 +69,8 @@ The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` | Name | Source | Version | |------|--------|---------| -| [cert\_manager](#module\_cert\_manager) | cloudposse/helm-release/aws | 0.9.1 | -| [cert\_manager\_issuer](#module\_cert\_manager\_issuer) | cloudposse/helm-release/aws | 0.9.1 | +| [cert\_manager](#module\_cert\_manager) | cloudposse/helm-release/aws | 0.10.0 | +| [cert\_manager\_issuer](#module\_cert\_manager\_issuer) | cloudposse/helm-release/aws | 0.10.0 | | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | diff --git a/modules/eks/cert-manager/main.tf b/modules/eks/cert-manager/main.tf index bc02675ae..8cfad310f 100644 --- a/modules/eks/cert-manager/main.tf +++ b/modules/eks/cert-manager/main.tf @@ -9,7 +9,7 @@ data "aws_partition" "current" { module "cert_manager" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = "" # avoids hitting length restrictions on IAM Role names chart = var.cert_manager_chart @@ -108,7 +108,7 @@ module "cert_manager" { module "cert_manager_issuer" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" # Only install the issuer if either letsencrypt_installed or selfsigned_installed is true enabled = local.enabled && (var.letsencrypt_enabled || var.cert_manager_issuer_selfsigned_enabled) diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index f93f86f6e..fe592ec6f 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -182,7 +182,7 @@ https-checks: | Name | Source | Version | |------|--------|---------| -| [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.9.1 | +| [datadog\_agent](#module\_datadog\_agent) | cloudposse/helm-release/aws | 0.10.0 | | [datadog\_cluster\_check\_yaml\_config](#module\_datadog\_cluster\_check\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | | [datadog\_configuration](#module\_datadog\_configuration) | ../../datadog-configuration/modules/datadog_keys | n/a | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | diff --git a/modules/eks/datadog-agent/main.tf b/modules/eks/datadog-agent/main.tf index 0adc069c8..c99b593a4 100644 --- a/modules/eks/datadog-agent/main.tf +++ b/modules/eks/datadog-agent/main.tf @@ -86,7 +86,7 @@ module "values_merge" { module "datadog_agent" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = module.this.name chart = var.chart diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index af29bb013..de7a28cec 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -89,7 +89,7 @@ components: | Name | Source | Version | |------|--------|---------| | [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.9.1 | +| [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.10.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/echo-server/main.tf b/modules/eks/echo-server/main.tf index 44158a076..b58d0dca2 100644 --- a/modules/eks/echo-server/main.tf +++ b/modules/eks/echo-server/main.tf @@ -5,7 +5,7 @@ locals { module "echo_server" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = module.this.name chart = "${path.module}/charts/echo-server" diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index 7f3f0ecf8..ff4ef476e 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -68,7 +68,7 @@ components: | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [dns\_gbl\_primary](#module\_dns\_gbl\_primary) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [external\_dns](#module\_external\_dns) | cloudposse/helm-release/aws | 0.9.1 | +| [external\_dns](#module\_external\_dns) | cloudposse/helm-release/aws | 0.10.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/external-dns/main.tf b/modules/eks/external-dns/main.tf index 0126b2952..082b3ad0b 100644 --- a/modules/eks/external-dns/main.tf +++ b/modules/eks/external-dns/main.tf @@ -19,7 +19,7 @@ data "aws_partition" "current" { module "external_dns" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = module.this.name chart = var.chart diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 7c93427c7..2ac053d53 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -111,8 +111,8 @@ components: |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.9.1 | -| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.9.1 | +| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.10.0 | +| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.10.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf index e4f155044..99e8d8265 100644 --- a/modules/eks/external-secrets-operator/main.tf +++ b/modules/eks/external-secrets-operator/main.tf @@ -18,7 +18,7 @@ resource "kubernetes_namespace" "default" { # https://external-secrets.io/v0.5.9/guides-getting-started/ module "external_secrets_operator" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = "" # avoid redundant release name in IAM role: ...-ekc-cluster-external-secrets-operator-external-secrets-operator@secrets description = var.chart_description @@ -78,7 +78,7 @@ module "external_secrets_operator" { module "external_ssm_secrets" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = "ssm" # distinguish from external_secrets_operator description = "This Chart uses creates a SecretStore and ExternalSecret to pull variables (under a given path) from AWS SSM Parameter Store into a Kubernetes secret." diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index 0832af1f3..d173a768b 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -43,7 +43,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [idp\_roles](#module\_idp\_roles) | cloudposse/helm-release/aws | 0.9.1 | +| [idp\_roles](#module\_idp\_roles) | cloudposse/helm-release/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/idp-roles/main.tf b/modules/eks/idp-roles/main.tf index d0e25886b..2957c35ed 100644 --- a/modules/eks/idp-roles/main.tf +++ b/modules/eks/idp-roles/main.tf @@ -4,7 +4,7 @@ locals { module "idp_roles" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" # Required arguments name = module.this.name diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 146427322..c3c9b6fe2 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -319,7 +319,7 @@ For more details, refer to: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.9.1 | +| [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index fd7151417..6807609d4 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -28,7 +28,7 @@ resource "aws_iam_instance_profile" "default" { # Deploy Karpenter helm chart module "karpenter" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" chart = var.chart repository = var.chart_repository diff --git a/modules/eks/keda/README.md b/modules/eks/keda/README.md index 753e05f72..cd686925a 100644 --- a/modules/eks/keda/README.md +++ b/modules/eks/keda/README.md @@ -55,7 +55,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [keda](#module\_keda) | cloudposse/helm-release/aws | 0.9.3 | +| [keda](#module\_keda) | cloudposse/helm-release/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/keda/main.tf b/modules/eks/keda/main.tf index 20cf5010d..327b61d6e 100644 --- a/modules/eks/keda/main.tf +++ b/modules/eks/keda/main.tf @@ -1,6 +1,6 @@ module "keda" { source = "cloudposse/helm-release/aws" - version = "0.9.3" + version = "0.10.0" name = module.this.name description = var.description diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 710fce7ae..087dff862 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -60,7 +60,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [metrics\_server](#module\_metrics\_server) | cloudposse/helm-release/aws | 0.9.1 | +| [metrics\_server](#module\_metrics\_server) | cloudposse/helm-release/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/metrics-server/main.tf b/modules/eks/metrics-server/main.tf index 038156404..e6ac57be8 100644 --- a/modules/eks/metrics-server/main.tf +++ b/modules/eks/metrics-server/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "metrics_server" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" name = "" # avoids hitting length restrictions on IAM Role names chart = var.chart diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 85f77c835..3abf87d46 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -88,7 +88,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [redis\_operator](#module\_redis\_operator) | cloudposse/helm-release/aws | 0.9.1 | +| [redis\_operator](#module\_redis\_operator) | cloudposse/helm-release/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/redis-operator/main.tf b/modules/eks/redis-operator/main.tf index 75a788013..ec1793768 100644 --- a/modules/eks/redis-operator/main.tf +++ b/modules/eks/redis-operator/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "redis_operator" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" chart = var.chart repository = var.chart_repository diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index 25319c0be..614bf730e 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -94,7 +94,7 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [redis](#module\_redis) | cloudposse/helm-release/aws | 0.9.1 | +| [redis](#module\_redis) | cloudposse/helm-release/aws | 0.10.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/eks/redis/main.tf b/modules/eks/redis/main.tf index fc7c46518..6e6f64305 100644 --- a/modules/eks/redis/main.tf +++ b/modules/eks/redis/main.tf @@ -14,7 +14,7 @@ resource "kubernetes_namespace" "default" { module "redis" { source = "cloudposse/helm-release/aws" - version = "0.9.1" + version = "0.10.0" chart = var.chart repository = var.chart_repository From e3f8c13956f85e1a8f3f6cc8e1e1fed5b63a4900 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 31 Aug 2023 20:57:23 -0700 Subject: [PATCH 253/501] [eks/cluster] Bug fixes for add-ons (#852) --- modules/eks/cluster/CHANGELOG.md | 23 ++++++++++ modules/eks/cluster/README.md | 13 +++++- modules/eks/cluster/addons.tf | 65 +++++++++++++---------------- modules/eks/cluster/main.tf | 6 +-- modules/eks/cluster/remote-state.tf | 6 +-- modules/eks/cluster/variables.tf | 1 + 6 files changed, 67 insertions(+), 47 deletions(-) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index dfc1f40e7..351f53e73 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,3 +1,26 @@ +## Components PR [#852](https://github.com/cloudposse/terraform-aws-components/pull/852) + +This is a bug fix and feature enhancement update. No action is necessary to upgrade. + +### Bug Fixes + +- Timeouts for Add-Ons are now honored (they were being ignored) +- If you supply a service account role ARN for an Add-On, it will be used, and + no new role will be created. Previously it was used, but the component created + a new role anyway. +- The EKS EFS controller add-on cannot be deployed to Fargate, and enabling it + along with `deploy_addons_to_fargate` will no longer attempt to deploy EFS + to Fargate. Note that this means to use the EFS Add-On, you must create + a managed node group. Track the status of this feature with [this issue](https://github.com/kubernetes-sigs/aws-efs-csi-driver/issues/1100). +- If you are using an old VPC component that does not supply `az_private_subnets_map`, + this module will now use the older the `private_subnet_ids` output. + +### Add-Ons have `enabled` option + +The EKS Add-Ons now have an optional "enabled" flag (defaults to `true`) so +that you can selectively disable them in a stack where the inherited configuration +has them enabled. + ## Upgrading to `v1.270.0` Components PR [#795](https://github.com/cloudposse/terraform-aws-components/pull/795) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 336546ee4..98bb007c0 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -135,6 +135,7 @@ components: # EKS addons # https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html + # Configuring EKS addons: https://aws.amazon.com/blogs/containers/amazon-eks-add-ons-advanced-configuration/ addons: # https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html vpc-cni: @@ -150,6 +151,9 @@ components: # https://github.com/kubernetes-sigs/aws-ebs-csi-driver aws-ebs-csi-driver: addon_version: "v1.20.0-eksbuild.1" # set `addon_version` to `null` to use the latest version + # If you are not using [volume snapshots](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/#how-to-use-volume-snapshots) + # (and you probably are not), disable the EBS Snapshotter with: + configuration_values: '{"sidecars":{"snapshotter":{"forceEnable":false}}}' # Only install the EFS driver if you are using EFS. # Create an EFS file system with the `efs` component. # Create an EFS StorageClass with the `eks/storage-class` component. @@ -299,6 +303,7 @@ Configure the addons like the following example: ```yaml # https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html # https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on +# https://aws.amazon.com/blogs/containers/amazon-eks-add-ons-advanced-configuration/ addons: # https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html # https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html @@ -312,12 +317,17 @@ addons: # https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html coredns: addon_version: "v1.9.3-eksbuild.2" # set `addon_version` to `null` to use the latest version + # Uncomment to override default replica count of 2 + # configuration_values: '{"replicaCount": 3}' # https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html # https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons # https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role # https://github.com/kubernetes-sigs/aws-ebs-csi-driver aws-ebs-csi-driver: addon_version: "v1.19.0-eksbuild.2" # set `addon_version` to `null` to use the latest version + # If you are not using [volume snapshots](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/#how-to-use-volume-snapshots) + # (and you probably are not), disable the EBS Snapshotter with: + configuration_values: '{"sidecars":{"snapshotter":{"forceEnable":false}}}' ``` Some addons, such as CoreDNS, require at least one node to be fully provisioned first. @@ -432,7 +442,6 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [aws\_ebs\_csi\_driver\_eks\_iam\_role](#module\_aws\_ebs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.1 | | [aws\_ebs\_csi\_driver\_fargate\_profile](#module\_aws\_ebs\_csi\_driver\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [aws\_efs\_csi\_driver\_eks\_iam\_role](#module\_aws\_efs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.1 | -| [aws\_efs\_csi\_driver\_fargate\_profile](#module\_aws\_efs\_csi\_driver\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [coredns\_fargate\_profile](#module\_coredns\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.9.0 | @@ -475,7 +484,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
map(object({
addon_version = optional(string, null)
# configuration_values is a JSON string, such as '{"computeType": "Fargate"}'.
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | +| [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
map(object({
enabled = optional(bool, true)
addon_version = optional(string, null)
# configuration_values is a JSON string, such as '{"computeType": "Fargate"}'.
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | | [addons\_depends\_on](#input\_addons\_depends\_on) | If set `true` (recommended), all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned.
See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. | `bool` | `true` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | diff --git a/modules/eks/cluster/addons.tf b/modules/eks/cluster/addons.tf index 5c534ab19..320e9f0a1 100644 --- a/modules/eks/cluster/addons.tf +++ b/modules/eks/cluster/addons.tf @@ -5,14 +5,18 @@ locals { eks_cluster_oidc_issuer_url = local.enabled ? replace(module.eks_cluster.eks_cluster_identity_oidc_issuer, "https://", "") : "" eks_cluster_id = local.enabled ? module.eks_cluster.eks_cluster_id : "" - addon_names = keys(var.addons) + addon_names = [for k, v in var.addons : k if v.enabled] vpc_cni_addon_enabled = local.enabled && contains(local.addon_names, "vpc-cni") aws_ebs_csi_driver_enabled = local.enabled && contains(local.addon_names, "aws-ebs-csi-driver") aws_efs_csi_driver_enabled = local.enabled && contains(local.addon_names, "aws-efs-csi-driver") coredns_enabled = local.enabled && contains(local.addon_names, "coredns") # The `vpc-cni`, `aws-ebs-csi-driver`, and `aws-efs-csi-driver` addons are special as they always require an - # IAM role for Kubernetes Service Account (IRSA). The roles are created by this component. + # IAM role for Kubernetes Service Account (IRSA). The roles are created by this component unless ARNs are provided. + # Use "?" operator to avoid evaluating map lookup when entry is missing + vpc_cni_sa_needed = local.vpc_cni_addon_enabled ? lookup(var.addons["vpc-cni"], "service_account_role_arn", null) == null : false + ebs_csi_sa_needed = local.aws_ebs_csi_driver_enabled ? lookup(var.addons["aws-ebs-csi-driver"], "service_account_role_arn", null) == null : false + efs_csi_sa_needed = local.aws_efs_csi_driver_enabled ? lookup(var.addons["aws-efs-csi-driver"], "service_account_role_arn", null) == null : false addon_service_account_role_arn_map = { vpc-cni = module.vpc_cni_eks_iam_role.service_account_role_arn aws-ebs-csi-driver = module.aws_ebs_csi_driver_eks_iam_role.service_account_role_arn @@ -28,19 +32,26 @@ locals { configuration_values = lookup(v, "configuration_values", null) resolve_conflicts = lookup(v, "resolve_conflicts", null) service_account_role_arn = try(coalesce(lookup(v, "service_account_role_arn", null), lookup(local.final_addon_service_account_role_arn_map, k, null)), null) - } + create_timeout = lookup(v, "create_timeout", null) + update_timeout = lookup(v, "update_timeout", null) + delete_timeout = lookup(v, "delete_timeout", null) + + } if v.enabled ] addons_depends_on = concat([ + module.vpc_cni_eks_iam_role, module.coredns_fargate_profile, + module.aws_ebs_csi_driver_eks_iam_role, module.aws_ebs_csi_driver_fargate_profile, - module.aws_efs_csi_driver_fargate_profile, + module.aws_efs_csi_driver_eks_iam_role, ], local.overridable_addons_depends_on) addons_require_fargate = var.deploy_addons_to_fargate && ( local.coredns_enabled || local.aws_ebs_csi_driver_enabled || - local.aws_efs_csi_driver_enabled || + # as of EFS add-on v1.5.8, it cannot be deployed to Fargate + # local.aws_efs_csi_driver_enabled || local.overridable_deploy_additional_addons_to_fargate ) addon_fargate_profiles = merge( @@ -50,9 +61,11 @@ locals { (local.aws_ebs_csi_driver_enabled && var.deploy_addons_to_fargate ? { aws_ebs_csi_driver = one(module.aws_ebs_csi_driver_fargate_profile[*]) } : {}), - (local.aws_efs_csi_driver_enabled && var.deploy_addons_to_fargate ? { - aws_efs_csi_driver = one(module.aws_efs_csi_driver_fargate_profile[*]) - } : {}), + # as of EFS add-on v1.5.8, it cannot be deployed to Fargate + # See https://github.com/kubernetes-sigs/aws-efs-csi-driver/issues/1100 + # (local.aws_efs_csi_driver_enabled && var.deploy_addons_to_fargate ? { + # aws_efs_csi_driver = one(module.aws_efs_csi_driver_fargate_profile[*]) + # } : {}), local.overridable_additional_addon_fargate_profiles ) } @@ -63,7 +76,7 @@ locals { # https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role # https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/#deploy-vpc-cni-managed-add-on data "aws_iam_policy_document" "vpc_cni_ipv6" { - count = local.vpc_cni_addon_enabled ? 1 : 0 + count = local.vpc_cni_sa_needed ? 1 : 0 # See https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-ipv6-policy statement { @@ -89,7 +102,7 @@ data "aws_iam_policy_document" "vpc_cni_ipv6" { } resource "aws_iam_role_policy_attachment" "vpc_cni" { - count = local.vpc_cni_addon_enabled ? 1 : 0 + count = local.vpc_cni_sa_needed ? 1 : 0 role = module.vpc_cni_eks_iam_role.service_account_role_name policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" @@ -99,7 +112,7 @@ module "vpc_cni_eks_iam_role" { source = "cloudposse/eks-iam-role/aws" version = "2.1.1" - enabled = local.vpc_cni_addon_enabled + enabled = local.vpc_cni_sa_needed eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url @@ -138,7 +151,7 @@ module "coredns_fargate_profile" { # https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role # https://github.com/kubernetes-sigs/aws-ebs-csi-driver resource "aws_iam_role_policy_attachment" "aws_ebs_csi_driver" { - count = local.aws_ebs_csi_driver_enabled ? 1 : 0 + count = local.ebs_csi_sa_needed ? 1 : 0 role = module.aws_ebs_csi_driver_eks_iam_role.service_account_role_name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy" @@ -148,7 +161,7 @@ module "aws_ebs_csi_driver_eks_iam_role" { source = "cloudposse/eks-iam-role/aws" version = "2.1.1" - enabled = local.aws_ebs_csi_driver_enabled + enabled = local.ebs_csi_sa_needed eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url @@ -184,7 +197,7 @@ module "aws_ebs_csi_driver_fargate_profile" { # https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html # https://github.com/kubernetes-sigs/aws-efs-csi-driver resource "aws_iam_role_policy_attachment" "aws_efs_csi_driver" { - count = local.aws_efs_csi_driver_enabled ? 1 : 0 + count = local.efs_csi_sa_needed ? 1 : 0 role = module.aws_efs_csi_driver_eks_iam_role.service_account_role_name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy" @@ -194,7 +207,7 @@ module "aws_efs_csi_driver_eks_iam_role" { source = "cloudposse/eks-iam-role/aws" version = "2.1.1" - enabled = local.aws_efs_csi_driver_enabled + enabled = local.efs_csi_sa_needed eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url @@ -205,25 +218,3 @@ module "aws_efs_csi_driver_eks_iam_role" { context = module.this.context } - -module "aws_efs_csi_driver_fargate_profile" { - count = local.aws_efs_csi_driver_enabled && var.deploy_addons_to_fargate ? 1 : 0 - - source = "cloudposse/eks-fargate-profile/aws" - version = "1.3.0" - - subnet_ids = local.private_subnet_ids - cluster_name = local.eks_cluster_id - kubernetes_namespace = "kube-system" - kubernetes_labels = { app = "efs-csi-controller" } # Only deploy the controller to Fargate, not the node driver - permissions_boundary = var.fargate_profile_iam_role_permissions_boundary - - iam_role_kubernetes_namespace_delimiter = var.fargate_profile_iam_role_kubernetes_namespace_delimiter - - fargate_profile_name = "${local.eks_cluster_id}-efs-csi" - fargate_pod_execution_role_enabled = false - fargate_pod_execution_role_arn = one(module.fargate_pod_execution_role[*].eks_fargate_pod_execution_role_arn) - - attributes = ["efs-csi"] - context = module.this.context -} diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 205f904f9..229935498 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -91,15 +91,13 @@ locals { # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` # `az_public_subnets_map` is a map of AZ names to list of public subnet IDs in the AZs - # LEGACY PATCH: for legacy VPC with no az_public_subnets_map - # public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]) + # LEGACY SUPPORT for legacy VPC with no az_public_subnets_map public_subnet_ids = try(flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]), local.vpc_outputs.public_subnet_ids) # Get only the private subnets that correspond to the AZs provided in `var.availability_zones` # `az_private_subnets_map` is a map of AZ names to list of private subnet IDs in the AZs - # LEGACY PATCH: for legacy VPC with no az_public_subnets_map - # private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]) + # LEGACY SUPPORT for legacy VPC with no az_public_subnets_map private_subnet_ids = try(flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]), local.vpc_outputs.private_subnet_ids) diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf index 040537ae0..48961d96b 100644 --- a/modules/eks/cluster/remote-state.tf +++ b/modules/eks/cluster/remote-state.tf @@ -20,10 +20,8 @@ module "vpc" { component = var.vpc_component_name defaults = { - az_public_subnets_map = {} - az_private_subnets_map = {} - public_subnet_ids = [] - private_subnet_ids = [] + public_subnet_ids = [] + private_subnet_ids = [] vpc = { subnet_type_tag_key = "" } diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index fb1f27f12..0b671fc70 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -510,6 +510,7 @@ variable "fargate_profile_iam_role_permissions_boundary" { variable "addons" { type = map(object({ + enabled = optional(bool, true) addon_version = optional(string, null) # configuration_values is a JSON string, such as '{"computeType": "Fargate"}'. configuration_values = optional(string, null) From 80cc74ce3ebb503d72a36424f5ca36da9dc5cdea Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 1 Sep 2023 13:36:31 -0400 Subject: [PATCH 254/501] feat: spacelift/worker-pool can fully name and re-use iam role (#849) --- modules/aws-teams/README.md | 2 +- modules/aws-teams/main.tf | 2 +- modules/aws-teams/variables.tf | 1 + modules/spacelift/worker-pool/README.md | 17 +++++++++++++---- modules/spacelift/worker-pool/main.tf | 3 ++- modules/spacelift/worker-pool/remote-state.tf | 14 +++++++------- modules/spacelift/worker-pool/variables.tf | 8 +++++++- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index 9eeb06754..edf7929f4 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -192,7 +192,7 @@ components: | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [teams\_config](#input\_teams\_config) | A roles map to configure the accounts. |
map(object({
denied_teams = list(string)
denied_permission_sets = list(string)
denied_role_arns = list(string)
max_session_duration = number # in seconds 3600 <= max <= 43200 (12 hours)
role_description = string
role_policy_arns = list(string)
aws_saml_login_enabled = bool
trusted_teams = list(string)
trusted_permission_sets = list(string)
trusted_role_arns = list(string)
}))
| n/a | yes | +| [teams\_config](#input\_teams\_config) | A roles map to configure the accounts. |
map(object({
denied_teams = list(string)
denied_permission_sets = list(string)
denied_role_arns = list(string)
max_session_duration = number # in seconds 3600 <= max <= 43200 (12 hours)
role_description = string
role_policy_arns = list(string)
aws_saml_login_enabled = bool
allowed_roles = optional(map(list(string)), {})
trusted_teams = list(string)
trusted_permission_sets = list(string)
trusted_role_arns = list(string)
}))
| n/a | yes | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [trusted\_github\_repos](#input\_trusted\_github\_repos) | Map where keys are role names (same keys as `teams_config`) and values are lists of
GitHub repositories allowed to assume those roles. See `account-map/modules/github-assume-role-policy.mixin.tf`
for specifics about repository designations. | `map(list(string))` | `{}` | no | diff --git a/modules/aws-teams/main.tf b/modules/aws-teams/main.tf index d3013f611..18b18bb44 100644 --- a/modules/aws-teams/main.tf +++ b/modules/aws-teams/main.tf @@ -36,7 +36,7 @@ module "assume_role" { for_each = local.roles_config source = "../account-map/modules/team-assume-role-policy" - allowed_roles = { (local.identity_account_account_name) = each.value.trusted_teams } + allowed_roles = merge(each.value.allowed_roles, { (local.identity_account_account_name) = each.value.trusted_teams }) denied_roles = { (local.identity_account_account_name) = each.value.denied_teams } allowed_principal_arns = each.value.trusted_role_arns denied_principal_arns = each.value.denied_role_arns diff --git a/modules/aws-teams/variables.tf b/modules/aws-teams/variables.tf index f50c1c513..026dd9465 100644 --- a/modules/aws-teams/variables.tf +++ b/modules/aws-teams/variables.tf @@ -13,6 +13,7 @@ variable "teams_config" { role_description = string role_policy_arns = list(string) aws_saml_login_enabled = bool + allowed_roles = optional(map(list(string)), {}) trusted_teams = list(string) trusted_permission_sets = list(string) trusted_role_arns = list(string) diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index f49162f5d..e13b58015 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -52,12 +52,12 @@ components: health_check_grace_period: 300 health_check_type: EC2 infracost_enabled: true - instance_type: m6i.large + instance_type: t3.small max_size: 3 min_size: 1 name: spacelift-worker-pool scale_down_cooldown_seconds: 2700 - spacelift_agents_per_node: 3 + spacelift_agents_per_node: 1 wait_for_capacity_timeout: 5m block_device_mappings: - device_name: "/dev/xvda" @@ -73,6 +73,14 @@ components: volume_type: "gp2" ``` +### Impacts on billing + +While scaling the workload for Spacelift, keep in mind that each agent connection counts +against your quota of self-hosted workers. The number of EC2 instances you have running is _not_ +going to affect your Spacelift bill. As an example, if you had 3 EC2 instances in your Spacelift +worker pool, and you configured `spacelift_agents_per_node` to be `3`, you would see your Spacelift +bill report 9 agents being run. Take care while configuring the worker pool for your Spacelift infrastructure. + ## Configuration ### Docker Image on ECR @@ -97,7 +105,7 @@ Spacelift worker pool will reside. _HINT_: The API key ID is displayed as an upper-case, 16-character alphanumeric value next to the key name in the API key list. -Save the keys using `chamber` using the correct profile for where spacelift worker pool is provisioned +Save the keys using `chamber` using the correct profile for where Spacelift worker pool is provisioned ``` AWS_PROFILE=acme-gbl-auto-admin chamber write spacelift key_id 1234567890123456 @@ -203,6 +211,7 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [launch\_template\_version](#input\_launch\_template\_version) | Launch template version to use for workers | `string` | `"$Latest"` | no | | [max\_size](#input\_max\_size) | The maximum size of the autoscale group | `number` | n/a | yes | | [min\_size](#input\_min\_size) | The minimum size of the autoscale group | `number` | n/a | yes | | [mixed\_instances\_policy](#input\_mixed\_instances\_policy) | Policy to use a mixed group of on-demand/spot of different types. Launch template is automatically generated. https://www.terraform.io/docs/providers/aws/r/autoscaling_group.html#mixed_instances_policy-1 |
object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
override = list(object({
instance_type = string
weighted_capacity = number
}))
})
| `null` | no | @@ -212,7 +221,7 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [scale\_down\_cooldown\_seconds](#input\_scale\_down\_cooldown\_seconds) | The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start | `number` | `300` | no | | [space\_name](#input\_space\_name) | The name of the Space to create the worker pool in | `string` | `"root"` | no | -| [spacelift\_agents\_per\_node](#input\_spacelift\_agents\_per\_node) | Number of Spacelift agents to run on one worker node | `number` | `1` | no | +| [spacelift\_agents\_per\_node](#input\_spacelift\_agents\_per\_node) | Number of Spacelift agents to run on one worker node. NOTE: This affects billable units. Spacelift charges per agent. | `number` | `1` | no | | [spacelift\_ami\_id](#input\_spacelift\_ami\_id) | AMI ID of Spacelift worker pool image | `string` | `null` | no | | [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `string` | n/a | yes | | [spacelift\_aws\_account\_id](#input\_spacelift\_aws\_account\_id) | AWS Account ID owned by Spacelift | `string` | `"643313122712"` | no | diff --git a/modules/spacelift/worker-pool/main.tf b/modules/spacelift/worker-pool/main.tf index cb2e53feb..51dc24950 100644 --- a/modules/spacelift/worker-pool/main.tf +++ b/modules/spacelift/worker-pool/main.tf @@ -120,7 +120,8 @@ module "autoscale_group" { # The instance refresh definition # If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated - instance_refresh = var.instance_refresh + instance_refresh = var.instance_refresh + launch_template_version = var.launch_template_version # this has to be empty for the instance refresh to work context = module.this.context } diff --git a/modules/spacelift/worker-pool/remote-state.tf b/modules/spacelift/worker-pool/remote-state.tf index 4179f36ff..01e086622 100644 --- a/modules/spacelift/worker-pool/remote-state.tf +++ b/modules/spacelift/worker-pool/remote-state.tf @@ -3,9 +3,9 @@ module "account_map" { version = "1.5.0" component = "account-map" - environment = coalesce(var.account_map_environment_name, module.this.environment) + environment = try(coalesce(var.account_map_environment_name, module.this.environment), null) stage = var.account_map_stage_name - tenant = coalesce(var.account_map_tenant_name, module.this.tenant) + tenant = try(coalesce(var.account_map_tenant_name, module.this.tenant), null) context = module.this.context } @@ -15,9 +15,9 @@ module "ecr" { version = "1.5.0" component = "ecr" - environment = coalesce(var.ecr_environment_name, module.this.environment) + environment = try(coalesce(var.ecr_environment_name, module.this.environment), null) stage = var.ecr_stage_name - tenant = coalesce(var.ecr_tenant_name, module.this.tenant) + tenant = try(coalesce(var.ecr_tenant_name, module.this.tenant), null) context = module.this.context } @@ -36,9 +36,9 @@ module "spaces" { version = "1.5.0" component = var.spacelift_spaces_component_name - environment = try(var.spacelift_spaces_environment_name, module.this.environment) - stage = try(var.spacelift_spaces_stage_name, module.this.stage) - tenant = try(var.spacelift_spaces_tenant_name, module.this.tenant) + environment = try(coalesce(var.spacelift_spaces_environment_name, module.this.environment), null) + stage = try(coalesce(var.spacelift_spaces_stage_name, module.this.stage), null) + tenant = try(coalesce(var.spacelift_spaces_tenant_name, module.this.tenant), null) context = module.this.context } diff --git a/modules/spacelift/worker-pool/variables.tf b/modules/spacelift/worker-pool/variables.tf index 549e4700c..7b6da34df 100644 --- a/modules/spacelift/worker-pool/variables.tf +++ b/modules/spacelift/worker-pool/variables.tf @@ -208,6 +208,12 @@ variable "iam_attributes" { default = [] } +variable "launch_template_version" { + type = string + description = "Launch template version to use for workers" + default = "$Latest" +} + variable "instance_refresh" { description = "The instance refresh definition. If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated" type = object({ @@ -283,7 +289,7 @@ variable "aws_profile" { variable "spacelift_agents_per_node" { type = number - description = "Number of Spacelift agents to run on one worker node" + description = "Number of Spacelift agents to run on one worker node. NOTE: This affects billable units. Spacelift charges per agent." default = 1 } From af18c2d4730ba91db57d0934751ab5806ab4fdca Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Mon, 4 Sep 2023 19:09:04 +0300 Subject: [PATCH 255/501] [eks/argocd] Added ArgoCD notification configuration (#851) Co-authored-by: cloudpossebot Co-authored-by: Andriy Knysh --- modules/argocd-repo/CHANGELOG.md | 36 ++++ modules/argocd-repo/README.md | 6 +- modules/argocd-repo/applicationset.tf | 13 +- modules/argocd-repo/providers.tf | 2 +- .../templates/applicationset.yaml.tpl | 44 +--- modules/argocd-repo/variables.tf | 30 +-- modules/eks/argocd/CHANGELOG.md | 126 +++++++++++ modules/eks/argocd/README.md | 92 ++++---- modules/eks/argocd/data.tf | 18 -- modules/eks/argocd/main.tf | 204 ++++++++---------- modules/eks/argocd/notifictations.tf | 176 +++++++++++++++ modules/eks/argocd/outputs.tf | 4 - modules/eks/argocd/provider-github.tf | 35 +++ modules/eks/argocd/providers.tf | 2 +- .../argocd-notifications-values.yaml.tpl | 37 +--- .../argocd/resources/argocd-values.yaml.tpl | 11 +- .../eks/argocd/resources/kustomize/.gitignore | 1 - .../resources/kustomize/kustomization.yaml | 11 - .../eks/argocd/resources/kustomize/patch.yaml | 3 - .../argocd/resources/kustomize/post-render.sh | 26 --- .../argocd/variables-argocd-notifications.tf | 91 +++----- modules/eks/argocd/variables-argocd.tf | 18 +- modules/eks/argocd/versions.tf | 8 + 23 files changed, 564 insertions(+), 430 deletions(-) create mode 100644 modules/argocd-repo/CHANGELOG.md create mode 100644 modules/eks/argocd/CHANGELOG.md create mode 100644 modules/eks/argocd/notifictations.tf create mode 100644 modules/eks/argocd/provider-github.tf delete mode 100644 modules/eks/argocd/resources/kustomize/.gitignore delete mode 100644 modules/eks/argocd/resources/kustomize/kustomization.yaml delete mode 100644 modules/eks/argocd/resources/kustomize/patch.yaml delete mode 100755 modules/eks/argocd/resources/kustomize/post-render.sh diff --git a/modules/argocd-repo/CHANGELOG.md b/modules/argocd-repo/CHANGELOG.md new file mode 100644 index 000000000..cb57e1d6e --- /dev/null +++ b/modules/argocd-repo/CHANGELOG.md @@ -0,0 +1,36 @@ +## Components PR [#851](https://github.com/cloudposse/terraform-aws-components/pull/851) + +This is a bug fix and feature enhancement update. +There are few actions necessary to upgrade. + +## Upgrade actions + +1. Enable `github_default_notifications_enabled` (set `true`) +```yaml +components: + terraform: + argocd-repo-defaults: + metadata: + type: abstract + vars: + enabled: true + github_default_notifications_enabled: true +``` +2. Apply changes with Atmos + + +## Features +* Support predefined GitHub commit status notifications for CD sync mode: + * `on-deploy-started` + * `app-repo-github-commit-status` + * `argocd-repo-github-commit-status` + * `on-deploy-succeded` + * `app-repo-github-commit-status` + * `argocd-repo-github-commit-status` + * `on-deploy-failed` + * `app-repo-github-commit-status` + * `argocd-repo-github-commit-status` + +### Bug Fixes + +* Remove legacy unnecessary helm values used in old ArgoCD versions (ex. `workflow auth` configs) and dropped notifications services diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index 890177d53..51448b505 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -133,9 +133,10 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [environments](#input\_environments) | Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for.

`auto-sync` determines whether or not the ArgoCD application will be automatically synced.

`ignore-differences` determines whether or not the ArgoCD application will ignore the number of
replicas in the deployment. Read more on ignore differences here:
https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs

Example:
tenant: plat
environment: use1
stage: sandbox
auto-sync: true
ignore-differences:
- group: apps
kind: Deployment
json-pointers:
- /spec/replicas
|
list(object({
tenant = string
environment = string
stage = string
auto-sync = bool
ignore-differences = list(object({
group = string,
kind = string,
json-pointers = list(string)
}))
}))
| `[]` | no | +| [environments](#input\_environments) | Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for.

`auto-sync` determines whether or not the ArgoCD application will be automatically synced. |
list(object({
tenant = string
environment = string
stage = string
auto-sync = bool
}))
| `[]` | no | | [github\_base\_url](#input\_github\_base\_url) | This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/` | `string` | `null` | no | | [github\_codeowner\_teams](#input\_github\_codeowner\_teams) | List of teams to use when populating the CODEOWNERS file.

For example: `["@ACME/cloud-admins", "@ACME/cloud-developers"]`. | `list(string)` | n/a | yes | +| [github\_default\_notifications\_enabled](#input\_github\_default\_notifications\_enabled) | Enable default GitHub commit statuses notifications (required for CD sync mode) | `bool` | `true` | no | | [github\_organization](#input\_github\_organization) | GitHub Organization | `string` | n/a | yes | | [github\_token\_override](#input\_github\_token\_override) | Use the value of this variable as the GitHub token instead of reading it from SSM | `string` | `null` | no | | [github\_user](#input\_github\_user) | Github user | `string` | n/a | yes | @@ -151,7 +152,6 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [permissions](#input\_permissions) | A list of Repository Permission objects used to configure the team permissions of the repository

`team_slug` should be the name of the team without the `@{org}` e.g. `@cloudposse/team` => `team`
`permission` is just one of the available values listed below |
list(object({
team_slug = string,
permission = string
}))
| `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [slack\_channel](#input\_slack\_channel) | The name of the slack channel to configure ArgoCD notifications for | `string` | `null` | no | | [ssm\_github\_api\_key](#input\_ssm\_github\_api\_key) | SSM path to the GitHub API key | `string` | `"/argocd/github/api_key"` | no | | [ssm\_github\_deploy\_key\_format](#input\_ssm\_github\_deploy\_key\_format) | Format string of the SSM parameter path to which the deploy keys will be written to (%s will be replaced with the environment name) | `string` | `"/argocd/deploy_keys/%s"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | @@ -173,7 +173,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/argocd-repo) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/argocd-repo/applicationset.tf b/modules/argocd-repo/applicationset.tf index b664271e6..5710e3ebe 100644 --- a/modules/argocd-repo/applicationset.tf +++ b/modules/argocd-repo/applicationset.tf @@ -5,13 +5,12 @@ resource "github_repository_file" "application_set" { branch = join("", github_repository.default.*.default_branch) file = "${each.value.tenant != null ? format("%s/", each.value.tenant) : ""}${each.value.environment}-${each.value.stage}/${local.manifest_kubernetes_namespace}/applicationset.yaml" content = templatefile("${path.module}/templates/applicationset.yaml.tpl", { - environment = each.key - auto-sync = each.value.auto-sync - ignore-differences = each.value.ignore-differences - name = module.this.namespace - namespace = local.manifest_kubernetes_namespace - ssh_url = join("", github_repository.default.*.ssh_clone_url) - slack_channel = var.slack_channel + environment = each.key + auto-sync = each.value.auto-sync + name = module.this.namespace + namespace = local.manifest_kubernetes_namespace + ssh_url = join("", github_repository.default.*.ssh_clone_url) + notifications = var.github_default_notifications_enabled }) commit_message = "Initialize environment: `${each.key}`." commit_author = var.github_user diff --git a/modules/argocd-repo/providers.tf b/modules/argocd-repo/providers.tf index ef923e10a..54257fd20 100644 --- a/modules/argocd-repo/providers.tf +++ b/modules/argocd-repo/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = assume_role.value + role_arn = module.iam_roles.terraform_role_arn } } } diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index 292c1644e..68de50c03 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -8,24 +8,6 @@ metadata: argocd-autopilot.argoproj-labs.io/default-dest-server: https://kubernetes.default.svc argocd.argoproj.io/sync-options: PruneLast=true argocd.argoproj.io/sync-wave: "-2" -%{if slack_channel != "" && slack_channel != null ~} - notifications.argoproj.io/subscribe.on-deployed.slack: ${slack_channel} - notifications.argoproj.io/subscribe.on-health-degraded.slack: ${slack_channel} - notifications.argoproj.io/subscribe.on-sync-failed.slack: ${slack_channel} - notifications.argoproj.io/subscribe.on-sync-running.slack: ${slack_channel} - notifications.argoproj.io/subscribe.on-sync-status-unknown.slack: ${slack_channel} - notifications.argoproj.io/subscribe.on-sync-succeeded.slack: ${slack_channel} - notifications.argoproj.io/subscribe.on-deleted.slack: ${slack_channel} -%{ endif ~} - notifications.argoproj.io/subscribe.on-deployed.datadog: "" - notifications.argoproj.io/subscribe.on-health-degraded.datadog: "" - notifications.argoproj.io/subscribe.on-sync-failed.datadog: "" - notifications.argoproj.io/subscribe.on-sync-running.datadog: "" - notifications.argoproj.io/subscribe.on-sync-status-unknown.datadog: "" - notifications.argoproj.io/subscribe.on-sync-succeeded.datadog: "" - notifications.argoproj.io/subscribe.on-deployed.github-deployment: "" - notifications.argoproj.io/subscribe.on-deployed.github-commit-status: "" - notifications.argoproj.io/subscribe.on-deleted.github-deployment: "" name: ${name} namespace: ${namespace} spec: @@ -49,6 +31,7 @@ kind: ApplicationSet metadata: annotations: argocd.argoproj.io/sync-wave: "0" + creationTimestamp: null name: ${name} namespace: ${namespace} spec: @@ -61,11 +44,18 @@ spec: template: metadata: annotations: + deployment_id: '{{deployment_id}}' app_repository: '{{app_repository}}' app_commit: '{{app_commit}}' - app_hostname: '{{app_hostname}}' - notifications.argoproj.io/subscribe.on-deployed.github: "" - notifications.argoproj.io/subscribe.on-deployed.github-commit-status: "" + app_hostname: 'https://{{app_hostname}}' +%{if notifications ~} + notifications.argoproj.io/subscribe.on-deploy-started.app-repo-github-commit-status: "" + notifications.argoproj.io/subscribe.on-deploy-started.argocd-repo-github-commit-status: "" + notifications.argoproj.io/subscribe.on-deploy-succeded.app-repo-github-commit-status: "" + notifications.argoproj.io/subscribe.on-deploy-succeded.argocd-repo-github-commit-status: "" + notifications.argoproj.io/subscribe.on-deploy-failed.app-repo-github-commit-status: "" + notifications.argoproj.io/subscribe.on-deploy-failed.argocd-repo-github-commit-status: "" +%{ endif ~} name: '{{name}}' spec: project: ${name} @@ -84,15 +74,3 @@ spec: %{ endif ~} syncOptions: - CreateNamespace=true -%{if length(ignore-differences) > 0 ~} - - RespectIgnoreDifferences=true - ignoreDifferences: -%{for item in ignore-differences ~} - - group: "${item.group}" - kind: "${item.kind}" - jsonPointers: -%{for pointer in item.json-pointers ~} - - ${pointer} -%{ endfor ~} -%{ endfor ~} -%{ endif ~} diff --git a/modules/argocd-repo/variables.tf b/modules/argocd-repo/variables.tf index 9114bcbd3..2309b7d38 100644 --- a/modules/argocd-repo/variables.tf +++ b/modules/argocd-repo/variables.tf @@ -15,33 +15,11 @@ variable "environments" { environment = string stage = string auto-sync = bool - ignore-differences = list(object({ - group = string, - kind = string, - json-pointers = list(string) - })) })) description = <<-EOT Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for. `auto-sync` determines whether or not the ArgoCD application will be automatically synced. - - `ignore-differences` determines whether or not the ArgoCD application will ignore the number of - replicas in the deployment. Read more on ignore differences here: - https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs - - Example: - ``` - tenant: plat - environment: use1 - stage: sandbox - auto-sync: true - ignore-differences: - - group: apps - kind: Deployment - json-pointers: - - /spec/replicas - ``` EOT default = [] } @@ -126,8 +104,8 @@ variable "permissions" { } } -variable "slack_channel" { - type = string - description = "The name of the slack channel to configure ArgoCD notifications for" - default = null +variable "github_default_notifications_enabled" { + type = bool + default = true + description = "Enable default GitHub commit statuses notifications (required for CD sync mode)" } diff --git a/modules/eks/argocd/CHANGELOG.md b/modules/eks/argocd/CHANGELOG.md new file mode 100644 index 000000000..c4a90f49a --- /dev/null +++ b/modules/eks/argocd/CHANGELOG.md @@ -0,0 +1,126 @@ +## Components PR [#851](https://github.com/cloudposse/terraform-aws-components/pull/851) + +This is a bug fix and feature enhancement update. +There are few actions necessary to upgrade. + +## Upgrade actions + +1. Update atmos stack yaml config + 1. Add `github_default_notifications_enabled: true` + 2. Add `github_webhook_enabled: true` + 3. Remove `notifications_triggers` + 4. Remove `notifications_templates` + 5. Remove `notifications_notifiers` +```diff + components: + terraform: + argocd: + settings: + spacelift: + workspace_enabled: true + metadata: + component: eks/argocd + vars: ++ github_default_notifications_enabled: true ++ github_webhook_enabled: true +- notifications_triggers: +- trigger_on-deployed: +- - when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy' +- oncePer: app.status.sync.revision +- send: [app-deployed] +- notifications_templates: +- template_app-deployed: +- message: | +- Application {{.app.metadata.name}} is now running new version of deployments manifests. +- github: +- status: +- state: success +- label: "continuous-delivery/{{.app.metadata.name}}" +- targetURL: "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true" +- notifications_notifiers: +- service_github: +- appID: xxxxxxx +- installationID: xxxxxxx +``` +2. Move secrets from `/argocd/notifications/notifiers/service_webhook_github-commit-status/github-token` to `argocd/notifications/notifiers/common/github-token` +```bash +chamber read -q argocd/notifications/notifiers/service_webhook_github-commit-status github-token | chamber write argocd/notifications/notifiers/common github-token +chamber delete argocd/notifications/notifiers/service_webhook_github-commit-status github-token +``` +3. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) with scope `admin:repo_hook` +4. Save the PAT to SSM `/argocd/github/api_key` +```bash +chamber write argocd/github api_key ${PAT} +``` +5. Apply changes with atmos + +## Features +* [Git Webhook Configuration](https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/) - makes GitHub trigger ArgoCD sync on each commit into argocd repo +* Replace [GitHub notification service](https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/services/github/) with predefined [Webhook notification service](https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/services/webhook/) +* Added predefined GitHub commit status notifications for CD sync mode: + * `on-deploy-started` + * `app-repo-github-commit-status` + * `argocd-repo-github-commit-status` + * `on-deploy-succeded` + * `app-repo-github-commit-status` + * `argocd-repo-github-commit-status` + * `on-deploy-failed` + * `app-repo-github-commit-status` + * `argocd-repo-github-commit-status` +* Support SSM secrets (`/argocd/notifications/notifiers/common/*`) common for all notification services. (Can be referenced with `$common_{secret-name}` ) + +### Bug Fixes + +* ArgoCD notifications pods recreated on deployment that change notifications related configs and secrets +* Remove `metadata` output that expose helm values configs (used in debug purpose) +* Remove legacy unnecessary helm values used in old ArgoCD versions (ex. `workflow auth` configs) and dropped notifications services + +## Breaking changes + +* Removed `service_github` from `notifications_notifiers` variable structure +* Renamed `service_webhook` to `webhook` in `notifications_notifiers` variable structure +```diff +variable "notifications_notifiers" { + type = object({ + ssm_path_prefix = optional(string, "/argocd/notifications/notifiers") +- service_github = optional(object({ +- appID = number +- installationID = number +- privateKey = optional(string) +- })) + # service.webhook.: +- service_webhook = optional(map( ++ webhook = optional(map( + object({ + url = string + headers = optional(list( + }) + )) + }) +``` +* Removed `github` from `notifications_templates` variable structure +```diff +variable "notifications_templates" { + type = map(object({ + message = string + alertmanager = optional(object({ + labels = map(string) + annotations = map(string) + generatorURL = string + })) +- github = optional(object({ +- status = object({ +- state = string +- label = string +- targetURL = string +- }) +- })) + webhook = optional(map( + object({ + method = optional(string) + path = optional(string) + body = optional(string) + }) + )) + })) +``` diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 0bbb37267..eacedf4cb 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -316,59 +316,36 @@ manifests: plat/use2-dev/apps/my-preview-acme-app/manifests Here's a configuration for letting argocd send notifications back to GitHub: +1. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) with scope `repo:status` +2. Save the PAT to SSM `/argocd/notifications/notifiers/common/github-token` +3. Use this atmos stack configuration + ```yaml components: terraform: eks/argocd/notifications: metadata: - type: abstract component: eks/argocd vars: - notifications_triggers: - trigger_on-deployed: - - when: app.status.operationState.phase in ['Succeeded'] - oncePer: app.status.sync.revision - send: [app-deployed, github-commit-status] - - notifications_templates: - template_app-deployed: - message: | - Application {{ .app.metadata.name }} is now running new version of deployments manifests. - github: - status: - state: success - label: "continuous-delivery/{{ .app.metadata.name }}" - targetURL: "{{ .context.argocdUrl }}/applications/{{ .app.metadata.name }}?operation=true" - - template_github-commit-status: - message: | - Application {{ .app.metadata.name }} is now running new version of deployments manifests. - webhook: - github-commit-status: - method: POST - path: /repos/{{call .repo.FullNameByRepoURL .app.metadata.annotations.app_repository}}/statuses/{{.app.metadata.annotations.app_commit}} - body: | - { - {{if eq .app.status.operationState.phase "Running"}} "state": "pending"{{end}} - {{if eq .app.status.operationState.phase "Succeeded"}} "state": "success"{{end}} - {{if eq .app.status.operationState.phase "Error"}} "state": "error"{{end}} - {{if eq .app.status.operationState.phase "Failed"}} "state": "error"{{end}}, - "description": "ArgoCD", - "target_url": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", - "context": "continuous-delivery/{{.app.metadata.name}}" - } - - notifications_notifiers: - service_github: - appID: 123456 - installationID: 12345678 - service_webhook: - github-commit-status: - url: https://api.github.com - headers: - - name: Authorization - value: token $service_webhook_github-commit-status_github-token + github_default_notifications_enabled: true +``` + +### Webhook +Here's a configuration Github notify ArgoCD on commit: + +1. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) with scope `admin:repo_hook` +2. Save the PAT to SSM `/argocd/github/api_key` +3. Use this atmos stack configuration + +```yaml +components: + terraform: + eks/argocd/notifications: + metadata: + component: eks/argocd + vars: + github_webhook_enabled: true ``` @@ -378,8 +355,10 @@ components: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | +| [github](#requirement\_github) | >= 4.0 | | [helm](#requirement\_helm) | >= 2.6.0 | | [kubernetes](#requirement\_kubernetes) | >= 2.9.0, != 2.21.0 | +| [random](#requirement\_random) | >= 3.5 | ## Providers @@ -387,7 +366,9 @@ components: |------|---------| | [aws](#provider\_aws) | >= 4.0 | | [aws.config\_secrets](#provider\_aws.config\_secrets) | >= 4.0 | +| [github](#provider\_github) | >= 4.0 | | [kubernetes](#provider\_kubernetes) | >= 2.9.0, != 2.21.0 | +| [random](#provider\_random) | >= 3.5 | ## Modules @@ -407,7 +388,10 @@ components: | Name | Type | |------|------| +| [github_repository_webhook.default](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_webhook) | resource | +| [random_password.webhook](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_ssm_parameter.github_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.github_deploy_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.oidc_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.oidc_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | @@ -446,15 +430,17 @@ components: | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `false` | no | -| [datadog\_notifications\_enabled](#input\_datadog\_notifications\_enabled) | Whether or not to notify Datadog of deployments via the Datadog Events API. | `bool` | `false` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [forecastle\_enabled](#input\_forecastle\_enabled) | Toggles Forecastle integration in the deployed chart | `bool` | `false` | no | -| [github\_notifications\_enabled](#input\_github\_notifications\_enabled) | Whether or not to enable GitHub deployment and commit status notifications. | `bool` | `false` | no | +| [github\_base\_url](#input\_github\_base\_url) | This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/` | `string` | `null` | no | +| [github\_default\_notifications\_enabled](#input\_github\_default\_notifications\_enabled) | Enable default GitHub commit statuses notifications (required for CD sync mode) | `bool` | `true` | no | | [github\_organization](#input\_github\_organization) | GitHub Organization | `string` | n/a | yes | +| [github\_token\_override](#input\_github\_token\_override) | Use the value of this variable as the GitHub token instead of reading it from SSM | `string` | `null` | no | +| [github\_webhook\_enabled](#input\_github\_webhook\_enabled) | Enable GitHub webhook integration | `bool` | `true` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [host](#input\_host) | Host name to use for ingress and ALB | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | @@ -475,8 +461,8 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
service_github = optional(object({
appID = number
installationID = number
privateKey = optional(string)
}))
# service.webhook.:
service_webhook = optional(map(
object({
url = string
headers = optional(list(
object({
name = string
value = string
})
), [])
basicAuth = optional(object({
username = string
password = string
}))
insecureSkipVerify = optional(bool, false)
})
))
})
| `{}` | no | -| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
github = optional(object({
status = object({
state = string
label = string
targetURL = string
})
}))
webhook = optional(map(
object({
method = optional(string)
path = optional(string)
body = optional(string)
})
))
}))
| `{}` | no | +| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
# service.webhook.:
webhook = optional(map(
object({
url = string
headers = optional(list(
object({
name = string
value = string
})
), [])
basicAuth = optional(object({
username = string
password = string
}))
insecureSkipVerify = optional(bool, false)
})
))
})
| `{}` | no | +| [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
webhook = optional(map(
object({
method = optional(string)
path = optional(string)
body = optional(string)
})
))
}))
| `{}` | no | | [notifications\_triggers](#input\_notifications\_triggers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
map(list(
object({
oncePer = optional(string)
send = list(string)
when = string
})
))
| `{}` | no | | [oidc\_enabled](#input\_oidc\_enabled) | Toggles OIDC integration in the deployed chart | `bool` | `false` | no | | [oidc\_issuer](#input\_oidc\_issuer) | OIDC issuer URL | `string` | `""` | no | @@ -490,9 +476,7 @@ components: | [saml\_enabled](#input\_saml\_enabled) | Toggles SAML integration in the deployed chart | `bool` | `false` | no | | [saml\_rbac\_scopes](#input\_saml\_rbac\_scopes) | SAML RBAC scopes to request | `string` | `"[email,groups]"` | no | | [saml\_sso\_providers](#input\_saml\_sso\_providers) | SAML SSO providers components |
map(object({
component = string
}))
| `{}` | no | -| [slack\_notifications\_enabled](#input\_slack\_notifications\_enabled) | Whether or not to enable Slack notifications. | `bool` | `false` | no | -| [slack\_notifications\_icon](#input\_slack\_notifications\_icon) | URI of custom image to use as the Slack notifications icon. | `string` | `null` | no | -| [slack\_notifications\_username](#input\_slack\_notifications\_username) | Custom username to use for Slack notifications. | `string` | `null` | no | +| [ssm\_github\_api\_key](#input\_ssm\_github\_api\_key) | SSM path to the GitHub API key | `string` | `"/argocd/github/api_key"` | no | | [ssm\_oidc\_client\_id](#input\_ssm\_oidc\_client\_id) | The SSM Parameter Store path for the ID of the IdP client | `string` | `"/argocd/oidc/client_id"` | no | | [ssm\_oidc\_client\_secret](#input\_ssm\_oidc\_client\_secret) | The SSM Parameter Store path for the secret of the IdP client | `string` | `"/argocd/oidc/client_secret"` | no | | [ssm\_store\_account](#input\_ssm\_store\_account) | Account storing SSM parameters | `string` | n/a | yes | @@ -506,9 +490,7 @@ components: ## Outputs -| Name | Description | -|------|-------------| -| [metadata](#output\_metadata) | Block status of the deployed release | +No outputs. ## References diff --git a/modules/eks/argocd/data.tf b/modules/eks/argocd/data.tf index 5cb530e3f..b8a6e6cd1 100644 --- a/modules/eks/argocd/data.tf +++ b/modules/eks/argocd/data.tf @@ -1,14 +1,6 @@ locals { - # kubernetes_host = local.enabled ? data.aws_eks_cluster.kubernetes[0].endpoint : "" - # kubernetes_token = local.enabled ? data.aws_eks_cluster_auth.kubernetes[0].token : "" - # kubernetes_cluster_ca_certificate = local.enabled ? base64decode(data.aws_eks_cluster.kubernetes[0].certificate_authority[0].data) : "" oidc_client_id = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_id[0].value : "" oidc_client_secret = local.oidc_enabled ? data.aws_ssm_parameter.oidc_client_secret[0].value : "" - - # saml_certificate = base64encode(format("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----", module.okta_saml_apps.outputs.certificates[var.saml_okta_app_name])) - # - # saml_sso_url = sensitive(local.saml_enabled ? module.okta_saml_apps.outputs.sso_urls[var.saml_okta_app_name] : "") - # saml_ca = sensitive(local.saml_enabled ? local.saml_certificate : "") } # NOTE: OIDC parameters are global, hence why they use a separate AWS provider @@ -33,16 +25,6 @@ data "aws_ssm_parameter" "oidc_client_secret" { provider = aws.config_secrets } -#data "aws_eks_cluster" "kubernetes" { -# count = local.count_enabled -# name = module.eks.outputs.eks_cluster_id -#} - -#data "aws_eks_cluster_auth" "kubernetes" { -# count = local.count_enabled -# name = module.eks.outputs.eks_cluster_id -#} - data "aws_ssm_parameter" "github_deploy_key" { for_each = local.enabled ? var.argocd_repositories : {} diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 5d86365c1..746eee577 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -1,15 +1,17 @@ locals { - enabled = module.this.enabled - kubernetes_namespace = var.kubernetes_namespace - oidc_enabled = local.enabled && var.oidc_enabled - oidc_enabled_count = local.oidc_enabled ? 1 : 0 - saml_enabled = local.enabled && var.saml_enabled + enabled = module.this.enabled + github_webhook_enabled = local.enabled && var.github_webhook_enabled + kubernetes_namespace = var.kubernetes_namespace + oidc_enabled = local.enabled && var.oidc_enabled + oidc_enabled_count = local.oidc_enabled ? 1 : 0 + saml_enabled = local.enabled && var.saml_enabled argocd_repositories = local.enabled ? { for k, v in var.argocd_repositories : k => { clone_url = module.argocd_repo[k].outputs.repository_ssh_clone_url github_deploy_key = data.aws_ssm_parameter.github_deploy_key[k].value } } : {} + webhook_github_secret = try(random_password.webhook["github"].result, null) credential_templates = flatten(concat([ for k, v in local.argocd_repositories : [ { @@ -34,12 +36,18 @@ locals { } ] ] - ])) + ], + local.github_webhook_enabled ? [ + { + name = "configs.secret.githubSecret" + value = nonsensitive(local.webhook_github_secret) + type = "string" + } + ] : [] + )) regional_service_discovery_domain = "${module.this.environment}.${module.dns_gbl_delegated.outputs.default_domain_name}" host = var.host != "" ? var.host : format("%s.%s", coalesce(var.alb_name, var.name), local.regional_service_discovery_domain) - enable_argo_workflows_auth = local.saml_enabled && var.argo_enable_workflows_auth - # enable_argo_workflows_auth_count = local.enable_argo_workflows_auth ? 1 : 0 - # argo_workflows_host = "${var.argo_workflows_name}.${local.regional_service_discovery_domain}" + url = format("https://%s", local.host) oidc_config_map = local.oidc_enabled ? { server : { @@ -61,7 +69,7 @@ locals { "dexserver.disable.tls" = true } cm : { - "url" = "https://${local.host}" + "url" = local.url "dex.config" = join("\n", [ local.dex_config_connectors ]) @@ -70,7 +78,8 @@ locals { } : {} dex_config_connectors = yamlencode({ - connectors = [for name, config in(local.enabled ? var.saml_sso_providers : {}) : + connectors = [ + for name, config in(local.enabled ? var.saml_sso_providers : {}) : { type = "saml" id = "saml" @@ -89,77 +98,30 @@ locals { ] } ) - - post_render_script = local.enable_argo_workflows_auth ? "./resources/kustomize/post-render.sh" : null - kustomize_files_values = local.enable_argo_workflows_auth ? { - __ignore = { - kustomize_files = { for f in fileset("./resources/kustomize", "[^_]*.{sh,yaml}") : f => filesha256("./resources/kustomize/${f}") } - } - } : {} -} - -data "aws_ssm_parameters_by_path" "argocd_notifications" { - for_each = local.notifications_notifiers_ssm_path - path = each.value - with_decryption = true -} - -locals { - notifications_notifiers_ssm_path = { for key, value in local.notifications_notifiers_variables : - key => format("%s/%s/", var.notifications_notifiers.ssm_path_prefix, key) - } - - notifications_notifiers_ssm_configs = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : - key => zipmap( - [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], - value.values - ) - } - - notifications_notifiers_ssm_configs_keys = { for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : - key => zipmap( - [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], - [for name in value.names : format("$%s_%s", key, trimprefix(name, local.notifications_notifiers_ssm_path[key]))] - ) - } - - notifications_notifiers_variables = merge({ for key, value in var.notifications_notifiers : - key => { for param_name, param_value in value : param_name => param_value if param_value != null } - if key != "ssm_path_prefix" && key != "service_webhook" - }, - { for key, value in coalesce(var.notifications_notifiers.service_webhook, {}) : - format("service_webhook_%s", key) => { for param_name, param_value in value : param_name => param_value if param_value != null } - }) - - notifications_notifiers = { - for key, value in local.notifications_notifiers_variables : - replace(key, "_", ".") => yamlencode(merge(local.notifications_notifiers_ssm_configs_keys[key], value)) - } } module "argocd" { source = "cloudposse/helm-release/aws" version = "0.10.0" - name = "argocd" # avoids hitting length restrictions on IAM Role names - chart = var.chart - repository = var.chart_repository - description = var.chart_description - chart_version = var.chart_version - kubernetes_namespace = local.kubernetes_namespace - create_namespace = var.create_namespace - wait = var.wait - atomic = var.atomic - cleanup_on_fail = var.cleanup_on_fail - timeout = var.timeout - postrender_binary_path = local.post_render_script + name = "argocd" # avoids hitting length restrictions on IAM Role names + chart = var.chart + repository = var.chart_repository + description = var.chart_description + chart_version = var.chart_version + kubernetes_namespace = local.kubernetes_namespace + create_namespace = var.create_namespace + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") service_account_name = module.this.name service_account_namespace = var.kubernetes_namespace - set_sensitive = nonsensitive(local.credential_templates) + set_sensitive = local.credential_templates values = compact([ # standard k8s object settings @@ -177,68 +139,41 @@ module "argocd" { templatefile( "${path.module}/resources/argocd-values.yaml.tpl", { - admin_enabled = var.admin_enabled - alb_group_name = var.alb_group_name == null ? "" : var.alb_group_name - alb_logs_bucket = var.alb_logs_bucket - alb_logs_prefix = var.alb_logs_prefix - alb_name = var.alb_name == null ? "" : var.alb_name - application_repos = { for k, v in local.argocd_repositories : k => v.clone_url } - argocd_host = local.host - cert_issuer = var.certificate_issuer - forecastle_enabled = var.forecastle_enabled - ingress_host = local.host - name = module.this.name - oidc_enabled = local.oidc_enabled - oidc_rbac_scopes = var.oidc_rbac_scopes - organization = var.github_organization - saml_enabled = local.saml_enabled - saml_rbac_scopes = var.saml_rbac_scopes - rbac_default_policy = var.argocd_rbac_default_policy - rbac_policies = var.argocd_rbac_policies - rbac_groups = var.argocd_rbac_groups - enable_argo_workflows_auth = local.enable_argo_workflows_auth + admin_enabled = var.admin_enabled + alb_group_name = var.alb_group_name == null ? "" : var.alb_group_name + alb_logs_bucket = var.alb_logs_bucket + alb_logs_prefix = var.alb_logs_prefix + alb_name = var.alb_name == null ? "" : var.alb_name + application_repos = { for k, v in local.argocd_repositories : k => v.clone_url } + argocd_host = local.host + cert_issuer = var.certificate_issuer + forecastle_enabled = var.forecastle_enabled + ingress_host = local.host + name = module.this.name + oidc_enabled = local.oidc_enabled + oidc_rbac_scopes = var.oidc_rbac_scopes + organization = var.github_organization + saml_enabled = local.saml_enabled + saml_rbac_scopes = var.saml_rbac_scopes + rbac_default_policy = var.argocd_rbac_default_policy + rbac_policies = var.argocd_rbac_policies + rbac_groups = var.argocd_rbac_groups } ), # argocd-notifications specific settings templatefile( "${path.module}/resources/argocd-notifications-values.yaml.tpl", { - argocd_host = "https://${local.host}" - slack_notifications_enabled = var.slack_notifications_enabled - slack_notifications_username = var.slack_notifications_username - slack_notifications_icon = var.slack_notifications_icon - github_notifications_enabled = var.github_notifications_enabled - datadog_notifications_enabled = var.datadog_notifications_enabled - } - ), - yamlencode( - { - notifications = { - templates = { for key, value in var.notifications_templates : replace(key, "_", ".") => yamlencode(value) } - } - } - ), - yamlencode( - { - notifications = { - triggers = { for key, value in var.notifications_triggers : - replace(key, "_", ".") => yamlencode(value) - } - } - } - ), - yamlencode( - { - notifications = { - notifiers = local.notifications_notifiers - } + argocd_host = "https://${local.host}" + configs-hash = md5(jsonencode(local.notifications)) + secrets-hash = md5(jsonencode(local.notifications_notifiers_ssm_configs)) } ), + yamlencode(local.notifications), yamlencode(merge( local.oidc_config_map, local.saml_config_map, )), - yamlencode(local.kustomize_files_values), yamlencode(var.chart_values) ]) @@ -286,3 +221,32 @@ module "argocd_apps" { module.argocd ] } + +resource "random_password" "webhook" { + for_each = toset(local.github_webhook_enabled ? ["github"] : []) + + # min 16, max 128 + length = 128 + special = true + + min_upper = 3 + min_lower = 3 + min_numeric = 3 + min_special = 3 +} + +resource "github_repository_webhook" "default" { + for_each = local.github_webhook_enabled ? local.argocd_repositories : {} + repository = each.key + + configuration { + url = format("%s/api/webhook", local.url) + content_type = "json" + secret = local.webhook_github_secret + insecure_ssl = false + } + + active = true + + events = ["push"] +} diff --git a/modules/eks/argocd/notifictations.tf b/modules/eks/argocd/notifictations.tf new file mode 100644 index 000000000..e806e8ff6 --- /dev/null +++ b/modules/eks/argocd/notifictations.tf @@ -0,0 +1,176 @@ +data "aws_ssm_parameters_by_path" "argocd_notifications" { + for_each = local.notifications_notifiers_ssm_path + path = each.value + with_decryption = true +} + +locals { + github_default_notifications_enabled = local.enabled && var.github_default_notifications_enabled + + notification_default_notifier_github_commot_status = { + url = "https://api.github.com" + headers = [ + { + name = "Authorization" + value = "token $common_github-token" + } + ] + } + + notification_default_notifiers = local.github_default_notifications_enabled ? { + webhook = { + app-repo-github-commit-status = local.notification_default_notifier_github_commot_status + argocd-repo-github-commit-status = local.notification_default_notifier_github_commot_status + } + } : {} + + notifications_notifiers = merge(var.notifications_notifiers, local.notification_default_notifiers) + + ## Get list of notifiers services + notifications_notifiers_variables = merge( + { + for key, value in local.notifications_notifiers : + key => { for param_name, param_value in value : param_name => param_value if param_value != null } + if key != "ssm_path_prefix" && key != "webhook" + }, + { + for key, value in coalesce(local.notifications_notifiers.webhook, {}) : + format("webhook_%s", key) => + { for param_name, param_value in value : param_name => param_value if param_value != null } + } + ) + + ## Get paths to read configs for each notifier service + notifications_notifiers_ssm_path = merge( + { + for key, value in local.notifications_notifiers_variables : + key => format("%s/%s/", local.notifications_notifiers.ssm_path_prefix, key) + }, + { + common = format("%s/common/", local.notifications_notifiers.ssm_path_prefix) + }, + ) + + ## Read SSM secrets into object for each notifier service + notifications_notifiers_ssm_configs = { + for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : + key => zipmap( + [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], + nonsensitive(value.values) + ) + } + + ## Define notifier service object with placeholders as values. This is ArgoCD convention + notifications_notifiers_ssm_configs_keys = { + for key, value in data.aws_ssm_parameters_by_path.argocd_notifications : + key => zipmap( + [for name in value.names : trimprefix(name, local.notifications_notifiers_ssm_path[key])], + [for name in value.names : format("$%s_%s", key, trimprefix(name, local.notifications_notifiers_ssm_path[key]))] + ) + } +} + +locals { + notifications_template_github_commit_status = { + method = "POST" + body = { + description = "ArgoCD" + target_url = "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}" + context = "continuous-delivery/{{.app.metadata.name}}" + } + } + + notifications_template_app_github_commit_status = merge(local.notifications_template_github_commit_status, { + path = "/repos/{{call .repo.FullNameByRepoURL .app.metadata.annotations.app_repository}}/statuses/{{.app.metadata.annotations.app_commit}}" + }) + + notifications_template_argocd_repo_github_commit_status = merge(local.notifications_template_github_commit_status, { + path = "/repos/{{call .repo.FullNameByRepoURL .app.spec.source.repoURL}}/statuses/{{.app.status.operationState.operation.sync.revision}}" + }) + + notifications_default_templates = local.github_default_notifications_enabled ? { + app-deploy-succeded = { + message = "Application {{ .app.metadata.name }} is now running new version of deployments manifests." + webhook = { + app-repo-github-commit-status = { + for k, v in local.notifications_template_app_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "success" })) : tostring(v) + } + argocd-repo-github-commit-status = { + for k, v in local.notifications_template_argocd_repo_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "success" })) : tostring(v) + } + } + } + app-deploy-started = { + message = "Application {{ .app.metadata.name }} is now running new version of deployments manifests." + webhook = { + app-repo-github-commit-status = { + for k, v in local.notifications_template_app_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "pending" })) : tostring(v) + } + argocd-repo-github-commit-status = { + for k, v in local.notifications_template_argocd_repo_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "pending" })) : tostring(v) + } + } + } + app-deploy-failed = { + message = "Application {{ .app.metadata.name }} failed deploying new version." + webhook = { + app-repo-github-commit-status = { + for k, v in local.notifications_template_app_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "error" })) : tostring(v) + } + argocd-repo-github-commit-status = { + for k, v in local.notifications_template_argocd_repo_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "error" })) : tostring(v) + } + } + } + } : {} + + notifications_templates = merge(var.notifications_templates, local.notifications_default_templates) +} + +locals { + notifications_default_triggers = local.github_default_notifications_enabled ? { + on-deploy-started = [ + { + when = "app.status.operationState.phase in ['Running'] or ( app.status.operationState.phase == 'Succeeded' and app.status.health.status == 'Progressing' )" + oncePer = "app.status.sync.revision" + send = ["app-deploy-started"] + } + ], + on-deploy-succeded = [ + { + when = "app.status.operationState.phase == 'Succeeded' and app.status.health.status == 'Healthy'" + oncePer = "app.status.sync.revision" + send = ["app-deploy-succeded"] + } + ], + on-deploy-failed = [ + { + when = "app.status.operationState.phase in ['Error', 'Failed' ] or ( app.status.operationState.phase == 'Succeeded' and app.status.health.status == 'Degraded' )" + oncePer = "app.status.sync.revision" + send = ["app-deploy-failed"] + } + ] + } : {} + + notifications_triggers = merge(var.notifications_triggers, local.notifications_default_triggers) +} + +locals { + notifications = { + notifications = { + templates = { for key, value in local.notifications_templates : format("template.%s", key) => yamlencode(value) } + triggers = { for key, value in local.notifications_triggers : format("trigger.%s", key) => yamlencode(value) } + notifiers = { + for key, value in local.notifications_notifiers_variables : + format("service.%s", replace(key, "_", ".")) => + yamlencode(merge(local.notifications_notifiers_ssm_configs_keys[key], value)) + } + } + } +} diff --git a/modules/eks/argocd/outputs.tf b/modules/eks/argocd/outputs.tf index 26b6561e7..e69de29bb 100644 --- a/modules/eks/argocd/outputs.tf +++ b/modules/eks/argocd/outputs.tf @@ -1,4 +0,0 @@ -output "metadata" { - value = module.argocd.metadata - description = "Block status of the deployed release" -} diff --git a/modules/eks/argocd/provider-github.tf b/modules/eks/argocd/provider-github.tf new file mode 100644 index 000000000..022ae13dd --- /dev/null +++ b/modules/eks/argocd/provider-github.tf @@ -0,0 +1,35 @@ +variable "github_base_url" { + type = string + description = "This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/`" + default = null +} + +variable "ssm_github_api_key" { + type = string + description = "SSM path to the GitHub API key" + default = "/argocd/github/api_key" +} + +variable "github_token_override" { + type = string + description = "Use the value of this variable as the GitHub token instead of reading it from SSM" + default = null +} + +locals { + github_token = local.github_webhook_enabled ? coalesce(var.github_token_override, try(data.aws_ssm_parameter.github_api_key[0].value, null)) : "" +} + +data "aws_ssm_parameter" "github_api_key" { + count = local.github_webhook_enabled ? 1 : 0 + name = var.ssm_github_api_key + with_decryption = true + + provider = aws.config_secrets +} + +provider "github" { + base_url = local.github_webhook_enabled ? var.github_base_url : null + owner = local.github_webhook_enabled ? var.github_organization : null + token = local.github_webhook_enabled ? local.github_token : null +} diff --git a/modules/eks/argocd/providers.tf b/modules/eks/argocd/providers.tf index 45d458575..89ed50a98 100644 --- a/modules/eks/argocd/providers.tf +++ b/modules/eks/argocd/providers.tf @@ -8,7 +8,7 @@ provider "aws" { # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. for_each = compact([module.iam_roles.terraform_role_arn]) content { - role_arn = module.iam_roles.terraform_role_arn + role_arn = assume_role.value } } } diff --git a/modules/eks/argocd/resources/argocd-notifications-values.yaml.tpl b/modules/eks/argocd/resources/argocd-notifications-values.yaml.tpl index 3a0facdb4..e8b4bab5e 100644 --- a/modules/eks/argocd/resources/argocd-notifications-values.yaml.tpl +++ b/modules/eks/argocd/resources/argocd-notifications-values.yaml.tpl @@ -4,37 +4,6 @@ notifications: create: true argocdUrl: ${argocd_host} - - notifiers: - %{ if slack_notifications_enabled == true } - service.slack: | - token: $slack-token - username: "${slack_notifications_username}" - icon: "${slack_notifications_icon}" - %{ endif } - %{ if github_notifications_enabled == true } - # The webhook service notification configuration for GitHub cannot be consolidated into a single service because at least - # one of the notification templates requires the use of more than one GitHub endpoint via the webhook service. Since the - # webhook service configuration is a map, with each endpoint having its own key, we must also configure each key below, - # even if the configuration itself is exactly the same. - # See: https://github.com/argoproj/notifications-engine/blob/32519f8f68ec85d8ac3741d4ad52f7f5476ce5e7/pkg/services/webhook.go#L23 - service.webhook.github-commit-status: | - url: "https://api.github.com" - headers: - - name: "Authorization" - value: "token $github-token" - service.webhook.github-deployment: | - url: "https://api.github.com" - headers: - - name: "Authorization" - value: "token $github-token" - %{ endif } - %{ if datadog_notifications_enabled == true } - service.webhook.datadog: | - url: "https://api.datadoghq.com/api/v1/events" - headers: - - name: "DD-API-KEY" - value: "$datadog-api-key" - - name: "Content-Type" - value: "application/json" - %{ endif } + podAnnotations: + checksum/config: ${configs-hash} + checksum/secrets: ${secrets-hash} diff --git a/modules/eks/argocd/resources/argocd-values.yaml.tpl b/modules/eks/argocd/resources/argocd-values.yaml.tpl index 16ae99c63..26d3ae928 100644 --- a/modules/eks/argocd/resources/argocd-values.yaml.tpl +++ b/modules/eks/argocd/resources/argocd-values.yaml.tpl @@ -9,14 +9,6 @@ dex: image: imagePullPolicy: IfNotPresent tag: v2.30.2 -%{ if enable_argo_workflows_auth ~} - env: - - name: ARGO_WORKFLOWS_SSO_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: argo-workflows-sso - key: client-secret -%{ endif ~} controller: replicas: 1 @@ -76,6 +68,9 @@ server: service: type: NodePort + secret: + create: true + config: url: https://${argocd_host} admin.enabled: "${admin_enabled}" diff --git a/modules/eks/argocd/resources/kustomize/.gitignore b/modules/eks/argocd/resources/kustomize/.gitignore deleted file mode 100644 index 3b808ddbe..000000000 --- a/modules/eks/argocd/resources/kustomize/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_*.yaml diff --git a/modules/eks/argocd/resources/kustomize/kustomization.yaml b/modules/eks/argocd/resources/kustomize/kustomization.yaml deleted file mode 100644 index 540b59ba4..000000000 --- a/modules/eks/argocd/resources/kustomize/kustomization.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: - - _all.yaml - -patches: - - path: patch.yaml - target: - kind: Deployment - labelSelector: "app.kubernetes.io/part-of=argocd,app.kubernetes.io/component=dex-server" diff --git a/modules/eks/argocd/resources/kustomize/patch.yaml b/modules/eks/argocd/resources/kustomize/patch.yaml deleted file mode 100644 index b37182533..000000000 --- a/modules/eks/argocd/resources/kustomize/patch.yaml +++ /dev/null @@ -1,3 +0,0 @@ -- op: add - path: /metadata/annotations/secret.reloader.stakater.com~1reload - value: argo-workflows-sso diff --git a/modules/eks/argocd/resources/kustomize/post-render.sh b/modules/eks/argocd/resources/kustomize/post-render.sh deleted file mode 100755 index d54756e00..000000000 --- a/modules/eks/argocd/resources/kustomize/post-render.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -KUSTOMIZE_VERSION=4.5.3 -KUSTOMIZE_REPO_COMMIT=d2e59002aeb1faa724c6fa6e8218df2ad12631f8 -KUSTOMIZE_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/kubernetes-sigs/kustomize/$KUSTOMIZE_REPO_COMMIT/hack/install_kustomize.sh" - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -ALL_YAML="$SCRIPT_DIR/_all.yaml" -KUSTOMIZE_INSTALL_DIR="$(mktemp -d)" - -on_exit() -{ - test -e "$ALL_YAML" && rm "$ALL_YAML" -} - -trap on_exit EXIT - -curl -s "$KUSTOMIZE_INSTALL_SCRIPT_URL" | bash /dev/stdin "$KUSTOMIZE_VERSION" "$KUSTOMIZE_INSTALL_DIR" > /dev/null - -cd "$SCRIPT_DIR" - -cat <&0 > "$ALL_YAML" - -"$KUSTOMIZE_INSTALL_DIR/kustomize" build --reorder none . diff --git a/modules/eks/argocd/variables-argocd-notifications.tf b/modules/eks/argocd/variables-argocd-notifications.tf index 603741896..681eebaae 100644 --- a/modules/eks/argocd/variables-argocd-notifications.tf +++ b/modules/eks/argocd/variables-argocd-notifications.tf @@ -1,4 +1,17 @@ +variable "github_default_notifications_enabled" { + type = bool + default = true + description = "Enable default GitHub commit statuses notifications (required for CD sync mode)" +} + variable "notifications_templates" { + description = <<-EOT + Notification Templates to configure. + + See: https://argocd-notifications.readthedocs.io/en/stable/templates/ + See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) + EOT + type = map(object({ message = string alertmanager = optional(object({ @@ -6,13 +19,6 @@ variable "notifications_templates" { annotations = map(string) generatorURL = string })) - github = optional(object({ - status = object({ - state = string - label = string - targetURL = string - }) - })) webhook = optional(map( object({ method = optional(string) @@ -21,16 +27,18 @@ variable "notifications_templates" { }) )) })) - default = {} - description = <<-EOT - Notification Templates to configure. - See: https://argocd-notifications.readthedocs.io/en/stable/templates/ - See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) - EOT + default = {} } variable "notifications_triggers" { + description = <<-EOT + Notification Triggers to configure. + + See: https://argocd-notifications.readthedocs.io/en/stable/triggers/ + See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) + EOT + type = map(list( object({ oncePer = optional(string) @@ -38,25 +46,15 @@ variable "notifications_triggers" { when = string }) )) - default = {} - description = <<-EOT - Notification Triggers to configure. - See: https://argocd-notifications.readthedocs.io/en/stable/triggers/ - See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) - EOT + default = {} } variable "notifications_notifiers" { type = object({ ssm_path_prefix = optional(string, "/argocd/notifications/notifiers") - service_github = optional(object({ - appID = number - installationID = number - privateKey = optional(string) - })) # service.webhook.: - service_webhook = optional(map( + webhook = optional(map( object({ url = string headers = optional(list( @@ -73,52 +71,11 @@ variable "notifications_notifiers" { }) )) }) - default = {} description = <<-EOT Notification Triggers to configure. See: https://argocd-notifications.readthedocs.io/en/stable/triggers/ See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) EOT -} - -#variable "notifications_default_triggers" { -# type = map(list(string)) -# default = {} -# description = <<-EOT -# Default notification Triggers to configure. -# -# See: https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/triggers/#default-triggers -# See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/790438efebf423c2d56cb4b93471f4adb3fcd448/charts/argo-cd/values.yaml#L2841) -# EOT -#} - -variable "slack_notifications_enabled" { - type = bool - default = false - description = "Whether or not to enable Slack notifications." -} - -variable "slack_notifications_username" { - type = string - default = null - description = "Custom username to use for Slack notifications." -} - -variable "slack_notifications_icon" { - type = string - default = null - description = "URI of custom image to use as the Slack notifications icon." -} - -variable "github_notifications_enabled" { - type = bool - default = false - description = "Whether or not to enable GitHub deployment and commit status notifications." -} - -variable "datadog_notifications_enabled" { - type = bool - default = false - description = "Whether or not to notify Datadog of deployments via the Datadog Events API." + default = {} } diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index 0ee31dc8f..94280feb5 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -137,12 +137,6 @@ variable "saml_enabled" { default = false } -#variable "saml_okta_app_name" { -# type = string -# description = "Name of the Okta SAML Integration" -# default = "ArgoCD" -#} - variable "saml_rbac_scopes" { type = string description = "SAML RBAC scopes to request" @@ -155,12 +149,6 @@ variable "argo_enable_workflows_auth" { description = "Allow argo-workflows to use Dex instance for SAML auth" } -# variable "argo_workflows_name" { -# type = string -# default = "argo-workflows" -# description = "Name of argo-workflows instance" -# } - variable "argocd_rbac_policies" { type = list(string) default = [] @@ -215,3 +203,9 @@ variable "saml_sso_providers" { default = {} description = "SAML SSO providers components" } + +variable "github_webhook_enabled" { + type = bool + default = true + description = "Enable GitHub webhook integration" +} diff --git a/modules/eks/argocd/versions.tf b/modules/eks/argocd/versions.tf index 3e6c990e3..0877dc2d0 100644 --- a/modules/eks/argocd/versions.tf +++ b/modules/eks/argocd/versions.tf @@ -14,5 +14,13 @@ terraform { source = "hashicorp/kubernetes" version = ">= 2.9.0, != 2.21.0" } + github = { + source = "integrations/github" + version = ">= 4.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } } } From 00ffa3ac4f8e651dca0433328dafc5a907a9ab78 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Mon, 4 Sep 2023 14:45:19 -0400 Subject: [PATCH 256/501] Update `waf` component (#854) Co-authored-by: cloudpossebot --- modules/waf/README.md | 33 +-- modules/waf/main.tf | 32 ++- modules/waf/outputs.tf | 5 - modules/waf/remote-state.tf | 15 + modules/waf/variables.tf | 540 ++++++++++++++++++++++++++++-------- 5 files changed, 482 insertions(+), 143 deletions(-) diff --git a/modules/waf/README.md b/modules/waf/README.md index 9f472f5da..6f8e70d8f 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -58,8 +58,9 @@ components: | Name | Source | Version | |------|--------|---------| | [association\_resource\_components](#module\_association\_resource\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 1.0.0 | +| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 1.2.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [log\_destination\_components](#module\_log\_destination\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -75,9 +76,9 @@ components: | [acl\_name](#input\_acl\_name) | Friendly name of the ACL. The ACL ARN will be stored in SSM under {ssm\_path\_prefix}/{acl\_name}/arn | `string` | n/a | yes | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [association\_resource\_arns](#input\_association\_resource\_arns) | A list of ARNs of the resources to associate with the web ACL.
This must be an ARN of an Application Load Balancer, Amazon API Gateway stage, or AWS AppSync.

Do not use this variable to associate a Cloudfront Distribution.
Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource.
For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html | `list(string)` | `[]` | no | -| [association\_resource\_component\_selectors](#input\_association\_resource\_component\_selectors) | A list of Atmos component selectors to get from the remote state and associate their ARNs with the web ACL.
The components must be Application Load Balancers, Amazon API Gateway stages, or AWS AppSync.

component:
Atmos component name
component\_arn\_output:
The component output that defines the component ARN

Do not use this variable to select a Cloudfront Distribution component.
Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource.
For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html |
list(object({
component = string
namespace = optional(string, null)
tenant = optional(string, null)
environment = optional(string, null)
stage = optional(string, null)
component_arn_output = string
}))
| `[]` | no | +| [association\_resource\_component\_selectors](#input\_association\_resource\_component\_selectors) | A list of Atmos component selectors to get from the remote state and associate their ARNs with the web ACL.
The components must be Application Load Balancers, Amazon API Gateway stages, or AWS AppSync.

component:
Atmos component name
component\_arn\_output:
The component output that defines the component ARN

Set `tenant`, `environment` and `stage` if the components are in different OUs, regions or accounts.

Do not use this variable to select a Cloudfront Distribution component.
Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource.
For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html |
list(object({
component = string
namespace = optional(string, null)
tenant = optional(string, null)
environment = optional(string, null)
stage = optional(string, null)
component_arn_output = string
}))
| `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [byte\_match\_statement\_rules](#input\_byte\_match\_statement\_rules) | A rule statement that defines a string match search for AWS WAF to apply to web requests.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [byte\_match\_statement\_rules](#input\_byte\_match\_statement\_rules) | A rule statement that defines a string match search for AWS WAF to apply to web requests.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
positional\_constraint:
Area within the portion of a web request that you want AWS WAF to search for search\_string. Valid values include the following: EXACTLY, STARTS\_WITH, ENDS\_WITH, CONTAINS, CONTAINS\_WORD.
search\_string
String value that you want AWS WAF to search for. AWS WAF searches only in the part of web requests that you designate for inspection in field\_to\_match.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [custom\_response\_body](#input\_custom\_response\_body) | Defines custom response bodies that can be referenced by custom\_response actions.
The map keys are used as the `key` attribute which is a unique key identifying the custom response body.
content:
Payload of the custom response.
The response body can be plain text, HTML or JSON and cannot exceed 4KB in size.
content\_type:
Content Type of Response Body.
Valid values are `TEXT_PLAIN`, `TEXT_HTML`, or `APPLICATION_JSON`. |
map(object({
content = string
content_type = string
}))
| `{}` | no | | [default\_action](#input\_default\_action) | Specifies that AWS WAF should allow requests by default. Possible values: `allow`, `block`. | `string` | `"block"` | no | @@ -86,43 +87,43 @@ components: | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [geo\_allowlist\_statement\_rules](#input\_geo\_allowlist\_statement\_rules) | A rule statement used to identify a list of allowed countries which should not be blocked by the WAF.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | -| [geo\_match\_statement\_rules](#input\_geo\_match\_statement\_rules) | A rule statement used to identify web requests based on country of origin.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [geo\_allowlist\_statement\_rules](#input\_geo\_allowlist\_statement\_rules) | A rule statement used to identify a list of allowed countries which should not be blocked by the WAF.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | +| [geo\_match\_statement\_rules](#input\_geo\_match\_statement\_rules) | A rule statement used to identify web requests based on country of origin.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [ip\_set\_reference\_statement\_rules](#input\_ip\_set\_reference\_statement\_rules) | A rule statement used to detect web requests coming from particular IP addresses or address ranges.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The ARN of the IP Set that this statement references.
ip\_set\_forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
position:
The position in the header to search for the IP address.
Possible values include: `FIRST`, `LAST`, or `ANY`.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [ip\_set\_reference\_statement\_rules](#input\_ip\_set\_reference\_statement\_rules) | A rule statement used to detect web requests coming from particular IP addresses or address ranges.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The ARN of the IP Set that this statement references.
ip\_set:
Defines a new IP Set

description:
A friendly description of the IP Set
addresses:
Contains an array of strings that specifies zero or more IP addresses or blocks of IP addresses.
All addresses must be specified using Classless Inter-Domain Routing (CIDR) notation.
ip\_address\_version:
Specify `IPV4` or `IPV6`
ip\_set\_forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
position:
The position in the header to search for the IP address.
Possible values include: `FIRST`, `LAST`, or `ANY`.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [log\_destination\_configs](#input\_log\_destination\_configs) | The Amazon Kinesis Data Firehose, CloudWatch Log log group, or S3 bucket Amazon Resource Names (ARNs) that you want to associate with the web ACL | `list(string)` | `[]` | no | +| [log\_destination\_component\_selectors](#input\_log\_destination\_component\_selectors) | A list of Atmos component selectors to get from the remote state and associate their names/ARNs with the WAF logs.
The components must be Amazon Kinesis Data Firehose, CloudWatch Log Group, or S3 bucket.

component:
Atmos component name
component\_output:
The component output that defines the component name or ARN

Set `tenant`, `environment` and `stage` if the components are in different OUs, regions or accounts.

Note: data firehose, log group, or bucket name must be prefixed with `aws-waf-logs-`,
e.g. `aws-waf-logs-example-firehose`, `aws-waf-logs-example-log-group`, or `aws-waf-logs-example-bucket`. |
list(object({
component = string
namespace = optional(string, null)
tenant = optional(string, null)
environment = optional(string, null)
stage = optional(string, null)
component_output = string
}))
| `[]` | no | +| [log\_destination\_configs](#input\_log\_destination\_configs) | A list of resource names/ARNs to associate Amazon Kinesis Data Firehose, Cloudwatch Log log group, or S3 bucket with the WAF logs.
Note: data firehose, log group, or bucket name must be prefixed with `aws-waf-logs-`,
e.g. `aws-waf-logs-example-firehose`, `aws-waf-logs-example-log-group`, or `aws-waf-logs-example-bucket`. | `list(string)` | `[]` | no | | [logging\_filter](#input\_logging\_filter) | A configuration block that specifies which web requests are kept in the logs and which are dropped.
You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation. |
object({
default_behavior = string
filter = list(object({
behavior = string
requirement = string
condition = list(object({
action_condition = optional(object({
action = string
}), null)
label_name_condition = optional(object({
label_name = string
}), null)
}))
}))
})
| `null` | no | -| [managed\_rule\_group\_statement\_rules](#input\_managed\_rule\_group\_statement\_rules) | A rule statement used to run the rules that are defined in a managed rule group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
name:
The name of the managed rule group.
vendor\_name:
The name of the managed rule group vendor.
version:
The version of the managed rule group.
You can set `Version_1.0` or `Version_1.1` etc. If you want to use the default version, do not set anything.
rule\_action\_override:
Action settings to use in the place of the rule actions that are configured inside the rule group.
You specify one override for each rule whose action you want to change.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [managed\_rule\_group\_statement\_rules](#input\_managed\_rule\_group\_statement\_rules) | A rule statement used to run the rules that are defined in a managed rule group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
name:
The name of the managed rule group.
vendor\_name:
The name of the managed rule group vendor.
version:
The version of the managed rule group.
You can set `Version_1.0` or `Version_1.1` etc. If you want to use the default version, do not set anything.
rule\_action\_override:
Action settings to use in the place of the rule actions that are configured inside the rule group.
You specify one override for each rule whose action you want to change.
managed\_rule\_group\_configs:
Additional information that's used by a managed rule group. Only one rule attribute is allowed in each config.
Refer to https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html for more details.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
override_action = optional(string)
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = object({
name = string
vendor_name = string
version = optional(string)
rule_action_override = optional(map(object({
action = string
custom_request_handling = optional(object({
insert_header = object({
name = string
value = string
})
}), null)
custom_response = optional(object({
response_code = string
response_header = optional(object({
name = string
value = string
}), null)
}), null)
})), null)
managed_rule_group_configs = optional(list(object({
aws_managed_rules_bot_control_rule_set = optional(object({
inspection_level = string
}), null)
aws_managed_rules_atp_rule_set = optional(object({
enable_regex_in_path = optional(bool)
login_path = string
request_inspection = optional(object({
payload_type = string
password_field = object({
identifier = string
})
username_field = object({
identifier = string
})
}), null)
response_inspection = optional(object({
body_contains = optional(object({
success_strings = list(string)
failure_strings = list(string)
}), null)
header = optional(object({
name = string
success_values = list(string)
failure_values = list(string)
}), null)
json = optional(object({

identifier = string
success_strings = list(string)
failure_strings = list(string)
}), null)
status_code = optional(object({
success_codes = list(string)
failure_codes = list(string)
}), null)
}), null)
}), null)
})), null)
})
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [rate\_based\_statement\_rules](#input\_rate\_based\_statement\_rules) | A rate-based rule tracks the rate of requests for each originating IP address,
and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
aggregate\_key\_type:
Setting that indicates how to aggregate the request counts.
Possible values include: `FORWARDED_IP` or `IP`
limit:
The limit on requests per 5-minute period for a single originating IP address.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [rate\_based\_statement\_rules](#input\_rate\_based\_statement\_rules) | A rate-based rule tracks the rate of requests for each originating IP address,
and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
aggregate\_key\_type:
Setting that indicates how to aggregate the request counts.
Possible values include: `FORWARDED_IP` or `IP`
limit:
The limit on requests per 5-minute period for a single originating IP address.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [redacted\_fields](#input\_redacted\_fields) | The parts of the request that you want to keep out of the logs.
You can only specify one of the following: `method`, `query_string`, `single_header`, or `uri_path`

method:
Whether to enable redaction of the HTTP method.
The method indicates the type of operation that the request is asking the origin to perform.
uri\_path:
Whether to enable redaction of the URI path.
This is the part of a web request that identifies a resource.
query\_string:
Whether to enable redaction of the query string.
This is the part of a URL that appears after a `?` character, if any.
single\_header:
The list of names of the query headers to redact. |
map(object({
method = optional(bool, false)
uri_path = optional(bool, false)
query_string = optional(bool, false)
single_header = optional(list(string), null)
}))
| `{}` | no | -| [regex\_match\_statement\_rules](#input\_regex\_match\_statement\_rules) | A rule statement used to search web request components for a match against a single regular expression.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
regex\_string:
String representing the regular expression. Minimum of 1 and maximum of 512 characters.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl.html#field_to_match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection. At least one required.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | -| [regex\_pattern\_set\_reference\_statement\_rules](#input\_regex\_pattern\_set\_reference\_statement\_rules) | A rule statement used to search web request components for matches with regular expressions.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The Amazon Resource Name (ARN) of the Regex Pattern Set that this statement references.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [regex\_match\_statement\_rules](#input\_regex\_match\_statement\_rules) | A rule statement used to search web request components for a match against a single regular expression.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
regex\_string:
String representing the regular expression. Minimum of 1 and maximum of 512 characters.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl.html#field_to_match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection. At least one required.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | +| [regex\_pattern\_set\_reference\_statement\_rules](#input\_regex\_pattern\_set\_reference\_statement\_rules) | A rule statement used to search web request components for matches with regular expressions.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The Amazon Resource Name (ARN) of the Regex Pattern Set that this statement references.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [rule\_group\_reference\_statement\_rules](#input\_rule\_group\_reference\_statement\_rules) | A rule statement used to run the rules that are defined in an WAFv2 Rule Group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The ARN of the `aws_wafv2_rule_group` resource.
rule\_action\_override:
Action settings to use in the place of the rule actions that are configured inside the rule group.
You specify one override for each rule whose action you want to change.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [rule\_group\_reference\_statement\_rules](#input\_rule\_group\_reference\_statement\_rules) | A rule statement used to run the rules that are defined in an WAFv2 Rule Group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The ARN of the `aws_wafv2_rule_group` resource.
rule\_action\_override:
Action settings to use in the place of the rule actions that are configured inside the rule group.
You specify one override for each rule whose action you want to change.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
override_action = optional(string)
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = object({
arn = string
rule_action_override = optional(map(object({
action = string
custom_request_handling = optional(object({
insert_header = object({
name = string
value = string
})
}), null)
custom_response = optional(object({
response_code = string
response_header = optional(object({
name = string
value = string
}), null)
}), null)
})), null)
})
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [scope](#input\_scope) | Specifies whether this is for an AWS CloudFront distribution or for a regional application.
Possible values are `CLOUDFRONT` or `REGIONAL`.
To work with CloudFront, you must also specify the region us-east-1 (N. Virginia) on the AWS provider. | `string` | `"REGIONAL"` | no | -| [size\_constraint\_statement\_rules](#input\_size\_constraint\_statement\_rules) | A rule statement that uses a comparison operator to compare a number of bytes against the size of a request component.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
comparison\_operator:
The operator to use to compare the request part to the size setting.
Possible values: `EQ`, `NE`, `LE`, `LT`, `GE`, or `GT`.
size:
The size, in bytes, to compare to the request part, after any transformations.
Valid values are integers between `0` and `21474836480`, inclusive.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | -| [sqli\_match\_statement\_rules](#input\_sqli\_match\_statement\_rules) | An SQL injection match condition identifies the part of web requests,
such as the URI or the query string, that you want AWS WAF to inspect.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [size\_constraint\_statement\_rules](#input\_size\_constraint\_statement\_rules) | A rule statement that uses a comparison operator to compare a number of bytes against the size of a request component.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
comparison\_operator:
The operator to use to compare the request part to the size setting.
Possible values: `EQ`, `NE`, `LE`, `LT`, `GE`, or `GT`.
size:
The size, in bytes, to compare to the request part, after any transformations.
Valid values are integers between `0` and `21474836480`, inclusive.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | +| [sqli\_match\_statement\_rules](#input\_sqli\_match\_statement\_rules) | An SQL injection match condition identifies the part of web requests,
such as the URI or the query string, that you want AWS WAF to inspect.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | SSM path prefix (with leading but not trailing slash) under which to store all WAF info | `string` | `"/waf"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [token\_domains](#input\_token\_domains) | Specifies the domains that AWS WAF should accept in a web request token.
This enables the use of tokens across multiple protected websites.
When AWS WAF provides a token, it uses the domain of the AWS resource that the web ACL is protecting.
If you don't specify a list of token domains, AWS WAF accepts tokens only for the domain of the protected resource.
With a token domain list, AWS WAF accepts the resource's host domain plus all domains in the token domain list,
including their prefixed subdomains. | `list(string)` | `null` | no | | [visibility\_config](#input\_visibility\_config) | Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
object({
cloudwatch_metrics_enabled = bool
metric_name = string
sampled_requests_enabled = bool
})
| n/a | yes | -| [xss\_match\_statement\_rules](#input\_xss\_match\_statement\_rules) | A rule statement that defines a cross-site scripting (XSS) match search for AWS WAF to apply to web requests.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. | `list(any)` | `null` | no | +| [xss\_match\_statement\_rules](#input\_xss\_match\_statement\_rules) | A rule statement that defines a cross-site scripting (XSS) match search for AWS WAF to apply to web requests.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | ## Outputs | Name | Description | |------|-------------| | [arn](#output\_arn) | The ARN of the WAF WebACL. | -| [capacity](#output\_capacity) | The web ACL capacity units (WCUs) currently being used by this web ACL. | | [id](#output\_id) | The ID of the WAF WebACL. | | [logging\_config\_id](#output\_logging\_config\_id) | The ARN of the WAFv2 Web ACL logging configuration. | diff --git a/modules/waf/main.tf b/modules/waf/main.tf index 3d2b9a7a5..e2f497a33 100644 --- a/modules/waf/main.tf +++ b/modules/waf/main.tf @@ -7,24 +7,34 @@ locals { ] association_resource_arns = concat(var.association_resource_arns, local.association_resource_component_selectors_arns) + + log_destination_component_selectors = [ + for i, v in var.log_destination_component_selectors : module.log_destination_components[i].outputs[v.component_output] + if local.enabled + ] + + log_destination_configs = concat(var.log_destination_configs, local.log_destination_component_selectors) } module "aws_waf" { source = "cloudposse/waf/aws" - version = "1.0.0" - - description = var.description - default_action = var.default_action - custom_response_body = var.custom_response_body - scope = var.scope - visibility_config = var.visibility_config - token_domains = var.token_domains - log_destination_configs = var.log_destination_configs - redacted_fields = var.redacted_fields - logging_filter = var.logging_filter + version = "1.2.0" + + description = var.description + default_action = var.default_action + custom_response_body = var.custom_response_body + scope = var.scope + visibility_config = var.visibility_config + token_domains = var.token_domains + # Association resources association_resource_arns = local.association_resource_arns + # Logging configuration + redacted_fields = var.redacted_fields + logging_filter = var.logging_filter + log_destination_configs = local.log_destination_configs + # Rules byte_match_statement_rules = var.byte_match_statement_rules geo_allowlist_statement_rules = var.geo_allowlist_statement_rules diff --git a/modules/waf/outputs.tf b/modules/waf/outputs.tf index 3a6f3adcb..2897e7b9f 100644 --- a/modules/waf/outputs.tf +++ b/modules/waf/outputs.tf @@ -8,11 +8,6 @@ output "arn" { value = module.aws_waf.arn } -output "capacity" { - description = "The web ACL capacity units (WCUs) currently being used by this web ACL." - value = module.aws_waf.capacity -} - output "logging_config_id" { description = "The ARN of the WAFv2 Web ACL logging configuration." value = module.aws_waf.logging_config_id diff --git a/modules/waf/remote-state.tf b/modules/waf/remote-state.tf index ad6674f34..db3a6c7e8 100644 --- a/modules/waf/remote-state.tf +++ b/modules/waf/remote-state.tf @@ -12,3 +12,18 @@ module "association_resource_components" { context = module.this.context } + +module "log_destination_components" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.enabled ? length(var.log_destination_component_selectors) : 0 + + component = var.log_destination_component_selectors[count.index].component + namespace = coalesce(lookup(var.log_destination_component_selectors[count.index], "namespace", null), module.this.namespace) + tenant = coalesce(lookup(var.log_destination_component_selectors[count.index], "tenant", null), module.this.tenant) + environment = coalesce(lookup(var.log_destination_component_selectors[count.index], "environment", null), module.this.environment) + stage = coalesce(lookup(var.log_destination_component_selectors[count.index], "stage", null), module.this.stage) + + context = module.this.context +} diff --git a/modules/waf/variables.tf b/modules/waf/variables.tf index 3d9b489ab..2b24c098c 100644 --- a/modules/waf/variables.tf +++ b/modules/waf/variables.tf @@ -98,108 +98,25 @@ variable "token_domains" { default = null } -# Logging configuration -# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration.html -variable "log_destination_configs" { - type = list(string) - default = [] - description = "The Amazon Kinesis Data Firehose, CloudWatch Log log group, or S3 bucket Amazon Resource Names (ARNs) that you want to associate with the web ACL" -} - -variable "redacted_fields" { - type = map(object({ - method = optional(bool, false) - uri_path = optional(bool, false) - query_string = optional(bool, false) - single_header = optional(list(string), null) - })) - default = {} - description = <<-DOC - The parts of the request that you want to keep out of the logs. - You can only specify one of the following: `method`, `query_string`, `single_header`, or `uri_path` - - method: - Whether to enable redaction of the HTTP method. - The method indicates the type of operation that the request is asking the origin to perform. - uri_path: - Whether to enable redaction of the URI path. - This is the part of a web request that identifies a resource. - query_string: - Whether to enable redaction of the query string. - This is the part of a URL that appears after a `?` character, if any. - single_header: - The list of names of the query headers to redact. - DOC - nullable = false -} - -variable "logging_filter" { - type = object({ - default_behavior = string - filter = list(object({ - behavior = string - requirement = string - condition = list(object({ - action_condition = optional(object({ - action = string - }), null) - label_name_condition = optional(object({ - label_name = string - }), null) - })) - })) - }) - default = null - description = <<-DOC - A configuration block that specifies which web requests are kept in the logs and which are dropped. - You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation. - DOC -} - -# Association resources -variable "association_resource_arns" { - type = list(string) - default = [] - description = <<-DOC - A list of ARNs of the resources to associate with the web ACL. - This must be an ARN of an Application Load Balancer, Amazon API Gateway stage, or AWS AppSync. - - Do not use this variable to associate a Cloudfront Distribution. - Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource. - For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html - DOC - nullable = false -} - -variable "association_resource_component_selectors" { - type = list(object({ - component = string - namespace = optional(string, null) - tenant = optional(string, null) - environment = optional(string, null) - stage = optional(string, null) - component_arn_output = string - })) - default = [] - description = <<-DOC - A list of Atmos component selectors to get from the remote state and associate their ARNs with the web ACL. - The components must be Application Load Balancers, Amazon API Gateway stages, or AWS AppSync. - - component: - Atmos component name - component_arn_output: - The component output that defines the component ARN - - Do not use this variable to select a Cloudfront Distribution component. - Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource. - For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html - DOC - nullable = false -} - # Rules variable "byte_match_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement that defines a string match search for AWS WAF to apply to web requests. @@ -226,6 +143,10 @@ variable "byte_match_statement_rules" { A List of labels to apply to web requests that match the rule match statement statement: + positional_constraint: + Area within the portion of a web request that you want AWS WAF to search for search_string. Valid values include the following: EXACTLY, STARTS_WITH, ENDS_WITH, CONTAINS, CONTAINS_WORD. + search_string + String value that you want AWS WAF to search for. AWS WAF searches only in the part of web requests that you designate for inspection in field_to_match. field_to_match: The part of a web request that you want AWS WAF to inspect. See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match @@ -246,7 +167,22 @@ variable "byte_match_statement_rules" { } variable "geo_allowlist_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement used to identify a list of allowed countries which should not be blocked by the WAF. @@ -293,7 +229,23 @@ variable "geo_allowlist_statement_rules" { } variable "geo_match_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement used to identify web requests based on country of origin. @@ -342,7 +294,23 @@ variable "geo_match_statement_rules" { } variable "ip_set_reference_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement used to detect web requests coming from particular IP addresses or address ranges. @@ -371,6 +339,16 @@ variable "ip_set_reference_statement_rules" { statement: arn: The ARN of the IP Set that this statement references. + ip_set: + Defines a new IP Set + + description: + A friendly description of the IP Set + addresses: + Contains an array of strings that specifies zero or more IP addresses or blocks of IP addresses. + All addresses must be specified using Classless Inter-Domain Routing (CIDR) notation. + ip_address_version: + Specify `IPV4` or `IPV6` ip_set_forwarded_ip_config: fallback_behavior: The match status to assign to the web request if the request doesn't have a valid IP address in the specified position. @@ -394,7 +372,82 @@ variable "ip_set_reference_statement_rules" { } variable "managed_rule_group_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + override_action = optional(string) + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = object({ + name = string + vendor_name = string + version = optional(string) + rule_action_override = optional(map(object({ + action = string + custom_request_handling = optional(object({ + insert_header = object({ + name = string + value = string + }) + }), null) + custom_response = optional(object({ + response_code = string + response_header = optional(object({ + name = string + value = string + }), null) + }), null) + })), null) + managed_rule_group_configs = optional(list(object({ + aws_managed_rules_bot_control_rule_set = optional(object({ + inspection_level = string + }), null) + aws_managed_rules_atp_rule_set = optional(object({ + enable_regex_in_path = optional(bool) + login_path = string + request_inspection = optional(object({ + payload_type = string + password_field = object({ + identifier = string + }) + username_field = object({ + identifier = string + }) + }), null) + response_inspection = optional(object({ + body_contains = optional(object({ + success_strings = list(string) + failure_strings = list(string) + }), null) + header = optional(object({ + name = string + success_values = list(string) + failure_values = list(string) + }), null) + json = optional(object({ + + identifier = string + success_strings = list(string) + failure_strings = list(string) + }), null) + status_code = optional(object({ + success_codes = list(string) + failure_codes = list(string) + }), null) + }), null) + }), null) + })), null) + }) + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement used to run the rules that are defined in a managed rule group. @@ -433,6 +486,9 @@ variable "managed_rule_group_statement_rules" { rule_action_override: Action settings to use in the place of the rule actions that are configured inside the rule group. You specify one override for each rule whose action you want to change. + managed_rule_group_configs: + Additional information that's used by a managed rule group. Only one rule attribute is allowed in each config. + Refer to https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html for more details. visibility_config: Defines and enables Amazon CloudWatch metrics and web request sample collection. @@ -447,7 +503,23 @@ variable "managed_rule_group_statement_rules" { } variable "rate_based_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rate-based rule tracks the rate of requests for each originating IP address, @@ -500,7 +572,23 @@ variable "rate_based_statement_rules" { } variable "regex_pattern_set_reference_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement used to search web request components for matches with regular expressions. @@ -549,7 +637,23 @@ variable "regex_pattern_set_reference_statement_rules" { } variable "regex_match_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement used to search web request components for a match against a single regular expression. @@ -598,7 +702,41 @@ variable "regex_match_statement_rules" { } variable "rule_group_reference_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + override_action = optional(string) + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = object({ + arn = string + rule_action_override = optional(map(object({ + action = string + custom_request_handling = optional(object({ + insert_header = object({ + name = string + value = string + }) + }), null) + custom_response = optional(object({ + response_code = string + response_header = optional(object({ + name = string + value = string + }), null) + }), null) + })), null) + }) + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement used to run the rules that are defined in an WAFv2 Rule Group. @@ -646,7 +784,23 @@ variable "rule_group_reference_statement_rules" { } variable "size_constraint_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement that uses a comparison operator to compare a number of bytes against the size of a request component. @@ -699,7 +853,23 @@ variable "size_constraint_statement_rules" { } variable "sqli_match_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC An SQL injection match condition identifies the part of web requests, @@ -747,7 +917,23 @@ variable "sqli_match_statement_rules" { } variable "xss_match_statement_rules" { - type = list(any) + type = list(object({ + name = string + priority = number + action = string + captcha_config = optional(object({ + immunity_time_property = object({ + immunity_time = number + }) + }), null) + rule_label = optional(list(string), null) + statement = any + visibility_config = optional(object({ + cloudwatch_metrics_enabled = optional(bool) + metric_name = string + sampled_requests_enabled = optional(bool) + }), null) + })) default = null description = <<-DOC A rule statement that defines a cross-site scripting (XSS) match search for AWS WAF to apply to web requests. @@ -792,3 +978,135 @@ variable "xss_match_statement_rules" { Whether AWS WAF should store a sampling of the web requests that match the rules. DOC } + +# Logging configuration +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration.html +variable "log_destination_configs" { + type = list(string) + default = [] + description = <<-DOC + A list of resource names/ARNs to associate Amazon Kinesis Data Firehose, Cloudwatch Log log group, or S3 bucket with the WAF logs. + Note: data firehose, log group, or bucket name must be prefixed with `aws-waf-logs-`, + e.g. `aws-waf-logs-example-firehose`, `aws-waf-logs-example-log-group`, or `aws-waf-logs-example-bucket`. + DOC +} + +variable "redacted_fields" { + type = map(object({ + method = optional(bool, false) + uri_path = optional(bool, false) + query_string = optional(bool, false) + single_header = optional(list(string), null) + })) + default = {} + description = <<-DOC + The parts of the request that you want to keep out of the logs. + You can only specify one of the following: `method`, `query_string`, `single_header`, or `uri_path` + + method: + Whether to enable redaction of the HTTP method. + The method indicates the type of operation that the request is asking the origin to perform. + uri_path: + Whether to enable redaction of the URI path. + This is the part of a web request that identifies a resource. + query_string: + Whether to enable redaction of the query string. + This is the part of a URL that appears after a `?` character, if any. + single_header: + The list of names of the query headers to redact. + DOC + nullable = false +} + +variable "logging_filter" { + type = object({ + default_behavior = string + filter = list(object({ + behavior = string + requirement = string + condition = list(object({ + action_condition = optional(object({ + action = string + }), null) + label_name_condition = optional(object({ + label_name = string + }), null) + })) + })) + }) + default = null + description = <<-DOC + A configuration block that specifies which web requests are kept in the logs and which are dropped. + You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation. + DOC +} + +variable "log_destination_component_selectors" { + type = list(object({ + component = string + namespace = optional(string, null) + tenant = optional(string, null) + environment = optional(string, null) + stage = optional(string, null) + component_output = string + })) + default = [] + description = <<-DOC + A list of Atmos component selectors to get from the remote state and associate their names/ARNs with the WAF logs. + The components must be Amazon Kinesis Data Firehose, CloudWatch Log Group, or S3 bucket. + + component: + Atmos component name + component_output: + The component output that defines the component name or ARN + + Set `tenant`, `environment` and `stage` if the components are in different OUs, regions or accounts. + + Note: data firehose, log group, or bucket name must be prefixed with `aws-waf-logs-`, + e.g. `aws-waf-logs-example-firehose`, `aws-waf-logs-example-log-group`, or `aws-waf-logs-example-bucket`. + DOC + nullable = false +} + +# Association resources +variable "association_resource_arns" { + type = list(string) + default = [] + description = <<-DOC + A list of ARNs of the resources to associate with the web ACL. + This must be an ARN of an Application Load Balancer, Amazon API Gateway stage, or AWS AppSync. + + Do not use this variable to associate a Cloudfront Distribution. + Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource. + For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html + DOC + nullable = false +} + +variable "association_resource_component_selectors" { + type = list(object({ + component = string + namespace = optional(string, null) + tenant = optional(string, null) + environment = optional(string, null) + stage = optional(string, null) + component_arn_output = string + })) + default = [] + description = <<-DOC + A list of Atmos component selectors to get from the remote state and associate their ARNs with the web ACL. + The components must be Application Load Balancers, Amazon API Gateway stages, or AWS AppSync. + + component: + Atmos component name + component_arn_output: + The component output that defines the component ARN + + Set `tenant`, `environment` and `stage` if the components are in different OUs, regions or accounts. + + Do not use this variable to select a Cloudfront Distribution component. + Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource. + For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html + DOC + nullable = false +} From 7e0e36ea54d2dd57604cd6c8e45303442b07faa0 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Tue, 5 Sep 2023 17:16:24 +0300 Subject: [PATCH 257/501] Fix components (#855) Co-authored-by: cloudpossebot Co-authored-by: Andriy Knysh --- .../external-secrets-operator/CHANGELOG.md | 10 + .../eks/external-secrets-operator/README.md | 1 + modules/eks/external-secrets-operator/main.tf | 8 + modules/ipam/CHANGELOG.md | 1 + modules/ipam/README.md | 128 ++++++++ modules/ipam/context.tf | 279 ++++++++++++++++++ modules/ipam/main.tf | 58 ++++ modules/ipam/outputs.tf | 4 + modules/ipam/providers.tf | 19 ++ modules/ipam/remote-state.tf | 11 + modules/ipam/variables.tf | 118 ++++++++ modules/ipam/versions.tf | 10 + modules/redshift/CHANGELOG.md | 7 + modules/redshift/README.md | 4 +- modules/redshift/versions.tf | 2 +- 15 files changed, 657 insertions(+), 3 deletions(-) create mode 100644 modules/eks/external-secrets-operator/CHANGELOG.md create mode 100644 modules/ipam/CHANGELOG.md create mode 100644 modules/ipam/README.md create mode 100644 modules/ipam/context.tf create mode 100644 modules/ipam/main.tf create mode 100644 modules/ipam/outputs.tf create mode 100644 modules/ipam/providers.tf create mode 100644 modules/ipam/remote-state.tf create mode 100644 modules/ipam/variables.tf create mode 100644 modules/ipam/versions.tf create mode 100644 modules/redshift/CHANGELOG.md diff --git a/modules/eks/external-secrets-operator/CHANGELOG.md b/modules/eks/external-secrets-operator/CHANGELOG.md new file mode 100644 index 000000000..901b6aae8 --- /dev/null +++ b/modules/eks/external-secrets-operator/CHANGELOG.md @@ -0,0 +1,10 @@ +## Components PR [Fix components](https://github.com/cloudposse/terraform-aws-components/pull/855) + +This is a bug fix and feature enhancement update. +No actions necessary to upgrade. + +## Notes +* Cold start needs to apply twice + +## Fixes +* Fix cold start bug because the CRD does not exist yet diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 2ac053d53..fc77cfef5 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -122,6 +122,7 @@ components: |------|------| | [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [kubernetes_resources.crd](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/resources) | data source | ## Inputs diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf index 99e8d8265..93ca69108 100644 --- a/modules/eks/external-secrets-operator/main.tf +++ b/modules/eks/external-secrets-operator/main.tf @@ -76,10 +76,18 @@ module "external_secrets_operator" { context = module.this.context } +data "kubernetes_resources" "crd" { + api_version = "apiextensions.k8s.io/v1" + kind = "CustomResourceDefinition" + field_selector = "metadata.name==externalsecrets.external-secrets.io" +} + module "external_ssm_secrets" { source = "cloudposse/helm-release/aws" version = "0.10.0" + enabled = local.enabled && length(data.kubernetes_resources.crd.objects) > 0 + name = "ssm" # distinguish from external_secrets_operator description = "This Chart uses creates a SecretStore and ExternalSecret to pull variables (under a given path) from AWS SSM Parameter Store into a Kubernetes secret." diff --git a/modules/ipam/CHANGELOG.md b/modules/ipam/CHANGELOG.md new file mode 100644 index 000000000..5b080efc8 --- /dev/null +++ b/modules/ipam/CHANGELOG.md @@ -0,0 +1 @@ +## Components PR [Fix components](https://github.com/cloudposse/terraform-aws-components/pull/855) diff --git a/modules/ipam/README.md b/modules/ipam/README.md new file mode 100644 index 000000000..2e9ea5f32 --- /dev/null +++ b/modules/ipam/README.md @@ -0,0 +1,128 @@ +# Component: `ipam` + +This component is responsible for provisioning IPAM per region in a centralized account. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + ipam: + vars: + enabled: true + top_cidr: [10.96.0.0/11] + pool_configurations: + core: + cidr: [10.96.0.0/12] + locale: us-east-2 + sub_pools: + network: + cidr: [10.96.0.0/16] + ram_share_accounts: [core-network] + auto: + cidr: [10.97.0.0/16] + ram_share_accounts: [core-auto] + corp: + cidr: [10.98.0.0/16] + ram_share_accounts: [core-corp] + plat: + cidr: [10.112.0.0/12] + locale: us-east-2 + sub_pools: + dev: + cidr: [10.112.0.0/16] + ram_share_accounts: [plat-dev] + staging: + cidr: [10.113.0.0/16] + ram_share_accounts: [plat-staging] + prod: + cidr: [10.114.0.0/16] + ram_share_accounts: [plat-prod] + sandbox: + cidr: [10.115.0.0/16] + ram_share_accounts: [plat-sandbox] +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [ipam](#module\_ipam) | aws-ia/ipam/aws | 1.2.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_environment\_name](#input\_account\_map\_environment\_name) | The name of the environment where `account_map` is provisioned | `string` | `"gbl"` | no | +| [account\_map\_stage\_name](#input\_account\_map\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | +| [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [address\_family](#input\_address\_family) | IPv4/6 address family. | `string` | `"ipv4"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [ipam\_scope\_id](#input\_ipam\_scope\_id) | (Optional) Required if `var.ipam_id` is set. Determines which scope to deploy pools into. | `string` | `null` | no | +| [ipam\_scope\_type](#input\_ipam\_scope\_type) | Which scope type to use. Valid inputs include `public` or `private`. You can alternatively provide your own scope ID. | `string` | `"private"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [pool\_configurations](#input\_pool\_configurations) | A multi-level, nested map describing nested IPAM pools. Can nest up to three levels with the top level being outside the `pool_configurations`. This attribute is quite complex, see README.md for further explanation. | `any` | `{}` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [top\_auto\_import](#input\_top\_auto\_import) | `auto_import` setting for top-level pool. | `bool` | `null` | no | +| [top\_cidr](#input\_top\_cidr) | Top-level CIDR blocks. | `list(string)` | n/a | yes | +| [top\_cidr\_authorization\_context](#input\_top\_cidr\_authorization\_context) | A signed document that proves that you are authorized to bring the specified IP address range to Amazon using BYOIP. Document is not stored in the state file. For more information, refer to https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_ipam_pool_cidr#cidr_authorization_context. | `any` | `null` | no | +| [top\_description](#input\_top\_description) | Description of top-level pool. | `string` | `""` | no | +| [top\_ram\_share\_principals](#input\_top\_ram\_share\_principals) | Principals to create RAM shares for top-level pool. | `list(string)` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [pool\_configurations](#output\_pool\_configurations) | Pool configurations | + + + +## References +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component + + +[](https://cpco.io/component) diff --git a/modules/ipam/context.tf b/modules/ipam/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/ipam/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/ipam/main.tf b/modules/ipam/main.tf new file mode 100644 index 000000000..109d6c042 --- /dev/null +++ b/modules/ipam/main.tf @@ -0,0 +1,58 @@ +locals { + enabled = module.this.enabled + + pool_configurations = { + for pool, poolval in var.pool_configurations : + pool => merge(poolval, { + locale = lookup(poolval, "locale", join("", data.aws_region.current.*.name)) + sub_pools = { + for subpool, subval in poolval.sub_pools : + subpool => merge(subval, { + ram_share_principals = concat( + lookup(subval, "ram_share_principals", []), + tolist( + setsubtract( + [ + for account in lookup(subval, "ram_share_accounts", []) : + module.account_map.outputs.full_account_map[account] + ], + [join("", data.aws_caller_identity.current.*.account_id)] + ) + ) + ) + allocation_resource_tags = merge( + lookup(subval, "allocation_resource_tags", {}), + module.this.tags + ) + }) + } + }) + } +} + +data "aws_caller_identity" "current" { + count = local.enabled ? 1 : 0 +} + +data "aws_region" "current" { + count = local.enabled ? 1 : 0 +} + +module "ipam" { + source = "aws-ia/ipam/aws" + version = "1.2.1" + + count = local.enabled ? 1 : 0 + + create_ipam = local.enabled + + address_family = var.address_family + ipam_scope_id = var.ipam_scope_id + ipam_scope_type = var.ipam_scope_type + pool_configurations = local.pool_configurations + top_auto_import = var.top_auto_import + top_cidr = var.top_cidr + top_cidr_authorization_context = var.top_cidr_authorization_context + top_description = var.top_description + top_ram_share_principals = var.top_ram_share_principals +} diff --git a/modules/ipam/outputs.tf b/modules/ipam/outputs.tf new file mode 100644 index 000000000..054d636b7 --- /dev/null +++ b/modules/ipam/outputs.tf @@ -0,0 +1,4 @@ +output "pool_configurations" { + value = local.pool_configurations + description = "Pool configurations" +} diff --git a/modules/ipam/providers.tf b/modules/ipam/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/ipam/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/ipam/remote-state.tf b/modules/ipam/remote-state.tf new file mode 100644 index 000000000..69f657564 --- /dev/null +++ b/modules/ipam/remote-state.tf @@ -0,0 +1,11 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "account-map" + environment = var.account_map_environment_name + stage = var.account_map_stage_name + tenant = var.account_map_tenant_name + + context = module.this.context +} diff --git a/modules/ipam/variables.tf b/modules/ipam/variables.tf new file mode 100644 index 000000000..7cf2847cc --- /dev/null +++ b/modules/ipam/variables.tf @@ -0,0 +1,118 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "account_map_environment_name" { + type = string + description = "The name of the environment where `account_map` is provisioned" + default = "gbl" +} + +variable "account_map_stage_name" { + type = string + description = "The name of the stage where `account_map` is provisioned" + default = "root" +} + +variable "account_map_tenant_name" { + type = string + description = <<-EOT + The name of the tenant where `account_map` is provisioned. + + If the `tenant` label is not used, leave this as `null`. + EOT + default = null +} + +# Copied from upstream module's variables.tf + +variable "pool_configurations" { + description = "A multi-level, nested map describing nested IPAM pools. Can nest up to three levels with the top level being outside the `pool_configurations`. This attribute is quite complex, see README.md for further explanation." + type = any + + # Below is an example of the actual expected structure for `pool_configurations`. type = any is currently being used, may adjust in the future + + # type = object({ + # cidr = optional(list(string)) + # ram_share_principals = optional(list(string)) + # locale = optional(string) + # allocation_default_netmask_length = optional(string) + # allocation_max_netmask_length = optional(string) + # allocation_min_netmask_length = optional(string) + # auto_import = optional(string) + # aws_service = optional(string) + # description = optional(string) + # name = optional(string) + # publicly_advertisable = optional(bool) + # allocation_resource_tags = optional(map(string)) + # tags = optional(map(string)) + # cidr_authorization_context = optional(map(string)) + + # sub_pools = (repeat of pool_configuration object above ) + # }) + default = {} + + # Validate no more than 3 layers of sub_pools specified + # TODO: fix validation, fails if less than 2 layers of pools + # validation { + # error_message = "Sub pools (sub_pools) is defined in the 3rd level of a nested pool. Sub pools can only be defined up to 3 levels." + # condition = flatten([for k, v in var.pool_configurations : [for k2, v2 in v.sub_pools : [for k3, v3 in try(v2.sub_pools, []) : "${k}/${k2}/${k3}" if try(v3.sub_pools, []) != []]]]) == [] + # } +} + +variable "top_cidr" { + description = "Top-level CIDR blocks." + type = list(string) +} + +variable "top_ram_share_principals" { + description = "Principals to create RAM shares for top-level pool." + type = list(string) + default = null +} + +variable "top_auto_import" { + description = "`auto_import` setting for top-level pool." + type = bool + default = null +} + +variable "top_description" { + description = "Description of top-level pool." + type = string + default = "" +} + +variable "top_cidr_authorization_context" { + description = "A signed document that proves that you are authorized to bring the specified IP address range to Amazon using BYOIP. Document is not stored in the state file. For more information, refer to https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_ipam_pool_cidr#cidr_authorization_context." + type = any + default = null +} + +variable "address_family" { + description = "IPv4/6 address family." + type = string + default = "ipv4" + validation { + condition = var.address_family == "ipv4" || var.address_family == "ipv6" + error_message = "Only valid options: \"ipv4\", \"ipv6\"." + } +} + +variable "ipam_scope_id" { + description = "(Optional) Required if `var.ipam_id` is set. Determines which scope to deploy pools into." + type = string + default = null +} + +variable "ipam_scope_type" { + description = "Which scope type to use. Valid inputs include `public` or `private`. You can alternatively provide your own scope ID." + type = string + default = "private" + + validation { + condition = var.ipam_scope_type == "public" || var.ipam_scope_type == "private" + error_message = "Scope type must be either public or private." + } +} diff --git a/modules/ipam/versions.tf b/modules/ipam/versions.tf new file mode 100644 index 000000000..f33ede77f --- /dev/null +++ b/modules/ipam/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} diff --git a/modules/redshift/CHANGELOG.md b/modules/redshift/CHANGELOG.md new file mode 100644 index 000000000..a211c5ef9 --- /dev/null +++ b/modules/redshift/CHANGELOG.md @@ -0,0 +1,7 @@ +## Components PR [Fix components](https://github.com/cloudposse/terraform-aws-components/pull/855) + +This is a bug fix and feature enhancement update. +No actions necessary to upgrade. + +## Fixes +* Fix bug related to the AWS provider `>= 5.0.0` removed `redshift_cluster.cluster_security_groups`. diff --git a/modules/redshift/README.md b/modules/redshift/README.md index 755a3c1f9..cce5ca377 100644 --- a/modules/redshift/README.md +++ b/modules/redshift/README.md @@ -45,14 +45,14 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.17 | +| [aws](#requirement\_aws) | >= 4.17, <=4.67.0 | | [random](#requirement\_random) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.17 | +| [aws](#provider\_aws) | >= 4.17, <=4.67.0 | | [random](#provider\_random) | >= 3.0 | ## Modules diff --git a/modules/redshift/versions.tf b/modules/redshift/versions.tf index 0fe97e02d..f04baf043 100644 --- a/modules/redshift/versions.tf +++ b/modules/redshift/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.17" + version = ">= 4.17, <=4.67.0" } random = { source = "hashicorp/random" From cba74057eceb8aeee2db6a8997979f98af133af5 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 6 Sep 2023 15:17:36 +0300 Subject: [PATCH 258/501] [eks/external-secrets-operator] Set default chart (#856) Co-authored-by: cloudpossebot --- modules/eks/external-secrets-operator/CHANGELOG.md | 7 ++----- modules/eks/external-secrets-operator/README.md | 8 ++++---- modules/eks/external-secrets-operator/helm-variables.tf | 9 +++++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/modules/eks/external-secrets-operator/CHANGELOG.md b/modules/eks/external-secrets-operator/CHANGELOG.md index 901b6aae8..5e1c3aa10 100644 --- a/modules/eks/external-secrets-operator/CHANGELOG.md +++ b/modules/eks/external-secrets-operator/CHANGELOG.md @@ -1,10 +1,7 @@ -## Components PR [Fix components](https://github.com/cloudposse/terraform-aws-components/pull/855) +## Components PR [[eks/external-secrets-operator] Set default chart](https://github.com/cloudposse/terraform-aws-components/pull/856) This is a bug fix and feature enhancement update. No actions necessary to upgrade. -## Notes -* Cold start needs to apply twice - ## Fixes -* Fix cold start bug because the CRD does not exist yet +* Set default chart diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index fc77cfef5..731cfe90e 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -131,11 +131,11 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `null` | no | -| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | -| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `null` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"external-secrets"` | no | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"External Secrets Operator is a Kubernetes operator that integrates external secret management systems including AWS SSM, Parameter Store, Hasicorp Vault, 1Password Secrets Automation, etc. It reads values from external vaults and injects values as a Kubernetes Secret"` | no | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://charts.external-secrets.io"` | no | | [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | -| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"0.6.0-rc1"` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `null` | no | diff --git a/modules/eks/external-secrets-operator/helm-variables.tf b/modules/eks/external-secrets-operator/helm-variables.tf index 4756e3f79..a0b007642 100644 --- a/modules/eks/external-secrets-operator/helm-variables.tf +++ b/modules/eks/external-secrets-operator/helm-variables.tf @@ -6,25 +6,26 @@ variable "kubernetes_namespace" { variable "chart_description" { type = string description = "Set release description attribute (visible in the history)." - default = null + default = "External Secrets Operator is a Kubernetes operator that integrates external secret management systems including AWS SSM, Parameter Store, Hasicorp Vault, 1Password Secrets Automation, etc. It reads values from external vaults and injects values as a Kubernetes Secret" } variable "chart_repository" { type = string description = "Repository URL where to locate the requested chart." - default = null + default = "https://charts.external-secrets.io" } variable "chart" { type = string description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." - default = null + default = "external-secrets" } variable "chart_version" { type = string description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." - default = null + default = "0.6.0-rc1" + # using RC to address this bug https://github.com/external-secrets/external-secrets/issues/1511 } variable "chart_values" { From 72144e154d71664a5b27fb387959db9af307f177 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Wed, 13 Sep 2023 18:12:16 +0300 Subject: [PATCH 259/501] Propose troubleshooting section for ArgoCD (#857) --- modules/eks/argocd/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index eacedf4cb..978ab00e7 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -348,6 +348,36 @@ components: github_webhook_enabled: true ``` +## Troubleshooting + +## Login to ArgoCD admin UI + +For ArgoCD v1.9 and later, the initial admin password is available from a Kubernetes secret named `argocd-initial-admin-secret`. +To get the initial password, execute the following command: + +```shell +kubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 --decode +``` + +Then open the ArgoCD admin UI and use the username `admin` and the password obtained in the previous step to log in to the ArgoCD admin. + +## Error "server.secretkey is missing" + +If you provision a new version of the `eks/argocd` component, and some Helm Chart values get updated, you might encounter the error +"server.secretkey is missing" in the ArgoCD admin UI. To fix the error, execute the following commands: + +```shell +# Download `kubeconfig` and set EKS cluster +set-eks-cluster cluster-name + +# Restart the `argocd-server` Pods +kubectl rollout restart deploy/argocd-server -n argocd + +# Get the new admin password from the Kubernetes secret +kubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 --decode +``` +Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-secretkey-is-missing + ## Requirements From e6585da9cb9537335508413ad866b805c3349282 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Mon, 18 Sep 2023 20:04:02 +0300 Subject: [PATCH 260/501] Bring back ignore-differences option of argo cd (#859) Co-authored-by: cloudpossebot --- modules/argocd-repo/README.md | 2 +- modules/argocd-repo/applicationset.tf | 13 ++++++------ .../templates/applicationset.yaml.tpl | 12 +++++++++++ modules/argocd-repo/variables.tf | 21 +++++++++++++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index 51448b505..cc0449fbc 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -133,7 +133,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [environments](#input\_environments) | Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for.

`auto-sync` determines whether or not the ArgoCD application will be automatically synced. |
list(object({
tenant = string
environment = string
stage = string
auto-sync = bool
}))
| `[]` | no | +| [environments](#input\_environments) | Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for.

`auto-sync` determines whether or not the ArgoCD application will be automatically synced.

`ignore-differences` determines whether or not the ArgoCD application will ignore the number of
replicas in the deployment. Read more on ignore differences here:
https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs
Example:
tenant: plat
environment: use1
stage: sandbox
auto-sync: true
ignore-differences:
- group: apps
kind: Deployment
json-pointers:
- /spec/replicas
|
list(object({
tenant = string
environment = string
stage = string
auto-sync = bool
ignore-differences = list(object({
group = string,
kind = string,
json-pointers = list(string)
}))
}))
| `[]` | no | | [github\_base\_url](#input\_github\_base\_url) | This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/` | `string` | `null` | no | | [github\_codeowner\_teams](#input\_github\_codeowner\_teams) | List of teams to use when populating the CODEOWNERS file.

For example: `["@ACME/cloud-admins", "@ACME/cloud-developers"]`. | `list(string)` | n/a | yes | | [github\_default\_notifications\_enabled](#input\_github\_default\_notifications\_enabled) | Enable default GitHub commit statuses notifications (required for CD sync mode) | `bool` | `true` | no | diff --git a/modules/argocd-repo/applicationset.tf b/modules/argocd-repo/applicationset.tf index 5710e3ebe..2d49a8084 100644 --- a/modules/argocd-repo/applicationset.tf +++ b/modules/argocd-repo/applicationset.tf @@ -5,12 +5,13 @@ resource "github_repository_file" "application_set" { branch = join("", github_repository.default.*.default_branch) file = "${each.value.tenant != null ? format("%s/", each.value.tenant) : ""}${each.value.environment}-${each.value.stage}/${local.manifest_kubernetes_namespace}/applicationset.yaml" content = templatefile("${path.module}/templates/applicationset.yaml.tpl", { - environment = each.key - auto-sync = each.value.auto-sync - name = module.this.namespace - namespace = local.manifest_kubernetes_namespace - ssh_url = join("", github_repository.default.*.ssh_clone_url) - notifications = var.github_default_notifications_enabled + environment = each.key + auto-sync = each.value.auto-sync + ignore-differences = each.value.ignore-differences + name = module.this.namespace + namespace = local.manifest_kubernetes_namespace + ssh_url = join("", github_repository.default.*.ssh_clone_url) + notifications = var.github_default_notifications_enabled }) commit_message = "Initialize environment: `${each.key}`." commit_author = var.github_user diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index 68de50c03..b3b389386 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -74,3 +74,15 @@ spec: %{ endif ~} syncOptions: - CreateNamespace=true +%{if length(ignore-differences) > 0 ~} + - RespectIgnoreDifferences=true + ignoreDifferences: +%{for item in ignore-differences ~} + - group: "${item.group}" + kind: "${item.kind}" + jsonPointers: +%{for pointer in item.json-pointers ~} + - ${pointer} +%{ endfor ~} +%{ endfor ~} +%{ endif ~} diff --git a/modules/argocd-repo/variables.tf b/modules/argocd-repo/variables.tf index 2309b7d38..67a6a6640 100644 --- a/modules/argocd-repo/variables.tf +++ b/modules/argocd-repo/variables.tf @@ -15,11 +15,32 @@ variable "environments" { environment = string stage = string auto-sync = bool + ignore-differences = list(object({ + group = string, + kind = string, + json-pointers = list(string) + })) })) description = <<-EOT Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for. `auto-sync` determines whether or not the ArgoCD application will be automatically synced. + + `ignore-differences` determines whether or not the ArgoCD application will ignore the number of + replicas in the deployment. Read more on ignore differences here: + https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs + Example: + ``` + tenant: plat + environment: use1 + stage: sandbox + auto-sync: true + ignore-differences: + - group: apps + kind: Deployment + json-pointers: + - /spec/replicas + ``` EOT default = [] } From ed7de701d0af202cef83231c8fbad95d28b0e24a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 18 Sep 2023 19:24:12 -0700 Subject: [PATCH 261/501] Bug fix: Multiple `eks/arc` Runners with Docker Login (#861) --- .../charts/actions-runner/templates/runnerdeployment.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index 6ecffda81..a44658dec 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -23,7 +23,7 @@ spec: apiVersion: v1 kind: Secret metadata: - name: regcred + name: {{ .Values.release_name }}-regcred type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: {{ .Values.docker_config_json }} @@ -117,7 +117,7 @@ spec: imagePullPolicy: IfNotPresent {{- if .Values.docker_config_json_enabled }} imagePullSecrets: - - name: regcred + - name: {{ .Values.release_name }}-regcred {{- end }} serviceAccountName: {{ .Values.service_account_name }} resources: @@ -171,7 +171,7 @@ spec: {{- if .Values.docker_config_json_enabled }} - name: docker-secret secret: - secretName: regcred + secretName: {{ .Values.release_name }}-regcred items: - key: .dockerconfigjson path: config.json From 115b0ecb0687a2d76b2a858e2f57541933a9d7c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=ABl=20Jackson?= Date: Thu, 21 Sep 2023 17:03:03 +0100 Subject: [PATCH 262/501] Update README.md (#862) Co-authored-by: cloudpossebot --- modules/dns-primary/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index a041aef77..ad0a6475f 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -47,12 +47,28 @@ components: ttl: 60 records: - 53.229.170.215 + # using a period at the end of a name - root_zone: example.net - name: www + name: www. type: CNAME ttl: 60 records: - example.net + # using numbers as name requires quotes + - root_zone: example.net + name: "123456." + type: CNAME + ttl: 60 + records: + - example.net + # strings that are very long, this could be a DKIM key + - root_zone: example.net + name: service._domainkey. + type: CNAME + ttl: 60 + records: + - !!str |- + YourVeryLongStringGoesHere ``` :::info From eac7f89c4eba5887163b6b7ecb3454faff69534a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 22 Sep 2023 10:22:03 -0700 Subject: [PATCH 263/501] ArgoCD Attribute Handling and Repo Integration for `argocd-repo` (#860) Co-authored-by: Andriy Knysh Co-authored-by: cloudpossebot --- modules/argocd-repo/README.md | 9 ++++-- modules/argocd-repo/applicationset.tf | 8 ++--- modules/argocd-repo/git-files.tf | 20 ++++++------- modules/argocd-repo/main.tf | 29 ++++++++++++++----- modules/argocd-repo/outputs.tf | 10 +++---- modules/argocd-repo/provider-github.tf | 27 +++++++++++++++++ modules/argocd-repo/ssm.tf | 26 ----------------- modules/argocd-repo/variables.tf | 16 +++++++--- modules/argocd-repo/versions.tf | 4 +++ modules/eks/argocd/data.tf | 5 ++-- modules/eks/argocd/main.tf | 1 + .../resources/argocd-apps-values.yaml.tpl | 2 +- 12 files changed, 95 insertions(+), 62 deletions(-) delete mode 100644 modules/argocd-repo/ssm.tf diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index cc0449fbc..1cb3fc7bc 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -84,6 +84,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | | [github](#requirement\_github) | >= 4.0 | +| [random](#requirement\_random) | >= 2.3 | | [tls](#requirement\_tls) | >= 3.0 | ## Providers @@ -118,6 +119,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [github_team_repository.default](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/team_repository) | resource | | [tls_private_key.default](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key) | resource | | [aws_ssm_parameter.github_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [github_repository.default](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source | | [github_team.default](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/team) | data source | | [github_user.automation_user](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/user) | data source | @@ -128,15 +130,16 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_repo](#input\_create\_repo) | Whether or not to create the repository or use an existing one | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | The description of the repository | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [environments](#input\_environments) | Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for.

`auto-sync` determines whether or not the ArgoCD application will be automatically synced.

`ignore-differences` determines whether or not the ArgoCD application will ignore the number of
replicas in the deployment. Read more on ignore differences here:
https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs
Example:
tenant: plat
environment: use1
stage: sandbox
auto-sync: true
ignore-differences:
- group: apps
kind: Deployment
json-pointers:
- /spec/replicas
|
list(object({
tenant = string
environment = string
stage = string
auto-sync = bool
ignore-differences = list(object({
group = string,
kind = string,
json-pointers = list(string)
}))
}))
| `[]` | no | +| [environments](#input\_environments) | Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for.

`auto-sync` determines whether or not the ArgoCD application will be automatically synced.

`ignore-differences` determines whether or not the ArgoCD application will ignore the number of
replicas in the deployment. Read more on ignore differences here:
https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs

Example:
tenant: plat
environment: use1
stage: sandbox
auto-sync: true
ignore-differences:
- group: apps
kind: Deployment
json-pointers:
- /spec/replicas
|
list(object({
tenant = optional(string, null)
environment = string
stage = string
attributes = optional(list(string), [])
auto-sync = bool
ignore-differences = optional(list(object({
group = string,
kind = string,
json-pointers = list(string)
})), [])
}))
| `[]` | no | | [github\_base\_url](#input\_github\_base\_url) | This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/` | `string` | `null` | no | | [github\_codeowner\_teams](#input\_github\_codeowner\_teams) | List of teams to use when populating the CODEOWNERS file.

For example: `["@ACME/cloud-admins", "@ACME/cloud-developers"]`. | `list(string)` | n/a | yes | -| [github\_default\_notifications\_enabled](#input\_github\_default\_notifications\_enabled) | Enable default GitHub commit statuses notifications (required for CD sync mode) | `bool` | `true` | no | +| [github\_default\_notifications\_enabled](#input\_github\_default\_notifications\_enabled) | Enable default GitHub commit statuses notifications (required for CD sync mode) | `string` | `true` | no | | [github\_organization](#input\_github\_organization) | GitHub Organization | `string` | n/a | yes | | [github\_token\_override](#input\_github\_token\_override) | Use the value of this variable as the GitHub token instead of reading it from SSM | `string` | `null` | no | | [github\_user](#input\_github\_user) | Github user | `string` | n/a | yes | @@ -173,7 +176,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/argocd-repo) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/argocd-repo/applicationset.tf b/modules/argocd-repo/applicationset.tf index 2d49a8084..bdc779c9f 100644 --- a/modules/argocd-repo/applicationset.tf +++ b/modules/argocd-repo/applicationset.tf @@ -1,16 +1,16 @@ resource "github_repository_file" "application_set" { for_each = local.environments - repository = join("", github_repository.default.*.name) - branch = join("", github_repository.default.*.default_branch) - file = "${each.value.tenant != null ? format("%s/", each.value.tenant) : ""}${each.value.environment}-${each.value.stage}/${local.manifest_kubernetes_namespace}/applicationset.yaml" + repository = local.github_repository.name + branch = local.github_repository.default_branch + file = "${each.value.tenant != null ? format("%s/", each.value.tenant) : ""}${each.value.environment}-${each.value.stage}${length(each.value.attributes) > 0 ? format("-%s", join("-", each.value.attributes)) : ""}/${local.manifest_kubernetes_namespace}/applicationset.yaml" content = templatefile("${path.module}/templates/applicationset.yaml.tpl", { environment = each.key auto-sync = each.value.auto-sync ignore-differences = each.value.ignore-differences name = module.this.namespace namespace = local.manifest_kubernetes_namespace - ssh_url = join("", github_repository.default.*.ssh_clone_url) + ssh_url = local.github_repository.ssh_clone_url notifications = var.github_default_notifications_enabled }) commit_message = "Initialize environment: `${each.key}`." diff --git a/modules/argocd-repo/git-files.tf b/modules/argocd-repo/git-files.tf index 977ced701..ddb4dc95d 100644 --- a/modules/argocd-repo/git-files.tf +++ b/modules/argocd-repo/git-files.tf @@ -1,8 +1,8 @@ resource "github_repository_file" "gitignore" { count = local.enabled ? 1 : 0 - repository = join("", github_repository.default.*.name) - branch = join("", github_repository.default.*.default_branch) + repository = local.github_repository.name + branch = local.github_repository.default_branch file = ".gitignore" content = templatefile("${path.module}/templates/.gitignore.tpl", { entries = var.gitignore_entries @@ -16,12 +16,12 @@ resource "github_repository_file" "gitignore" { resource "github_repository_file" "readme" { count = local.enabled ? 1 : 0 - repository = join("", github_repository.default.*.name) - branch = join("", github_repository.default.*.default_branch) + repository = local.github_repository.name + branch = local.github_repository.default_branch file = "README.md" content = templatefile("${path.module}/templates/README.md.tpl", { - repository_name = join("", github_repository.default.*.name) - repository_description = join("", github_repository.default.*.description) + repository_name = local.github_repository.name + repository_description = local.github_repository.description github_organization = var.github_organization }) commit_message = "Create README.md file." @@ -33,8 +33,8 @@ resource "github_repository_file" "readme" { resource "github_repository_file" "codeowners_file" { count = local.enabled ? 1 : 0 - repository = join("", github_repository.default.*.name) - branch = join("", github_repository.default.*.default_branch) + repository = local.github_repository.name + branch = local.github_repository.default_branch file = ".github/CODEOWNERS" content = templatefile("${path.module}/templates/CODEOWNERS.tpl", { codeowners = var.github_codeowner_teams @@ -48,8 +48,8 @@ resource "github_repository_file" "codeowners_file" { resource "github_repository_file" "pull_request_template" { count = local.enabled ? 1 : 0 - repository = join("", github_repository.default.*.name) - branch = join("", github_repository.default.*.default_branch) + repository = local.github_repository.name + branch = local.github_repository.default_branch file = ".github/PULL_REQUEST_TEMPLATE.md" content = file("${path.module}/templates/PULL_REQUEST_TEMPLATE.md") commit_message = "Create PULL_REQUEST_TEMPLATE.md file." diff --git a/modules/argocd-repo/main.tf b/modules/argocd-repo/main.tf index ff61154a6..c21a62ec7 100644 --- a/modules/argocd-repo/main.tf +++ b/modules/argocd-repo/main.tf @@ -1,14 +1,17 @@ locals { enabled = module.this.enabled + environments = local.enabled ? { for env in var.environments : (format( - "${env.tenant != null ? "%[1]s/" : ""}%[2]s-%[3]s", + "${env.tenant != null ? "%[1]s/" : ""}%[2]s-%[3]s${length(env.attributes) > 0 ? "-%[4]s" : "%[4]s"}", env.tenant, env.environment, env.stage, + "${join("-", env.attributes)}" )) => env } : {} + manifest_kubernetes_namespace = "argocd" team_slugs = toset(compact([ @@ -25,10 +28,22 @@ locals { permission = var.permissions[index].permission } } + + empty_repo = { + name = "" + default_branch = "" + } + + github_repository = try((var.create_repo ? github_repository.default : data.github_repository.default)[0], local.empty_repo) +} + +data "github_repository" "default" { + count = local.enabled && !var.create_repo ? 1 : 0 + name = var.name } resource "github_repository" "default" { - count = local.enabled ? 1 : 0 + count = local.enabled && var.create_repo ? 1 : 0 name = module.this.name description = var.description @@ -40,8 +55,8 @@ resource "github_repository" "default" { resource "github_branch_default" "default" { count = local.enabled ? 1 : 0 - repository = join("", github_repository.default.*.name) - branch = join("", github_repository.default.*.default_branch) + repository = local.github_repository.name + branch = local.github_repository.default_branch } data "github_user" "automation_user" { @@ -55,7 +70,7 @@ resource "github_branch_protection" "default" { # the main branch. Those commits made by the automation user, which is an admin. count = local.enabled ? 1 : 0 - repository_id = join("", github_repository.default.*.name) + repository_id = local.github_repository.name pattern = join("", github_branch_default.default.*.branch) enforce_admins = false # needs to be false in order to allow automation user to push @@ -81,7 +96,7 @@ data "github_team" "default" { resource "github_team_repository" "default" { for_each = local.team_permissions - repository = join("", github_repository.default[*].name) + repository = local.github_repository.name team_id = each.value.id permission = each.value.permission } @@ -96,7 +111,7 @@ resource "tls_private_key" "default" { resource "github_repository_deploy_key" "default" { for_each = local.environments - title = "Deploy key for ArgoCD environment: ${each.key} (${join("", github_repository.default.*.default_branch)} branch)" + title = "Deploy key for ArgoCD environment: ${each.key} (${local.github_repository.default_branch} branch)" repository = join("", github_repository.default.*.name) key = tls_private_key.default[each.key].public_key_openssh read_only = true diff --git a/modules/argocd-repo/outputs.tf b/modules/argocd-repo/outputs.tf index b2bb304d1..49f29ca59 100644 --- a/modules/argocd-repo/outputs.tf +++ b/modules/argocd-repo/outputs.tf @@ -10,25 +10,25 @@ output "deploy_keys_ssm_path_format" { output "repository_description" { description = "Repository description" - value = join("", github_repository.default.*.description) + value = local.github_repository.description } output "repository_default_branch" { description = "Repository default branch" - value = join("", github_repository.default.*.default_branch) + value = local.github_repository.default_branch } output "repository_url" { description = "Repository URL" - value = join("", github_repository.default.*.html_url) + value = local.github_repository.html_url } output "repository_git_clone_url" { description = "Repository git clone URL" - value = join("", github_repository.default.*.git_clone_url) + value = local.github_repository.git_clone_url } output "repository_ssh_clone_url" { description = "Repository SSH clone URL" - value = join("", github_repository.default.*.ssh_clone_url) + value = local.github_repository.ssh_clone_url } diff --git a/modules/argocd-repo/provider-github.tf b/modules/argocd-repo/provider-github.tf index 81949874e..60ed4e0b4 100644 --- a/modules/argocd-repo/provider-github.tf +++ b/modules/argocd-repo/provider-github.tf @@ -1,3 +1,30 @@ +locals { + github_token = local.enabled ? coalesce(var.github_token_override, data.aws_ssm_parameter.github_api_key[0].value) : "" +} + +data "aws_ssm_parameter" "github_api_key" { + count = local.enabled ? 1 : 0 + name = var.ssm_github_api_key + with_decryption = true +} + +module "store_write" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.11.0" + + parameter_write = [for k, v in local.environments : + { + name = format(var.ssm_github_deploy_key_format, k) + value = tls_private_key.default[k].private_key_pem + type = "SecureString" + overwrite = true + description = github_repository_deploy_key.default[k].title + } + ] + + context = module.this.context +} + provider "github" { base_url = var.github_base_url owner = var.github_organization diff --git a/modules/argocd-repo/ssm.tf b/modules/argocd-repo/ssm.tf deleted file mode 100644 index 2bfa912af..000000000 --- a/modules/argocd-repo/ssm.tf +++ /dev/null @@ -1,26 +0,0 @@ -locals { - github_token = local.enabled ? coalesce(var.github_token_override, data.aws_ssm_parameter.github_api_key[0].value) : "" -} - -data "aws_ssm_parameter" "github_api_key" { - count = local.enabled ? 1 : 0 - name = var.ssm_github_api_key - with_decryption = true -} - -module "store_write" { - source = "cloudposse/ssm-parameter-store/aws" - version = "0.11.0" - - parameter_write = [for k, v in local.environments : - { - name = format(var.ssm_github_deploy_key_format, k) - value = tls_private_key.default[k].private_key_pem - type = "SecureString" - overwrite = true - description = github_repository_deploy_key.default[k].title - } - ] - - context = module.this.context -} diff --git a/modules/argocd-repo/variables.tf b/modules/argocd-repo/variables.tf index 67a6a6640..1a8c0bd5b 100644 --- a/modules/argocd-repo/variables.tf +++ b/modules/argocd-repo/variables.tf @@ -11,15 +11,16 @@ variable "description" { variable "environments" { type = list(object({ - tenant = string + tenant = optional(string, null) environment = string stage = string + attributes = optional(list(string), []) auto-sync = bool - ignore-differences = list(object({ + ignore-differences = optional(list(object({ group = string, kind = string, json-pointers = list(string) - })) + })), []) })) description = <<-EOT Environments to populate `applicationset.yaml` files and repository deploy keys (for ArgoCD) for. @@ -29,6 +30,7 @@ variable "environments" { `ignore-differences` determines whether or not the ArgoCD application will ignore the number of replicas in the deployment. Read more on ignore differences here: https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs + Example: ``` tenant: plat @@ -126,7 +128,13 @@ variable "permissions" { } variable "github_default_notifications_enabled" { + type = string + description = "Enable default GitHub commit statuses notifications (required for CD sync mode)" + default = true +} + +variable "create_repo" { type = bool + description = "Whether or not to create the repository or use an existing one" default = true - description = "Enable default GitHub commit statuses notifications (required for CD sync mode)" } diff --git a/modules/argocd-repo/versions.tf b/modules/argocd-repo/versions.tf index a1ac788f4..2c76e7b55 100644 --- a/modules/argocd-repo/versions.tf +++ b/modules/argocd-repo/versions.tf @@ -14,5 +14,9 @@ terraform { source = "hashicorp/tls" version = ">= 3.0" } + random = { + source = "hashicorp/random" + version = ">= 2.3" + } } } diff --git a/modules/eks/argocd/data.tf b/modules/eks/argocd/data.tf index b8a6e6cd1..b745c8f33 100644 --- a/modules/eks/argocd/data.tf +++ b/modules/eks/argocd/data.tf @@ -31,10 +31,11 @@ data "aws_ssm_parameter" "github_deploy_key" { name = local.enabled ? format( module.argocd_repo[each.key].outputs.deploy_keys_ssm_path_format, format( - "${module.this.tenant != null ? "%[1]s/" : ""}%[2]s-%[3]s", + "${module.this.tenant != null ? "%[1]s/" : ""}%[2]s-%[3]s${length(module.this.attributes) > 0 ? "-%[4]s" : "%[4]s"}", module.this.tenant, module.this.environment, - module.this.stage + module.this.stage, + "${join("-", module.this.attributes)}" ) ) : null diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 746eee577..4937c48c1 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -213,6 +213,7 @@ module "argocd_apps" { tenant = module.this.tenant environment = var.environment stage = var.stage + attributes = var.attributes } ), ]) diff --git a/modules/eks/argocd/resources/argocd-apps-values.yaml.tpl b/modules/eks/argocd/resources/argocd-apps-values.yaml.tpl index e1cb7f900..afef8d8bc 100644 --- a/modules/eks/argocd/resources/argocd-apps-values.yaml.tpl +++ b/modules/eks/argocd/resources/argocd-apps-values.yaml.tpl @@ -8,7 +8,7 @@ applications: source: repoURL: ${url} targetRevision: HEAD - path: ./%{ if tenant != null }${tenant}/%{ endif }${environment}-${stage}/${namespace} + path: ./%{ if tenant != null }${tenant}/%{ endif }${environment}-${stage}%{ for attr in attributes }-${attr}%{ endfor }/${namespace} directory: recurse: false destination: From 5c63af5213e5bb9273525be99ea757836a11e161 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Mon, 25 Sep 2023 19:33:14 +0300 Subject: [PATCH 264/501] Update keda component (#863) Co-authored-by: cloudpossebot --- modules/eks/keda/README.md | 30 ++++++++++++----------- modules/eks/keda/main.tf | 12 ++++++++-- modules/eks/keda/outputs.tf | 48 +++++++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/modules/eks/keda/README.md b/modules/eks/keda/README.md index cd686925a..89b213ed5 100644 --- a/modules/eks/keda/README.md +++ b/modules/eks/keda/README.md @@ -16,20 +16,14 @@ components: eks/keda: vars: enabled: true - name: "keda" - kubernetes_namespace: "keda" + name: keda create_namespace: true - timeout: 90 - wait: true - atomic: true - cleanup_on_fail: true - resources: - requests: - cpu: 200m - memory: 256Mi - limits: - cpu: 1000m - memory: 1024Mi + kubernetes_namespace: "keda" + chart_repository: "https://kedacore.github.io/charts" + chart: "keda" + chart_version: "2.11.2" + chart_values: {} + timeout: 180 ``` @@ -116,7 +110,15 @@ components: | Name | Description | |------|-------------| -| [metadata](#output\_metadata) | Block status of the deployed release | +| [metadata](#output\_metadata) | Block status of the deployed release. | +| [service\_account\_name](#output\_service\_account\_name) | Kubernetes Service Account name | +| [service\_account\_namespace](#output\_service\_account\_namespace) | Kubernetes Service Account namespace | +| [service\_account\_policy\_arn](#output\_service\_account\_policy\_arn) | IAM policy ARN | +| [service\_account\_policy\_id](#output\_service\_account\_policy\_id) | IAM policy ID | +| [service\_account\_policy\_name](#output\_service\_account\_policy\_name) | IAM policy name | +| [service\_account\_role\_arn](#output\_service\_account\_role\_arn) | IAM role ARN | +| [service\_account\_role\_name](#output\_service\_account\_role\_name) | IAM role name | +| [service\_account\_role\_unique\_id](#output\_service\_account\_role\_unique\_id) | IAM role unique ID | ## References diff --git a/modules/eks/keda/main.tf b/modules/eks/keda/main.tf index 327b61d6e..857148b8c 100644 --- a/modules/eks/keda/main.tf +++ b/modules/eks/keda/main.tf @@ -21,8 +21,16 @@ module "keda" { service_account_name = module.this.name service_account_namespace = var.kubernetes_namespace - iam_role_enabled = false - iam_policy_statements = {} + iam_role_enabled = true + + iam_policy_statements = [ + { + sid = "KedaOperatorSQS" + effect = "Allow" + actions = ["SQS:GetQueueAttributes"] + resources = ["*"] + } + ] values = compact([ yamlencode({ diff --git a/modules/eks/keda/outputs.tf b/modules/eks/keda/outputs.tf index 8a5b6e428..cab379b79 100644 --- a/modules/eks/keda/outputs.tf +++ b/modules/eks/keda/outputs.tf @@ -1,4 +1,48 @@ +## eks_iam_role + +output "service_account_namespace" { + value = module.keda.service_account_namespace + description = "Kubernetes Service Account namespace" +} + +output "service_account_name" { + value = module.keda.service_account_name + description = "Kubernetes Service Account name" +} + +output "service_account_role_name" { + value = module.keda.service_account_role_name + description = "IAM role name" +} + +output "service_account_role_unique_id" { + value = module.keda.service_account_role_unique_id + description = "IAM role unique ID" +} + +output "service_account_role_arn" { + value = module.keda.service_account_role_arn + description = "IAM role ARN" +} + +output "service_account_policy_name" { + value = module.keda.service_account_policy_name + description = "IAM policy name" +} + +output "service_account_policy_id" { + value = module.keda.service_account_policy_id + description = "IAM policy ID" +} + +output "service_account_policy_arn" { + value = module.keda.service_account_policy_arn + description = "IAM policy ARN" +} + +## keda + output "metadata" { - value = try(one(module.keda.metadata), null) - description = "Block status of the deployed release" + description = "Block status of the deployed release." + value = module.keda.metadata } From 51b59522d73bf04b428e67ea3fa267408b1291d0 Mon Sep 17 00:00:00 2001 From: Matthias Fuhrmeister Date: Thu, 5 Oct 2023 16:12:05 +0200 Subject: [PATCH 265/501] Passthrough default block response (#865) --- modules/waf/README.md | 3 ++- modules/waf/main.tf | 3 ++- modules/waf/variables.tf | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/waf/README.md b/modules/waf/README.md index 6f8e70d8f..2fc72784c 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -58,7 +58,7 @@ components: | Name | Source | Version | |------|--------|---------| | [association\_resource\_components](#module\_association\_resource\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 1.2.0 | +| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 1.3.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [log\_destination\_components](#module\_log\_destination\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -82,6 +82,7 @@ components: | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [custom\_response\_body](#input\_custom\_response\_body) | Defines custom response bodies that can be referenced by custom\_response actions.
The map keys are used as the `key` attribute which is a unique key identifying the custom response body.
content:
Payload of the custom response.
The response body can be plain text, HTML or JSON and cannot exceed 4KB in size.
content\_type:
Content Type of Response Body.
Valid values are `TEXT_PLAIN`, `TEXT_HTML`, or `APPLICATION_JSON`. |
map(object({
content = string
content_type = string
}))
| `{}` | no | | [default\_action](#input\_default\_action) | Specifies that AWS WAF should allow requests by default. Possible values: `allow`, `block`. | `string` | `"block"` | no | +| [default\_block\_response](#input\_default\_block\_response) | A HTTP response code that is sent when default action is used. Only takes effect if default\_action is set to `block`. | `string` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | A friendly description of the WebACL. | `string` | `"Managed by Terraform"` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/waf/main.tf b/modules/waf/main.tf index e2f497a33..d1e5340b8 100644 --- a/modules/waf/main.tf +++ b/modules/waf/main.tf @@ -18,7 +18,7 @@ locals { module "aws_waf" { source = "cloudposse/waf/aws" - version = "1.2.0" + version = "1.3.0" description = var.description default_action = var.default_action @@ -48,6 +48,7 @@ module "aws_waf" { size_constraint_statement_rules = var.size_constraint_statement_rules sqli_match_statement_rules = var.sqli_match_statement_rules xss_match_statement_rules = var.xss_match_statement_rules + default_block_response = var.default_block_response context = module.this.context } diff --git a/modules/waf/variables.tf b/modules/waf/variables.tf index 2b24c098c..fc165a555 100644 --- a/modules/waf/variables.tf +++ b/modules/waf/variables.tf @@ -31,6 +31,15 @@ variable "default_action" { } } +variable "default_block_response" { + type = string + default = null + description = <<-DOC + A HTTP response code that is sent when default action is used. Only takes effect if default_action is set to `block`. + DOC + nullable = true +} + variable "custom_response_body" { type = map(object({ content = string From 79694feff20daa514086f037c579e442aab78448 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 9 Oct 2023 10:59:28 -0700 Subject: [PATCH 266/501] `eks/alb-controller-ingress-group`: Set default SSL policy to AWS recommendation (#866) --- modules/eks/alb-controller-ingress-group/README.md | 2 +- modules/eks/alb-controller-ingress-group/variables.tf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index d1df6874d..03e98184f 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -85,7 +85,7 @@ components: | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `false` | no | -| [default\_annotations](#input\_default\_annotations) | Default annotations to add to the Kubernetes ingress | `map(any)` |
{
"alb.ingress.kubernetes.io/listen-ports": "[{\"HTTP\": 80}, {\"HTTPS\": 443}]",
"alb.ingress.kubernetes.io/scheme": "internet-facing",
"alb.ingress.kubernetes.io/target-type": "ip",
"kubernetes.io/ingress.class": "alb"
}
| no | +| [default\_annotations](#input\_default\_annotations) | Default annotations to add to the Kubernetes ingress | `map(any)` |
{
"alb.ingress.kubernetes.io/listen-ports": "[{\"HTTP\": 80}, {\"HTTPS\": 443}]",
"alb.ingress.kubernetes.io/scheme": "internet-facing",
"alb.ingress.kubernetes.io/ssl-policy": "ELBSecurityPolicy-TLS13-1-2-2021-06",
"alb.ingress.kubernetes.io/target-type": "ip",
"kubernetes.io/ingress.class": "alb"
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | The name of the `dns_delegated` component | `string` | `"dns-delegated"` | no | diff --git a/modules/eks/alb-controller-ingress-group/variables.tf b/modules/eks/alb-controller-ingress-group/variables.tf index fe4b64bf5..70a48527d 100644 --- a/modules/eks/alb-controller-ingress-group/variables.tf +++ b/modules/eks/alb-controller-ingress-group/variables.tf @@ -29,6 +29,7 @@ variable "default_annotations" { "alb.ingress.kubernetes.io/target-type" = "ip" "kubernetes.io/ingress.class" = "alb" "alb.ingress.kubernetes.io/listen-ports" = "[{\"HTTP\": 80}, {\"HTTPS\": 443}]" + "alb.ingress.kubernetes.io/ssl-policy" = "ELBSecurityPolicy-TLS13-1-2-2021-06" } } From 0860f5ae95fb7f6f89ab19ab3662d85d11907a13 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:25:40 -0500 Subject: [PATCH 267/501] `alb`: Set default SSL policy to AWS Recommendation (#867) --- modules/alb/README.md | 2 +- modules/alb/variables.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/alb/README.md b/modules/alb/README.md index a6f2651ad..daf9950de 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -84,7 +84,7 @@ No resources. | [https\_ingress\_cidr\_blocks](#input\_https\_ingress\_cidr\_blocks) | List of CIDR blocks to allow in HTTPS security group | `list(string)` |
[
"0.0.0.0/0"
]
| no | | [https\_ingress\_prefix\_list\_ids](#input\_https\_ingress\_prefix\_list\_ids) | List of prefix list IDs for allowing access to HTTPS ingress security group | `list(string)` | `[]` | no | | [https\_port](#input\_https\_port) | The port for the HTTPS listener | `number` | `443` | no | -| [https\_ssl\_policy](#input\_https\_ssl\_policy) | The name of the SSL Policy for the listener | `string` | `"ELBSecurityPolicy-TLS-1-1-2017-01"` | no | +| [https\_ssl\_policy](#input\_https\_ssl\_policy) | The name of the SSL Policy for the listener | `string` | `"ELBSecurityPolicy-TLS13-1-2-2021-06"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [idle\_timeout](#input\_idle\_timeout) | The time in seconds that the connection is allowed to be idle | `number` | `60` | no | | [internal](#input\_internal) | A boolean flag to determine whether the ALB should be internal | `bool` | `false` | no | diff --git a/modules/alb/variables.tf b/modules/alb/variables.tf index 3b196ab92..23f244c25 100644 --- a/modules/alb/variables.tf +++ b/modules/alb/variables.tf @@ -66,7 +66,7 @@ variable "https_ingress_prefix_list_ids" { variable "https_ssl_policy" { type = string description = "The name of the SSL Policy for the listener" - default = "ELBSecurityPolicy-TLS-1-1-2017-01" + default = "ELBSecurityPolicy-TLS13-1-2-2021-06" } variable "access_logs_prefix" { From 975397edee70a797247ec4d8149d8ff32447f620 Mon Sep 17 00:00:00 2001 From: "Erik Osterman (CEO @ Cloud Posse)" Date: Thu, 12 Oct 2023 12:33:38 -0500 Subject: [PATCH 268/501] Update README (#869) --- README.md | 44 ++++++++------------------------------------ README.yaml | 34 ++++++++++++++-------------------- docs/terraform.md | 25 ------------------------- 3 files changed, 22 insertions(+), 81 deletions(-) delete mode 100644 docs/terraform.md diff --git a/README.md b/README.md index a22a5271f..528a0aa53 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ --> -This is a collection of reusable Terraform components and blueprints for provisioning reference architectures. +This is a collection of reusable Terraform components for provisioning infrastructure used by the Cloud Posse [reference architectures](https://cloudposse.com). --- @@ -60,9 +60,9 @@ It's 100% Open Source and licensed under the [APACHE2](LICENSE). In this repo you'll find real-world examples of how we've implemented various common patterns using our [terraform modules](https://cpco.io/terraform-modules) for our customers. -The component catalog captures the business logic, opinions, best practices and non-functional requirements. +The [component library](https://docs.cloudposse.com/components/) captures the business logic, opinions, best practices and non-functional requirements. -It's from this catalog that other developers in your organization will pick and choose from anytime they need to deploy some new capability. +It's from this library that other developers in your organization will pick and choose from anytime they need to deploy some new capability. These components make a lot of assumptions about how we've configured our environments. That said, they can still serve as an excellent reference for others. @@ -99,7 +99,7 @@ make rebuild-docs -Please take a look at each [component's README](./modules) for usage. +Please take a look at each [component's README](https://docs.cloudposse.com/components/) for usage. @@ -120,31 +120,6 @@ Available targets: ``` - -## Requirements - -No requirements. - -## Providers - -No providers. - -## Modules - -No modules. - -## Resources - -No resources. - -## Inputs - -No inputs. - -## Outputs - -No outputs. - @@ -160,11 +135,8 @@ Are you using this project or any of our other projects? Consider [leaving a tes Check out these related projects. -- [reference-architectures](https://github.com/cloudposse/reference-architectures) - Get up and running quickly with one of our reference architecture using our fully automated cold-start process. -- [audit.cloudposse.co](https://github.com/cloudposse/audit.cloudposse.co) - Example Terraform Reference Architecture of a Geodesic Module for an Audit Logs Organization in AWS. -- [prod.cloudposse.co](https://github.com/cloudposse/prod.cloudposse.co) - Example Terraform Reference Architecture of a Geodesic Module for a Production Organization in AWS. -- [staging.cloudposse.co](https://github.com/cloudposse/staging.cloudposse.co) - Example Terraform Reference Architecture of a Geodesic Module for a Staging Organization in AWS. -- [dev.cloudposse.co](https://github.com/cloudposse/dev.cloudposse.co) - Example Terraform Reference Architecture of a Geodesic Module for a Development Sandbox Organization in AWS. +- [Cloud Posse Terraform Modules](https://docs.cloudposse.com/modules/) - Our collection of reusable Terraform modules used by our reference architectures. +- [Atmos](https://atmos.tools) - Atmos is like docker-compose but for your infrastructure ## References @@ -172,6 +144,7 @@ Check out these related projects. For additional context, refer to some of these links. - [Cloud Posse Documentation](https://docs.cloudposse.com) - Complete documentation for the Cloud Posse solution +- [reference-architectures](https://cloudposse.com/) - Get up and running quickly with one of our reference architecture using our fully automated cold-start process. ## Help @@ -322,7 +295,7 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply [![README Footer][readme_footer_img]][readme_footer_link] [![Beacon][beacon]][website] - + [logo]: https://cloudposse.com/logo-300x69.svg [docs]: https://cpco.io/docs?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=docs [website]: https://cpco.io/homepage?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=website @@ -353,4 +326,3 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply [share_googleplus]: https://plus.google.com/share?url=https://github.com/cloudposse/terraform-aws-components [share_email]: mailto:?subject=terraform-aws-components&body=https://github.com/cloudposse/terraform-aws-components [beacon]: https://ga-beacon.cloudposse.com/UA-76589703-4/cloudposse/terraform-aws-components?pixel&cs=github&cm=readme&an=terraform-aws-components - diff --git a/README.yaml b/README.yaml index 21f2f33fb..69a0b5ddc 100644 --- a/README.yaml +++ b/README.yaml @@ -46,34 +46,29 @@ references: - name: "Cloud Posse Documentation" description: "Complete documentation for the Cloud Posse solution" url: "https://docs.cloudposse.com" + - name: "Reference Architectures" + description: "Launch effortlessly with our turnkey reference architectures, built either by your team or ours." + url: "https://cloudposse.com/" related: - - name: "reference-architectures" - description: "Get up and running quickly with one of our reference architecture using our fully automated cold-start process." - url: "https://github.com/cloudposse/reference-architectures" - - name: "audit.cloudposse.co" - description: "Example Terraform Reference Architecture of a Geodesic Module for an Audit Logs Organization in AWS." - url: "https://github.com/cloudposse/audit.cloudposse.co" - - name: "prod.cloudposse.co" - description: "Example Terraform Reference Architecture of a Geodesic Module for a Production Organization in AWS." - url: "https://github.com/cloudposse/prod.cloudposse.co" - - name: "staging.cloudposse.co" - description: "Example Terraform Reference Architecture of a Geodesic Module for a Staging Organization in AWS." - url: "https://github.com/cloudposse/staging.cloudposse.co" - - name: "dev.cloudposse.co" - description: "Example Terraform Reference Architecture of a Geodesic Module for a Development Sandbox Organization in AWS." - url: "https://github.com/cloudposse/dev.cloudposse.co" +- name: "Cloud Posse Terraform Modules" + description: Our collection of reusable Terraform modules used by our reference architectures. + url: "https://docs.cloudposse.com/modules/" +- name: "Atmos" + description: "Atmos is like docker-compose but for your infrastructure" + url: "https://atmos.tools" + # Short description of this project description: |- - This is a collection of reusable Terraform components and blueprints for provisioning reference architectures. + This is a collection of reusable Terraform components for provisioning infrastructure used by the Cloud Posse [reference architectures](https://cloudposse.com). introduction: |- In this repo you'll find real-world examples of how we've implemented various common patterns using our [terraform modules](https://cpco.io/terraform-modules) for our customers. - The component catalog captures the business logic, opinions, best practices and non-functional requirements. + The [component library](https://docs.cloudposse.com/components/) captures the business logic, opinions, best practices and non-functional requirements. - It's from this catalog that other developers in your organization will pick and choose from anytime they need to deploy some new capability. + It's from this library that other developers in your organization will pick and choose from anytime they need to deploy some new capability. These components make a lot of assumptions about how we've configured our environments. That said, they can still serve as an excellent reference for others. @@ -104,11 +99,10 @@ introduction: |- # How to use this project usage: |- - Please take a look at each [component's README](./modules) for usage. + Please take a look at each [component's README](https://docs.cloudposse.com/components/) for usage. include: - "docs/targets.md" - - "docs/terraform.md" # Contributors to this project contributors: diff --git a/docs/terraform.md b/docs/terraform.md deleted file mode 100644 index 128ef8b46..000000000 --- a/docs/terraform.md +++ /dev/null @@ -1,25 +0,0 @@ - -## Requirements - -No requirements. - -## Providers - -No providers. - -## Modules - -No modules. - -## Resources - -No resources. - -## Inputs - -No inputs. - -## Outputs - -No outputs. - From 984db6a151ec6cf07cc3ba56c3c890f62423fda4 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 12 Oct 2023 11:36:24 -0700 Subject: [PATCH 269/501] `account-map` Documentation for Dynamic Roles (#870) Co-authored-by: cloudpossebot Co-authored-by: Nuru --- modules/account-map/README.md | 2 +- modules/account-map/dynamic-roles.tf | 27 ++++++++++++++++++++++++--- modules/account-map/outputs.tf | 9 ++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/modules/account-map/README.md b/modules/account-map/README.md index 9b619c21d..a4ba1c398 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -143,7 +143,7 @@ components: | [profiles\_enabled](#output\_profiles\_enabled) | Whether or not to enable profiles instead of roles for the backend | | [root\_account\_account\_name](#output\_root\_account\_account\_name) | The short name for the root account | | [root\_account\_aws\_name](#output\_root\_account\_aws\_name) | The name of the root account as reported by AWS | -| [terraform\_access\_map](#output\_terraform\_access\_map) | Mapping of team Role ARN to map of account name to terraform action role ARN to assume | +| [terraform\_access\_map](#output\_terraform\_access\_map) | Mapping of team Role ARN to map of account name to terraform action role ARN to assume

For each team in `aws-teams`, look at every account and see if that team has access to the designated "apply" role.
If so, add an entry ` = "apply"` to the `terraform_access_map` entry for that team.
If not, see if it has access to the "plan" role, and if so, add a "plan" entry.
Otherwise, no entry is added. | | [terraform\_dynamic\_role\_enabled](#output\_terraform\_dynamic\_role\_enabled) | True if dynamic role for Terraform is enabled | | [terraform\_profiles](#output\_terraform\_profiles) | A list of all SSO profiles used to run terraform updates | | [terraform\_role\_name\_map](#output\_terraform\_role\_name\_map) | Mapping of Terraform action (plan or apply) to aws-team-role name to assume for that action | diff --git a/modules/account-map/dynamic-roles.tf b/modules/account-map/dynamic-roles.tf index 48f1e4545..75d84271c 100644 --- a/modules/account-map/dynamic-roles.tf +++ b/modules/account-map/dynamic-roles.tf @@ -1,4 +1,23 @@ - +# The `utils_describe_stacks` data resources use the Cloud Posse Utils provider to describe Atmos stacks, and then +# we merge the results into `local.all_team_vars`. This is the same as running the following locally: +# ``` +# atmos describe stacks --components=aws-teams,aws-team-roles --component-types=terraform --sections=vars +# ``` +# The result of these stack descriptions includes all metadata for the given components. For example, we now +# can filter the result to find all stacks where either `aws-teams` or `aws-team-roles` are deployed. +# +# In particular, we can use this data to find the name of the account via `null-label` (defined by +# `null-label.descriptor_formats.account_name`, typically `-`) where team roles are deployed. +# We then determine which roles are provisioned and which teams can access any given role in any particular account. +# +# `descriptor_formats.account_name` is typically defined in `stacks/orgs/NAMESPACE/_defaults.yaml`, and if not +# defined, the stack name will default to `stage`.` +# +# If `namespace` is included in `descriptor_formats.account_name`, then we additionally filter to only stacks with +# the same `namespace` as `module.this.namespace`. See `local.stack_namespace_index` and `local.stack_namespace_index` +# +# https://atmos.tools/cli/commands/describe/stacks/ +# https://registry.terraform.io/providers/cloudposse/utils/latest/docs/data-sources/describe_stacks data "utils_describe_stacks" "teams" { count = local.dynamic_role_enabled ? 1 : 0 @@ -18,9 +37,11 @@ data "utils_describe_stacks" "team_roles" { locals { dynamic_role_enabled = module.this.enabled && var.terraform_dynamic_role_enabled + # `var.terraform_role_name_map` maps some team role in the `aws-team-roles` configuration to "plan" and some other team to "apply". apply_role = var.terraform_role_name_map.apply plan_role = var.terraform_role_name_map.plan + # If a namespace is included with the stack name, only loop through stacks in the same namespace # zero-based index showing position of the namespace in the stack name stack_namespace_index = try(index(module.this.normalized_context.descriptor_formats.stack.labels, "namespace"), -1) stack_has_namespace = local.stack_namespace_index >= 0 @@ -53,11 +74,11 @@ locals { team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform.aws-team-roles.vars } + all_team_vars = merge(local.teams_vars, local.team_roles_vars) + stack_planners = { for k, v in local.team_roles_vars : k => v.roles[local.plan_role].trusted_teams if try(length(v.roles[local.plan_role].trusted_teams), 0) > 0 && try(v.roles[local.plan_role].enabled, true) } stack_terraformers = { for k, v in local.team_roles_vars : k => v.roles[local.apply_role].trusted_teams if try(length(v.roles[local.apply_role].trusted_teams), 0) > 0 && try(v.roles[local.apply_role].enabled, true) } - all_team_vars = merge(local.teams_vars, local.team_roles_vars) - team_planners = { for team in local.team_names : team => { for stack, trusted in local.stack_planners : local.stack_account_map[stack] => "plan" if contains(trusted, team) } } diff --git a/modules/account-map/outputs.tf b/modules/account-map/outputs.tf index 1354e55f9..76dc133dd 100644 --- a/modules/account-map/outputs.tf +++ b/modules/account-map/outputs.tf @@ -97,7 +97,14 @@ output "terraform_dynamic_role_enabled" { output "terraform_access_map" { value = local.dynamic_role_enabled ? local.role_arn_terraform_access : null - description = "Mapping of team Role ARN to map of account name to terraform action role ARN to assume" + description = <<-EOT + Mapping of team Role ARN to map of account name to terraform action role ARN to assume + + For each team in `aws-teams`, look at every account and see if that team has access to the designated "apply" role. + If so, add an entry ` = "apply"` to the `terraform_access_map` entry for that team. + If not, see if it has access to the "plan" role, and if so, add a "plan" entry. + Otherwise, no entry is added. + EOT } output "terraform_role_name_map" { From f2552e9cc6ea455080cdf773304c5b3ee99df9bc Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Thu, 12 Oct 2023 16:34:10 -0400 Subject: [PATCH 270/501] chore: update vpc flow logs bucket to 1.0.1 (#873) --- modules/vpc-flow-logs-bucket/README.md | 2 +- modules/vpc-flow-logs-bucket/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/vpc-flow-logs-bucket/README.md b/modules/vpc-flow-logs-bucket/README.md index 750e1e5db..e5eb20cfa 100644 --- a/modules/vpc-flow-logs-bucket/README.md +++ b/modules/vpc-flow-logs-bucket/README.md @@ -39,7 +39,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [flow\_logs\_s3\_bucket](#module\_flow\_logs\_s3\_bucket) | cloudposse/vpc-flow-logs-s3-bucket/aws | 0.18.0 | +| [flow\_logs\_s3\_bucket](#module\_flow\_logs\_s3\_bucket) | cloudposse/vpc-flow-logs-s3-bucket/aws | 1.0.1 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/vpc-flow-logs-bucket/main.tf b/modules/vpc-flow-logs-bucket/main.tf index 1e69db2ae..88eaa98fe 100644 --- a/modules/vpc-flow-logs-bucket/main.tf +++ b/modules/vpc-flow-logs-bucket/main.tf @@ -1,6 +1,6 @@ module "flow_logs_s3_bucket" { source = "cloudposse/vpc-flow-logs-s3-bucket/aws" - version = "0.18.0" + version = "1.0.1" lifecycle_prefix = var.lifecycle_prefix lifecycle_tags = var.lifecycle_tags From 05ad24dec1d3abc13519e528c74531a28749ba28 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 12 Oct 2023 15:01:56 -0700 Subject: [PATCH 271/501] Handle `enabled: false` with Deprecated `eks/efs-controller` (#874) --- deprecated/eks/efs-controller/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deprecated/eks/efs-controller/main.tf b/deprecated/eks/efs-controller/main.tf index c09a9d4a2..0867d5ccf 100644 --- a/deprecated/eks/efs-controller/main.tf +++ b/deprecated/eks/efs-controller/main.tf @@ -50,7 +50,7 @@ module "efs_controller" { # annotations: # storageclass.kubernetes.io/is-default-class: "true" parameters = { - fileSystemId = module.efs.outputs.efs_id + fileSystemId = local.enabled ? module.efs.outputs.efs_id : "" provisioningMode = "efs-ap" directoryPerms = "700" basePath = "/efs_controller" From 8ceb1117a0ef39d4859dac59bf6abb86fde1ce07 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 13 Oct 2023 16:37:06 -0700 Subject: [PATCH 272/501] Variable names for `aurora-mysql` (#875) --- modules/aurora-mysql/README.md | 6 +++--- modules/aurora-mysql/ssm.tf | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index 8520f852d..7bf18fd10 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -73,9 +73,9 @@ components: - aurora-mysql/defaults vars: instance_type: db.r5.large - cluster_size: 1 - cluster_name: main - database_name: main + mysql_cluster_size: 1 + mysql_name: main + mysql_db_name: main ``` Example deployment with primary cluster deployed to us-east-1 in a `platform-dev` account: `atmos terraform apply aurora-mysql/dev -s platform-use1-dev` diff --git a/modules/aurora-mysql/ssm.tf b/modules/aurora-mysql/ssm.tf index 621c579bc..34db56720 100644 --- a/modules/aurora-mysql/ssm.tf +++ b/modules/aurora-mysql/ssm.tf @@ -34,7 +34,7 @@ locals { overwrite = true } ] - cluster_parameters = var.cluster_size > 0 ? [ + cluster_parameters = var.mysql_cluster_size > 0 ? [ { name = format("%s/%s", local.ssm_path_prefix, "replicas_hostname") value = module.aurora_mysql.replicas_host From b4365db9247c3067496e6f2944fb83e32089a824 Mon Sep 17 00:00:00 2001 From: Kevin Mahoney Date: Mon, 16 Oct 2023 19:29:44 +0200 Subject: [PATCH 273/501] [aurora-postgres] add intra_security_group_traffic_enabled (#876) Co-authored-by: cloudpossebot --- modules/aurora-postgres/README.md | 1 + modules/aurora-postgres/cluster-regional.tf | 67 +++++++++++---------- modules/aurora-postgres/variables.tf | 6 ++ 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index dd7dee3da..5934bccb5 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -317,6 +317,7 @@ components: | [iam\_database\_authentication\_enabled](#input\_iam\_database\_authentication\_enabled) | Specifies whether or mappings of AWS Identity and Access Management (IAM) accounts to database accounts is enabled | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [instance\_type](#input\_instance\_type) | EC2 instance type for Postgres cluster | `string` | n/a | yes | +| [intra\_security\_group\_traffic\_enabled](#input\_intra\_security\_group\_traffic\_enabled) | Whether to allow traffic between resources inside the database's security group. | `bool` | `false` | no | | [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/aurora-postgres/cluster-regional.tf b/modules/aurora-postgres/cluster-regional.tf index ad825ca40..d9de0f7bb 100644 --- a/modules/aurora-postgres/cluster-regional.tf +++ b/modules/aurora-postgres/cluster-regional.tf @@ -17,39 +17,40 @@ module "aurora_postgres_cluster" { admin_user = local.admin_user admin_password = local.admin_password - db_name = local.database_name - publicly_accessible = var.publicly_accessible - db_port = var.database_port - vpc_id = local.vpc_id - subnets = local.private_subnet_ids - zone_id = local.zone_id - cluster_dns_name = local.cluster_dns_name - reader_dns_name = local.reader_dns_name - security_groups = local.allowed_security_groups - allowed_cidr_blocks = local.allowed_cidr_blocks - iam_database_authentication_enabled = var.iam_database_authentication_enabled - storage_encrypted = var.storage_encrypted - kms_key_arn = var.storage_encrypted ? module.kms_key_rds.key_arn : null - performance_insights_kms_key_id = var.performance_insights_enabled ? module.kms_key_rds.key_arn : null - maintenance_window = var.maintenance_window - enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports - enhanced_monitoring_role_enabled = var.enhanced_monitoring_role_enabled - enhanced_monitoring_attributes = var.enhanced_monitoring_attributes - performance_insights_enabled = var.performance_insights_enabled - rds_monitoring_interval = var.rds_monitoring_interval - autoscaling_enabled = var.autoscaling_enabled - autoscaling_policy_type = var.autoscaling_policy_type - autoscaling_target_metrics = var.autoscaling_target_metrics - autoscaling_target_value = var.autoscaling_target_value - autoscaling_scale_in_cooldown = var.autoscaling_scale_in_cooldown - autoscaling_scale_out_cooldown = var.autoscaling_scale_out_cooldown - autoscaling_min_capacity = var.autoscaling_min_capacity - autoscaling_max_capacity = var.autoscaling_max_capacity - scaling_configuration = var.scaling_configuration - serverlessv2_scaling_configuration = var.serverlessv2_scaling_configuration - skip_final_snapshot = var.skip_final_snapshot - deletion_protection = var.deletion_protection - snapshot_identifier = var.snapshot_identifier + db_name = local.database_name + publicly_accessible = var.publicly_accessible + db_port = var.database_port + vpc_id = local.vpc_id + subnets = local.private_subnet_ids + zone_id = local.zone_id + cluster_dns_name = local.cluster_dns_name + reader_dns_name = local.reader_dns_name + security_groups = local.allowed_security_groups + intra_security_group_traffic_enabled = var.intra_security_group_traffic_enabled + allowed_cidr_blocks = local.allowed_cidr_blocks + iam_database_authentication_enabled = var.iam_database_authentication_enabled + storage_encrypted = var.storage_encrypted + kms_key_arn = var.storage_encrypted ? module.kms_key_rds.key_arn : null + performance_insights_kms_key_id = var.performance_insights_enabled ? module.kms_key_rds.key_arn : null + maintenance_window = var.maintenance_window + enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports + enhanced_monitoring_role_enabled = var.enhanced_monitoring_role_enabled + enhanced_monitoring_attributes = var.enhanced_monitoring_attributes + performance_insights_enabled = var.performance_insights_enabled + rds_monitoring_interval = var.rds_monitoring_interval + autoscaling_enabled = var.autoscaling_enabled + autoscaling_policy_type = var.autoscaling_policy_type + autoscaling_target_metrics = var.autoscaling_target_metrics + autoscaling_target_value = var.autoscaling_target_value + autoscaling_scale_in_cooldown = var.autoscaling_scale_in_cooldown + autoscaling_scale_out_cooldown = var.autoscaling_scale_out_cooldown + autoscaling_min_capacity = var.autoscaling_min_capacity + autoscaling_max_capacity = var.autoscaling_max_capacity + scaling_configuration = var.scaling_configuration + serverlessv2_scaling_configuration = var.serverlessv2_scaling_configuration + skip_final_snapshot = var.skip_final_snapshot + deletion_protection = var.deletion_protection + snapshot_identifier = var.snapshot_identifier cluster_parameters = [ { diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index d852a49f9..e3cae7258 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -329,3 +329,9 @@ variable "serverlessv2_scaling_configuration" { default = null description = "Nested attribute with scaling properties for ServerlessV2. Only valid when `engine_mode` is set to `provisioned.` This is required for Serverless v2" } + +variable "intra_security_group_traffic_enabled" { + type = bool + default = false + description = "Whether to allow traffic between resources inside the database's security group." +} From 0be801b83e777d08bbb5f4a79b20703f02cb3e09 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 16 Oct 2023 12:01:56 -0700 Subject: [PATCH 274/501] Fix `eks/cluster` README Link (#877) --- modules/eks/cluster/README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 98bb007c0..1b3300073 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -170,18 +170,21 @@ When picking a Kubernetes version, be sure to review the [end-of-life dates for | cycle | release | latest | latest release | eol | |:------|:----------:|:------------|:--------------:|:----------:| -| 1.27 | 2023-05-24 | 1.27-eks-3 | 2023-06-30 | 2024-07-01 | -| 1.26 | 2023-04-11 | 1.26-eks-4 | 2023-06-30 | 2024-06-01 | -| 1.25 | 2023-02-21 | 1.25-eks-5 | 2023-06-30 | 2024-05-01 | -| 1.24 | 2022-11-15 | 1.24-eks-8 | 2023-06-30 | 2024-01-01 | -| 1.23 | 2022-08-11 | 1.23-eks-10 | 2023-06-30 | 2023-10-11 | +| 1.28 | 2023-09-26 | 1.28-eks-1 | 2023-09-26 | 2024-11-01 | +| 1.27 | 2023-05-24 | 1.27-eks-5 | 2023-08-30 | 2024-07-01 | +| 1.26 | 2023-04-11 | 1.26-eks-6 | 2023-08-30 | 2024-06-01 | +| 1.25 | 2023-02-21 | 1.25-eks-7 | 2023-08-30 | 2024-05-01 | +| 1.24 | 2022-11-15 | 1.24-eks-10 | 2023-08-30 | 2024-01-31 | +| 1.23 | 2022-08-11 | 1.23-eks-12 | 2023-08-30 | 2023-10-11 | | 1.22 | 2022-04-04 | 1.22-eks-14 | 2023-06-30 | 2023-06-04 | | 1.21 | 2021-07-19 | 1.21-eks-18 | 2023-06-09 | 2023-02-15 | | 1.20 | 2021-05-18 | 1.20-eks-14 | 2023-05-05 | 2022-11-01 | | 1.19 | 2021-02-16 | 1.19-eks-11 | 2022-08-15 | 2022-08-01 | | 1.18 | 2020-10-13 | 1.18-eks-13 | 2022-08-15 | 2022-08-15 | -*This Chart was updated as of 08/04/2023 and is generated with [the `eol` tool](https://github.com/hugovk/norwegianblue). Check the latest updates by running `eol amazon-eks` locally or [on the website directly]((https://endoflife.date/amazon-eks)). +*This Chart was updated as of 10/16/2023 and is generated with [the `eol` tool](https://github.com/hugovk/norwegianblue). Check the latest updates by running `eol amazon-eks` locally or [on the website directly](https://endoflife.date/amazon-eks). + +You can also view the release and support timeline for [the Kubernetes project itself](https://endoflife.date/kubernetes). ### Usage with Node Groups From 5fb307b819b469bd73e312befa0cd46570e3c112 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 17 Oct 2023 14:07:35 -0700 Subject: [PATCH 275/501] Rebuild README (#878) --- .gitignore | 1 + README.md | 64 ++++++++++++++---------------------------------------- 2 files changed, 17 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index 252dc829d..0b987f3b3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build-harness/ aws-assumed-role/ .idea/ *.iml +docs/terraform.md vendir.lock.yml diff --git a/README.md b/README.md index 528a0aa53..fdfe316c3 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,6 @@ This is a collection of reusable Terraform components for provisioning infrastru --- This project is part of our comprehensive ["SweetOps"](https://cpco.io/sweetops) approach towards DevOps. -[][share_email] -[][share_googleplus] -[][share_facebook] -[][share_reddit] -[][share_linkedin] -[][share_twitter] @@ -127,8 +121,6 @@ Available targets: Like this project? Please give it a ★ on [our GitHub](https://github.com/cloudposse/terraform-aws-components)! (it helps us **a lot**) -Are you using this project or any of our other projects? Consider [leaving a testimonial][testimonial]. =) - ## Related Projects @@ -144,7 +136,7 @@ Check out these related projects. For additional context, refer to some of these links. - [Cloud Posse Documentation](https://docs.cloudposse.com) - Complete documentation for the Cloud Posse solution -- [reference-architectures](https://cloudposse.com/) - Get up and running quickly with one of our reference architecture using our fully automated cold-start process. +- [Reference Architectures](https://cloudposse.com/) - Launch effortlessly with our turnkey reference architectures, built either by your team or ours. ## Help @@ -181,10 +173,6 @@ We deliver 10x the value for a fraction of the cost of a full-time engineer. Our Join our [Open Source Community][slack] on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure. -## Discourse Forums - -Participate in our [Discourse Forums][discourse]. Here you'll find answers to commonly asked questions. Most questions will be related to the enormous number of projects we support on our GitHub. Come here to collaborate on answers, find solutions, and get ideas about the products and services we value. It only takes a minute to get started! Just sign in with SSO using your GitHub account. - ## Newsletter Sign up for [our newsletter][newsletter] that covers everything on our technology radar. Receive updates on what we're up to on GitHub as well as awesome new projects we discover. @@ -195,7 +183,18 @@ Sign up for [our newsletter][newsletter] that covers everything on our technolog [![zoom](https://img.cloudposse.com/fit-in/200x200/https://cloudposse.com/wp-content/uploads/2019/08/Powered-by-Zoom.png")][office_hours] -## Contributing +## ✨ Contributing + + + +This project is under active development, and we encourage contributions from our community. +Many thanks to our outstanding contributors: + + + + + + ### Bug Reports & Feature Requests @@ -269,33 +268,9 @@ We're a [DevOps Professional Services][hire] company based in Los Angeles, CA. W We offer [paid support][commercial_support] on all of our projects. -Check out [our other projects][github], [follow us on twitter][twitter], [apply for a job][jobs], or [hire us][hire] to help with your cloud strategy and implementation. - - - -### Contributors - - -| [![Erik Osterman][osterman_avatar]][osterman_homepage]
[Erik Osterman][osterman_homepage] | [![Igor Rodionov][goruha_avatar]][goruha_homepage]
[Igor Rodionov][goruha_homepage] | [![Andriy Knysh][aknysh_avatar]][aknysh_homepage]
[Andriy Knysh][aknysh_homepage] | [![Matt Gowie][Gowiem_avatar]][Gowiem_homepage]
[Matt Gowie][Gowiem_homepage] | [![Yonatan Koren][korenyoni_avatar]][korenyoni_homepage]
[Yonatan Koren][korenyoni_homepage] | [![Matt Calhoun][mcalhoun_avatar]][mcalhoun_homepage]
[Matt Calhoun][mcalhoun_homepage] | -|---|---|---|---|---|---| - - - [osterman_homepage]: https://github.com/osterman - [osterman_avatar]: https://img.cloudposse.com/150x150/https://github.com/osterman.png - [goruha_homepage]: https://github.com/goruha - [goruha_avatar]: https://img.cloudposse.com/150x150/https://github.com/goruha.png - [aknysh_homepage]: https://github.com/aknysh - [aknysh_avatar]: https://img.cloudposse.com/150x150/https://github.com/aknysh.png - [Gowiem_homepage]: https://github.com/Gowiem - [Gowiem_avatar]: https://img.cloudposse.com/150x150/https://github.com/Gowiem.png - [korenyoni_homepage]: https://github.com/korenyoni - [korenyoni_avatar]: https://img.cloudposse.com/150x150/https://github.com/korenyoni.png - [mcalhoun_homepage]: https://github.com/mcalhoun - [mcalhoun_avatar]: https://img.cloudposse.com/150x150/https://github.com/mcalhoun.png - -[![README Footer][readme_footer_img]][readme_footer_link] +Check out [our other projects][github], [follow us on twitter][twitter], [apply for a job][jobs], or [hire us][hire] to help with your cloud strategy and implementation.[![README Footer][readme_footer_img]][readme_footer_link] [![Beacon][beacon]][website] - + [logo]: https://cloudposse.com/logo-300x69.svg [docs]: https://cpco.io/docs?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=docs [website]: https://cpco.io/homepage?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=website @@ -303,12 +278,10 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply [jobs]: https://cpco.io/jobs?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=jobs [hire]: https://cpco.io/hire?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=hire [slack]: https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=slack - [linkedin]: https://cpco.io/linkedin?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=linkedin [twitter]: https://cpco.io/twitter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=twitter [testimonial]: https://cpco.io/leave-testimonial?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=testimonial [office_hours]: https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=office_hours [newsletter]: https://cpco.io/newsletter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=newsletter - [discourse]: https://ask.sweetops.com/?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=discourse [email]: https://cpco.io/email?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=email [commercial_support]: https://cpco.io/commercial-support?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=commercial_support [we_love_open_source]: https://cpco.io/we-love-open-source?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=we_love_open_source @@ -319,10 +292,5 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply [readme_footer_link]: https://cloudposse.com/readme/footer/link?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=readme_footer_link [readme_commercial_support_img]: https://cloudposse.com/readme/commercial-support/img [readme_commercial_support_link]: https://cloudposse.com/readme/commercial-support/link?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=readme_commercial_support_link - [share_twitter]: https://twitter.com/intent/tweet/?text=terraform-aws-components&url=https://github.com/cloudposse/terraform-aws-components - [share_linkedin]: https://www.linkedin.com/shareArticle?mini=true&title=terraform-aws-components&url=https://github.com/cloudposse/terraform-aws-components - [share_reddit]: https://reddit.com/submit/?url=https://github.com/cloudposse/terraform-aws-components - [share_facebook]: https://facebook.com/sharer/sharer.php?u=https://github.com/cloudposse/terraform-aws-components - [share_googleplus]: https://plus.google.com/share?url=https://github.com/cloudposse/terraform-aws-components - [share_email]: mailto:?subject=terraform-aws-components&body=https://github.com/cloudposse/terraform-aws-components [beacon]: https://ga-beacon.cloudposse.com/UA-76589703-4/cloudposse/terraform-aws-components?pixel&cs=github&cm=readme&an=terraform-aws-components + From 0910272ee5e682272e6bd6d0dae1b2082d974fb9 Mon Sep 17 00:00:00 2001 From: Kevin Mahoney Date: Wed, 18 Oct 2023 23:31:02 +0200 Subject: [PATCH 276/501] [aurora-postgres] fix tflint and markdownlint (#872) Co-authored-by: cloudpossebot Co-authored-by: Dan Miller --- modules/aurora-postgres/README.md | 6 +----- modules/aurora-postgres/main.tf | 6 +++--- modules/aurora-postgres/ssm.tf | 10 ---------- modules/aurora-postgres/variables.tf | 23 ----------------------- 4 files changed, 4 insertions(+), 41 deletions(-) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 5934bccb5..b6f461c45 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -109,7 +109,7 @@ Generally there are three different engine configurations for Aurora: provisione ### Provisioned Aurora Postgres -[See the default usage example above](#Usage) +[See the default usage example above](#usage) ### Serverless v1 Aurora Postgres @@ -272,13 +272,11 @@ components: | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.kms_key_rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | -| [aws_ssm_parameter.password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [additional\_databases](#input\_additional\_databases) | Additional databases to be created with the cluster | `set(string)` | `[]` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [admin\_password](#input\_admin\_password) | Postgres password for the admin user | `string` | `""` | no | | [admin\_user](#input\_admin\_user) | Postgres admin user name | `string` | `""` | no | @@ -318,7 +316,6 @@ components: | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [instance\_type](#input\_instance\_type) | EC2 instance type for Postgres cluster | `string` | n/a | yes | | [intra\_security\_group\_traffic\_enabled](#input\_intra\_security\_group\_traffic\_enabled) | Whether to allow traffic between resources inside the database's security group. | `bool` | `false` | no | -| [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | @@ -336,7 +333,6 @@ components: | [serverlessv2\_scaling\_configuration](#input\_serverlessv2\_scaling\_configuration) | Nested attribute with scaling properties for ServerlessV2. Only valid when `engine_mode` is set to `provisioned.` This is required for Serverless v2 |
object({
min_capacity = number
max_capacity = number
})
| `null` | no | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Normally AWS makes a snapshot of the database before deleting it. Set this to `true` in order to skip this.
NOTE: The final snapshot has a name derived from the cluster name. If you delete a cluster, get a final snapshot,
then create a cluster of the same name, its final snapshot will fail with a name collision unless you delete
the previous final snapshot first. | `bool` | `false` | no | | [snapshot\_identifier](#input\_snapshot\_identifier) | Specifies whether or not to create this cluster from a snapshot | `string` | `null` | no | -| [ssm\_password\_source](#input\_ssm\_password\_source) | If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using
`var.ssm_password_source` and the database username. If this value is not set,
a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. | `string` | `""` | no | | [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | Top level SSM path prefix (without leading or trailing slash) | `string` | `"aurora-postgres"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [storage\_encrypted](#input\_storage\_encrypted) | Specifies whether the DB cluster is encrypted | `bool` | `true` | no | diff --git a/modules/aurora-postgres/main.tf b/modules/aurora-postgres/main.tf index ea99b06cd..77191700a 100644 --- a/modules/aurora-postgres/main.tf +++ b/modules/aurora-postgres/main.tf @@ -12,9 +12,9 @@ locals { zone_id = module.dns_gbl_delegated.outputs.default_dns_zone_id - admin_user = length(var.admin_user) > 0 ? var.admin_user : join("", random_pet.admin_user.*.id) - admin_password = length(var.admin_password) > 0 ? var.admin_password : join("", random_password.admin_password.*.result) - database_name = length(var.database_name) > 0 ? var.database_name : join("", random_pet.database_name.*.id) + admin_user = length(var.admin_user) > 0 ? var.admin_user : join("", random_pet.admin_user[*].id) + admin_password = length(var.admin_password) > 0 ? var.admin_password : join("", random_password.admin_password[*].result) + database_name = length(var.database_name) > 0 ? var.database_name : join("", random_pet.database_name[*].id) cluster_dns_name_prefix = format("%v%v%v%v", module.this.name, module.this.delimiter, var.cluster_name, module.this.delimiter) cluster_dns_name = format("%v%v", local.cluster_dns_name_prefix, var.cluster_dns_name_part) diff --git a/modules/aurora-postgres/ssm.tf b/modules/aurora-postgres/ssm.tf index 20619ddb6..9b74979dd 100644 --- a/modules/aurora-postgres/ssm.tf +++ b/modules/aurora-postgres/ssm.tf @@ -1,6 +1,4 @@ locals { - fetch_admin_password = length(var.ssm_password_source) > 0 - ssm_path_prefix = format("/%s/%s", var.ssm_path_prefix, module.cluster.id) admin_user_key = format("%s/%s/%s", local.ssm_path_prefix, "admin", "user") @@ -67,14 +65,6 @@ locals { parameter_write = concat(local.default_parameters, local.cluster_parameters, local.admin_user_parameters) } -data "aws_ssm_parameter" "password" { - count = local.fetch_admin_password ? 1 : 0 - - name = format(var.ssm_password_source, local.admin_user) - - with_decryption = true -} - module "parameter_store_write" { source = "cloudposse/ssm-parameter-store/aws" version = "0.11.0" diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index e3cae7258..0f2e465f9 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -66,13 +66,6 @@ variable "cluster_family" { default = "aurora-postgresql13" } -# AWS KMS alias used for encryption/decryption of SSM secure strings -variable "kms_alias_name_ssm" { - type = string - default = "alias/aws/ssm" - description = "KMS alias name for SSM" -} - variable "database_port" { type = number description = "Database port" @@ -146,12 +139,6 @@ variable "reader_dns_name_part" { default = "reader" } -variable "additional_databases" { - type = set(string) - default = [] - description = "Additional databases to be created with the cluster" -} - variable "ssm_path_prefix" { type = string default = "aurora-postgres" @@ -293,16 +280,6 @@ variable "allow_ingress_from_vpc_accounts" { EOF } -variable "ssm_password_source" { - type = string - default = "" - description = <<-EOT - If `var.ssm_passwords_enabled` is `true`, DB user passwords will be retrieved from SSM using - `var.ssm_password_source` and the database username. If this value is not set, - a default path will be created using the SSM path prefix and ID of the associated Aurora Cluster. - EOT -} - variable "vpc_component_name" { type = string default = "vpc" From 016ad80d400583e07ea41dafde73aae7b1499ce1 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 18 Oct 2023 17:31:26 -0400 Subject: [PATCH 277/501] fix: nlb should be optional for api gateway (#864) --- modules/api-gateway-rest-api/README.md | 1 + modules/api-gateway-rest-api/nlb.tf | 1 + modules/api-gateway-rest-api/variables.tf | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/modules/api-gateway-rest-api/README.md b/modules/api-gateway-rest-api/README.md index d2e496445..2683a498c 100644 --- a/modules/api-gateway-rest-api/README.md +++ b/modules/api-gateway-rest-api/README.md @@ -83,6 +83,7 @@ components: | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [deregistration\_delay](#input\_deregistration\_delay) | The amount of time to wait in seconds before changing the state of a deregistering target to unused | `number` | `15` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enable\_private\_link\_nlb](#input\_enable\_private\_link\_nlb) | A flag to indicate whether to enable private link. | `bool` | `false` | no | | [enable\_private\_link\_nlb\_deletion\_protection](#input\_enable\_private\_link\_nlb\_deletion\_protection) | A flag to indicate whether to enable private link deletion protection. | `bool` | `false` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [endpoint\_type](#input\_endpoint\_type) | The type of the endpoint. One of - PUBLIC, PRIVATE, REGIONAL | `string` | `"REGIONAL"` | no | diff --git a/modules/api-gateway-rest-api/nlb.tf b/modules/api-gateway-rest-api/nlb.tf index ceadf8f36..403e5647e 100644 --- a/modules/api-gateway-rest-api/nlb.tf +++ b/modules/api-gateway-rest-api/nlb.tf @@ -1,6 +1,7 @@ module "nlb" { source = "cloudposse/nlb/aws" version = "0.12.0" + count = var.enable_private_link_nlb ? 1 : 0 enabled = local.enabled diff --git a/modules/api-gateway-rest-api/variables.tf b/modules/api-gateway-rest-api/variables.tf index feb44ecf8..d3971b5fd 100644 --- a/modules/api-gateway-rest-api/variables.tf +++ b/modules/api-gateway-rest-api/variables.tf @@ -126,3 +126,9 @@ variable "deregistration_delay" { default = 15 description = "The amount of time to wait in seconds before changing the state of a deregistering target to unused" } + +variable "enable_private_link_nlb" { + description = "A flag to indicate whether to enable private link." + type = bool + default = false +} From d304b3b38295b5fc3833ba86e7af7ab2e4fe690e Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:32:10 -0500 Subject: [PATCH 278/501] Standardize acm-request-certificate module in components to on 0.16.3 (#850) Co-authored-by: cloudpossebot --- modules/acm/README.md | 2 +- modules/acm/main.tf | 2 +- modules/dns-primary/README.md | 2 +- modules/dns-primary/acm.tf | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/acm/README.md b/modules/acm/README.md index 24d15500d..55d0ab71a 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -68,7 +68,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.16.0 | +| [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.16.3 | | [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [private\_ca](#module\_private\_ca) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | diff --git a/modules/acm/main.tf b/modules/acm/main.tf index a2234c44d..1b1d1e67c 100644 --- a/modules/acm/main.tf +++ b/modules/acm/main.tf @@ -22,7 +22,7 @@ data "aws_route53_zone" "default" { # https://github.com/cloudposse/terraform-aws-acm-request-certificate module "acm" { source = "cloudposse/acm-request-certificate/aws" - version = "0.16.0" + version = "0.16.3" certificate_authority_arn = local.private_ca_enabled ? module.private_ca[0].outputs.private_ca[var.certificate_authority_component_key].certificate_authority.arn : null validation_method = local.private_ca_enabled ? null : var.validation_method diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index ad0a6475f..9c0023715 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -94,7 +94,7 @@ Use the [acm](/components/library/aws/acm) component for more advanced certific | Name | Source | Version | |------|--------|---------| -| [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.16.2 | +| [acm](#module\_acm) | cloudposse/acm-request-certificate/aws | 0.16.3 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/dns-primary/acm.tf b/modules/dns-primary/acm.tf index a86be253a..88fd79a1e 100644 --- a/modules/dns-primary/acm.tf +++ b/modules/dns-primary/acm.tf @@ -13,7 +13,7 @@ module "acm" { source = "cloudposse/acm-request-certificate/aws" // Note: 0.17.0 is a 'preview' release, so we're using 0.16.2 - version = "0.16.2" + version = "0.16.3" enabled = local.certificate_enabled From c37534b242598d6734a8540648ea6511e905ca2c Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Mon, 23 Oct 2023 11:59:04 -0400 Subject: [PATCH 279/501] ref: spacelift/admin-stack supports custom spaces components (#884) --- modules/spacelift/admin-stack/README.md | 1 + modules/spacelift/admin-stack/remote-state.tf | 2 +- modules/spacelift/admin-stack/variables.tf | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index bf8f352e9..e09da20d8 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -237,6 +237,7 @@ components: | [showcase](#input\_showcase) | Showcase settings | `map(any)` | `null` | no | | [space\_id](#input\_space\_id) | Place the stack in the specified space\_id. | `string` | `"root"` | no | | [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | +| [spacelift\_spaces\_component\_name](#input\_spacelift\_spaces\_component\_name) | The component name of the spacelift spaces component | `string` | `"spacelift/spaces"` | no | | [spacelift\_spaces\_environment\_name](#input\_spacelift\_spaces\_environment\_name) | The environment name of the spacelift spaces component | `string` | `null` | no | | [spacelift\_spaces\_stage\_name](#input\_spacelift\_spaces\_stage\_name) | The stage name of the spacelift spaces component | `string` | `null` | no | | [spacelift\_spaces\_tenant\_name](#input\_spacelift\_spaces\_tenant\_name) | The tenant name of the spacelift spaces component | `string` | `null` | no | diff --git a/modules/spacelift/admin-stack/remote-state.tf b/modules/spacelift/admin-stack/remote-state.tf index ae42e4713..69834fa59 100644 --- a/modules/spacelift/admin-stack/remote-state.tf +++ b/modules/spacelift/admin-stack/remote-state.tf @@ -2,7 +2,7 @@ module "spaces" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" - component = "spacelift/spaces" + component = var.spacelift_spaces_component_name environment = try(var.spacelift_spaces_environment_name, module.this.environment) stage = try(var.spacelift_spaces_stage_name, module.this.stage) tenant = try(var.spacelift_spaces_tenant_name, module.this.tenant) diff --git a/modules/spacelift/admin-stack/variables.tf b/modules/spacelift/admin-stack/variables.tf index 169e062ab..2779e71b9 100644 --- a/modules/spacelift/admin-stack/variables.tf +++ b/modules/spacelift/admin-stack/variables.tf @@ -278,6 +278,12 @@ variable "spacelift_spaces_tenant_name" { default = null } +variable "spacelift_spaces_component_name" { + type = string + description = "The component name of the spacelift spaces component" + default = "spacelift/spaces" +} + variable "spacelift_stack_dependency_enabled" { type = bool description = "If enabled, the `spacelift_stack_dependency` Spacelift resource will be used to create dependencies between stacks instead of using the `depends-on` labels. The `depends-on` labels will be removed from the stacks and the trigger policies for dependencies will be detached" From 70bff64f6167311bbf54ad82b5911623c87b1b13 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 23 Oct 2023 10:14:21 -0700 Subject: [PATCH 280/501] `datadog-synthetics-private-location` Kubernetes Namespace (#879) --- modules/datadog-synthetics-private-location/README.md | 2 +- modules/datadog-synthetics-private-location/main.tf | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index 0aa34dc2e..8567d102f 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -146,7 +146,7 @@ Environment variables: | Name | Source | Version | |------|--------|---------| | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/helm-release/aws | 0.10.0 | +| [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/helm-release/aws | 0.10.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/datadog-synthetics-private-location/main.tf b/modules/datadog-synthetics-private-location/main.tf index 821ed9b05..107cfc8eb 100644 --- a/modules/datadog-synthetics-private-location/main.tf +++ b/modules/datadog-synthetics-private-location/main.tf @@ -16,7 +16,7 @@ resource "datadog_synthetics_private_location" "this" { module "datadog_synthetics_private_location" { source = "cloudposse/helm-release/aws" - version = "0.10.0" + version = "0.10.1" name = module.this.name chart = var.chart @@ -24,7 +24,9 @@ module "datadog_synthetics_private_location" { repository = var.repository chart_version = var.chart_version - namespace = var.kubernetes_namespace + kubernetes_namespace = var.kubernetes_namespace + + # Usually set to `false` if deploying eks/datadog-agent, since namespace will already be created create_namespace_with_kubernetes = var.create_namespace verify = var.verify From a241f209a1326ede0cea21e41995b511df9b423e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 23 Oct 2023 10:18:11 -0700 Subject: [PATCH 281/501] Allow VPC Ingress from Multiple Regions in Same Stage for `eks/cluster` (#881) --- modules/eks/cluster/remote-state.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf index 48961d96b..772c7caf5 100644 --- a/modules/eks/cluster/remote-state.tf +++ b/modules/eks/cluster/remote-state.tf @@ -1,6 +1,6 @@ locals { accounts_with_vpc = local.enabled ? { - for i, account in var.allow_ingress_from_vpc_accounts : try(account.tenant, module.this.tenant) != null ? format("%s-%s", account.tenant, account.stage) : account.stage => account + for i, account in var.allow_ingress_from_vpc_accounts : try(account.tenant, module.this.tenant) != null ? format("%s-%s-%s", account.tenant, account.stage, try(account.environment, module.this.environment)) : format("%s-%s", account.stage, try(account.environment, module.this.environment)) => account } : {} } From 119624bd986480567580875be7198d32938a2ab9 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 23 Oct 2023 10:25:05 -0700 Subject: [PATCH 282/501] Support for Multi Region Clusters for ArgoCD (#880) --- modules/eks/argocd/README.md | 2 +- modules/eks/argocd/remote-state.tf | 3 ++- modules/eks/argocd/variables-argocd.tf | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 978ab00e7..d43fe1464 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -505,7 +505,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| `null` | no | | [saml\_enabled](#input\_saml\_enabled) | Toggles SAML integration in the deployed chart | `bool` | `false` | no | | [saml\_rbac\_scopes](#input\_saml\_rbac\_scopes) | SAML RBAC scopes to request | `string` | `"[email,groups]"` | no | -| [saml\_sso\_providers](#input\_saml\_sso\_providers) | SAML SSO providers components |
map(object({
component = string
}))
| `{}` | no | +| [saml\_sso\_providers](#input\_saml\_sso\_providers) | SAML SSO providers components |
map(object({
component = string
environment = optional(string, null)
}))
| `{}` | no | | [ssm\_github\_api\_key](#input\_ssm\_github\_api\_key) | SSM path to the GitHub API key | `string` | `"/argocd/github/api_key"` | no | | [ssm\_oidc\_client\_id](#input\_ssm\_oidc\_client\_id) | The SSM Parameter Store path for the ID of the IdP client | `string` | `"/argocd/oidc/client_id"` | no | | [ssm\_oidc\_client\_secret](#input\_ssm\_oidc\_client\_secret) | The SSM Parameter Store path for the secret of the IdP client | `string` | `"/argocd/oidc/client_secret"` | no | diff --git a/modules/eks/argocd/remote-state.tf b/modules/eks/argocd/remote-state.tf index dcb414db9..c63f3572f 100644 --- a/modules/eks/argocd/remote-state.tf +++ b/modules/eks/argocd/remote-state.tf @@ -22,7 +22,8 @@ module "saml_sso_providers" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" - component = each.value.component + component = each.value.component + environment = each.value.environment context = module.this.context } diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index 94280feb5..5a7861e6a 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -197,7 +197,8 @@ variable "eks_component_name" { variable "saml_sso_providers" { type = map(object({ - component = string + component = string + environment = optional(string, null) })) default = {} From 85d1a789263cea35013aa7c5e23e956b989c9b3f Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 23 Oct 2023 10:27:16 -0700 Subject: [PATCH 283/501] `eks/external-secrets-operator` Overridable Additional Policy Statements (#882) --- .../eks/external-secrets-operator/README.md | 4 +- .../additional-iam-policy-statements.tf | 17 ++++++ modules/eks/external-secrets-operator/main.tf | 54 ++++++++++--------- .../external-secrets-operator/remote-state.tf | 8 +-- 4 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 modules/eks/external-secrets-operator/additional-iam-policy-statements.tf diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 731cfe90e..d7381ec7c 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -111,8 +111,8 @@ components: |------|--------|---------| | [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.10.0 | -| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.10.0 | +| [external\_secrets\_operator](#module\_external\_secrets\_operator) | cloudposse/helm-release/aws | 0.10.1 | +| [external\_ssm\_secrets](#module\_external\_ssm\_secrets) | cloudposse/helm-release/aws | 0.10.1 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/eks/external-secrets-operator/additional-iam-policy-statements.tf b/modules/eks/external-secrets-operator/additional-iam-policy-statements.tf new file mode 100644 index 000000000..5d9cd000d --- /dev/null +++ b/modules/eks/external-secrets-operator/additional-iam-policy-statements.tf @@ -0,0 +1,17 @@ +locals { + # If you have custom policy statements, override this declaration by creating + # a file called `additional-iam-policy-statements_override.tf`. + # Then add the custom policy statements to the overridable_additional_iam_policy_statements in that file. + overridable_additional_iam_policy_statements = [ + # { + # sid = "UseKMS" + # effect = "Allow" + # actions = [ + # "kms:Decrypt" + # ] + # resources = [ + # "*" + # ] + # } + ] +} diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf index 93ca69108..a360438b0 100644 --- a/modules/eks/external-secrets-operator/main.tf +++ b/modules/eks/external-secrets-operator/main.tf @@ -18,7 +18,7 @@ resource "kubernetes_namespace" "default" { # https://external-secrets.io/v0.5.9/guides-getting-started/ module "external_secrets_operator" { source = "cloudposse/helm-release/aws" - version = "0.10.0" + version = "0.10.1" name = "" # avoid redundant release name in IAM role: ...-ekc-cluster-external-secrets-operator-external-secrets-operator@secrets description = var.chart_description @@ -26,12 +26,13 @@ module "external_secrets_operator" { repository = var.chart_repository chart = var.chart chart_version = var.chart_version - kubernetes_namespace = join("", kubernetes_namespace.default.*.id) + kubernetes_namespace = join("", kubernetes_namespace.default[*].id) create_namespace = false wait = var.wait atomic = var.atomic cleanup_on_fail = var.cleanup_on_fail timeout = var.timeout + verify = var.verify eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") @@ -39,26 +40,31 @@ module "external_secrets_operator" { service_account_namespace = var.kubernetes_namespace iam_role_enabled = true - iam_policy_statements = { - ReadParameterStore = { - effect = "Allow" - actions = [ - "ssm:GetParameter*" - ] - resources = [for parameter_store_path in var.parameter_store_paths : ( - "arn:aws:ssm:${var.region}:${local.account}:parameter/${parameter_store_path}/*" - )] - } - DescribeParameters = { - effect = "Allow" - actions = [ - "ssm:DescribeParameter*" - ] - resources = [ - "arn:aws:ssm:${var.region}:${local.account}:*" - ] - } - } + iam_policy = [{ + statements = concat([ + { + sid = "ReadParameterStore" + effect = "Allow" + actions = [ + "ssm:GetParameter*" + ] + resources = [for parameter_store_path in var.parameter_store_paths : ( + "arn:aws:ssm:${var.region}:${local.account}:parameter/${parameter_store_path}/*" + )] + }, + { + sid = "DescribeParameters" + effect = "Allow" + actions = [ + "ssm:DescribeParameter*" + ] + resources = [ + "arn:aws:ssm:${var.region}:${local.account}:*" + ] + }], + local.overridable_additional_iam_policy_statements + ) + }] values = compact([ yamlencode({ @@ -84,7 +90,7 @@ data "kubernetes_resources" "crd" { module "external_ssm_secrets" { source = "cloudposse/helm-release/aws" - version = "0.10.0" + version = "0.10.1" enabled = local.enabled && length(data.kubernetes_resources.crd.objects) > 0 @@ -92,7 +98,7 @@ module "external_ssm_secrets" { description = "This Chart uses creates a SecretStore and ExternalSecret to pull variables (under a given path) from AWS SSM Parameter Store into a Kubernetes secret." chart = "${path.module}/charts/external-ssm-secrets" - kubernetes_namespace = join("", kubernetes_namespace.default.*.id) + kubernetes_namespace = join("", kubernetes_namespace.default[*].id) create_namespace = false wait = var.wait atomic = var.atomic diff --git a/modules/eks/external-secrets-operator/remote-state.tf b/modules/eks/external-secrets-operator/remote-state.tf index 37c866a99..7863c9586 100644 --- a/modules/eks/external-secrets-operator/remote-state.tf +++ b/modules/eks/external-secrets-operator/remote-state.tf @@ -8,11 +8,13 @@ module "eks" { } module "account_map" { - source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.5.0" + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + component = "account-map" tenant = module.iam_roles.global_tenant_name environment = module.iam_roles.global_environment_name stage = module.iam_roles.global_stage_name - context = module.this.context + + context = module.this.context } From 716bc6af99948f8d3bbbed147aed5179f3eb0120 Mon Sep 17 00:00:00 2001 From: Brad Janke Date: Wed, 25 Oct 2023 10:15:45 -0500 Subject: [PATCH 284/501] updates config-storage version to 1.0.2 in config-bucket (#886) Co-authored-by: cloudpossebot --- modules/config-bucket/README.md | 2 +- modules/config-bucket/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/config-bucket/README.md b/modules/config-bucket/README.md index aba560b5a..b4b140bb6 100644 --- a/modules/config-bucket/README.md +++ b/modules/config-bucket/README.md @@ -47,7 +47,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [config\_bucket](#module\_config\_bucket) | cloudposse/config-storage/aws | 1.0.0 | +| [config\_bucket](#module\_config\_bucket) | cloudposse/config-storage/aws | 1.0.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/config-bucket/main.tf b/modules/config-bucket/main.tf index c029fe72b..20f09eb32 100644 --- a/modules/config-bucket/main.tf +++ b/modules/config-bucket/main.tf @@ -1,6 +1,6 @@ module "config_bucket" { source = "cloudposse/config-storage/aws" - version = "1.0.0" + version = "1.0.2" expiration_days = var.expiration_days force_destroy = false From b9fc77d7636e09bd4c60cf65305d19d7d2036b3c Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 25 Oct 2023 09:11:35 -0700 Subject: [PATCH 285/501] [datadog-synthetics] Accept Datadog API output as input (#885) --- modules/datadog-synthetics/CHANGELOG.md | 20 +++++ modules/datadog-synthetics/README.md | 81 +++++++++++-------- .../synthetics/examples/browser-test.yaml | 61 +++++++------- modules/datadog-synthetics/main.tf | 6 +- modules/datadog-synthetics/outputs.tf | 10 +-- modules/datadog-synthetics/variables.tf | 4 +- modules/tfstate-backend/README.md | 10 +++ 7 files changed, 123 insertions(+), 69 deletions(-) create mode 100644 modules/datadog-synthetics/CHANGELOG.md diff --git a/modules/datadog-synthetics/CHANGELOG.md b/modules/datadog-synthetics/CHANGELOG.md new file mode 100644 index 000000000..16bb69d8c --- /dev/null +++ b/modules/datadog-synthetics/CHANGELOG.md @@ -0,0 +1,20 @@ +## Changes approximately v1.329.0 + +### API Schema accepted + +Test can now be defined using the Datadog API schema, meaning that the test definition +returned by +- `https://api.datadoghq.com/api/v1/synthetics/tests/api/{public_id}` +- `https://api.datadoghq.com/api/v1/synthetics/tests/browser/{public_id}` + +can be directly used a map value (you still need to supply a key, though). + +You can mix tests using the API schema with tests using the old Terraform schema. +You could probably get away with mixing them in the same test, but it is not recommended. + +### Default locations + +Previously, the default locations for Synthetics tests were "all" public locations. +Now the default is no locations, in favor of locations being specified in each test configuration, +which is more flexible. Also, since the tests are expensive, it is better to err on the side of +too few test locations than too many. diff --git a/modules/datadog-synthetics/README.md b/modules/datadog-synthetics/README.md index 64cccbda8..26eccb1db 100644 --- a/modules/datadog-synthetics/README.md +++ b/modules/datadog-synthetics/README.md @@ -39,41 +39,56 @@ components: Below are examples of Datadog browser and API synthetic tests. -The synthetic tests are defined in YAML using the [Datadog Terraform provider](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) schema. +The synthetic tests are defined in YAML using either the [Datadog Terraform provider](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) schema +or the [Datadog Synthetics API](https://docs.datadoghq.com/api/latest/synthetics) schema. +See the `terraform-datadog-platform` Terraform module [README](https://github.com/cloudposse/terraform-datadog-platform/blob/main/modules/synthetics/README.md) for more details. +We recommend using the API schema so you can more create and edit tests using the Datadog +web API and then import them into this module by downloading the test using +the Datadog REST API. (See the Datadog API documentation for the appropriate +`curl` commands to use.) ```yaml +# API schema my-browser-test: - name: "Browser Test" - message: "Browser Test Failed" + name: My Browser Test + status: live type: browser - subtype: http - device_ids: - - "laptop_large" - tags: - - "managed-by:Terraform" - status: "live" - request_definition: - url: "CHANGEME" - method: GET - request_headers: - Accept-Charset: "utf-8, iso-8859-1;q=0.5" - Accept: "text/html" - options_list: - tick_every: 1800 - no_screenshot: false - follow_redirects: false + config: + request: + method: GET + headers: {} + url: https://example.com/login + setCookie: |- + DatadogTest=true + message: "My Browser Test Failed" + options: + device_ids: + - chrome.laptop_large + - edge.tablet + - firefox.mobile_small + ignoreServerCertificateError: false + disableCors: false + disableCsp: false + noScreenshot: false + tick_every: 86400 + min_failure_duration: 0 + min_location_failed: 1 retry: - count: 2 - interval: 10 + count: 0 + interval: 300 monitor_options: - renotify_interval: 300 - browser_step: - - name: "Check current URL" - type: assertCurrentUrl - params: - check: contains - value: "CHANGEME" - + renotify_interval: 0 + ci: + executionRule: non_blocking + rumSettings: + isEnabled: false + enableProfiling: false + enableSecurityTesting: false + locations: + - aws:us-east-1 + - aws:us-west-2 + +# Terraform schema my-api-test: name: "API Test" message: "API Test Failed" @@ -145,7 +160,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_synthetics](#module\_datadog\_synthetics) | cloudposse/platform/datadog//modules/synthetics | 1.0.1 | +| [datadog\_synthetics](#module\_datadog\_synthetics) | cloudposse/platform/datadog//modules/synthetics | 1.3.0 | | [datadog\_synthetics\_merge](#module\_datadog\_synthetics\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | | [datadog\_synthetics\_private\_location](#module\_datadog\_synthetics\_private\_location) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [datadog\_synthetics\_yaml\_config](#module\_datadog\_synthetics\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | @@ -164,7 +179,7 @@ No resources. | [alert\_tags](#input\_alert\_tags) | List of alert tags to add to all alert messages, e.g. `["@opsgenie"]` or `["@devops", "@opsgenie"]` | `list(string)` | `null` | no | | [alert\_tags\_separator](#input\_alert\_tags\_separator) | Separator for the alert tags. All strings from the `alert_tags` variable will be joined into one string using the separator and then added to the alert message | `string` | `"\n"` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [config\_parameters](#input\_config\_parameters) | Map of parameters to Datadog Synthetic configurations | `map(any)` | `{}` | no | +| [config\_parameters](#input\_config\_parameters) | Map of parameter values to interpolate into Datadog Synthetic configurations | `map(any)` | `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [context\_tags](#input\_context\_tags) | List of context tags to add to each synthetic check | `set(string)` |
[
"namespace",
"tenant",
"environment",
"stage"
]
| no | | [context\_tags\_enabled](#input\_context\_tags\_enabled) | Whether to add context tags to add to each synthetic check | `bool` | `true` | no | @@ -178,7 +193,7 @@ No resources. | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [locations](#input\_locations) | Array of locations used to run synthetic tests | `list(string)` |
[
"all"
]
| no | +| [locations](#input\_locations) | Array of locations used to run synthetic tests | `list(string)` | `[]` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [private\_location\_test\_enabled](#input\_private\_location\_test\_enabled) | Use private locations or the public locations provided by datadog | `bool` | `false` | no | @@ -195,9 +210,9 @@ No resources. | Name | Description | |------|-------------| | [datadog\_synthetics\_test\_ids](#output\_datadog\_synthetics\_test\_ids) | IDs of the created Datadog synthetic tests | +| [datadog\_synthetics\_test\_maps](#output\_datadog\_synthetics\_test\_maps) | Map (name: id) of the created Datadog synthetic tests | | [datadog\_synthetics\_test\_monitor\_ids](#output\_datadog\_synthetics\_test\_monitor\_ids) | IDs of the monitors associated with the Datadog synthetics tests | | [datadog\_synthetics\_test\_names](#output\_datadog\_synthetics\_test\_names) | Names of the created Datadog synthetic tests | -| [datadog\_synthetics\_tests](#output\_datadog\_synthetics\_tests) | The synthetic tests created in Datadog | ## References diff --git a/modules/datadog-synthetics/catalog/synthetics/examples/browser-test.yaml b/modules/datadog-synthetics/catalog/synthetics/examples/browser-test.yaml index eced215a6..f623d6523 100644 --- a/modules/datadog-synthetics/catalog/synthetics/examples/browser-test.yaml +++ b/modules/datadog-synthetics/catalog/synthetics/examples/browser-test.yaml @@ -1,31 +1,38 @@ my-browser-test: - name: "Browser Test" - message: "Browser Test Failed" + name: My Browser Test + status: live type: browser - subtype: http - device_ids: - - "laptop_large" - tags: - - "managed-by:Terraform" - status: "live" - request_definition: - url: "CHANGEME" - method: GET - request_headers: - Accept-Charset: "utf-8, iso-8859-1;q=0.5" - Accept: "text/html" - options_list: - tick_every: 1800 - no_screenshot: false - follow_redirects: false + config: + request: + method: GET + headers: {} + url: https://example.com/login + setCookie: |- + DatadogTest=true + message: "My Browser Test Failed" + options: + device_ids: + - chrome.laptop_large + - edge.tablet + - firefox.mobile_small + ignoreServerCertificateError: false + disableCors: false + disableCsp: false + noScreenshot: false + tick_every: 86400 + min_failure_duration: 0 + min_location_failed: 1 retry: - count: 2 - interval: 10 + count: 0 + interval: 300 monitor_options: - renotify_interval: 300 - browser_step: - - name: "Check current URL" - type: assertCurrentUrl - params: - check: contains - value: "CHANGEME" + renotify_interval: 0 + ci: + executionRule: non_blocking + rumSettings: + isEnabled: false + enableProfiling: false + enableSecurityTesting: false + locations: + - aws:us-east-1 + - aws:us-west-2 diff --git a/modules/datadog-synthetics/main.tf b/modules/datadog-synthetics/main.tf index f6d16aff7..0cf1493e8 100644 --- a/modules/datadog-synthetics/main.tf +++ b/modules/datadog-synthetics/main.tf @@ -50,9 +50,11 @@ module "datadog_synthetics_merge" { module "datadog_synthetics" { source = "cloudposse/platform/datadog//modules/synthetics" - version = "1.0.1" + version = "1.3.0" - datadog_synthetics = local.synthetics_merged + # Disable default tags because we manage them ourselves in this module, because we want to make them lowercase. + default_tags_enabled = false + datadog_synthetics = local.synthetics_merged locations = distinct(compact(concat( var.locations, diff --git a/modules/datadog-synthetics/outputs.tf b/modules/datadog-synthetics/outputs.tf index ab3daaea4..6b6c811d6 100644 --- a/modules/datadog-synthetics/outputs.tf +++ b/modules/datadog-synthetics/outputs.tf @@ -1,8 +1,3 @@ -output "datadog_synthetics_tests" { - value = module.datadog_synthetics.datadog_synthetic_tests - description = "The synthetic tests created in Datadog" -} - output "datadog_synthetics_test_names" { value = module.datadog_synthetics.datadog_synthetics_test_names description = "Names of the created Datadog synthetic tests" @@ -17,3 +12,8 @@ output "datadog_synthetics_test_monitor_ids" { value = module.datadog_synthetics.datadog_synthetics_test_monitor_ids description = "IDs of the monitors associated with the Datadog synthetics tests" } + +output "datadog_synthetics_test_maps" { + value = { for v in module.datadog_synthetics.datadog_synthetic_tests : v.name => v.id } + description = "Map (name: id) of the created Datadog synthetic tests" +} diff --git a/modules/datadog-synthetics/variables.tf b/modules/datadog-synthetics/variables.tf index c3ff9f236..3cda7a56c 100644 --- a/modules/datadog-synthetics/variables.tf +++ b/modules/datadog-synthetics/variables.tf @@ -34,7 +34,7 @@ variable "context_tags" { variable "config_parameters" { type = map(any) - description = "Map of parameters to Datadog Synthetic configurations" + description = "Map of parameter values to interpolate into Datadog Synthetic configurations" default = {} } @@ -47,7 +47,7 @@ variable "datadog_synthetics_globals" { variable "locations" { type = list(string) description = "Array of locations used to run synthetic tests" - default = ["all"] + default = [] } variable "private_location_test_enabled" { diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 44482e130..0de1f8eb3 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -51,6 +51,16 @@ You can configure who is allowed to assume these roles. helpful because it allows that user, presumably SuperAdmin, to deploy the normal components that expect the user does not have direct access to Terraform state. +### Quotas + +When allowing access to both SAML and AWS SSO users, the trust policy for the IAM roles created by this component +can exceed the default 2048 character limit. If you encounter this error, you can increase the limit by +requesting a quota increase [here](https://us-east-1.console.aws.amazon.com/servicequotas/home/services/iam/quotas/L-C07B4B0D). +Note that this is the IAM limit on "The maximum number of characters in an IAM role trust policy" and it must be +configured in the `us-east-1` region, regardless of what region you are deploying to. Normally 3072 characters +is sufficient, and is recommended so that you still have room to expand the trust policy in the future while +perhaps considering how to reduce its size. + ## Usage **Stack Level**: Regional (because DynamoDB is region-specific), but deploy only in a single region and only in the `root` account From a823d47cdb3fdf0ff6d97e66e230d8726d9e8133 Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Sat, 28 Oct 2023 00:09:58 +0300 Subject: [PATCH 286/501] All DD monitor names must be namespaced (#887) --- modules/datadog-monitor/catalog/monitors/lambda.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/datadog-monitor/catalog/monitors/lambda.yaml b/modules/datadog-monitor/catalog/monitors/lambda.yaml index 9f6754cb7..77683e878 100644 --- a/modules/datadog-monitor/catalog/monitors/lambda.yaml +++ b/modules/datadog-monitor/catalog/monitors/lambda.yaml @@ -2,7 +2,7 @@ # https://docs.datadoghq.com/api/v1/monitors/#create-a-monitor lambda-errors: - name: AWS Lambda [{{functionname.name}}] has errors + name: "(Lambda) ${tenant} ${ stage } - Lambda [{{functionname.name}}] has errors" type: query alert query: sum(last_5m):sum:aws.lambda.errors{*} by {stage,tenant,environment,functionname}.as_count() > 0 message: | From 1698f3e9785e9184394bda80be4b5d03f3bc1d4c Mon Sep 17 00:00:00 2001 From: Max Lobur Date: Sat, 28 Oct 2023 00:29:52 +0300 Subject: [PATCH 287/501] All DD monitor names must be namespaced (#888) --- .../datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml b/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml index 7c583f437..e4a83061a 100644 --- a/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml +++ b/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml @@ -2,7 +2,7 @@ # https://docs.datadoghq.com/api/v1/monitors/#create-a-monitor datadog-lambda-forwarder-config-modification: - name: "(Lambda) ${ stage } - Datadog Lambda Forwarder Config Changed" + name: "(Lambda) ${tenant} ${ stage } - - Datadog Lambda Forwarder Config Changed" type: event-v2 alert query: | events("source:amazon_lambda functionname:${tenant}-${environment}-${ stage }-datadog-lambda-forwarder-logs").rollup("count").last("15m") >= 1 From 15e3bbc9b0847c6fb1289c27c4b836e6da42522a Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Sat, 28 Oct 2023 00:55:32 +0300 Subject: [PATCH 288/501] Bumped datadog monitor module (#824) Co-authored-by: Max Lobur --- modules/datadog-monitor/README.md | 2 +- modules/datadog-monitor/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index cd4312cba..ad69c0e8f 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -178,7 +178,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_monitors](#module\_datadog\_monitors) | cloudposse/platform/datadog//modules/monitors | 1.0.1 | +| [datadog\_monitors](#module\_datadog\_monitors) | cloudposse/platform/datadog//modules/monitors | 1.2.0 | | [datadog\_monitors\_merge](#module\_datadog\_monitors\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [local\_datadog\_monitors\_yaml\_config](#module\_local\_datadog\_monitors\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | diff --git a/modules/datadog-monitor/main.tf b/modules/datadog-monitor/main.tf index 65581bd5f..1bacd8555 100644 --- a/modules/datadog-monitor/main.tf +++ b/modules/datadog-monitor/main.tf @@ -100,7 +100,7 @@ module "datadog_monitors" { count = local.datadog_monitors_enabled ? 1 : 0 source = "cloudposse/platform/datadog//modules/monitors" - version = "1.0.1" + version = "1.2.0" datadog_monitors = local.datadog_monitors From 97051a0c7e9d2b24a9e104dcf918d98bc66e5d74 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 27 Oct 2023 14:57:08 -0700 Subject: [PATCH 289/501] datadog site passed to agent (#691) Co-authored-by: Max Lobur --- modules/eks/datadog-agent/main.tf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/eks/datadog-agent/main.tf b/modules/eks/datadog-agent/main.tf index c99b593a4..a22c81898 100644 --- a/modules/eks/datadog-agent/main.tf +++ b/modules/eks/datadog-agent/main.tf @@ -5,6 +5,7 @@ locals { datadog_api_key = module.datadog_configuration.datadog_api_key datadog_app_key = module.datadog_configuration.datadog_app_key + datadog_site = module.datadog_configuration.datadog_site # combine context tags with passed in datadog_tags # skip name since that won't be relevant for each metric @@ -119,6 +120,11 @@ module "datadog_agent" { name = "datadog.appKey" type = "string" value = local.datadog_app_key + }, + { + name = "datadog.site" + type = "string" + value = local.datadog_site } ] From 0b20a4564b8c7f6d52bcc482465e2c7002f8570f Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 31 Oct 2023 14:45:34 -0700 Subject: [PATCH 290/501] [opgenie-team] Bugfix: add members to teams (#889) --- modules/opsgenie-team/CHANGELOG.md | 16 ++++++++++ modules/opsgenie-team/README.md | 31 +++++++++++++------ modules/opsgenie-team/main.tf | 22 ++++++------- .../opsgenie-team/modules/escalation/main.tf | 13 ++++---- modules/opsgenie-team/outputs.tf | 8 ++--- modules/opsgenie-team/variables.tf | 14 +++++++-- modules/opsgenie-team/versions.tf | 2 +- 7 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 modules/opsgenie-team/CHANGELOG.md diff --git a/modules/opsgenie-team/CHANGELOG.md b/modules/opsgenie-team/CHANGELOG.md new file mode 100644 index 000000000..ed8ac39d2 --- /dev/null +++ b/modules/opsgenie-team/CHANGELOG.md @@ -0,0 +1,16 @@ +## Changes in PR #889, expected Component version ~1.334.0 + +### `team` replaced with `team_options` + +The `team` variable has been replaced with `team_options` to reduce confusion. +The component only ever creates at most one team, with the name +specified in the `name` variable. The `team` variable was introduced to +provide a single object to specify other options, but was not implemented +properly. + +### Team membership now managed by this component by default + +Previously, the default behavior was to not manage team membership, +allowing users to be managed via the Opsgenie UI. Now the default is to manage +via the `members` input. To restore the previous behavior, set +`team_options.ignore_members` to `true`. diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index 92989c135..9f769d80a 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -7,12 +7,23 @@ This component is responsible for provisioning Opsgenie teams and related servic #### Pre-requisites You need an API Key stored in `/opsgenie/opsgenie_api_key` of SSM, this is configurable using the `ssm_parameter_name_format` and `ssm_path` variables. -Generate an API Key by going [here](https://id.atlassian.com/manage-profile/security/api-tokens) and Clicking **Create API Token**. +Opsgenie is now part of Atlassian, so you need to make sure you are creating +an Opsgenie API Key, which looks like `abcdef12-3456-7890-abcd-ef0123456789` +and not an Atlassian API key, which looks like + +```shell +ATAfT3xFfGF0VFXAfl8EmQNPVv1Hlazp3wsJgTmM8Ph7iP-RtQyiEfw-fkDS2LvymlyUOOhc5XiSx46vQWnznCJolq-GMX4KzdvOSPhEWr-BF6LEkJQC4CSjDJv0N7d91-0gVekNmCD2kXY9haUHUSpO4H7X6QxyImUb9VmOKIWTbQi8rf4CF28=63CB21B9 +``` + +Generate an API Key by going to Settings -> API key management on your Opsgenie +control panel, which will have an address like `https://.app.opsgenie.com/settings/api-key-management`, +and click the "Add new API key" button. For more information, see the +[Opsgenie API key management documentation](https://support.atlassian.com/opsgenie/docs/api-key-management/). Once you have the key, you'll need to test it with a curl to verify that you are at least on a Standard plan with OpsGenie: ``` -curl -X GET 'https://api.opsgenie.com/v2/account' +curl -X GET 'https://api.opsgenie.com/v2/account' \ --header "Authorization: GenieKey $API_KEY" ``` @@ -28,7 +39,7 @@ The result should be something similar to below: } ``` -If you see anything other than `Standard` or `Enterprise` in the plan, then you won't be able +If you see `Free` or `Essentials` in the plan, then you won't be able to use this component. You can see more details here: [OpsGenie pricing/features](https://www.atlassian.com/software/opsgenie/pricing#) @@ -199,7 +210,7 @@ AWS_PROFILE=foo chamber list opsgenie-team/ ``` ### ClickOps Work - - After deploying the opsgenie-team component the created team will have a schedule named after the team. This is purposely left to be clickOps’d so the UI can be used to set who is on call, as that is the usual way (not through code). Additionally We do not want a re-apply of the terraform to delete or shuffle who is planned to be on call, thus we left who is on-call on a schedule out of the component. + - After deploying the opsgenie-team component the created team will have a schedule named after the team. This is purposely left to be clickOps’d so the UI can be used to set who is on call, as that is the usual way (not through code). Additionally, we do not want a re-apply of the Terraform to delete or shuffle who is planned to be on call, thus we left who is on-call on a schedule out of the component. ## Known Issues @@ -210,6 +221,10 @@ The problem is there are 3 different api endpoints in use - `/v2/` - robust with some differences from `webapp` - `/v1/` - the oldest and furthest from the live UI. +### Cannot create users + +This module does not create users. Users must have already been created to be added to a team. + ### Cannot Add dependent Services - Api Currently doesn't support Multiple ServiceIds for incident Rules @@ -218,10 +233,6 @@ The problem is there are 3 different api endpoints in use - Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/278 -### There isn’t a resource for datadog to create an opsgenie integration so this has to be done manually via ClickOps - - - Track the issue: x - ### No Resource to create Slack Integration - Track the issue: https://github.com/DataDog/terraform-provider-datadog/issues/67 @@ -271,7 +282,7 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [datadog](#requirement\_datadog) | >= 3.3.0 | | [opsgenie](#requirement\_opsgenie) | >= 0.6.7 | @@ -343,9 +354,9 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [ssm\_path](#input\_ssm\_path) | SSM path | `string` | `"opsgenie"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [team](#input\_team) | Configure the team inputs | `map(any)` | `{}` | no | | [team\_name](#input\_team\_name) | Current OpsGenie Team Name | `string` | `null` | no | | [team\_naming\_format](#input\_team\_naming\_format) | OpsGenie Team Naming Format | `string` | `"%s_%s"` | no | +| [team\_options](#input\_team\_options) | Configure the team options.
See `opsgenie_team` Terraform resource [documentation](https://registry.terraform.io/providers/opsgenie/opsgenie/latest/docs/resources/team#argument-reference) for more details. |
object({
description = optional(string)
ignore_members = optional(bool, false)
delete_default_resources = optional(bool, false)
})
| `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | ## Outputs diff --git a/modules/opsgenie-team/main.tf b/modules/opsgenie-team/main.tf index 1e5d4c250..1944e3a4e 100644 --- a/modules/opsgenie-team/main.tf +++ b/modules/opsgenie-team/main.tf @@ -23,7 +23,7 @@ data "opsgenie_team" "existing" { } data "opsgenie_user" "team_members" { - for_each = local.enabled ? { + for_each = local.enabled && !var.team_options.ignore_members ? { for member in var.members : member.user => member } : {} @@ -37,7 +37,7 @@ module "members_merge" { # Cannot use context to disable # See issue: https://github.com/cloudposse/terraform-yaml-config/issues/18 - count = local.enabled && lookup(var.team, "ignore_members", false) ? 1 : 0 + count = local.enabled && !var.team_options.ignore_members ? 1 : 0 maps = [ data.opsgenie_user.team_members, @@ -57,7 +57,7 @@ module "team" { team = merge({ name = module.this.name members = try(module.members_merge[0].merged, []) - }, var.team) + }, var.team_options, try(length(var.team_options.description), 0) == 0 ? { description = module.this.name } : {}) context = module.this.context } @@ -67,7 +67,7 @@ module "integration" { # We add Datadog here because we need the core input for the team. # Can be overridden by var.integrations.datadog - for_each = var.integrations_enabled ? merge({ + for_each = local.enabled && var.integrations_enabled ? merge({ datadog : { type : "Datadog" } @@ -102,7 +102,7 @@ module "service" { source = "cloudposse/incident-management/opsgenie//modules/service" version = "0.16.0" - for_each = var.services + for_each = local.enabled ? var.services : {} # Only create if not reusing an existing team enabled = local.create_all_enabled @@ -122,11 +122,11 @@ module "schedule" { source = "cloudposse/incident-management/opsgenie//modules/schedule" version = "0.16.0" - for_each = { + for_each = local.enabled ? { for k, v in var.schedules : k => v if try(v.enabled == true, false) - } + } : {} # Only create if not reusing an existing team enabled = local.create_all_enabled @@ -148,11 +148,11 @@ module "schedule" { module "routing" { source = "./modules/routing" - for_each = { + for_each = local.enabled ? { for k, v in var.routing_rules : k => v if try(v.enabled == true, false) - } + } : {} # Only create if not reusing an existing team enabled = local.create_all_enabled @@ -192,11 +192,11 @@ module "routing" { module "escalation" { source = "./modules/escalation" - for_each = { + for_each = local.enabled ? { for k, v in var.escalations : k => v if try(v.enabled == true, false) - } + } : {} # Only create if not reusing an existing team enabled = local.create_all_enabled diff --git a/modules/opsgenie-team/modules/escalation/main.tf b/modules/opsgenie-team/modules/escalation/main.tf index f98dba087..8083d710c 100644 --- a/modules/opsgenie-team/modules/escalation/main.tf +++ b/modules/opsgenie-team/modules/escalation/main.tf @@ -1,19 +1,20 @@ locals { - lookup_teams = distinct(flatten([ + enabled = module.this.enabled && var.escalation != null && length(var.escalation.rules) > 0 + lookup_teams = local.enabled ? distinct(flatten([ for rule in var.escalation.rules : rule.recipient.name if rule.recipient.type == "team" - ])) - lookup_users = distinct(flatten([ + ])) : [] + lookup_users = local.enabled ? distinct(flatten([ for rule in var.escalation.rules : rule.recipient.name if rule.recipient.type == "user" - ])) - lookup_schedules = distinct(flatten([ + ])) : [] + lookup_schedules = local.enabled ? distinct(flatten([ for rule in var.escalation.rules : format(var.team_naming_format, var.team_name, rule.recipient.name) if rule.recipient.type == "schedule" && module.this.enabled - ])) + ])) : [] } data "opsgenie_team" "recipient" { diff --git a/modules/opsgenie-team/outputs.tf b/modules/opsgenie-team/outputs.tf index d53b72a99..129575969 100644 --- a/modules/opsgenie-team/outputs.tf +++ b/modules/opsgenie-team/outputs.tf @@ -4,7 +4,7 @@ output "team_members" { } output "team_name" { - value = local.team_name + value = local.enabled ? local.team_name : null description = "Team Name" } @@ -14,16 +14,16 @@ output "team_id" { } output "integration" { - value = module.integration + value = local.enabled ? module.integration : null description = "Integrations created" } output "routing" { - value = module.routing + value = local.enabled ? module.routing : null description = "Routing rules created" } output "escalation" { - value = module.escalation + value = local.enabled ? module.escalation : null description = "Escalation rules created" } diff --git a/modules/opsgenie-team/variables.tf b/modules/opsgenie-team/variables.tf index 2ea925d27..fffae0d5a 100644 --- a/modules/opsgenie-team/variables.tf +++ b/modules/opsgenie-team/variables.tf @@ -46,10 +46,18 @@ variable "integrations_enabled" { description = "Whether to enable the integrations submodule or not" } -variable "team" { - type = map(any) +variable "team_options" { + type = object({ + description = optional(string) + ignore_members = optional(bool, false) + delete_default_resources = optional(bool, false) + }) + description = <<-EOT + Configure the team options. + See `opsgenie_team` Terraform resource [documentation](https://registry.terraform.io/providers/opsgenie/opsgenie/latest/docs/resources/team#argument-reference) for more details. + EOT default = {} - description = "Configure the team inputs" + nullable = false } variable "escalations" { diff --git a/modules/opsgenie-team/versions.tf b/modules/opsgenie-team/versions.tf index 9e545cdee..8dfc3b0bf 100644 --- a/modules/opsgenie-team/versions.tf +++ b/modules/opsgenie-team/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { From 181cb8d6c10b4b3fe51dd3f1ccf612451d59ac17 Mon Sep 17 00:00:00 2001 From: Matthias Fuhrmeister Date: Wed, 1 Nov 2023 16:18:09 +0100 Subject: [PATCH 291/501] Passthough AMI ID list to dynamic-subnets module (#892) Co-authored-by: Andriy Knysh --- modules/vpc/README.md | 1 + modules/vpc/main.tf | 1 + modules/vpc/variables.tf | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 19dadd33c..f464210ba 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -120,6 +120,7 @@ components: | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [nat\_eip\_aws\_shield\_protection\_enabled](#input\_nat\_eip\_aws\_shield\_protection\_enabled) | Enable or disable AWS Shield Advanced protection for NAT EIPs. If set to 'true', a subscription to AWS Shield Advanced must exist in this account. | `bool` | `false` | no | | [nat\_gateway\_enabled](#input\_nat\_gateway\_enabled) | Flag to enable/disable NAT gateways | `bool` | `true` | no | +| [nat\_instance\_ami\_id](#input\_nat\_instance\_ami\_id) | A list optionally containing the ID of the AMI to use for the NAT instance.
If the list is empty (the default), the latest official AWS NAT instance AMI
will be used. NOTE: The Official NAT instance AMI is being phased out and
does not support NAT64. Use of a NAT gateway is recommended instead. | `list(string)` | `[]` | no | | [nat\_instance\_enabled](#input\_nat\_instance\_enabled) | Flag to enable/disable NAT instances | `bool` | `false` | no | | [nat\_instance\_type](#input\_nat\_instance\_type) | NAT Instance type | `string` | `"t3.micro"` | no | | [public\_subnets\_enabled](#input\_public\_subnets\_enabled) | If false, do not create public subnets.
Since NAT gateways and instances must be created in public subnets, these will also not be created when `false`. | `bool` | `true` | no | diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index e082e94ad..da314dca3 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -152,6 +152,7 @@ module "subnets" { nat_gateway_enabled = var.nat_gateway_enabled nat_instance_enabled = var.nat_instance_enabled nat_instance_type = var.nat_instance_type + nat_instance_ami_id = var.nat_instance_ami_id public_subnets_enabled = var.public_subnets_enabled public_subnets_additional_tags = local.public_subnets_additional_tags private_subnets_additional_tags = local.private_subnets_additional_tags diff --git a/modules/vpc/variables.tf b/modules/vpc/variables.tf index 36a72cbb7..683f0c514 100644 --- a/modules/vpc/variables.tf +++ b/modules/vpc/variables.tf @@ -122,6 +122,17 @@ variable "nat_instance_type" { default = "t3.micro" } +variable "nat_instance_ami_id" { + type = list(string) + description = <<-EOT + A list optionally containing the ID of the AMI to use for the NAT instance. + If the list is empty (the default), the latest official AWS NAT instance AMI + will be used. NOTE: The Official NAT instance AMI is being phased out and + does not support NAT64. Use of a NAT gateway is recommended instead. + EOT + default = [] +} + variable "map_public_ip_on_launch" { type = bool default = true From 6ddbb03e9d7ea506f2d98705221ad84011bad8a0 Mon Sep 17 00:00:00 2001 From: Matthias Fuhrmeister Date: Thu, 2 Nov 2023 15:12:58 +0100 Subject: [PATCH 292/501] Passthought variables for rds-cluster module (#894) --- modules/aurora-postgres/README.md | 2 ++ modules/aurora-postgres/cluster-regional.tf | 2 ++ modules/aurora-postgres/variables.tf | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index b6f461c45..dcf371345 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -281,6 +281,7 @@ components: | [admin\_password](#input\_admin\_password) | Postgres password for the admin user | `string` | `""` | no | | [admin\_user](#input\_admin\_user) | Postgres admin user name | `string` | `""` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.
e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
}

Defaults to the "vpc" component in the given account |
list(object({
vpc = optional(string, "vpc")
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | +| [allow\_major\_version\_upgrade](#input\_allow\_major\_version\_upgrade) | Enable to allow major engine version upgrades when changing engine versions. Defaults to false. | `bool` | `false` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDRs allowed to access the database (in addition to security groups and subnets) | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [autoscaling\_enabled](#input\_autoscaling\_enabled) | Whether to enable cluster autoscaling | `bool` | `false` | no | @@ -291,6 +292,7 @@ components: | [autoscaling\_scale\_out\_cooldown](#input\_autoscaling\_scale\_out\_cooldown) | The amount of time, in seconds, after a scaling activity completes and before the next scaling up activity can start. Default is 300s | `number` | `300` | no | | [autoscaling\_target\_metrics](#input\_autoscaling\_target\_metrics) | The metrics type to use. If this value isn't provided the default is CPU utilization | `string` | `"RDSReaderAverageCPUUtilization"` | no | | [autoscaling\_target\_value](#input\_autoscaling\_target\_value) | The target value to scale with respect to target metrics | `number` | `75` | no | +| [ca\_cert\_identifier](#input\_ca\_cert\_identifier) | The identifier of the CA certificate for the DB instance | `string` | `null` | no | | [cluster\_dns\_name\_part](#input\_cluster\_dns\_name\_part) | Part of DNS name added to module and cluster name for DNS for cluster endpoint | `string` | `"writer"` | no | | [cluster\_family](#input\_cluster\_family) | Family of the DB parameter group. Valid values for Aurora PostgreSQL: `aurora-postgresql9.6`, `aurora-postgresql10`, `aurora-postgresql11`, `aurora-postgresql12` | `string` | `"aurora-postgresql13"` | no | | [cluster\_name](#input\_cluster\_name) | Short name for this cluster | `string` | n/a | yes | diff --git a/modules/aurora-postgres/cluster-regional.tf b/modules/aurora-postgres/cluster-regional.tf index d9de0f7bb..ccd911d6c 100644 --- a/modules/aurora-postgres/cluster-regional.tf +++ b/modules/aurora-postgres/cluster-regional.tf @@ -51,6 +51,8 @@ module "aurora_postgres_cluster" { skip_final_snapshot = var.skip_final_snapshot deletion_protection = var.deletion_protection snapshot_identifier = var.snapshot_identifier + allow_major_version_upgrade = var.allow_major_version_upgrade + ca_cert_identifier = var.ca_cert_identifier cluster_parameters = [ { diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 0f2e465f9..3c3de97ea 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -55,6 +55,18 @@ variable "engine_version" { default = "13.4" } +variable "allow_major_version_upgrade" { + type = bool + default = false + description = "Enable to allow major engine version upgrades when changing engine versions. Defaults to false." +} + +variable "ca_cert_identifier" { + description = "The identifier of the CA certificate for the DB instance" + type = string + default = null +} + variable "engine_mode" { type = string description = "The database engine mode. Valid values: `global`, `multimaster`, `parallelquery`, `provisioned`, `serverless`" From b581b7da3d6cdcbc1a6aa7b065d604109bcca791 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 2 Nov 2023 16:41:30 -0700 Subject: [PATCH 293/501] [eks/echo-server] Deprecate ALB controller specific echo-server (#893) --- .pre-commit-config.yaml | 1 + deprecated/eks/echo-server/README.md | 161 ++++++++++ .../charts/echo-server/.helmignore | 23 ++ .../echo-server/charts/echo-server/Chart.yaml | 24 ++ .../charts/echo-server/templates/NOTES.txt | 22 ++ .../charts/echo-server/templates/_helpers.tpl | 63 ++++ .../echo-server/templates/deployment.yaml | 30 ++ .../charts/echo-server/templates/ingress.yaml | 57 ++++ .../charts/echo-server/templates/service.yaml | 15 + .../charts/echo-server/values.yaml | 96 ++++++ deprecated/eks/echo-server/context.tf | 279 ++++++++++++++++++ deprecated/eks/echo-server/helm-variables.tf | 72 +++++ deprecated/eks/echo-server/main.tf | 63 ++++ deprecated/eks/echo-server/outputs.tf | 4 + deprecated/eks/echo-server/provider-helm.tf | 166 +++++++++++ deprecated/eks/echo-server/providers.tf | 19 ++ deprecated/eks/echo-server/remote-state.tf | 17 ++ deprecated/eks/echo-server/variables.tf | 22 ++ deprecated/eks/echo-server/versions.tf | 18 ++ modules/eks/echo-server/CHANGELOG.md | 22 ++ modules/eks/echo-server/README.md | 54 +++- .../echo-server/charts/echo-server/Chart.yaml | 4 +- .../echo-server/templates/deployment.yaml | 24 ++ .../charts/echo-server/templates/ingress.yaml | 43 +-- .../charts/echo-server/values.yaml | 83 ++---- modules/eks/echo-server/main.tf | 16 +- modules/eks/echo-server/outputs.tf | 5 + modules/eks/echo-server/remote-state.tf | 9 - modules/eks/echo-server/variables.tf | 6 - 29 files changed, 1297 insertions(+), 121 deletions(-) create mode 100644 deprecated/eks/echo-server/README.md create mode 100644 deprecated/eks/echo-server/charts/echo-server/.helmignore create mode 100644 deprecated/eks/echo-server/charts/echo-server/Chart.yaml create mode 100644 deprecated/eks/echo-server/charts/echo-server/templates/NOTES.txt create mode 100644 deprecated/eks/echo-server/charts/echo-server/templates/_helpers.tpl create mode 100644 deprecated/eks/echo-server/charts/echo-server/templates/deployment.yaml create mode 100644 deprecated/eks/echo-server/charts/echo-server/templates/ingress.yaml create mode 100644 deprecated/eks/echo-server/charts/echo-server/templates/service.yaml create mode 100644 deprecated/eks/echo-server/charts/echo-server/values.yaml create mode 100644 deprecated/eks/echo-server/context.tf create mode 100644 deprecated/eks/echo-server/helm-variables.tf create mode 100644 deprecated/eks/echo-server/main.tf create mode 100644 deprecated/eks/echo-server/outputs.tf create mode 100644 deprecated/eks/echo-server/provider-helm.tf create mode 100644 deprecated/eks/echo-server/providers.tf create mode 100644 deprecated/eks/echo-server/remote-state.tf create mode 100644 deprecated/eks/echo-server/variables.tf create mode 100644 deprecated/eks/echo-server/versions.tf create mode 100644 modules/eks/echo-server/CHANGELOG.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8cc1f010..e73fb4263 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,7 @@ repos: - id: check-yaml # checks yaml files for parseable syntax. exclude: | (?x)^( + deprecated/eks/.*/charts/.*/templates/.*.yaml deprecated/github-actions-runner/runners/actions-runner/chart/templates/.*.yaml | modules/eks/cert-manager/cert-manager-issuer/templates/.*.yaml | modules/strongdm/charts/strongdm/templates/.*.yaml | diff --git a/deprecated/eks/echo-server/README.md b/deprecated/eks/echo-server/README.md new file mode 100644 index 000000000..de7a28cec --- /dev/null +++ b/deprecated/eks/echo-server/README.md @@ -0,0 +1,161 @@ +# Component: `eks/echo-server` + +This is copied from [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/echo-server). + +This component installs the [Ealenn/Echo-Server](https://github.com/Ealenn/Echo-Server) to EKS clusters. +The echo server is a server that sends it back to the client a JSON representation of all the data +the server received, which is a combination of information sent by the client and information sent +by the web server infrastructure. For further details, please see [Echo-Server documentation](https://ealenn.github.io/Echo-Server/). + +## Prerequisites + +Echo server is intended to provide end-to-end testing of everything needed to deploy an application or service with a public HTTPS endpoint. +Therefore, it requires several other components. + +At the moment, it supports 2 configurations: + +1. ALB with ACM Certificate + - AWS Load Balancer Controller (ALB) version 2.2.0 or later, with ACM certificate auto-discovery enabled + - Pre-provisioned ACM TLS certificate covering the provisioned host name (typically a wildcard certificate covering all hosts in the domain) +2. Nginx with Cert Manager Certificate + - Nginx (via `kubernetes/ingress-nginx` controller). We recommend `ingress-nginx` v1.1.0 or later, but `echo-server` + should work with any version that supports Ingress API version `networking.k8s.io/v1`. + - `jetstack/cert-manager` configured to automatically (via Ingress Shim, installed by default) generate TLS certificates via a Cluster Issuer + (by default, named `letsEncrypt-prod`). + +In both configurations, it has these common requirements: +- Kubernetes version 1.19 or later +- Ingress API version `networking.k8s.io/v1` +- [kubernetes-sigs/external-dns](https://github.com/kubernetes-sigs/external-dns) +- A default IngressClass, either explicitly provisioned or supported without provisioning by the Ingress controller. + +## Warnings + +A Terraform plan may fail to apply, giving a Kubernetes authentication failure. This is due to a known issue with +Terraform and the Kubernetes provider. During the "plan" phase Terraform gets a short-lived Kubernetes +authentication token and caches it, and then tries to use it during "apply". If the token has expired by +the time you try to run "apply", the "apply" will fail. The workaround is to run `terraform apply -auto-approve` without +a "plan" file. + +## Usage + +**Stack Level**: Regional + +Use this in the catalog or use these variables to overwrite the catalog values. + +```yaml +components: + terraform: + eks/echo-server: + metadata: + component: eks/echo-server + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: "echo-server" + kubernetes_namespace: "echo" + description: "Echo server, for testing purposes" + create_namespace: true + timeout: 180 + wait: true + atomic: true + cleanup_on_fail: true + + ingress_type: "alb" + # %[1]v is the tenant name, %[2]v is the stage name, %[3]v is the region name + hostname_template: "echo.%[3]v.%[2]v.%[1]v.sample-domain.net" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.10.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_controller\_ingress\_group\_component\_name](#input\_alb\_controller\_ingress\_group\_component\_name) | The name of the alb\_controller\_ingress\_group component | `string` | `"eks/alb-controller-ingress-group"` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart\_values](#input\_chart\_values) | Addition map values to yamlencode as `helm_release` values. | `any` | `{}` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [hostname\_template](#input\_hostname\_template) | The `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`"
Typically something like `"echo.%[3]v.%[2]v.example.com"`. | `string` | n/a | yes | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [ingress\_type](#input\_ingress\_type) | Set to 'nginx' to create an ingress resource relying on an NGiNX backend for the echo-server service. Set to 'alb' to create an ingress resource relying on an AWS ALB backend for the echo-server service. Leave blank to not create any ingress for the echo-server service. | `string` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [repository](#input\_repository) | Repository URL where to locate the requested chart. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | +| [verify](#input\_verify) | Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart | `bool` | `false` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [metadata](#output\_metadata) | Block status of the deployed release | + + +## References +* https://github.com/Ealenn/Echo-Server diff --git a/deprecated/eks/echo-server/charts/echo-server/.helmignore b/deprecated/eks/echo-server/charts/echo-server/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/deprecated/eks/echo-server/charts/echo-server/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deprecated/eks/echo-server/charts/echo-server/Chart.yaml b/deprecated/eks/echo-server/charts/echo-server/Chart.yaml new file mode 100644 index 000000000..24c16905b --- /dev/null +++ b/deprecated/eks/echo-server/charts/echo-server/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: "echo-server" +description: A server that replicates the request sent by the client and sends it back. + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.3.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.3.0" diff --git a/deprecated/eks/echo-server/charts/echo-server/templates/NOTES.txt b/deprecated/eks/echo-server/charts/echo-server/templates/NOTES.txt new file mode 100644 index 000000000..c2e6e75b1 --- /dev/null +++ b/deprecated/eks/echo-server/charts/echo-server/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "echo-server.name" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "echo-server.name" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "echo-server.name" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "echo-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deprecated/eks/echo-server/charts/echo-server/templates/_helpers.tpl b/deprecated/eks/echo-server/charts/echo-server/templates/_helpers.tpl new file mode 100644 index 000000000..6ac8f57cb --- /dev/null +++ b/deprecated/eks/echo-server/charts/echo-server/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "echo-server.name" -}} + {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "echo-server.fullname" -}} + {{- if .Values.fullnameOverride }} + {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} + {{- else }} + {{- $name := default .Chart.Name .Values.nameOverride }} + {{- if contains $name .Release.Name }} + {{- .Release.Name | trunc 63 | trimSuffix "-" }} + {{- else }} + {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} + {{- end }} + {{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "echo-server.chart" -}} + {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels + helm.sh/chart: {{ include "echo-server.chart" . }} + {{- if .Chart.AppVersion }} + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + {{- end }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +*/}} +{{- define "echo-server.labels" -}} + {{ include "echo-server.selectorLabels" . }} +{{- end }} + +{{/* +Selector labels + app.kubernetes.io/name: {{ include "echo-server.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +*/}} +{{- define "echo-server.selectorLabels" -}} + app: {{ include "echo-server.fullname" . }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "echo-server.serviceAccountName" -}} + {{- if .Values.serviceAccount.create }} + {{- default (include "echo-server.fullname" .) .Values.serviceAccount.name }} + {{- else }} + {{- default "default" .Values.serviceAccount.name }} + {{- end }} +{{- end }} diff --git a/deprecated/eks/echo-server/charts/echo-server/templates/deployment.yaml b/deprecated/eks/echo-server/charts/echo-server/templates/deployment.yaml new file mode 100644 index 000000000..1e85f1c36 --- /dev/null +++ b/deprecated/eks/echo-server/charts/echo-server/templates/deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "echo-server.fullname" . }} + labels: + {{- include "echo-server.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "echo-server.selectorLabels" . | nindent 6 }} + template: + metadata: + name: {{ include "echo-server.fullname" . }} + labels: + {{- include "echo-server.selectorLabels" . | nindent 8 }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + # Disable the feature that turns the echo server into a file browser on the server (security risk) + - "--enable:file=false" + ports: + - name: http + containerPort: 80 + protocol: TCP diff --git a/deprecated/eks/echo-server/charts/echo-server/templates/ingress.yaml b/deprecated/eks/echo-server/charts/echo-server/templates/ingress.yaml new file mode 100644 index 000000000..86c8bb577 --- /dev/null +++ b/deprecated/eks/echo-server/charts/echo-server/templates/ingress.yaml @@ -0,0 +1,57 @@ +{{- if or (eq (printf "%v" .Values.ingress.nginx.enabled) "true") (eq (printf "%v" .Values.ingress.alb.enabled) "true") -}} + {{- $fullName := include "echo-server.fullname" . -}} + {{- $svcName := include "echo-server.name" . -}} + {{- $svcPort := .Values.service.port -}} + {{- $nginxTlsEnabled := and (eq (printf "%v" .Values.ingress.nginx.enabled) "true") (eq (printf "%v" .Values.tlsEnabled) "true")}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + annotations: + {{- if eq (printf "%v" .Values.ingress.nginx.enabled) "true" }} + kubernetes.io/ingress.class: {{ .Values.ingress.nginx.class }} + {{- if (index .Values.ingress.nginx "tls_certificate_cluster_issuer") }} + cert-manager.io/cluster-issuer: {{ .Values.ingress.nginx.tls_certificate_cluster_issuer }} + {{- end }} + {{- else if eq (printf "%v" .Values.ingress.alb.enabled) "true" }} + kubernetes.io/ingress.class: {{ .Values.ingress.alb.class }} + {{- if not .Values.ingress.alb.group_name }} + alb.ingress.kubernetes.io/load-balancer-name: {{ index .Values.ingress.alb "load_balancer_name" | default "k8s-common" }} + {{- end }} + alb.ingress.kubernetes.io/group.name: {{ index .Values.ingress.alb "group_name" | default "common" }} + alb.ingress.kubernetes.io/scheme: {{ index .Values.ingress.alb "scheme" | default "internet-facing" }} + {{- if .Values.ingress.alb.access_logs.enabled }} + alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket={{.Values.ingress.alb.access_logs.s3_bucket_name}},access_logs.s3.prefix={{.Values.ingress.alb.access_logs.s3_bucket_prefix}} + {{- end }} + alb.ingress.kubernetes.io/target-type: 'ip' + {{- if eq (printf "%v" .Values.ingress.alb.ssl_redirect.enabled) "true" }} + alb.ingress.kubernetes.io/ssl-redirect: '{{ .Values.ingress.alb.ssl_redirect.port }}' + {{- end }} + {{- if eq (printf "%v" .Values.tlsEnabled) "true" }} + alb.ingress.kubernetes.io/backend-protocol: HTTP + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80},{"HTTPS":443}]' + {{- else }} + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]' + {{- end }} + {{- end }} + labels: + {{- include "echo-server.labels" . | nindent 4 }} +spec: + {{- if $nginxTlsEnabled }} + tls: # < placing a host in the TLS config will indicate a certificate should be created + - hosts: + - {{ .Values.ingress.hostname }} + secretName: {{ $svcName }}-cert # < cert-manager will store the created certificate in this secret. + {{- end }} + rules: + - host: {{ .Values.ingress.hostname }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ $svcName }} + port: + number: {{ $svcPort }} +{{- end }} diff --git a/deprecated/eks/echo-server/charts/echo-server/templates/service.yaml b/deprecated/eks/echo-server/charts/echo-server/templates/service.yaml new file mode 100644 index 000000000..014977e05 --- /dev/null +++ b/deprecated/eks/echo-server/charts/echo-server/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "echo-server.name" . }} + labels: + {{- include "echo-server.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "echo-server.selectorLabels" . | nindent 4 }} diff --git a/deprecated/eks/echo-server/charts/echo-server/values.yaml b/deprecated/eks/echo-server/charts/echo-server/values.yaml new file mode 100644 index 000000000..777654c4d --- /dev/null +++ b/deprecated/eks/echo-server/charts/echo-server/values.yaml @@ -0,0 +1,96 @@ +# Default values for echo-server. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + # image.repository -- https://hub.docker.com/r/ealen/echo-server + repository: ealen/echo-server + # image.tag -- https://github.com/Ealenn/Echo-Server/releases + tag: 0.4.2 + pullPolicy: Always + +#imagePullSecrets: [] +nameOverride: "" +#fullnameOverride: "" + +#serviceAccount: +# # Specifies whether a service account should be created +# create: true +# # Annotations to add to the service account +# annotations: {} +# # The name of the service account to use. +# # If not set and create is true, a name is generated using the fullname template +# name: "" + +#podAnnotations: {} + +#podSecurityContext: {} +# # fsGroup: 2000 + +#securityContext: {} +# # capabilities: +# # drop: +# # - ALL +# # readOnlyRootFilesystem: true +# # runAsNonRoot: true +# # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +tlsEnabled: true + +ingress: + nginx: + # ingress.nginx.enabled -- Enable NGiNX ingress + enabled: false + # annotation values + ## kubernetes.io/ingress.class: + class: "nginx" + ## cert-manager.io/cluster-issuer: + tls_certificate_cluster_issuer: "letsencrypt-prod" + alb: + enabled: true + # annotation values + ## kubernetes.io/ingress.class: + class: "alb" + ## alb.ingress.kubernetes.io/load-balancer-name: + ### load_balancer_name: "k8s-common" + ## alb.ingress.kubernetes.io/group.name: + ### group_name: "common" + ssl_redirect: + enabled: true + ## alb.ingress.kubernetes.io/ssl-redirect: + port: 443 + access_logs: + enabled: false + ## s3_bucket_name: "acme-ue2-prod-eks-cluster-alb-access-logs" + s3_bucket_prefix: "echo-server" + +#resources: {} +# # We usually recommend not to specify default resources and to leave this as a conscious +# # choice for the user. This also increases chances charts run on environments with little +# # resources, such as Minikube. If you do want to specify resources, uncomment the following +# # lines, adjust them as necessary, and remove the curly braces after 'resources:'. +# # limits: +# # cpu: 100m +# # memory: 128Mi +# # requests: +# # cpu: 100m +# # memory: 128Mi + +autoscaling: + enabled: false + #minReplicas: 1 + #maxReplicas: 100 + #targetCPUUtilizationPercentage: 80 + #targetMemoryUtilizationPercentage: 80 + +#nodeSelector: {} + +#tolerations: [] + +#affinity: {} diff --git a/deprecated/eks/echo-server/context.tf b/deprecated/eks/echo-server/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/deprecated/eks/echo-server/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/deprecated/eks/echo-server/helm-variables.tf b/deprecated/eks/echo-server/helm-variables.tf new file mode 100644 index 000000000..53ca0364a --- /dev/null +++ b/deprecated/eks/echo-server/helm-variables.tf @@ -0,0 +1,72 @@ +variable "kubernetes_namespace" { + type = string + description = "The namespace to install the release into." +} + +variable "description" { + type = string + description = "Set release description attribute (visible in the history)." + default = null +} + +variable "repository" { + type = string + description = "Repository URL where to locate the requested chart." + default = null +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." + default = null +} + +variable "create_namespace" { + type = bool + description = "Create the Kubernetes namespace if it does not yet exist" + default = true +} + +variable "verify" { + type = bool + description = "Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart" + default = false +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`." + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used." + default = true +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails." + default = true +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = null +} + +variable "ingress_type" { + type = string + default = null + description = "Set to 'nginx' to create an ingress resource relying on an NGiNX backend for the echo-server service. Set to 'alb' to create an ingress resource relying on an AWS ALB backend for the echo-server service. Leave blank to not create any ingress for the echo-server service." +} + +variable "hostname_template" { + type = string + description = <<-EOT + The `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`" + Typically something like `"echo.%[3]v.%[2]v.example.com"`. + EOT +} diff --git a/deprecated/eks/echo-server/main.tf b/deprecated/eks/echo-server/main.tf new file mode 100644 index 000000000..b58d0dca2 --- /dev/null +++ b/deprecated/eks/echo-server/main.tf @@ -0,0 +1,63 @@ +locals { + ingress_nginx_enabled = var.ingress_type == "nginx" ? true : false + ingress_alb_enabled = var.ingress_type == "alb" ? true : false +} + +module "echo_server" { + source = "cloudposse/helm-release/aws" + version = "0.10.0" + + name = module.this.name + chart = "${path.module}/charts/echo-server" + + # Optional arguments + description = var.description + repository = var.repository + chart_version = var.chart_version + verify = var.verify + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + create_namespace_with_kubernetes = var.create_namespace + kubernetes_namespace = var.kubernetes_namespace + kubernetes_namespace_labels = merge(module.this.tags, { name = var.kubernetes_namespace }) + + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") + + set = [ + { + name = "ingress.hostname" + value = format(var.hostname_template, var.tenant, var.stage, var.environment) + type = "auto" + }, + { + name = "ingress.nginx.enabled" + value = local.ingress_nginx_enabled + type = "auto" + }, + { + name = "ingress.alb.group_name" + value = module.alb.outputs.group_name + type = "auto" + }, + { + name = "ingress.alb.enabled" + value = local.ingress_alb_enabled + type = "auto" + }, + { + name = "ingress.alb.scheme" + value = module.alb.outputs.load_balancer_scheme + type = "auto" + }, + ] + + values = compact([ + # additional values + try(length(var.chart_values), 0) == 0 ? null : yamlencode(var.chart_values) + ]) + + context = module.this.context +} diff --git a/deprecated/eks/echo-server/outputs.tf b/deprecated/eks/echo-server/outputs.tf new file mode 100644 index 000000000..3199457ce --- /dev/null +++ b/deprecated/eks/echo-server/outputs.tf @@ -0,0 +1,4 @@ +output "metadata" { + value = try(one(module.echo_server.metadata), null) + description = "Block status of the deployed release" +} diff --git a/deprecated/eks/echo-server/provider-helm.tf b/deprecated/eks/echo-server/provider-helm.tf new file mode 100644 index 000000000..64459d4f4 --- /dev/null +++ b/deprecated/eks/echo-server/provider-helm.tf @@ -0,0 +1,166 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/deprecated/eks/echo-server/providers.tf b/deprecated/eks/echo-server/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/deprecated/eks/echo-server/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/deprecated/eks/echo-server/remote-state.tf b/deprecated/eks/echo-server/remote-state.tf new file mode 100644 index 000000000..a31ae6bb6 --- /dev/null +++ b/deprecated/eks/echo-server/remote-state.tf @@ -0,0 +1,17 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + context = module.this.context +} + +module "alb" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.alb_controller_ingress_group_component_name + + context = module.this.context +} diff --git a/deprecated/eks/echo-server/variables.tf b/deprecated/eks/echo-server/variables.tf new file mode 100644 index 000000000..48da55209 --- /dev/null +++ b/deprecated/eks/echo-server/variables.tf @@ -0,0 +1,22 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "alb_controller_ingress_group_component_name" { + type = string + description = "The name of the alb_controller_ingress_group component" + default = "eks/alb-controller-ingress-group" +} + +variable "chart_values" { + type = any + description = "Addition map values to yamlencode as `helm_release` values." + default = {} +} diff --git a/deprecated/eks/echo-server/versions.tf b/deprecated/eks/echo-server/versions.tf new file mode 100644 index 000000000..fb8857fab --- /dev/null +++ b/deprecated/eks/echo-server/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1, != 2.21.0" + } + } +} diff --git a/modules/eks/echo-server/CHANGELOG.md b/modules/eks/echo-server/CHANGELOG.md new file mode 100644 index 000000000..a2a187cae --- /dev/null +++ b/modules/eks/echo-server/CHANGELOG.md @@ -0,0 +1,22 @@ +## Changes in PR #893, components version ~v1.337.0 + +- Moved `eks/echo-server` v1.147.0 to `/deprecated/eks/echo-server` for those +who still need it and do not want to switch. It may later become the basis +for an example app or something similar. +- Removed dependency on and connection to the `eks/alb-controller-ingress-group` component +- Added liveness probe, and disabled logging of probe requests. Probe request +logging can be restored by setting `livenessProbeLogging: true` in `chart_values` +- This component no longer configures automatic redirects from HTTP to HTTPS. This +is because for ALB controller, setting that on one ingress sets it for all +ingresses in the same IngressGroup, and it is a design goal that deploying +this component does not affect other Ingresses (with the obvious exception +of possibly being the first to create the Application Load Balancer). +- Removed from `chart_values`:`ingress.nginx.class` (was set to "nginx") and +`ingress.alb.class` (was set to "alb"). IngressClass should usually not be set, +as this component is intended to be used to test the defaults, including the +default IngressClass. However, if you do want to set it, you can do so by +setting `ingress.class` in `chart_values`. +- Removed the deprecated `kubernetes.io/ingress.class` annotation by default. +It can be restored by setting `ingress.use_ingress_class_annotation: true` in `chart_values`. +IngressClass is now set using the preferred `ingressClassName` field of the +Ingress resource. diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index de7a28cec..34efca50f 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -5,17 +5,26 @@ This is copied from [cloudposse/terraform-aws-components](https://github.com/clo This component installs the [Ealenn/Echo-Server](https://github.com/Ealenn/Echo-Server) to EKS clusters. The echo server is a server that sends it back to the client a JSON representation of all the data the server received, which is a combination of information sent by the client and information sent -by the web server infrastructure. For further details, please see [Echo-Server documentation](https://ealenn.github.io/Echo-Server/). +by the web server infrastructure. For further details, please consult the [Echo-Server documentation](https://ealenn.github.io/Echo-Server/). ## Prerequisites -Echo server is intended to provide end-to-end testing of everything needed to deploy an application or service with a public HTTPS endpoint. -Therefore, it requires several other components. +Echo server is intended to provide end-to-end testing of everything needed +to deploy an application or service with a public HTTPS endpoint. It uses +defaults where possible, such as using the default IngressClass, in order +to verify that the defaults are sufficient for a typical application. -At the moment, it supports 2 configurations: +In order to minimize the impact of the echo server on the rest of the cluster, +it does not set any configuration that would affect other ingresses, such +as WAF rules, logging, or redirecting HTTP to HTTPS. Those settings should +be configured in the IngressClass where possible. + +Therefore, it requires several other components. At the moment, it supports 2 configurations: 1. ALB with ACM Certificate - AWS Load Balancer Controller (ALB) version 2.2.0 or later, with ACM certificate auto-discovery enabled + - A default IngressClass, which can be provisioned by the `alb-controller` component as part of deploying + the controller, or can be provisioned separately, for example by the `alb-controller-ingress-class` component. - Pre-provisioned ACM TLS certificate covering the provisioned host name (typically a wildcard certificate covering all hosts in the domain) 2. Nginx with Cert Manager Certificate - Nginx (via `kubernetes/ingress-nginx` controller). We recommend `ingress-nginx` v1.1.0 or later, but `echo-server` @@ -24,6 +33,7 @@ At the moment, it supports 2 configurations: (by default, named `letsEncrypt-prod`). In both configurations, it has these common requirements: +- EKS component deployed, with component name specified in `eks_component_name` (defaults to "eks/cluster") - Kubernetes version 1.19 or later - Ingress API version `networking.k8s.io/v1` - [kubernetes-sigs/external-dns](https://github.com/kubernetes-sigs/external-dns) @@ -43,12 +53,31 @@ a "plan" file. Use this in the catalog or use these variables to overwrite the catalog values. +Set `ingress_type` to "alb" if using `alb-controller` or "nginx" if using `ingress-nginx`. + +Normally, you should not set the IngressClass or IngressGroup, as this component is intended to test the defaults. +However, if you need to, set them in `chart_values`: +```yaml +chart_values: + ingress: + class: "other-ingress-class" + alb: + # IngressGroup is specific to alb-controller + group_name: "other-ingress-group" +``` + +Note that if you follow recommendations and do not set the ingress class name, +the deployed Ingress will have the ingressClassName setting injected by the +Ingress controller, set to the then-current default. This means that if later +you change the default IngressClass, the Ingress will be NOT be updated to use +the new default. Furthermore, because of limitations in the Helm provider, this +will not be detected as drift. You will need to destroy and re-deploy the +echo server to update the Ingress to the new default. + ```yaml components: terraform: - eks/echo-server: - metadata: - component: eks/echo-server + echo-server: settings: spacelift: workspace_enabled: true @@ -63,11 +92,15 @@ components: atomic: true cleanup_on_fail: true - ingress_type: "alb" + ingress_type: "alb" # or "nginx" # %[1]v is the tenant name, %[2]v is the stage name, %[3]v is the region name hostname_template: "echo.%[3]v.%[2]v.%[1]v.sample-domain.net" ``` +In rare cases where some ingress controllers do not support the `ingressClassName` field, +you can restore the old `kubernetes.io/ingress.class` annotation by setting +`ingress.use_ingress_class_annotation: true` in `chart_values`. + ## Requirements @@ -88,8 +121,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.10.0 | +| [echo\_server](#module\_echo\_server) | cloudposse/helm-release/aws | 0.10.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -105,7 +137,6 @@ components: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [alb\_controller\_ingress\_group\_component\_name](#input\_alb\_controller\_ingress\_group\_component\_name) | The name of the alb\_controller\_ingress\_group component | `string` | `"eks/alb-controller-ingress-group"` | no | | [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [chart\_values](#input\_chart\_values) | Addition map values to yamlencode as `helm_release` values. | `any` | `{}` | no | @@ -154,6 +185,7 @@ components: | Name | Description | |------|-------------| +| [hostname](#output\_hostname) | Hostname of the deployed echo server | | [metadata](#output\_metadata) | Block status of the deployed release | diff --git a/modules/eks/echo-server/charts/echo-server/Chart.yaml b/modules/eks/echo-server/charts/echo-server/Chart.yaml index 24c16905b..8fae0334e 100644 --- a/modules/eks/echo-server/charts/echo-server/Chart.yaml +++ b/modules/eks/echo-server/charts/echo-server/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.0 +version: 0.4.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.3.0" +appVersion: "0.8.0" diff --git a/modules/eks/echo-server/charts/echo-server/templates/deployment.yaml b/modules/eks/echo-server/charts/echo-server/templates/deployment.yaml index 1e85f1c36..7d690e39c 100644 --- a/modules/eks/echo-server/charts/echo-server/templates/deployment.yaml +++ b/modules/eks/echo-server/charts/echo-server/templates/deployment.yaml @@ -24,7 +24,31 @@ spec: args: # Disable the feature that turns the echo server into a file browser on the server (security risk) - "--enable:file=false" + {{- if eq (printf "%v" .Values.livenessProbeLogging) "false" }} + - "--logs:ignore:ping=true" + {{- end }} ports: - name: http containerPort: 80 protocol: TCP + livenessProbe: + httpGet: + port: http + path: /ping + httpHeaders: + - name: x-echo-code + value: "200" + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + successThreshold: 1 + {{- with index .Values "resources" }} + resources: + limits: + cpu: {{ index . "limits.cpu" | default "50m" }} + memory: {{ index . "limits.memory" | default "128Mi" }} + requests: + cpu: {{ index . "requests.cpu" | default "50m" }} + memory: {{ index . "requests.memory" | default "128Mi" }} + {{- end }} diff --git a/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml b/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml index 86c8bb577..703af694c 100644 --- a/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml +++ b/modules/eks/echo-server/charts/echo-server/templates/ingress.yaml @@ -2,41 +2,42 @@ {{- $fullName := include "echo-server.fullname" . -}} {{- $svcName := include "echo-server.name" . -}} {{- $svcPort := .Values.service.port -}} - {{- $nginxTlsEnabled := and (eq (printf "%v" .Values.ingress.nginx.enabled) "true") (eq (printf "%v" .Values.tlsEnabled) "true")}} + {{- $nginxTlsEnabled := and (eq (printf "%v" .Values.ingress.nginx.enabled) "true") (eq (printf "%v" .Values.tlsEnabled) "true") }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ $fullName }} annotations: - {{- if eq (printf "%v" .Values.ingress.nginx.enabled) "true" }} - kubernetes.io/ingress.class: {{ .Values.ingress.nginx.class }} - {{- if (index .Values.ingress.nginx "tls_certificate_cluster_issuer") }} - cert-manager.io/cluster-issuer: {{ .Values.ingress.nginx.tls_certificate_cluster_issuer }} - {{- end }} - {{- else if eq (printf "%v" .Values.ingress.alb.enabled) "true" }} - kubernetes.io/ingress.class: {{ .Values.ingress.alb.class }} - {{- if not .Values.ingress.alb.group_name }} - alb.ingress.kubernetes.io/load-balancer-name: {{ index .Values.ingress.alb "load_balancer_name" | default "k8s-common" }} - {{- end }} - alb.ingress.kubernetes.io/group.name: {{ index .Values.ingress.alb "group_name" | default "common" }} - alb.ingress.kubernetes.io/scheme: {{ index .Values.ingress.alb "scheme" | default "internet-facing" }} - {{- if .Values.ingress.alb.access_logs.enabled }} - alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket={{.Values.ingress.alb.access_logs.s3_bucket_name}},access_logs.s3.prefix={{.Values.ingress.alb.access_logs.s3_bucket_prefix}} - {{- end }} - alb.ingress.kubernetes.io/target-type: 'ip' - {{- if eq (printf "%v" .Values.ingress.alb.ssl_redirect.enabled) "true" }} - alb.ingress.kubernetes.io/ssl-redirect: '{{ .Values.ingress.alb.ssl_redirect.port }}' + {{- with and (eq (printf "%v" .Values.ingress.use_ingress_class_annotation) "true") (index .Values.ingress "class") }} + kubernetes.io/ingress.class: {{ . }} + {{- end }} + {{- with and $nginxTlsEnabled (index .Values.ingress.nginx "tls_certificate_cluster_issuer") }} + cert-manager.io/cluster-issuer: {{ . }} + {{- end }} + {{- if eq (printf "%v" .Values.ingress.alb.enabled) "true" }} + alb.ingress.kubernetes.io/healthcheck-path: /ping + {{- with index .Values.ingress.alb "group_name" }} + alb.ingress.kubernetes.io/group.name: {{ . }} {{- end }} {{- if eq (printf "%v" .Values.tlsEnabled) "true" }} alb.ingress.kubernetes.io/backend-protocol: HTTP - alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80},{"HTTPS":443}]' + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80},{"HTTPS":443}]' {{- else }} - alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]' + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]' {{- end }} + # See https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.5/guide/ingress/annotations/#target-type + alb.ingress.kubernetes.io/target-type: {{ if eq (printf "%v" .Values.service.type) "NodePort" -}} "instance" {{- else -}} "ip" {{- end }} {{- end }} labels: {{- include "echo-server.labels" . | nindent 4 }} spec: + # If not specified, the Ingress controller will insert the ingressClassName field + # when creating the Ingress resource, setting ingressClassName to the name of the then-default IngressClass. + {{- with and (ne (printf "%v" .Values.ingress.use_ingress_class_annotation) "true") (index .Values.ingress "class") }} + ingressClassName: {{ . }} + {{- end }} + # ALB controller will auto-discover the ACM certificate based on rules[].host + # Nginx needs explicit configuration of location of cert-manager TLS certificate {{- if $nginxTlsEnabled }} tls: # < placing a host in the TLS config will indicate a certificate should be created - hosts: diff --git a/modules/eks/echo-server/charts/echo-server/values.yaml b/modules/eks/echo-server/charts/echo-server/values.yaml index 777654c4d..0f5e270a2 100644 --- a/modules/eks/echo-server/charts/echo-server/values.yaml +++ b/modules/eks/echo-server/charts/echo-server/values.yaml @@ -8,79 +8,48 @@ image: # image.repository -- https://hub.docker.com/r/ealen/echo-server repository: ealen/echo-server # image.tag -- https://github.com/Ealenn/Echo-Server/releases - tag: 0.4.2 - pullPolicy: Always + tag: 0.8.12 + pullPolicy: IfNotPresent #imagePullSecrets: [] nameOverride: "" #fullnameOverride: "" -#serviceAccount: -# # Specifies whether a service account should be created -# create: true -# # Annotations to add to the service account -# annotations: {} -# # The name of the service account to use. -# # If not set and create is true, a name is generated using the fullname template -# name: "" - -#podAnnotations: {} - -#podSecurityContext: {} -# # fsGroup: 2000 - -#securityContext: {} -# # capabilities: -# # drop: -# # - ALL -# # readOnlyRootFilesystem: true -# # runAsNonRoot: true -# # runAsUser: 1000 service: type: ClusterIP port: 80 tlsEnabled: true +# If livenessProbeLogging is false, requests to /ping will not be logged +livenessProbeLogging: false ingress: + ## Allow class to be specified, but use default class (not class named "default") by default + # class: default + + # Use deprecated `kubernetes.io/ingress.class` annotation + use_ingress_class_annotation: false nginx: # ingress.nginx.enabled -- Enable NGiNX ingress enabled: false - # annotation values - ## kubernetes.io/ingress.class: - class: "nginx" - ## cert-manager.io/cluster-issuer: tls_certificate_cluster_issuer: "letsencrypt-prod" alb: - enabled: true - # annotation values - ## kubernetes.io/ingress.class: - class: "alb" - ## alb.ingress.kubernetes.io/load-balancer-name: - ### load_balancer_name: "k8s-common" - ## alb.ingress.kubernetes.io/group.name: - ### group_name: "common" - ssl_redirect: - enabled: true - ## alb.ingress.kubernetes.io/ssl-redirect: - port: 443 - access_logs: - enabled: false - ## s3_bucket_name: "acme-ue2-prod-eks-cluster-alb-access-logs" - s3_bucket_prefix: "echo-server" + enabled: false + ## Allow group to be specified, but use default by default + # group_name: common -#resources: {} -# # We usually recommend not to specify default resources and to leave this as a conscious -# # choice for the user. This also increases chances charts run on environments with little -# # resources, such as Minikube. If you do want to specify resources, uncomment the following -# # lines, adjust them as necessary, and remove the curly braces after 'resources:'. -# # limits: -# # cpu: 100m -# # memory: 128Mi -# # requests: -# # cpu: 100m -# # memory: 128Mi + # Do NOT allow SSL redirect to be specified, because that affects other ingresses. + # "Once defined on a single Ingress, it impacts every Ingress within IngressGroup." + # See https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.6/guide/ingress/annotations/#ssl-redirect + +resources: + limits: + cpu: 50m + memory: 128Mi +# requests: +# cpu: 50m +# memory: 128Mi autoscaling: enabled: false @@ -88,9 +57,3 @@ autoscaling: #maxReplicas: 100 #targetCPUUtilizationPercentage: 80 #targetMemoryUtilizationPercentage: 80 - -#nodeSelector: {} - -#tolerations: [] - -#affinity: {} diff --git a/modules/eks/echo-server/main.tf b/modules/eks/echo-server/main.tf index b58d0dca2..5d24c2681 100644 --- a/modules/eks/echo-server/main.tf +++ b/modules/eks/echo-server/main.tf @@ -1,11 +1,13 @@ locals { ingress_nginx_enabled = var.ingress_type == "nginx" ? true : false ingress_alb_enabled = var.ingress_type == "alb" ? true : false + + hostname = module.this.enabled ? format(var.hostname_template, var.tenant, var.stage, var.environment) : null } module "echo_server" { source = "cloudposse/helm-release/aws" - version = "0.10.0" + version = "0.10.1" name = module.this.name chart = "${path.module}/charts/echo-server" @@ -29,7 +31,7 @@ module "echo_server" { set = [ { name = "ingress.hostname" - value = format(var.hostname_template, var.tenant, var.stage, var.environment) + value = local.hostname type = "auto" }, { @@ -37,21 +39,11 @@ module "echo_server" { value = local.ingress_nginx_enabled type = "auto" }, - { - name = "ingress.alb.group_name" - value = module.alb.outputs.group_name - type = "auto" - }, { name = "ingress.alb.enabled" value = local.ingress_alb_enabled type = "auto" }, - { - name = "ingress.alb.scheme" - value = module.alb.outputs.load_balancer_scheme - type = "auto" - }, ] values = compact([ diff --git a/modules/eks/echo-server/outputs.tf b/modules/eks/echo-server/outputs.tf index 3199457ce..05893b697 100644 --- a/modules/eks/echo-server/outputs.tf +++ b/modules/eks/echo-server/outputs.tf @@ -2,3 +2,8 @@ output "metadata" { value = try(one(module.echo_server.metadata), null) description = "Block status of the deployed release" } + +output "hostname" { + value = local.hostname + description = "Hostname of the deployed echo server" +} diff --git a/modules/eks/echo-server/remote-state.tf b/modules/eks/echo-server/remote-state.tf index a31ae6bb6..c1ec8226d 100644 --- a/modules/eks/echo-server/remote-state.tf +++ b/modules/eks/echo-server/remote-state.tf @@ -6,12 +6,3 @@ module "eks" { context = module.this.context } - -module "alb" { - source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.5.0" - - component = var.alb_controller_ingress_group_component_name - - context = module.this.context -} diff --git a/modules/eks/echo-server/variables.tf b/modules/eks/echo-server/variables.tf index 48da55209..f28f8f89a 100644 --- a/modules/eks/echo-server/variables.tf +++ b/modules/eks/echo-server/variables.tf @@ -9,12 +9,6 @@ variable "eks_component_name" { default = "eks/cluster" } -variable "alb_controller_ingress_group_component_name" { - type = string - description = "The name of the alb_controller_ingress_group component" - default = "eks/alb-controller-ingress-group" -} - variable "chart_values" { type = any description = "Addition map values to yamlencode as `helm_release` values." From 08ae165415c7b8aee5a3c12e0bcfe5794194d6a6 Mon Sep 17 00:00:00 2001 From: Matthias Fuhrmeister Date: Mon, 6 Nov 2023 17:57:52 +0100 Subject: [PATCH 294/501] Argocd hostname must not depend on ingress name (#896) --- modules/eks/argocd/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 4937c48c1..679598adb 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -46,7 +46,7 @@ locals { ] : [] )) regional_service_discovery_domain = "${module.this.environment}.${module.dns_gbl_delegated.outputs.default_domain_name}" - host = var.host != "" ? var.host : format("%s.%s", coalesce(var.alb_name, var.name), local.regional_service_discovery_domain) + host = var.host != "" ? var.host : format("%s.%s", var.name, local.regional_service_discovery_domain) url = format("https://%s", local.host) oidc_config_map = local.oidc_enabled ? { From 9bc545587154d7d87dbee30be732f78b076f7d86 Mon Sep 17 00:00:00 2001 From: Brad Janke Date: Mon, 6 Nov 2023 14:53:47 -0600 Subject: [PATCH 295/501] aws-shield is now able to protect alb's from ingress-groups (#897) Co-authored-by: cloudpossebot Co-authored-by: Dan Miller --- modules/aws-shield/README.md | 2 ++ modules/aws-shield/alb.tf | 2 +- modules/aws-shield/main.tf | 2 +- modules/aws-shield/remote-state.tf | 9 +++++++++ modules/aws-shield/variables.tf | 6 ++++++ 5 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 modules/aws-shield/remote-state.tf diff --git a/modules/aws-shield/README.md b/modules/aws-shield/README.md index d20d875c5..b6345477e 100644 --- a/modules/aws-shield/README.md +++ b/modules/aws-shield/README.md @@ -102,6 +102,7 @@ This leads to more simplified inter-component dependencies and minimizes the nee | Name | Source | Version | |------|--------|---------| +| [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -126,6 +127,7 @@ This leads to more simplified inter-component dependencies and minimizes the nee |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [alb\_names](#input\_alb\_names) | list of ALB names which will be protected with AWS Shield Advanced | `list(string)` | `[]` | no | +| [alb\_protection\_enabled](#input\_alb\_protection\_enabled) | Enable ALB protection. By default, ALB names are read from the EKS cluster ALB control group | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [cloudfront\_distribution\_ids](#input\_cloudfront\_distribution\_ids) | list of CloudFront Distribution IDs which will be protected with AWS Shield Advanced | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | diff --git a/modules/aws-shield/alb.tf b/modules/aws-shield/alb.tf index e1238519d..ff0eac196 100644 --- a/modules/aws-shield/alb.tf +++ b/modules/aws-shield/alb.tf @@ -1,5 +1,5 @@ data "aws_alb" "alb" { - for_each = local.alb_protection_enabled ? toset(var.alb_names) : [] + for_each = local.alb_protection_enabled == false ? [] : length(var.alb_names) > 0 ? toset(var.alb_names) : toset([module.alb[0].outputs.load_balancer_name]) name = each.key } diff --git a/modules/aws-shield/main.tf b/modules/aws-shield/main.tf index bed51d5f6..097d0a3e9 100644 --- a/modules/aws-shield/main.tf +++ b/modules/aws-shield/main.tf @@ -7,7 +7,7 @@ locals { # Used to determine correct partition (i.e. - `aws`, `aws-gov`, `aws-cn`, etc.) partition = one(data.aws_partition.current[*].partition) - alb_protection_enabled = local.enabled && length(var.alb_names) > 0 + alb_protection_enabled = local.enabled && local.alb_protection_enabled cloudfront_distribution_protection_enabled = local.enabled && length(var.cloudfront_distribution_ids) > 0 eip_protection_enabled = local.enabled && length(var.eips) > 0 route53_protection_enabled = local.enabled && length(var.route53_zone_names) > 0 diff --git a/modules/aws-shield/remote-state.tf b/modules/aws-shield/remote-state.tf new file mode 100644 index 000000000..98b19290f --- /dev/null +++ b/modules/aws-shield/remote-state.tf @@ -0,0 +1,9 @@ +module "alb" { + count = length(var.alb_names) > 0 ? 0 : 1 + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "eks/alb-controller-ingress-group" + + context = module.this.context +} diff --git a/modules/aws-shield/variables.tf b/modules/aws-shield/variables.tf index bcf1c1b9e..882e3fb86 100644 --- a/modules/aws-shield/variables.tf +++ b/modules/aws-shield/variables.tf @@ -9,6 +9,12 @@ variable "alb_names" { default = [] } +variable "alb_protection_enabled" { + description = "Enable ALB protection. By default, ALB names are read from the EKS cluster ALB control group" + type = bool + default = false +} + variable "cloudfront_distribution_ids" { description = "list of CloudFront Distribution IDs which will be protected with AWS Shield Advanced" type = list(string) From 0ee9cb13428d9fcccddee8ced4e0bddfbeed078d Mon Sep 17 00:00:00 2001 From: Brad Janke Date: Mon, 6 Nov 2023 15:08:19 -0600 Subject: [PATCH 296/501] Fixes typo in aws-shield (#899) --- modules/aws-shield/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aws-shield/main.tf b/modules/aws-shield/main.tf index 097d0a3e9..764f4995a 100644 --- a/modules/aws-shield/main.tf +++ b/modules/aws-shield/main.tf @@ -7,7 +7,7 @@ locals { # Used to determine correct partition (i.e. - `aws`, `aws-gov`, `aws-cn`, etc.) partition = one(data.aws_partition.current[*].partition) - alb_protection_enabled = local.enabled && local.alb_protection_enabled + alb_protection_enabled = local.enabled && var.alb_protection_enabled cloudfront_distribution_protection_enabled = local.enabled && length(var.cloudfront_distribution_ids) > 0 eip_protection_enabled = local.enabled && length(var.eips) > 0 route53_protection_enabled = local.enabled && length(var.route53_zone_names) > 0 From d69fe516c381c329859125d2acda869bf7c53b68 Mon Sep 17 00:00:00 2001 From: Brad Janke Date: Mon, 6 Nov 2023 16:17:00 -0600 Subject: [PATCH 297/501] fix(aws-shield would fail plan if disabled) (#900) --- modules/aws-shield/remote-state.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aws-shield/remote-state.tf b/modules/aws-shield/remote-state.tf index 98b19290f..109fa6c7b 100644 --- a/modules/aws-shield/remote-state.tf +++ b/modules/aws-shield/remote-state.tf @@ -1,5 +1,5 @@ module "alb" { - count = length(var.alb_names) > 0 ? 0 : 1 + count = local.alb_protection_enabled == false ? 0 : length(var.alb_names) > 0 ? 0 : 1 source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" From 7784f16980f1aea4b9c7cf64935f502e4176ef1a Mon Sep 17 00:00:00 2001 From: Brad Janke Date: Mon, 6 Nov 2023 18:03:09 -0600 Subject: [PATCH 298/501] fixes type mismatch in aws-shiield ternary (#901) --- modules/aws-shield/alb.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aws-shield/alb.tf b/modules/aws-shield/alb.tf index ff0eac196..5a7c70aae 100644 --- a/modules/aws-shield/alb.tf +++ b/modules/aws-shield/alb.tf @@ -1,5 +1,5 @@ data "aws_alb" "alb" { - for_each = local.alb_protection_enabled == false ? [] : length(var.alb_names) > 0 ? toset(var.alb_names) : toset([module.alb[0].outputs.load_balancer_name]) + for_each = local.alb_protection_enabled == false ? toset([]) : length(var.alb_names) > 0 ? toset(var.alb_names) : toset([module.alb[0].outputs.load_balancer_name]) name = each.key } From 474ab2f07db593b7b2533a982127371c4b5d445f Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Mon, 6 Nov 2023 19:44:10 -0500 Subject: [PATCH 299/501] fix: Karpenter resources (#895) --- modules/eks/karpenter/main.tf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index 6807609d4..e20f7011f 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -129,7 +129,9 @@ module "karpenter" { serviceAccount = { name = module.this.name } - resources = var.resources + controller = { + resources = var.resources + } rbac = { create = var.rbac_enabled } From 1887df62a3e716c14db949a9f412b97650d2b77f Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 8 Nov 2023 12:03:19 -0800 Subject: [PATCH 300/501] Bugfix: Okta Submodule in `aws-saml` (#903) --- modules/aws-saml/modules/okta-user/main.tf | 4 ++-- modules/aws-saml/modules/okta-user/outputs.tf | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/aws-saml/modules/okta-user/main.tf b/modules/aws-saml/modules/okta-user/main.tf index 146e3717d..e309cfe18 100644 --- a/modules/aws-saml/modules/okta-user/main.tf +++ b/modules/aws-saml/modules/okta-user/main.tf @@ -30,13 +30,13 @@ resource "aws_iam_policy" "default" { } resource "aws_iam_user_policy_attachment" "default" { - user = aws_iam_user.default.name + user = one(aws_iam_user.default[*].name) policy_arn = aws_iam_policy.default.arn } # Generate API credentials resource "aws_iam_access_key" "default" { - user = aws_iam_user.default.name + user = one(aws_iam_user.default[*].name) } resource "aws_ssm_parameter" "okta_user_access_key_id" { diff --git a/modules/aws-saml/modules/okta-user/outputs.tf b/modules/aws-saml/modules/okta-user/outputs.tf index 8caa1f211..9472405e1 100644 --- a/modules/aws-saml/modules/okta-user/outputs.tf +++ b/modules/aws-saml/modules/okta-user/outputs.tf @@ -1,14 +1,14 @@ output "user_name" { - value = aws_iam_user.default.name + value = one(aws_iam_user.default[*].name) description = "User name" } output "user_arn" { - value = aws_iam_user.default.arn + value = one(aws_iam_user.default[*].arn) description = "User ARN" } output "ssm_prefix" { - value = "AWS Key for ${aws_iam_user.default.name} is in Systems Manager Parameter Store under ${aws_ssm_parameter.okta_user_access_key_id.name} and ${aws_ssm_parameter.okta_user_secret_access_key.name}" + value = "AWS Key for ${one(aws_iam_user.default[*].name)} is in Systems Manager Parameter Store under ${aws_ssm_parameter.okta_user_access_key_id.name} and ${aws_ssm_parameter.okta_user_secret_access_key.name}" description = "Where to find the AWS API key information for the user" } From 8ba0deed1e5e4d6d216758897c83d9d960c3376e Mon Sep 17 00:00:00 2001 From: PePe Amengual Date: Mon, 13 Nov 2023 16:44:05 -0800 Subject: [PATCH 301/501] Adding Support for Child OUs (#898) Co-authored-by: cloudpossebot --- modules/account/README.md | 1 + modules/account/main.tf | 36 +++++++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/modules/account/README.md b/modules/account/README.md index 73428af61..045e7ca27 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -360,6 +360,7 @@ atmos terraform apply account --stack gbl-root | [aws_organizations_account.organization_accounts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_account) | resource | | [aws_organizations_account.organizational_units_accounts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_account) | resource | | [aws_organizations_organization.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_organization) | resource | +| [aws_organizations_organizational_unit.child](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_organizational_unit) | resource | | [aws_organizations_organizational_unit.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_organizational_unit) | resource | | [aws_organizations_organization.existing](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/organizations_organization) | data source | diff --git a/modules/account/main.tf b/modules/account/main.tf index fa3c46684..114772d0d 100644 --- a/modules/account/main.tf +++ b/modules/account/main.tf @@ -3,8 +3,10 @@ locals { organization = lookup(var.organization_config, "organization", {}) # Organizational Units list and map configuration - organizational_units = lookup(var.organization_config, "organizational_units", []) - organizational_units_map = { for ou in local.organizational_units : ou.name => ou } + organizational_units = lookup(var.organization_config, "organizational_units", []) + organizational_units_map = { for ou in local.organizational_units : ou.name => merge(ou, { + parent_ou = contains(keys(ou), "parent_ou") ? ou.parent_ou : "none" + }) } # Organization's Accounts list and map configuration organization_accounts = lookup(var.organization_config, "accounts", []) @@ -13,7 +15,7 @@ locals { # Organizational Units' Accounts list and map configuration organizational_units_accounts = flatten([ for ou in local.organizational_units : [ - for account in lookup(ou, "accounts", []) : merge({ "ou" = ou.name, "account_email_format" = lookup(ou, "account_email_format", var.account_email_format) }, account) + for account in lookup(ou, "accounts", []) : merge({ "ou" = ou.name, "account_email_format" = lookup(ou, "account_email_format", var.account_email_format), parent_ou = contains(keys(ou), "parent_ou") ? ou.parent_ou : "none" }, account) ] ]) organizational_units_accounts_map = { for acc in local.organizational_units_accounts : acc.name => acc } @@ -22,13 +24,22 @@ locals { all_accounts = concat(local.organization_accounts, local.organizational_units_accounts) # List of Organizational Unit names - organizational_unit_names = values(aws_organizations_organizational_unit.this)[*]["name"] + organizational_unit_names = concat( + values(aws_organizations_organizational_unit.this)[*]["name"], + values(aws_organizations_organizational_unit.child)[*]["name"] + ) # List of Organizational Unit ARNs - organizational_unit_arns = values(aws_organizations_organizational_unit.this)[*]["arn"] + organizational_unit_arns = concat( + values(aws_organizations_organizational_unit.this)[*]["arn"], + values(aws_organizations_organizational_unit.child)[*]["arn"] + ) # List of Organizational Unit IDs - organizational_unit_ids = values(aws_organizations_organizational_unit.this)[*]["id"] + organizational_unit_ids = concat( + values(aws_organizations_organizational_unit.this)[*]["id"], + values(aws_organizations_organizational_unit.child)[*]["id"] + ) # Map of account names to OU names (used for lookup `parent_id` for each account under an OU) account_names_organizational_unit_names_map = length(local.organizational_units) > 0 ? merge( @@ -127,18 +138,25 @@ resource "aws_organizations_account" "organization_accounts" { } } -# Provision Organizational Units +# Provision Organizational Units w/o Child Orgs resource "aws_organizations_organizational_unit" "this" { - for_each = local.organizational_units_map + for_each = { for key, value in local.organizational_units_map : key => value if value.parent_ou == "none" } name = each.value.name parent_id = local.organization_root_account_id } +# Provision Child Organizational Units +resource "aws_organizations_organizational_unit" "child" { + for_each = { for key, value in local.organizational_units_map : key => value if value.parent_ou != "none" } + name = each.value.name + parent_id = aws_organizations_organizational_unit.this[each.value.parent_ou].id +} + # Provision Accounts connected to Organizational Units resource "aws_organizations_account" "organizational_units_accounts" { for_each = local.organizational_units_accounts_map name = each.value.name - parent_id = aws_organizations_organizational_unit.this[local.account_names_organizational_unit_names_map[each.value.name]].id + parent_id = each.value.parent_ou != "none" ? aws_organizations_organizational_unit.child[each.value.ou].id : aws_organizations_organizational_unit.this[local.account_names_organizational_unit_names_map[each.value.name]].id email = try(format(each.value.account_email_format, each.value.name), each.value.account_email_format) iam_user_access_to_billing = var.account_iam_user_access_to_billing tags = merge(module.this.tags, try(each.value.tags, {}), { Name : each.value.name }) From 3dc7660f02511644b346bd7c467d9511b6002977 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 14 Nov 2023 08:22:19 -0800 Subject: [PATCH 302/501] Spacelift Component `tflint` and Variable Usage (#904) --- modules/spacelift/admin-stack/README.md | 16 ++--- modules/spacelift/admin-stack/child-stacks.tf | 68 ++++++++++--------- .../spacelift/admin-stack/root-admin-stack.tf | 41 +++++------ modules/spacelift/admin-stack/variables.tf | 36 ++++------ modules/spacelift/admin-stack/versions.tf | 4 ++ modules/spacelift/spaces/README.md | 1 - modules/spacelift/spaces/variables.tf | 6 -- modules/spacelift/worker-pool/iam.tf | 16 ++--- modules/spacelift/worker-pool/main.tf | 12 ++-- modules/spacelift/worker-pool/outputs.tf | 10 +-- 10 files changed, 97 insertions(+), 113 deletions(-) diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index e09da20d8..29a93178f 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -142,6 +142,7 @@ components: | [aws](#requirement\_aws) | >= 4.0 | | [null](#requirement\_null) | >= 3.0 | | [spacelift](#requirement\_spacelift) | >= 0.1.31 | +| [utils](#requirement\_utils) | >= 1.14.0 | ## Providers @@ -154,11 +155,11 @@ components: | Name | Source | Version | |------|--------|---------| -| [all\_admin\_stacks\_config](#module\_all\_admin\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.0.0 | -| [child\_stack](#module\_child\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.0.0 | -| [child\_stacks\_config](#module\_child\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.0.0 | -| [root\_admin\_stack](#module\_root\_admin\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.0.0 | -| [root\_admin\_stack\_config](#module\_root\_admin\_stack\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.0.0 | +| [all\_admin\_stacks\_config](#module\_all\_admin\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.4.0 | +| [child\_stack](#module\_child\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.4.0 | +| [child\_stacks\_config](#module\_child\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.4.0 | +| [root\_admin\_stack](#module\_root\_admin\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.4.0 | +| [root\_admin\_stack\_config](#module\_root\_admin\_stack\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.4.0 | | [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -181,7 +182,6 @@ components: |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [admin\_stack\_label](#input\_admin\_stack\_label) | Label to use to identify the admin stack when creating the child stacks | `string` | `"admin-stack-name"` | no | -| [administrative](#input\_administrative) | Whether this stack can manage other stacks | `bool` | `false` | no | | [allow\_public\_workers](#input\_allow\_public\_workers) | Whether to allow public workers to be used for this stack | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [autodeploy](#input\_autodeploy) | Controls the Spacelift 'autodeploy' option for a stack | `bool` | `false` | no | @@ -212,6 +212,7 @@ components: | [drift\_detection\_timezone](#input\_drift\_detection\_timezone) | Timezone in which the schedule is expressed. Defaults to UTC. | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [excluded\_context\_filters](#input\_excluded\_context\_filters) | Context filters to exclude from stacks matching specific criteria of `var.context_filters`. |
object({
namespaces = optional(list(string), [])
environments = optional(list(string), [])
tenants = optional(list(string), [])
stages = optional(list(string), [])
tags = optional(map(string), {})
})
| `{}` | no | | [github\_enterprise](#input\_github\_enterprise) | GitHub Enterprise (self-hosted) VCS settings | `map(any)` | `null` | no | | [gitlab](#input\_gitlab) | GitLab VCS settings | `map(any)` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | @@ -224,12 +225,9 @@ components: | [manage\_state](#input\_manage\_state) | Flag to enable/disable manage\_state setting in stack | `bool` | `false` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [parent\_space\_id](#input\_parent\_space\_id) | If creating a dedicated space for this stack, specify the ID of the parent space in Spacelift. | `string` | `null` | no | -| [policy\_ids](#input\_policy\_ids) | Set of Rego policy IDs to attach to this stack | `set(string)` | `[]` | no | | [protect\_from\_deletion](#input\_protect\_from\_deletion) | Flag to enable/disable deletion protection. | `bool` | `false` | no | | [pulumi](#input\_pulumi) | Pulumi-specific configuration. Presence means this Stack is a Pulumi Stack. | `map(any)` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input\_region) | The AWS region to use | `string` | `"us-east-1"` | no | | [repository](#input\_repository) | The name of your infrastructure repo | `string` | n/a | yes | | [root\_admin\_stack](#input\_root\_admin\_stack) | Flag to indicate if this stack is the root admin stack. In this case, the stack will be created in the root space and will create all the other admin stacks as children. | `bool` | `false` | no | | [root\_stack\_policy\_attachments](#input\_root\_stack\_policy\_attachments) | List of policy attachments to attach to the root admin stack | `set(string)` | `[]` | no | diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index 37cb17887..4b02f69d6 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -44,16 +44,17 @@ resource "null_resource" "child_stack_parent_precondition" { # for each one. module "child_stacks_config" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" - version = "1.0.0" + version = "1.4.0" - context_filters = var.context_filters + context_filters = var.context_filters + excluded_context_filters = var.excluded_context_filters context = module.this.context } module "child_stack" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" - version = "1.0.0" + version = "1.4.0" for_each = local.child_stacks depends_on = [ @@ -70,8 +71,8 @@ module "child_stack" { after_perform = try(each.value.settings.spacelift.after_perform, []) after_plan = try(each.value.settings.spacelift.after_plan, []) atmos_stack_name = try(each.value.stack, null) - autodeploy = try(each.value.settings.spacelift.autodeploy, false) - autoretry = try(each.value.settings.spacelift.autoretry, false) + autodeploy = try(each.value.settings.spacelift.autodeploy, var.autodeploy) + autoretry = try(each.value.settings.spacelift.autoretry, var.autoretry) aws_role_enabled = try(each.value.settings.aws_role_enabled, var.aws_role_enabled) aws_role_arn = try(each.value.settings.aws_role_arn, var.aws_role_arn) aws_role_external_id = try(each.value.settings.aws_role_external_id, var.aws_role_external_id) @@ -83,11 +84,11 @@ module "child_stack" { before_plan = try(each.value.settings.spacelift.before_plan, []) branch = try(each.value.branch, var.branch) commit_sha = var.commit_sha != null ? var.commit_sha : try(each.value.commit_sha, null) - component_env = try(each.value.env, {}) + component_env = try(each.value.env, var.component_env) component_name = try(each.value.component, null) component_root = try(join("/", [var.component_root, try(each.value.metadata.component, each.value.component)])) - component_vars = try(each.value.vars, null) - context_attachments = try(each.value.context_attachments, []) + component_vars = try(each.value.vars, var.component_vars) + context_attachments = try(each.value.context_attachments, var.context_attachments) description = try(each.value.description, var.description) drift_detection_enabled = try(each.value.settings.spacelift.drift_detection_enabled, var.drift_detection_enabled) drift_detection_reconcile = try(each.value.settings.spacelift.drift_detection_reconcile, var.drift_detection_reconcile) @@ -99,32 +100,33 @@ module "child_stack" { ["managed-by:${local.managed_by}"], local.create_root_admin_stack ? ["depends-on:${local.root_admin_stack_name}", ""] : [] ) - local_preview_enabled = try(each.value.local_preview_enabled, var.local_preview_enabled) - manage_state = try(each.value.manage_state, var.manage_state) - policy_ids = try(local.child_policy_ids, []) - protect_from_deletion = try(each.value.settings.spacelift.protect_from_deletion, false) - repository = var.repository - runner_image = try(each.value.settings.spacelift.runner_image, var.runner_image) - space_id = local.spaces[each.value.settings.spacelift.space_name] - spacelift_run_enabled = try(each.value.settings.spacelift.spacelift_run_enabled, var.spacelift_run_enabled) - stack_destructor_enabled = try(each.value.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) - stack_name = try(each.value.settings.spacelift.stack_name, each.key) - terraform_smart_sanitization = try(each.value.terraform_smart_sanitization, false) - terraform_version = lookup(var.terraform_version_map, try(each.value.terraform_version, ""), var.terraform_version) - terraform_workspace = try(each.value.workspace, null) - webhook_enabled = try(each.value.webhook_enabled, var.webhook_enabled) - webhook_endpoint = try(each.value.webhook_endpoint, var.webhook_endpoint) - webhook_secret = try(each.value.webhook_secret, var.webhook_secret) - worker_pool_id = try(local.worker_pools[each.value.worker_pool_name], local.worker_pools[var.worker_pool_name]) + local_preview_enabled = try(each.value.local_preview_enabled, var.local_preview_enabled) + manage_state = try(each.value.manage_state, var.manage_state) + policy_ids = try(local.child_policy_ids, []) + protect_from_deletion = try(each.value.settings.spacelift.protect_from_deletion, var.protect_from_deletion) + repository = var.repository + runner_image = try(each.value.settings.spacelift.runner_image, var.runner_image) + space_id = local.spaces[each.value.settings.spacelift.space_name] + spacelift_run_enabled = try(each.value.settings.spacelift.spacelift_run_enabled, var.spacelift_run_enabled) + spacelift_stack_dependency_enabled = try(each.value.settings.spacelift.spacelift_stack_dependency_enabled, var.spacelift_stack_dependency_enabled) + stack_destructor_enabled = try(each.value.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) + stack_name = try(each.value.settings.spacelift.stack_name, each.key) + terraform_smart_sanitization = try(each.value.terraform_smart_sanitization, var.terraform_smart_sanitization) + terraform_version = lookup(var.terraform_version_map, try(each.value.terraform_version, ""), var.terraform_version) + terraform_workspace = try(each.value.workspace, var.terraform_workspace) + webhook_enabled = try(each.value.webhook_enabled, var.webhook_enabled) + webhook_endpoint = try(each.value.webhook_endpoint, var.webhook_endpoint) + webhook_secret = try(each.value.webhook_secret, var.webhook_secret) + worker_pool_id = try(local.worker_pools[each.value.worker_pool_name], local.worker_pools[var.worker_pool_name]) - azure_devops = try(each.value.azure_devops, null) - bitbucket_cloud = try(each.value.bitbucket_cloud, null) - bitbucket_datacenter = try(each.value.bitbucket_datacenter, null) - cloudformation = try(each.value.cloudformation, null) - github_enterprise = try(local.root_admin_stack_config.settings.spacelift.github_enterprise, null) - gitlab = try(local.root_admin_stack_config.settings.spacelift.gitlab, null) - pulumi = try(local.root_admin_stack_config.settings.spacelift.pulumi, null) - showcase = try(local.root_admin_stack_config.settings.spacelift.showcase, null) + azure_devops = try(each.value.azure_devops, var.azure_devops) + bitbucket_cloud = try(each.value.bitbucket_cloud, var.bitbucket_cloud) + bitbucket_datacenter = try(each.value.bitbucket_datacenter, var.bitbucket_datacenter) + cloudformation = try(each.value.cloudformation, var.cloudformation) + github_enterprise = try(each.value.github_enterprise, var.github_enterprise) + gitlab = try(each.value.gitlab, var.gitlab) + pulumi = try(each.value.pulumi, var.pulumi) + showcase = try(each.value.showcase, var.showcase) context = module.this.context } diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf index 622977914..0086498ee 100644 --- a/modules/spacelift/admin-stack/root-admin-stack.tf +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -3,7 +3,7 @@ # such stack is allowed in the Spacelift organization. module "root_admin_stack_config" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" - version = "1.0.0" + version = "1.4.0" enabled = local.create_root_admin_stack @@ -15,7 +15,7 @@ module "root_admin_stack_config" { # This gets the atmos stack config for all of the administrative stacks module "all_admin_stacks_config" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" - version = "1.0.0" + version = "1.4.0" enabled = local.create_root_admin_stack context_filters = { @@ -25,7 +25,7 @@ module "all_admin_stacks_config" { module "root_admin_stack" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" - version = "1.0.0" + version = "1.4.0" enabled = local.create_root_admin_stack depends_on = [null_resource.spaces_precondition, null_resource.workers_precondition] @@ -37,8 +37,8 @@ module "root_admin_stack" { after_perform = try(local.root_admin_stack_config.settings.spacelift.after_perform, []) after_plan = try(local.root_admin_stack_config.settings.spacelift.after_plan, []) atmos_stack_name = try(local.root_admin_stack_config.stack, null) - autodeploy = try(local.root_admin_stack_config.settings.spacelift.autodeploy, false) - autoretry = try(local.root_admin_stack_config.settings.spacelift.autoretry, false) + autodeploy = try(local.root_admin_stack_config.settings.spacelift.autodeploy, var.autodeploy) + autoretry = try(local.root_admin_stack_config.settings.spacelift.autoretry, var.autoretry) aws_role_enabled = try(local.root_admin_stack_config.settings.aws_role_enabled, var.aws_role_enabled) aws_role_arn = try(local.root_admin_stack_config.settings.aws_role_arn, var.aws_role_arn) aws_role_external_id = try(local.root_admin_stack_config.settings.aws_role_external_id, var.aws_role_external_id) @@ -50,11 +50,11 @@ module "root_admin_stack" { before_plan = try(local.root_admin_stack_config.settings.spacelift.before_plan, []) branch = try(local.root_admin_stack_config.branch, var.branch) commit_sha = var.commit_sha != null ? var.commit_sha : try(local.root_admin_stack_config.commit_sha, null) - component_env = try(local.root_admin_stack_config.env, {}) + component_env = try(local.root_admin_stack_config.env, var.component_env) component_name = try(local.root_admin_stack_config.component, null) component_root = try(join("/", [var.component_root, local.root_admin_stack_config.metadata.component]), null) - component_vars = try(local.root_admin_stack_config.vars, null) - context_attachments = try(local.root_admin_stack_config.context_attachments, []) + component_vars = try(local.root_admin_stack_config.vars, var.component_vars) + context_attachments = try(local.root_admin_stack_config.context_attachments, var.context_attachments) description = try(local.root_admin_stack_config.description, var.description) drift_detection_enabled = try(local.root_admin_stack_config.settings.spacelift.drift_detection_enabled, var.drift_detection_enabled) drift_detection_reconcile = try(local.root_admin_stack_config.settings.spacelift.drift_detection_reconcile, var.drift_detection_reconcile) @@ -63,29 +63,30 @@ module "root_admin_stack" { labels = concat(try(local.root_admin_stack_config.labels, []), try(var.labels, [])) local_preview_enabled = try(local.root_admin_stack_config.local_preview_enabled, var.local_preview_enabled) manage_state = try(local.root_admin_stack_config.manage_state, var.manage_state) - protect_from_deletion = try(local.root_admin_stack_config.settings.spacelift.protect_from_deletion, false) + protect_from_deletion = try(local.root_admin_stack_config.settings.spacelift.protect_from_deletion, var.protect_from_deletion) repository = var.repository runner_image = try(local.root_admin_stack_config.settings.spacelift.runner_image, var.runner_image) - space_id = "root" + space_id = var.space_id spacelift_run_enabled = coalesce(try(local.root_admin_stack_config.settings.spacelift.spacelift_run_enabled, null), var.spacelift_run_enabled) + spacelift_stack_dependency_enabled = try(local.root_admin_stack_config.settings.spacelift.spacelift_stack_dependency_enabled, var.spacelift_stack_dependency_enabled) stack_destructor_enabled = try(local.root_admin_stack_config.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) stack_name = var.stack_name != null ? var.stack_name : local.root_admin_stack_name - terraform_smart_sanitization = try(local.root_admin_stack_config.terraform_smart_sanitization, false) + terraform_smart_sanitization = try(local.root_admin_stack_config.terraform_smart_sanitization, var.terraform_smart_sanitization) terraform_version = lookup(var.terraform_version_map, try(local.root_admin_stack_config.terraform_version, ""), var.terraform_version) - terraform_workspace = try(local.root_admin_stack_config.workspace, null) + terraform_workspace = try(local.root_admin_stack_config.workspace, var.terraform_workspace) webhook_enabled = try(local.root_admin_stack_config.webhook_enabled, var.webhook_enabled) webhook_endpoint = try(local.root_admin_stack_config.webhook_endpoint, var.webhook_endpoint) webhook_secret = try(local.root_admin_stack_config.webhook_secret, var.webhook_secret) worker_pool_id = local.worker_pools[var.worker_pool_name] - azure_devops = try(local.root_admin_stack_config.settings.spacelift.azure_devops, null) - bitbucket_cloud = try(local.root_admin_stack_config.settings.spacelift.bitbucket_cloud, null) - bitbucket_datacenter = try(local.root_admin_stack_config.settings.spacelift.bitbucket_datacenter, null) - cloudformation = try(local.root_admin_stack_config.settings.spacelift.cloudformation, null) - github_enterprise = try(local.root_admin_stack_config.settings.spacelift.github_enterprise, null) - gitlab = try(local.root_admin_stack_config.settings.spacelift.gitlab, null) - pulumi = try(local.root_admin_stack_config.settings.spacelift.pulumi, null) - showcase = try(local.root_admin_stack_config.settings.spacelift.showcase, null) + azure_devops = try(local.root_admin_stack_config.settings.spacelift.azure_devops, var.azure_devops) + bitbucket_cloud = try(local.root_admin_stack_config.settings.spacelift.bitbucket_cloud, var.bitbucket_cloud) + bitbucket_datacenter = try(local.root_admin_stack_config.settings.spacelift.bitbucket_datacenter, var.bitbucket_datacenter) + cloudformation = try(local.root_admin_stack_config.settings.spacelift.cloudformation, var.cloudformation) + github_enterprise = try(local.root_admin_stack_config.settings.spacelift.github_enterprise, var.github_enterprise) + gitlab = try(local.root_admin_stack_config.settings.spacelift.gitlab, var.gitlab) + pulumi = try(local.root_admin_stack_config.settings.spacelift.pulumi, var.pulumi) + showcase = try(local.root_admin_stack_config.settings.spacelift.showcase, var.showcase) } resource "spacelift_policy_attachment" "root" { diff --git a/modules/spacelift/admin-stack/variables.tf b/modules/spacelift/admin-stack/variables.tf index 2779e71b9..d67ce38e0 100644 --- a/modules/spacelift/admin-stack/variables.tf +++ b/modules/spacelift/admin-stack/variables.tf @@ -4,12 +4,6 @@ variable "admin_stack_label" { default = "admin-stack-name" } -variable "administrative" { - type = bool - description = "Whether this stack can manage other stacks" - default = false -} - variable "allow_public_workers" { type = bool description = "Whether to allow public workers to be used for this stack" @@ -129,6 +123,18 @@ variable "context_filters" { }) } +variable "excluded_context_filters" { + description = "Context filters to exclude from stacks matching specific criteria of `var.context_filters`." + default = {} + type = object({ + namespaces = optional(list(string), []) + environments = optional(list(string), []) + tenants = optional(list(string), []) + stages = optional(list(string), []) + tags = optional(map(string), {}) + }) +} + variable "description" { type = string description = "Specify description of stack" @@ -189,18 +195,6 @@ variable "manage_state" { default = false } -variable "parent_space_id" { - type = string - description = "If creating a dedicated space for this stack, specify the ID of the parent space in Spacelift." - default = null -} - -variable "policy_ids" { - type = set(string) - default = [] - description = "Set of Rego policy IDs to attach to this stack" -} - variable "protect_from_deletion" { type = bool description = "Flag to enable/disable deletion protection." @@ -213,12 +207,6 @@ variable "pulumi" { default = null } -variable "region" { - type = string - description = "The AWS region to use" - default = "us-east-1" -} - variable "repository" { type = string description = "The name of your infrastructure repo" diff --git a/modules/spacelift/admin-stack/versions.tf b/modules/spacelift/admin-stack/versions.tf index 89c50bdbe..1bcb05a75 100644 --- a/modules/spacelift/admin-stack/versions.tf +++ b/modules/spacelift/admin-stack/versions.tf @@ -14,5 +14,9 @@ terraform { source = "hashicorp/null" version = ">= 3.0" } + utils = { + source = "cloudposse/utils" + version = ">= 1.14.0" + } } } diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index 670a07e9b..f1122a01b 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -109,7 +109,6 @@ No resources. | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [spaces](#input\_spaces) | A map of all Spaces to create in Spacelift |
map(object({
parent_space_id = string,
description = optional(string),
inherit_entities = optional(bool, false),
labels = optional(set(string), []),
policies = optional(map(object({
body = optional(string),
body_url = optional(string),
body_url_version = optional(string, "master"),
type = optional(string),
labels = optional(set(string), []),
})), {}),
}))
| n/a | yes | -| [ssm\_params\_enabled](#input\_ssm\_params\_enabled) | Whether to write the IDs of the created spaces to SSM parameters | `bool` | `true` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/spacelift/spaces/variables.tf b/modules/spacelift/spaces/variables.tf index 4db9772f9..534bb86c4 100644 --- a/modules/spacelift/spaces/variables.tf +++ b/modules/spacelift/spaces/variables.tf @@ -14,9 +14,3 @@ variable "spaces" { })) description = "A map of all Spaces to create in Spacelift" } - -variable "ssm_params_enabled" { - type = bool - description = "Whether to write the IDs of the created spaces to SSM parameters" - default = true -} diff --git a/modules/spacelift/worker-pool/iam.tf b/modules/spacelift/worker-pool/iam.tf index 3ce1756ba..f1df55935 100644 --- a/modules/spacelift/worker-pool/iam.tf +++ b/modules/spacelift/worker-pool/iam.tf @@ -58,7 +58,7 @@ resource "aws_iam_policy" "default" { count = local.enabled ? 1 : 0 name = module.iam_label.id - policy = join("", data.aws_iam_policy_document.default.*.json) + policy = join("", data.aws_iam_policy_document.default[*].json) tags = module.iam_label.tags } @@ -67,13 +67,13 @@ resource "aws_iam_role" "default" { count = local.enabled ? 1 : 0 name = module.iam_label.id - assume_role_policy = join("", data.aws_iam_policy_document.assume_role_policy.*.json) + assume_role_policy = join("", data.aws_iam_policy_document.assume_role_policy[*].json) managed_policy_arns = [ - join("", aws_iam_policy.default.*.arn), - "arn:${join("", data.aws_partition.current.*.partition)}:iam::aws:policy/AutoScalingReadOnlyAccess", - "arn:${join("", data.aws_partition.current.*.partition)}:iam::aws:policy/CloudWatchAgentServerPolicy", - "arn:${join("", data.aws_partition.current.*.partition)}:iam::aws:policy/AmazonSSMManagedInstanceCore", - "arn:${join("", data.aws_partition.current.*.partition)}:iam::aws:policy/AWSXRayDaemonWriteAccess" + join("", aws_iam_policy.default[*].arn), + "arn:${join("", data.aws_partition.current[*].partition)}:iam::aws:policy/AutoScalingReadOnlyAccess", + "arn:${join("", data.aws_partition.current[*].partition)}:iam::aws:policy/CloudWatchAgentServerPolicy", + "arn:${join("", data.aws_partition.current[*].partition)}:iam::aws:policy/AmazonSSMManagedInstanceCore", + "arn:${join("", data.aws_partition.current[*].partition)}:iam::aws:policy/AWSXRayDaemonWriteAccess" ] tags = module.iam_label.tags @@ -83,7 +83,7 @@ resource "aws_iam_instance_profile" "default" { count = local.enabled ? 1 : 0 name = module.iam_label.id - role = join("", aws_iam_role.default.*.name) + role = join("", aws_iam_role.default[*].name) tags = module.iam_label.tags } diff --git a/modules/spacelift/worker-pool/main.tf b/modules/spacelift/worker-pool/main.tf index 51dc24950..8f90dc894 100644 --- a/modules/spacelift/worker-pool/main.tf +++ b/modules/spacelift/worker-pool/main.tf @@ -2,8 +2,6 @@ locals { enabled = module.this.enabled vpc_id = module.vpc.outputs.vpc_id vpc_private_subnet_ids = module.vpc.outputs.private_subnet_ids - identity_account_name = module.account_map.outputs.identity_account_account_name - identity_account_id = module.account_map.outputs.full_account_map[local.identity_account_name] ecr_repo_arn = module.ecr.outputs.ecr_repo_arn_map[var.ecr_repo_name] ecr_repo_url = module.ecr.outputs.ecr_repo_url_map[var.ecr_repo_name] ecr_account_id = element(split(".", local.ecr_repo_url), 0) @@ -61,8 +59,8 @@ data "cloudinit_config" "config" { ecr_region = local.ecr_region ecr_account_id = local.ecr_account_id spacelift_runner_image = local.spacelift_runner_image - spacelift_worker_pool_private_key = join("", spacelift_worker_pool.primary.*.private_key) - spacelift_worker_pool_config = join("", spacelift_worker_pool.primary.*.config) + spacelift_worker_pool_private_key = join("", spacelift_worker_pool.primary[*].private_key) + spacelift_worker_pool_config = join("", spacelift_worker_pool.primary[*].config) spacelift_domain_name = var.spacelift_domain_name github_netrc_enabled = var.github_netrc_enabled github_netrc_ssm_path_token = var.github_netrc_ssm_path_token @@ -92,16 +90,16 @@ module "autoscale_group" { source = "cloudposse/ec2-autoscale-group/aws" version = "0.35.1" - image_id = var.spacelift_ami_id == null ? join("", data.aws_ami.spacelift.*.image_id) : var.spacelift_ami_id + image_id = var.spacelift_ami_id == null ? join("", data.aws_ami.spacelift[*].image_id) : var.spacelift_ami_id instance_type = var.instance_type mixed_instances_policy = var.mixed_instances_policy subnet_ids = local.vpc_private_subnet_ids health_check_type = var.health_check_type health_check_grace_period = var.health_check_grace_period - user_data_base64 = join("", data.cloudinit_config.config.*.rendered) + user_data_base64 = join("", data.cloudinit_config.config[*].rendered) associate_public_ip_address = false block_device_mappings = var.block_device_mappings - iam_instance_profile_name = join("", aws_iam_instance_profile.default.*.name) + iam_instance_profile_name = join("", aws_iam_instance_profile.default[*].name) security_group_ids = [module.security_group.id] termination_policies = var.termination_policies wait_for_capacity_timeout = var.wait_for_capacity_timeout diff --git a/modules/spacelift/worker-pool/outputs.tf b/modules/spacelift/worker-pool/outputs.tf index fba712d6f..7db7df4f6 100644 --- a/modules/spacelift/worker-pool/outputs.tf +++ b/modules/spacelift/worker-pool/outputs.tf @@ -1,10 +1,10 @@ output "worker_pool_id" { - value = join("", spacelift_worker_pool.primary.*.id) + value = join("", spacelift_worker_pool.primary[*].id) description = "Spacelift worker pool ID" } output "worker_pool_name" { - value = join("", spacelift_worker_pool.primary.*.name) + value = join("", spacelift_worker_pool.primary[*].name) description = "Spacelift worker pool name" } @@ -74,16 +74,16 @@ output "autoscaling_group_health_check_type" { } output "iam_role_name" { - value = join("", aws_iam_role.default.*.name) + value = join("", aws_iam_role.default[*].name) description = "Spacelift IAM Role name" } output "iam_role_id" { - value = join("", aws_iam_role.default.*.unique_id) + value = join("", aws_iam_role.default[*].unique_id) description = "Spacelift IAM Role ID" } output "iam_role_arn" { - value = join("", aws_iam_role.default.*.arn) + value = join("", aws_iam_role.default[*].arn) description = "Spacelift IAM Role ARN" } From f8c22881bd2da46ba6326878923615743477bb4f Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 16 Nov 2023 12:11:25 -0800 Subject: [PATCH 303/501] ECS Cluster ALB Subnet decision per ALB (#908) --- modules/ecs/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ecs/main.tf b/modules/ecs/main.tf index 33a848bfa..97d38cfda 100644 --- a/modules/ecs/main.tf +++ b/modules/ecs/main.tf @@ -167,7 +167,7 @@ module "alb" { for_each = local.enabled ? var.alb_configuration : {} vpc_id = module.vpc.outputs.vpc_id - subnet_ids = var.internal_enabled ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids + subnet_ids = lookup(each.value, "internal_enabled", var.internal_enabled) ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids ip_address_type = lookup(each.value, "ip_address_type", "ipv4") internal = lookup(each.value, "internal_enabled", var.internal_enabled) From 7cb5d8b312f7a876cc91f50d01f088eb57830247 Mon Sep 17 00:00:00 2001 From: Veronika Gnilitska <30597968+gberenice@users.noreply.github.com> Date: Fri, 17 Nov 2023 20:21:04 +0200 Subject: [PATCH 304/501] feat: add module for AWS Inspector V2 (#781) Co-authored-by: cloudpossebot Co-authored-by: Brad Janke --- modules/aws-inspector2/README.md | 127 +++++++++++ modules/aws-inspector2/context.tf | 279 +++++++++++++++++++++++++ modules/aws-inspector2/main.tf | 43 ++++ modules/aws-inspector2/outputs.tf | 4 + modules/aws-inspector2/providers.tf | 18 ++ modules/aws-inspector2/remote-state.tf | 12 ++ modules/aws-inspector2/variables.tf | 77 +++++++ modules/aws-inspector2/versions.tf | 15 ++ 8 files changed, 575 insertions(+) create mode 100644 modules/aws-inspector2/README.md create mode 100644 modules/aws-inspector2/context.tf create mode 100644 modules/aws-inspector2/main.tf create mode 100644 modules/aws-inspector2/outputs.tf create mode 100644 modules/aws-inspector2/providers.tf create mode 100644 modules/aws-inspector2/remote-state.tf create mode 100644 modules/aws-inspector2/variables.tf create mode 100644 modules/aws-inspector2/versions.tf diff --git a/modules/aws-inspector2/README.md b/modules/aws-inspector2/README.md new file mode 100644 index 000000000..351e97ad7 --- /dev/null +++ b/modules/aws-inspector2/README.md @@ -0,0 +1,127 @@ +# Component: `aws-inspector2` + +This component is responsible for configuring Inspector V2 within an AWS Organization. + +## Usage + +**Stack Level**: Regional + +## Deployment Overview + +The deployment of this component requires multiple runs with different variable settings to properly configure the AWS Organization. First, you delegate Inspector V2 central management to the Administrator account (usually `security` account). After the Adminstrator account is delegated, we configure the it to manage Inspector V2 across all the Organization accounts and send all their findings to that account. + +In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization Delegated Administrator account is `security`. + +### Deploy to Organization Management Account + +First, the component is deployed to the AWS Organization Management account `root` in each region in order to configure the [AWS Delegated Administrator account](https://docs.aws.amazon.com/inspector/latest/user/designating-admin.html) that operates Amazon Inspector V2. + +```yaml +# ue1-root +components: + terraform: + aws-inspector2/delegate-orgadmin/ue1: + metadata: + component: aws-inspector2 + vars: + enabled: true + region: us-east-1 +``` + +### Deploy Organization Settings in Delegated Administrator Account + +Now the component can be deployed to the Delegated Administrator Account `security` to create the organization-wide configuration for all the Organization accounts. Note that `var.admin_delegated` set to `true` indicates that the delegation has already been performed from the Organization Management account, and only the resources required for organization-wide configuration will be created. + +```yaml +# ue1-security +components: + terraform: + aws-inspector2/orgadmin-configuration/ue1: + metadata: + component: aws-inspector2 + vars: + enabled: true + region: us-east-1 + admin_delegated: true +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 5.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.3 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_inspector2_delegated_admin_account.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/inspector2_delegated_admin_account) | resource | +| [aws_inspector2_member_association.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/inspector2_member_association) | resource | +| [aws_inspector2_organization_configuration.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/inspector2_organization_configuration) | resource | +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `"core"` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Admininstrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [auto\_enable\_ec2](#input\_auto\_enable\_ec2) | Whether Amazon EC2 scans are automatically enabled for new members of the Amazon Inspector organization. | `bool` | `true` | no | +| [auto\_enable\_ecr](#input\_auto\_enable\_ecr) | Whether Amazon ECR scans are automatically enabled for new members of the Amazon Inspector organization. | `bool` | `true` | no | +| [auto\_enable\_lambda](#input\_auto\_enable\_lambda) | Whether Lambda Function scans are automatically enabled for new members of the Amazon Inspector organization. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delegated\_administrator\_account\_name](#input\_delegated\_administrator\_account\_name) | The name of the account that is the AWS Organization Delegated Administrator account | `string` | `"security"` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [member\_association\_excludes](#input\_member\_association\_excludes) | List of account names to exlude from Amazon Inspector member association | `list(string)` | `[]` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [organization\_management\_account\_name](#input\_organization\_management\_account\_name) | The name of the AWS Organization management account | `string` | `null` | no | +| [privileged](#input\_privileged) | true if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account. This is used to lookup account IDs from account names
using the `account-map` component. | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [aws\_inspector2\_member\_association](#output\_aws\_inspector2\_member\_association) | The Inspector2 member association resource. | + + +## References + +- [Amazon Inspector V2 Documentation](https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html) +- [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/guardduty/common/) + +[](https://cpco.io/component) diff --git a/modules/aws-inspector2/context.tf b/modules/aws-inspector2/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/aws-inspector2/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/aws-inspector2/main.tf b/modules/aws-inspector2/main.tf new file mode 100644 index 000000000..1f1c26719 --- /dev/null +++ b/modules/aws-inspector2/main.tf @@ -0,0 +1,43 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map + + current_account_id = one(data.aws_caller_identity.this[*].account_id) + member_account_ids = [for a in keys(local.account_map) : (local.account_map[a]) if(local.account_map[a] != local.current_account_id) && !contains(var.member_association_excludes, local.account_map[a])] + + org_delegated_administrator_account_id = local.account_map[var.delegated_administrator_account_name] + org_management_account_id = var.organization_management_account_name == null ? local.account_map[module.account_map.outputs.root_account_account_name] : local.account_map[var.organization_management_account_name] + + is_org_delegated_administrator_account = local.current_account_id == local.org_delegated_administrator_account_id + is_org_management_account = local.current_account_id == local.org_management_account_id + + create_org_delegation = local.enabled && local.is_org_management_account + create_org_configuration = local.enabled && local.is_org_delegated_administrator_account && var.admin_delegated +} + +data "aws_caller_identity" "this" { + count = local.enabled ? 1 : 0 +} + +# If we are in the AWS Organization management account, delegate Inspector2 to +# the administrator account (usually the security account). +resource "aws_inspector2_delegated_admin_account" "default" { + count = local.create_org_delegation ? 1 : 0 + account_id = local.org_delegated_administrator_account_id +} + +# If we are are in the AWS Organization designated administrator account, +# configure all other accounts to send their Inspector2 findings. +resource "aws_inspector2_organization_configuration" "default" { + count = local.create_org_configuration ? 1 : 0 + auto_enable { + ec2 = var.auto_enable_ec2 + ecr = var.auto_enable_ecr + lambda = var.auto_enable_lambda + } +} + +resource "aws_inspector2_member_association" "default" { + for_each = local.create_org_configuration ? toset(local.member_account_ids) : [] + account_id = each.value +} diff --git a/modules/aws-inspector2/outputs.tf b/modules/aws-inspector2/outputs.tf new file mode 100644 index 000000000..7c3779cfc --- /dev/null +++ b/modules/aws-inspector2/outputs.tf @@ -0,0 +1,4 @@ +output "aws_inspector2_member_association" { + value = aws_inspector2_member_association.default + description = "The Inspector2 member association resource." +} diff --git a/modules/aws-inspector2/providers.tf b/modules/aws-inspector2/providers.tf new file mode 100644 index 000000000..582f2f95c --- /dev/null +++ b/modules/aws-inspector2/providers.tf @@ -0,0 +1,18 @@ +provider "aws" { + region = var.region + + profile = !var.privileged && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null + dynamic "assume_role" { + for_each = var.privileged || module.iam_roles.profiles_enabled || (module.iam_roles.terraform_role_arn == null) ? [] : ["role"] + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/aws-inspector2/remote-state.tf b/modules/aws-inspector2/remote-state.tf new file mode 100644 index 000000000..da115834a --- /dev/null +++ b/modules/aws-inspector2/remote-state.tf @@ -0,0 +1,12 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.3" + + component = "account-map" + tenant = var.account_map_tenant != "" ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/aws-inspector2/variables.tf b/modules/aws-inspector2/variables.tf new file mode 100644 index 000000000..cde735da7 --- /dev/null +++ b/modules/aws-inspector2/variables.tf @@ -0,0 +1,77 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "auto_enable_ec2" { + description = "Whether Amazon EC2 scans are automatically enabled for new members of the Amazon Inspector organization." + type = bool + default = true +} + +variable "auto_enable_ecr" { + description = "Whether Amazon ECR scans are automatically enabled for new members of the Amazon Inspector organization." + type = bool + default = true +} + +variable "auto_enable_lambda" { + description = "Whether Lambda Function scans are automatically enabled for new members of the Amazon Inspector organization." + type = bool + default = true +} + +variable "account_map_tenant" { + type = string + default = "core" + description = "The tenant where the `account_map` component required by remote-state is deployed" +} + +variable "root_account_stage" { + type = string + default = "root" + description = <<-DOC + The stage name for the Organization root (management) account. This is used to lookup account IDs from account names + using the `account-map` component. + DOC +} + +variable "global_environment" { + type = string + default = "gbl" + description = "Global environment name" +} + +variable "privileged" { + type = bool + default = false + description = "true if the default provider already has access to the backend" +} + +variable "organization_management_account_name" { + type = string + default = null + description = "The name of the AWS Organization management account" +} + +variable "member_association_excludes" { + description = "List of account names to exlude from Amazon Inspector member association" + type = list(string) + default = [] +} + +variable "delegated_administrator_account_name" { + type = string + default = "security" + description = "The name of the account that is the AWS Organization Delegated Administrator account" +} + +variable "admin_delegated" { + type = bool + default = false + description = < Date: Tue, 21 Nov 2023 06:42:05 -0800 Subject: [PATCH 305/501] Support `karpenter-crd` Helm Chart and Fix Node Interruption Handling (#868) --- modules/eks/karpenter/CHANGELOG.md | 66 ++++++ modules/eks/karpenter/README.md | 56 ++++- modules/eks/karpenter/interruption_handler.tf | 6 +- modules/eks/karpenter/karpenter-crd-upgrade | 12 +- modules/eks/karpenter/main.tf | 199 ++++++++++++------ modules/eks/karpenter/variables.tf | 12 ++ 6 files changed, 266 insertions(+), 85 deletions(-) create mode 100644 modules/eks/karpenter/CHANGELOG.md diff --git a/modules/eks/karpenter/CHANGELOG.md b/modules/eks/karpenter/CHANGELOG.md new file mode 100644 index 000000000..14dd2a56b --- /dev/null +++ b/modules/eks/karpenter/CHANGELOG.md @@ -0,0 +1,66 @@ +## Version 1.348.0 + +Components PR [#868](https://github.com/cloudposse/terraform-aws-components/pull/868) + +The `karpenter-crd` helm chart can now be installed alongside the `karpenter` helm chart to automatically manage the lifecycle of Karpenter CRDs. However since this chart must be installed before the `karpenter` helm chart, the Kubernetes namespace must be available before either chart is deployed. Furthermore, this namespace should persist whether or not the `karpenter-crd` chart is deployed, so it should not be installed with that given `helm-release` resource. Therefore, we've moved namespace creation to a separate resource that runs before both charts. Terraform will handle that namespace state migration with the `moved` block. + +There are several scenarios that may or may not require additional steps. Please review the following scenarios and follow the steps for your given requirements. + +### Upgrading an existing `eks/karpenter` deployment without changes + +If you currently have `eks/karpenter` deployed to an EKS cluster and have upgraded to this version of the component, no changes are required. `var.crd_chart_enabled` will default to `false`. + +### Upgrading an existing `eks/karpenter` deployment and deploying the `karpenter-crd` chart + +If you currently have `eks/karpenter` deployed to an EKS cluster, have upgraded to this version of the component, do not currently have the `karpenter-crd` chart installed, and want to now deploy the `karpenter-crd` helm chart, a few additional steps are required! + +First, set `var.crd_chart_enabled` to `true`. + +Next, update the installed Karpenter CRDs in order for Helm to automatically take over their management when the `karpenter-crd` chart is deployed. We have included a script to run that upgrade. Run the `./karpenter-crd-upgrade` script or run the following commands on the given cluster before deploying the chart. Please note that this script or commands will only need to be run on first use of the CRD chart. + +Before running the script, ensure that the `kubectl` context is set to the cluster where the `karpenter` helm chart is deployed. In Geodesic, you can usually do this with the `set-cluster` command, though your configuration may vary. + +```bash +set-cluster -- terraform +``` + +Then run the script or commands: + +```bash +kubectl label crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh app.kubernetes.io/managed-by=Helm --overwrite +kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-name=karpenter-crd --overwrite +kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-namespace=karpenter --overwrite +``` + +:::info + +Previously the `karpenter-crd-upgrade` script included deploying the `karpenter-crd` chart. Now that this chart is moved to Terraform, that helm deployment is no longer necessary. + +For reference, the `karpenter-crd` chart can be installed with helm with the following: +```bash +helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version "$VERSION" --namespace karpenter +``` + +::: + +Now that the CRDs are upgraded, the component is ready to be applied. Apply the `eks/karpenter` component and then apply `eks/karpenter-provisioner`. + +#### Note for upgrading Karpenter from before v0.27.3 to v0.27.3 or later + +If you are upgrading Karpenter from before v0.27.3 to v0.27.3 or later, +you may need to run the following command to remove an obsolete webhook: + +```bash +kubectl delete mutatingwebhookconfigurations defaulting.webhook.karpenter.sh +``` + +See [the Karpenter upgrade guide](https://karpenter.sh/v0.32/upgrading/upgrade-guide/#upgrading-to-v0273) +for more details. + +### Upgrading an existing `eks/karpenter` deployment where the `karpenter-crd` chart is already deployed + +If you currently have `eks/karpenter` deployed to an EKS cluster, have upgraded to this version of the component, and already have the `karpenter-crd` chart installed, simply set `var.crd_chart_enabled` to `true` and redeploy Terraform to have Terraform manage the helm release for `karpenter-crd`. + +### Net new deployments + +If you are initially deploying `eks/karpenter`, no changes are required, but we recommend installing the CRD chart. Set `var.crd_chart_enabled` to `true` and continue with deployment. diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index c3c9b6fe2..b4be40954 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -21,19 +21,14 @@ components: eks/karpenter: metadata: type: abstract - settings: - spacelift: - workspace_enabled: true vars: enabled: true - tags: - Team: sre - Service: karpenter - eks_component_name: eks/cluster + eks_component_name: "eks/cluster" name: "karpenter" + # https://github.com/aws/karpenter/tree/main/charts/karpenter + chart_repository: "oci://public.ecr.aws/karpenter" chart: "karpenter" - chart_repository: "https://charts.karpenter.sh" - chart_version: "v0.16.3" + chart_version: "v0.31.0" create_namespace: true kubernetes_namespace: "karpenter" resources: @@ -47,9 +42,14 @@ components: atomic: true wait: true rbac_enabled: true + # "karpenter-crd" can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs + crd_chart_enabled: true + crd_chart: "karpenter-crd" # Set `legacy_create_karpenter_instance_profile` to `false` to allow the `eks/cluster` component # to manage the instance profile for the nodes launched by Karpenter (recommended for all new clusters). legacy_create_karpenter_instance_profile: false + # Enable interruption handling to deploy a SQS queue and a set of Event Bridge rules to handle interruption with Karpenter. + interruption_handler_enabled: true # Provision `karpenter` component on the blue EKS cluster eks/karpenter-blue: @@ -281,6 +281,37 @@ For your cluster, you will need to review the following configurations for the K ttl_seconds_until_expired: 2592000 ``` +## Node Interruption + +Karpenter also supports listening for and responding to Node Interruption events. If interruption handling is enabled, Karpenter will watch for upcoming involuntary interruption events that would cause disruption to your workloads. These interruption events include: + +- Spot Interruption Warnings +- Scheduled Change Health Events (Maintenance Events) +- Instance Terminating Events +- Instance Stopping Events + +:::info + +The Node Interruption Handler is not the same as the Node Termination Handler. The latter is always enabled and cleanly shuts down the node in 2 minutes in response to a Node Termination event. The former gets advance notice that a node will soon be terminated, so it can have 5-10 minutes to shut down a node. + +::: + +For more details, see refer to the [Karpenter docs](https://karpenter.sh/v0.32/concepts/disruption/#interruption) and [FAQ](https://karpenter.sh/v0.32/faq/#interruption-handling) + +To enable Node Interruption handling, set `var.interruption_handler_enabled` to `true`. This will create an SQS queue and a set of Event Bridge rules to deliver interruption events to Karpenter. + +## Custom Resource Definition (CRD) Management + +Karpenter ships with a few Custom Resource Definitions (CRDs). In earlier versions +of this component, when installing a new version of the `karpenter` helm chart, CRDs +were not be upgraded at the same time, requiring manual steps to upgrade CRDs after deploying the latest chart. +However Karpenter now supports an additional, independent helm chart for CRD management. +This helm chart, `karpenter-crd`, can be installed alongside the `karpenter` helm chart to automatically manage the lifecycle of these CRDs. + +To deploy the `karpenter-crd` helm chart, set `var.crd_chart_enabled` to `true`. +(Installing the `karpenter-crd` chart is recommended. `var.crd_chart_enabled` defaults +to `false` to preserve backward compatibility with older versions of this component.) + ## Troubleshooting For Karpenter issues, checkout the [Karpenter Troubleshooting Guide](https://karpenter.sh/docs/troubleshooting/) @@ -312,6 +343,7 @@ For more details, refer to: | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Modules @@ -319,7 +351,8 @@ For more details, refer to: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.10.0 | +| [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.10.1 | +| [karpenter\_crd](#module\_karpenter\_crd) | cloudposse/helm-release/aws | 0.10.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -331,6 +364,7 @@ For more details, refer to: | [aws_iam_instance_profile.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | | [aws_sqs_queue.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource | | [aws_sqs_queue_policy.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | +| [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | | [aws_iam_policy_document.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | @@ -349,6 +383,8 @@ For more details, refer to: | [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed | `string` | `null` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [crd\_chart](#input\_crd\_chart) | The name of the Karpenter CRD chart to be installed, if `var.crd_chart_enabled` is set to `true`. | `string` | `"karpenter-crd"` | no | +| [crd\_chart\_enabled](#input\_crd\_chart\_enabled) | `karpenter-crd` can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs. Set to `true` to install this CRD helm chart before the primary karpenter chart. | `bool` | `false` | no | | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false` | `bool` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/eks/karpenter/interruption_handler.tf b/modules/eks/karpenter/interruption_handler.tf index 3f173b98e..558ee7de1 100644 --- a/modules/eks/karpenter/interruption_handler.tf +++ b/modules/eks/karpenter/interruption_handler.tf @@ -2,7 +2,7 @@ locals { interruption_handler_enabled = local.enabled && var.interruption_handler_enabled interruption_handler_queue_name = module.this.id - dns_suffix = data.aws_partition.current.dns_suffix + dns_suffix = join("", data.aws_partition.current[*].dns_suffix) events = { health_event = { @@ -40,7 +40,9 @@ locals { } } -data "aws_partition" "current" {} +data "aws_partition" "current" { + count = local.interruption_handler_enabled ? 1 : 0 +} resource "aws_sqs_queue" "interruption_handler" { count = local.interruption_handler_enabled ? 1 : 0 diff --git a/modules/eks/karpenter/karpenter-crd-upgrade b/modules/eks/karpenter/karpenter-crd-upgrade index a3e3ce05c..e6274deb3 100755 --- a/modules/eks/karpenter/karpenter-crd-upgrade +++ b/modules/eks/karpenter/karpenter-crd-upgrade @@ -2,27 +2,23 @@ function usage() { cat >&2 <<'EOF' -./karpenter-crd-upgrade +./karpenter-crd-upgrade -Use this script to upgrade the Karpenter CRDs by installing or upgrading the karpenter-crd helm chart. +Use this script to prepare a cluster for karpenter-crd helm chart support by upgrading Karpenter CRDs. EOF } function upgrade() { - VERSION="${1}" - [[ $VERSION =~ ^v ]] || VERSION="v${VERSION}" - set -x kubectl label crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh app.kubernetes.io/managed-by=Helm --overwrite kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-name=karpenter-crd --overwrite kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-namespace=karpenter --overwrite - helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version "$VERSION" --namespace karpenter } if (($# == 0)); then - usage + upgrade else - upgrade $1 + usage fi diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index e20f7011f..1ebf263c4 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -25,10 +25,72 @@ resource "aws_iam_instance_profile" "default" { tags = module.this.tags } +# See CHANGELOG for PR #868: +# https://github.com/cloudposse/terraform-aws-components/pull/868 +# +# Namespace was moved from the karpenter module to an independent resource in order to be +# shared between both the karpenter and karpenter-crd modules. +moved { + from = module.karpenter.kubernetes_namespace.default[0] + to = kubernetes_namespace.default[0] +} + +resource "kubernetes_namespace" "default" { + count = local.enabled && var.create_namespace ? 1 : 0 + + metadata { + name = var.kubernetes_namespace + annotations = {} + labels = merge(module.this.tags, { name = var.kubernetes_namespace }) + } +} + +# Deploy karpenter-crd helm chart +# "karpenter-crd" can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs +module "karpenter_crd" { + enabled = local.enabled && var.crd_chart_enabled + + source = "cloudposse/helm-release/aws" + version = "0.10.1" + + name = var.crd_chart + chart = var.crd_chart + repository = var.chart_repository + description = var.chart_description + chart_version = var.chart_version + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + create_namespace_with_kubernetes = false # Namespace is created with kubernetes_namespace resources to be shared between charts + kubernetes_namespace = join("", kubernetes_namespace.default[*].id) + kubernetes_namespace_labels = merge(module.this.tags, { name = join("", kubernetes_namespace.default[*].id) }) + + eks_cluster_oidc_issuer_url = coalesce(replace(local.eks_cluster_identity_oidc_issuer, "https://", ""), "deleted") + + values = compact([ + # standard k8s object settings + yamlencode({ + fullnameOverride = module.this.name + resources = var.resources + rbac = { + create = var.rbac_enabled + } + }), + ]) + + context = module.this.context + + depends_on = [ + kubernetes_namespace.default + ] +} + # Deploy Karpenter helm chart module "karpenter" { source = "cloudposse/helm-release/aws" - version = "0.10.0" + version = "0.10.1" chart = var.chart repository = var.chart_repository @@ -39,14 +101,14 @@ module "karpenter" { cleanup_on_fail = var.cleanup_on_fail timeout = var.timeout - create_namespace_with_kubernetes = var.create_namespace - kubernetes_namespace = var.kubernetes_namespace - kubernetes_namespace_labels = merge(module.this.tags, { name = var.kubernetes_namespace }) + create_namespace_with_kubernetes = false # Namespace is created with kubernetes_namespace resources to be shared between charts + kubernetes_namespace = join("", kubernetes_namespace.default[*].id) + kubernetes_namespace_labels = merge(module.this.tags, { name = join("", kubernetes_namespace.default[*].id) }) eks_cluster_oidc_issuer_url = coalesce(replace(local.eks_cluster_identity_oidc_issuer, "https://", ""), "deleted") service_account_name = module.this.name - service_account_namespace = var.kubernetes_namespace + service_account_namespace = join("", kubernetes_namespace.default[*].id) iam_role_enabled = true @@ -55,72 +117,75 @@ module "karpenter" { # https://github.com/aws/karpenter/issues/2649 # Apparently the source of truth for the best IAM policy is the `data.aws_iam_policy_document.karpenter_controller` in # https://github.com/terraform-aws-modules/terraform-aws-iam/blob/master/modules/iam-role-for-service-accounts-eks/policies.tf - iam_policy_statements = concat([ - { - sid = "KarpenterController" - effect = "Allow" - resources = ["*"] - - actions = [ - # https://github.com/terraform-aws-modules/terraform-aws-iam/blob/99c69ad54d985f67acf211885aa214a3a6cc931c/modules/iam-role-for-service-accounts-eks/policies.tf#L511-L581 - # The reference policy is broken up into multiple statements with different resource restrictions based on tags. - # This list has breaks where statements are separated in the reference policy for easier comparison and maintenance. - "ec2:CreateLaunchTemplate", - "ec2:CreateFleet", - "ec2:CreateTags", - "ec2:DescribeLaunchTemplates", - "ec2:DescribeImages", - "ec2:DescribeInstances", - "ec2:DescribeSecurityGroups", - "ec2:DescribeSubnets", - "ec2:DescribeInstanceTypes", - "ec2:DescribeInstanceTypeOfferings", - "ec2:DescribeAvailabilityZones", - "ec2:DescribeSpotPriceHistory", - "pricing:GetProducts", - - "ec2:TerminateInstances", - "ec2:DeleteLaunchTemplate", - - "ec2:RunInstances", - - "iam:PassRole", - ] - }, - { - sid = "KarpenterControllerSSM" - effect = "Allow" - # Allow Karpenter to read AMI IDs from SSM - actions = ["ssm:GetParameter"] - resources = ["arn:aws:ssm:*:*:parameter/aws/service/*"] - }, - { - sid = "KarpenterControllerClusterAccess" - effect = "Allow" - actions = [ - "eks:DescribeCluster" - ] - resources = [ - module.eks.outputs.eks_cluster_arn - ] - } - ], - local.interruption_handler_enabled ? [ + iam_policy = [{ + statements = concat([ + { + sid = "KarpenterController" + effect = "Allow" + resources = ["*"] + + actions = [ + # https://github.com/terraform-aws-modules/terraform-aws-iam/blob/99c69ad54d985f67acf211885aa214a3a6cc931c/modules/iam-role-for-service-accounts-eks/policies.tf#L511-L581 + # The reference policy is broken up into multiple statements with different resource restrictions based on tags. + # This list has breaks where statements are separated in the reference policy for easier comparison and maintenance. + "ec2:CreateLaunchTemplate", + "ec2:CreateFleet", + "ec2:CreateTags", + "ec2:DescribeLaunchTemplates", + "ec2:DescribeImages", + "ec2:DescribeInstances", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceTypeOfferings", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeSpotPriceHistory", + "pricing:GetProducts", + + "ec2:TerminateInstances", + "ec2:DeleteLaunchTemplate", + + "ec2:RunInstances", + + "iam:PassRole", + ] + }, + { + sid = "KarpenterControllerSSM" + effect = "Allow" + # Allow Karpenter to read AMI IDs from SSM + actions = ["ssm:GetParameter"] + resources = ["arn:aws:ssm:*:*:parameter/aws/service/*"] + }, { - sid = "KarpenterInterruptionHandlerAccess" + sid = "KarpenterControllerClusterAccess" effect = "Allow" actions = [ - "sqs:DeleteMessage", - "sqs:GetQueueUrl", - "sqs:GetQueueAttributes", - "sqs:ReceiveMessage", + "eks:DescribeCluster" ] resources = [ - aws_sqs_queue.interruption_handler[0].arn + module.eks.outputs.eks_cluster_arn ] } - ] : [] - ) + ], + local.interruption_handler_enabled ? [ + { + sid = "KarpenterInterruptionHandlerAccess" + effect = "Allow" + actions = [ + "sqs:DeleteMessage", + "sqs:GetQueueUrl", + "sqs:GetQueueAttributes", + "sqs:ReceiveMessage", + ] + resources = [ + one(aws_sqs_queue.interruption_handler[*].arn) + ] + } + ] : [] + ) + }] + values = compact([ # standard k8s object settings @@ -163,5 +228,9 @@ module "karpenter" { context = module.this.context - depends_on = [aws_iam_instance_profile.default] + depends_on = [ + aws_iam_instance_profile.default, + module.karpenter_crd, + kubernetes_namespace.default + ] } diff --git a/modules/eks/karpenter/variables.tf b/modules/eks/karpenter/variables.tf index 8b366c557..9b84ba3b4 100644 --- a/modules/eks/karpenter/variables.tf +++ b/modules/eks/karpenter/variables.tf @@ -25,6 +25,18 @@ variable "chart_version" { default = null } +variable "crd_chart_enabled" { + type = bool + description = "`karpenter-crd` can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs. Set to `true` to install this CRD helm chart before the primary karpenter chart." + default = false +} + +variable "crd_chart" { + type = string + description = "The name of the Karpenter CRD chart to be installed, if `var.crd_chart_enabled` is set to `true`." + default = "karpenter-crd" +} + variable "resources" { type = object({ limits = object({ From 38fc86b6ce60d78d7494aa7d7c5cca64f10437da Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 21 Nov 2023 08:20:24 -0800 Subject: [PATCH 306/501] [eks/cluster] Document Migration to Managed Node Group. Bugfix. (#910) --- modules/eks/cluster/CHANGELOG.md | 166 ++++++++++++++++++++++++++++++- modules/eks/cluster/main.tf | 2 +- 2 files changed, 166 insertions(+), 2 deletions(-) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 351f53e73..50beb2380 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,4 +1,168 @@ -## Components PR [#852](https://github.com/cloudposse/terraform-aws-components/pull/852) +## Components PR [#910](https://github.com/cloudposse/terraform-aws-components/pull/910) + +Bug fix and updates to Changelog, no action required. + +Fixed: Error about managed node group ARNs list being null, which could happen +when adding a managed node group to an existing cluster that never had one. + +## Upgrading to `v1.303.0` + +Components PR [#852](https://github.com/cloudposse/terraform-aws-components/pull/852) + +This is a bug fix and feature enhancement update. No action is necessary to upgrade. +However, with the new features and new recommendations, you may want to change +your configuration. + +## Recommended (optional) changes + +Previously, we recommended deploying Karpenter to Fargate and not provisioning +any nodes. However, this causes issues with add-ons that require compute power +to fully initialize, such as `coredns`, and it can reduce the cluster to a +single node, removing the high availability that comes from having a node +per Availability Zone and replicas of pods spread across those nodes. + +As a result, we now recommend deploying a minimal node group with a single +instance (currently recommended to be a `c6a.large`) in each of 3 Availability +Zones. This will provide the compute power needed to initialize add-ons, and +will provide high availability for the cluster. As a bonus, it will also +remove the need to deploy Karpenter to Fargate. + +**NOTE about instance type**: The `c6a.large` instance type is relatively +new. If you have deployed an old version of our ServiceControlPolicy +`DenyEC2NonNitroInstances`, `DenyNonNitroInstances` (obsolete, replaced by +`DenyEC2NonNitroInstances`), and/or `DenyEC2InstancesWithoutEncryptionInTransit`, +you will want to update them to v0.12.0 or choose a difference instance type. + +### Migration procedure + +To perform the recommended migration, follow these steps: + +#### 1. Deploy a minimal node group, move addons to it + +Change your `eks/cluster` configuration to set `deploy_addons_to_fargate: false`. + +Add the following to your `eks/cluster` configuration, but +copy the block device name, volume size, and volume type from your existing +Karpenter provisioner configuration. Also select the correct `ami_type` +according to the `ami_family` in your Karpenter provisioner configuration. + +```yaml + node_groups: + # will create 1 node group for each item in map + # Provision a minimal static node group for add-ons and redundant replicas + main: + # EKS AMI version to use, e.g. "1.16.13-20200821" (no "v"). + ami_release_version: null + # Type of Amazon Machine Image (AMI) associated with the EKS Node Group + # Typically AL2_x86_64 or BOTTLEROCKET_x86_64 + ami_type: BOTTLEROCKET_x86_64 + # Additional name attributes (e.g. `1`) for the node group + attributes: [] + # will create 1 auto scaling group in each specified availability zone + # or all AZs with subnets if none are specified anywhere + availability_zones: null + # Whether to enable Node Group to scale its AutoScaling Group + cluster_autoscaler_enabled: false + # True (recommended) to create new node_groups before deleting old ones, avoiding a temporary outage + create_before_destroy: true + # Configure storage for the root block device for instances in the Auto Scaling Group + # For Bottlerocket, use /dev/xvdb. For all others, use /dev/xvda. + block_device_map: + "/dev/xvdb": + ebs: + volume_size: 125 # in GiB + volume_type: gp3 + encrypted: true + delete_on_termination: true + # Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided. + instance_types: + - c6a.large + # Desired number of worker nodes when initially provisioned + desired_group_size: 3 + max_group_size: 3 + min_group_size: 3 + resources_to_tag: + - instance + - volume + tags: null +``` + +You do not need to apply the above changes yet, although you can if you +want to. To reduce overhead, you can apply the changes in the next step. + +#### 2. Move Karpenter to the node group, remove legacy support + +Delete the `fargate_profiles` section from your `eks/cluster` configuration, +or at least remove the `karpenter` profile from it. Disable legacy support +by adding: + +```yaml + legacy_fargate_1_role_per_profile_enabled: false +``` + +#### 2.a Optional: Move Karpenter instance profile to `eks/cluster` component + +If you have the patience to manually import and remove a Terraform +resource, you should move the Karpenter instance profile to the `eks/cluster` +component. This fixes an issue where the Karpenter instance profile +could be broken by certain sequences of Terraform operations. +However, if you have multiple clusters to migrate, this can be tedious, +and the issue is not a serious one, so you may want to skip this step. + +To do this, add the following to your `eks/cluster` configuration: + +```yaml + legacy_do_not_create_karpenter_instance_profile: false +``` + + +**BEFORE APPLYING CHANGES**: +Run `atmos terraform plan` (with the appropriate arguments) to see the changes +that will be made. Among the resources to be created will be +`aws_iam_instance_profile.default[0]`. Using the same arguments as before, run +`atmos`, but replace `plan` with `import 'aws_iam_instance_profile.default[0]' `, +where `` is the name of the profile the plan indicated it would create. +It will be something like `-karpenter`. + +**NOTE**: If you perform this step, you must also perform 3.a below. + +#### 2.b Apply the changes + +Apply the changes with `atmos terraform apply`. + +#### 3. Upgrade Karpenter + +Upgrade the `eks/karpenter` component to the latest version. Follow the upgrade +instructions to enable the new `karpenter-crd` chart by setting `crd_chart_enabled: true`. + +Upgrade to at least Karpenter v0.30.0, which is the first version to support +factoring in the existing node group when determining the number of nodes to +provision. This will prevent Karpenter from provisioning nodes when they are not +needed because the existing node group already has enough capacity. Be +careful about upgrading to v0.32.0 or later, as that version introduces +significant breaking changes. We recommend updating to v0.31.2 or later +versions of v0.31.x, but not v0.32.0 or later, as a first step. This +provides a safe (revertible) upgrade path to v0.32.0 or later. + +#### 3.a Finish Move of Karpenter instance profile to `eks/cluster` component + +If you performed step 2.a above, you must also perform this step. If you did +not perform step 2.a, you must NOT perform this step. + +In the `eks/karpenter` stack, set `legacy_create_karpenter_instance_profile: false`. + +**BEFORE APPLYING CHANGES**: Remove the Karpenter instance profile from the Terraform state, since +it is now managed by the `eks/cluster` component, or else Terraform will delete it. + +```shell +atmos terraform state eks/karpenter rm 'aws_iam_instance_profile.default[0]' -s= +``` + +#### 3.b Apply the changes + +Apply the changes with `atmos terraform apply`. + +## Changes included in `v1.303.0` This is a bug fix and feature enhancement update. No action is necessary to upgrade. diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 229935498..2415c74c5 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -41,7 +41,7 @@ locals { ) # Existing managed worker role ARNs - managed_worker_role_arns = local.eks_outputs.eks_managed_node_workers_role_arns + managed_worker_role_arns = coalesce(local.eks_outputs.eks_managed_node_workers_role_arns, []) # If Karpenter IAM role is enabled, add it to the `aws-auth` ConfigMap to allow the nodes launched by Karpenter to join the EKS cluster karpenter_role_arn = one(aws_iam_role.karpenter[*].arn) From 756dcf66021f5ae2e6886a66f2e3be1622cba399 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 21 Nov 2023 08:44:02 -0800 Subject: [PATCH 307/501] New Component: `philips-labs-github-runners` (#909) --- modules/philips-labs-github-runners/README.md | 147 +++++++++ .../philips-labs-github-runners/context.tf | 279 ++++++++++++++++++ modules/philips-labs-github-runners/main.tf | 101 +++++++ .../modules/README.md | 22 ++ .../modules/webhook-github-app/README.md | 41 +++ .../webhook-github-app/bin/update-app.sh | 95 ++++++ .../modules/webhook-github-app/main.tf | 13 + .../modules/webhook-github-app/variables.tf | 13 + .../modules/webhook-github-app/versions.tf | 10 + .../philips-labs-github-runners/outputs.tf | 19 ++ .../philips-labs-github-runners/providers.tf | 19 ++ .../remote-state.tf | 8 + .../templates/userdata_post_install.sh | 3 + .../philips-labs-github-runners/variables.tf | 60 ++++ .../philips-labs-github-runners/versions.tf | 18 ++ 15 files changed, 848 insertions(+) create mode 100644 modules/philips-labs-github-runners/README.md create mode 100644 modules/philips-labs-github-runners/context.tf create mode 100644 modules/philips-labs-github-runners/main.tf create mode 100644 modules/philips-labs-github-runners/modules/README.md create mode 100644 modules/philips-labs-github-runners/modules/webhook-github-app/README.md create mode 100755 modules/philips-labs-github-runners/modules/webhook-github-app/bin/update-app.sh create mode 100644 modules/philips-labs-github-runners/modules/webhook-github-app/main.tf create mode 100644 modules/philips-labs-github-runners/modules/webhook-github-app/variables.tf create mode 100644 modules/philips-labs-github-runners/modules/webhook-github-app/versions.tf create mode 100644 modules/philips-labs-github-runners/outputs.tf create mode 100644 modules/philips-labs-github-runners/providers.tf create mode 100644 modules/philips-labs-github-runners/remote-state.tf create mode 100644 modules/philips-labs-github-runners/templates/userdata_post_install.sh create mode 100644 modules/philips-labs-github-runners/variables.tf create mode 100644 modules/philips-labs-github-runners/versions.tf diff --git a/modules/philips-labs-github-runners/README.md b/modules/philips-labs-github-runners/README.md new file mode 100644 index 000000000..e38d2ef79 --- /dev/null +++ b/modules/philips-labs-github-runners/README.md @@ -0,0 +1,147 @@ +# Component: `philips-labs-github-runners` + +This component is responsible for provisioning the surrounding infrastructure for the github runners. + +## Prerequisites + +* Github App installed on the organization + * For more details see [Philips Lab's Setting up a Github App](https://github.com/philips-labs/terraform-aws-github-runner/tree/main#setup-github-app-part-1) + * Ensure you create a **PRIVATE KEY** and store it in SSM, **NOT** to be confused with a **Client Secret**. Private Keys are created in the GitHub App Configuration and scrolling to the bottom. +* Github App ID and private key stored in SSM under `/pl-github-runners/id` (or the value of `var.github_app_id_ssm_path`) +* Github App Private Key stored in SSM (base64 encoded) under `/pl-github-runners/key` (or the value of `var.github_app_key_ssm_path`) + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + philips-labs-github-runners: + vars: + enabled: true +``` + +The following will create + +- An API Gateway +- Lambdas +- SQS Queue +- EC2 Launch Template instances + +The API Gateway is registered as a webhook within the GitHub app. Which scales up or down, via lambdas, the EC2 Launch Template +by the number of messages in the SQS queue. + +![Architecture](https://github.com/philips-labs/terraform-aws-github-runner/blob/main/docs/component-overview.svg) + +## Modules + +### `webhook-github-app` + +This is a fork of https://github.com/philips-labs/terraform-aws-github-runner/tree/main/modules/webhook-github-app. + +We customized it until this PR is resolved as it does not update the github app webhook until this is merged. +* https://github.com/philips-labs/terraform-aws-github-runner/pull/3625 + +This module also requires an environment variable +* `GH_TOKEN` - a github token be set + +This module also requires the `gh` cli to be installed. Your Dockerfile can be updated to include the following to install it: +```dockerfile +ARG GH_CLI_VERSION=2.39.1 +# ... +ARG GH_CLI_VERSION +RUN apt-get update && apt-get install -y --allow-downgrades \ + gh="${GH_CLI_VERSION}-*" +``` + +By default, we leave this disabled, as it requires a github token to be set. You can enable it by setting `var.enable_update_github_app_webhook` to `true`. +When enabled, it will update the github app webhook to point to the API Gateway. This can occur if the API Gateway is deleted and recreated. + +When disabled, you will need to manually update the github app webhook to point to the API Gateway. +This is output by the component, and available via the `webhook` output under `endpoint`. + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [local](#requirement\_local) | >= 2.4.0 | +| [random](#requirement\_random) | >= 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [random](#provider\_random) | >= 3.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [fetch\_lambdas](#module\_fetch\_lambdas) | philips-labs/github-runner/aws//modules/download-lambda | 5.4.0 | +| [github\_runner](#module\_github\_runner) | philips-labs/github-runner/aws | 5.4.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [store\_read](#module\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.11.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [webhook\_github\_app](#module\_webhook\_github\_app) | ./modules/webhook-github-app | n/a | + +## Resources + +| Name | Type | +|------|------| +| [random_id.webhook_secret](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enable\_update\_github\_app\_webhook](#input\_enable\_update\_github\_app\_webhook) | Enable updating the github app webhook | `bool` | `false` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github\_app\_id\_ssm\_path](#input\_github\_app\_id\_ssm\_path) | Path to the github app id in SSM | `string` | `"/pl-github-runners/id"` | no | +| [github\_app\_key\_ssm\_path](#input\_github\_app\_key\_ssm\_path) | Path to the github key in SSM | `string` | `"/pl-github-runners/key"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [instance\_types](#input\_instance\_types) | List of instance types for the action runner. Defaults are based on runner\_os (al2023 for linux and Windows Server Core for win). | `list(string)` |
[
"m5.large",
"c5.large"
]
| no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lambda\_repo\_url](#input\_lambda\_repo\_url) | URL of the lambda repository | `string` | `"https://github.com/philips-labs/terraform-aws-github-runner"` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS region | `string` | n/a | yes | +| [release\_version](#input\_release\_version) | Version of the application | `string` | `"v5.4.0"` | no | +| [repository\_white\_list](#input\_repository\_white\_list) | List of github repository full names (owner/repo\_name) that will be allowed to use the github app. Leave empty for no filtering. | `list(string)` | `[]` | no | +| [runner\_extra\_labels](#input\_runner\_extra\_labels) | Extra (custom) labels for the runners (GitHub). Labels checks on the webhook can be enforced by setting `enable_workflow_job_labels_check`. GitHub read-only labels should not be provided. | `list(string)` |
[
"default"
]
| no | +| [scale\_up\_reserved\_concurrent\_executions](#input\_scale\_up\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `-1` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [github\_runners](#output\_github\_runners) | Information about the GitHub runners. | +| [queues](#output\_queues) | Information about the GitHub runner queues. Such as `build_queue_arn` the ARN of the SQS queue to use for the build queue. | +| [ssm\_parameters](#output\_ssm\_parameters) | Information about the SSM parameters to use to register the runner. | +| [webhook](#output\_webhook) | Information about the webhook to use to register the runner. | + + +## References +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecs) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/philips-labs-github-runners/context.tf b/modules/philips-labs-github-runners/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/philips-labs-github-runners/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/philips-labs-github-runners/main.tf b/modules/philips-labs-github-runners/main.tf new file mode 100644 index 000000000..befa158f2 --- /dev/null +++ b/modules/philips-labs-github-runners/main.tf @@ -0,0 +1,101 @@ +locals { + enabled = var.enabled + version = var.enabled ? var.release_version : null + lambda_repo = "https://github.com/philips-labs/terraform-aws-github-runner" + + lambdas = var.enabled ? [ + { + name = "webhook" + tag = local.version + }, + { + name = "runners" + tag = local.version + }, + { + name = "runner-binaries-syncer" + tag = local.version + } + ] : [] +} + +module "store_read" { + count = local.enabled ? 1 : 0 + + source = "cloudposse/ssm-parameter-store/aws" + version = "0.11.0" + + parameter_read = [ + var.github_app_key_ssm_path, + var.github_app_id_ssm_path + ] +} + +resource "random_id" "webhook_secret" { + byte_length = 20 +} + +module "fetch_lambdas" { + count = local.enabled ? 1 : 0 + + source = "philips-labs/github-runner/aws//modules/download-lambda" + version = "5.4.0" + + lambdas = local.lambdas +} + +module "github_runner" { + count = local.enabled ? 1 : 0 + + source = "philips-labs/github-runner/aws" + version = "5.4.0" + + aws_region = var.region + vpc_id = module.vpc.outputs.vpc_id + subnet_ids = module.vpc.outputs.private_subnet_ids + + github_app = { + key_base64 = module.store_read[0].map[var.github_app_key_ssm_path] + id = module.store_read[0].map[var.github_app_id_ssm_path] + webhook_secret = random_id.webhook_secret.hex + } + + # here we hardcode the names of the lambda zips because they always have the same name, + # the output of the fetch lambdas module is a list of zip names, which we cannot be certain will have the same order. + webhook_lambda_zip = "webhook.zip" + runner_binaries_syncer_lambda_zip = "runner-binaries-syncer.zip" + runners_lambda_zip = "runners.zip" + + enable_organization_runners = true + enable_ssm_on_runners = true + create_service_linked_role_spot = true + enable_fifo_build_queue = true + scale_up_reserved_concurrent_executions = var.scale_up_reserved_concurrent_executions + + enable_user_data_debug_logging_runner = true + + # this variable is substituted in the user-data.sh startup script. It cannot point to another script if using a base ami. + # instead this will just run after the runner is installed. Hence we use `file` to read the contents of the file which is injected into the user-data.sh + userdata_post_install = file("${path.module}/templates/userdata_post_install.sh") + + runner_extra_labels = var.runner_extra_labels + + tags = module.this.tags +} + +module "webhook_github_app" { + count = local.enabled && var.enable_update_github_app_webhook ? 1 : 0 + ## See README.md for more info on why we use this source instead of: + # source = "philips-labs/github-runner/aws//modules/webhook-github-app" + # version = "5.4.0" + source = "./modules/webhook-github-app" + + depends_on = [module.github_runner] + + github_app = { + key_base64 = module.store_read[0].map[var.github_app_key_ssm_path] + id = module.store_read[0].map[var.github_app_id_ssm_path] + webhook_secret = random_id.webhook_secret.hex + } + webhook_endpoint = one(module.github_runner[*].webhook.endpoint) +} diff --git a/modules/philips-labs-github-runners/modules/README.md b/modules/philips-labs-github-runners/modules/README.md new file mode 100644 index 000000000..85f6ef43b --- /dev/null +++ b/modules/philips-labs-github-runners/modules/README.md @@ -0,0 +1,22 @@ +# Modules + +## `webhook-github-app` + +This is a fork of https://github.com/philips-labs/terraform-aws-github-runner/tree/main/modules/webhook-github-app. + +We customized it until this PR is resolved as it does not update the github app webhook until this is merged. + * https://github.com/philips-labs/terraform-aws-github-runner/pull/3625 + +This module also requires an environment variable + * `GH_TOKEN` - a github token be set + +This module also requires the `gh` cli to be installed. Your Dockerfile can be updated to include the following to install it: +```dockerfile +ARG GH_CLI_VERSION=2.39.1 +# ... +ARG GH_CLI_VERSION +RUN apt-get update && apt-get install -y --allow-downgrades \ + gh="${GH_CLI_VERSION}-*" +``` + +You can disable this module with `enable_update_github_app_webhook` set to `false`. This means you must manually diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/README.md b/modules/philips-labs-github-runners/modules/webhook-github-app/README.md new file mode 100644 index 000000000..ba0ca7190 --- /dev/null +++ b/modules/philips-labs-github-runners/modules/webhook-github-app/README.md @@ -0,0 +1,41 @@ +# Module - Update GitHub App Webhook + +> This module is using the local executor to run a bash script. + +This module updates the GitHub App webhook with the endpoint and secret and can be changed with the root module. See the examples for usages. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [null](#requirement\_null) | ~> 3 | + +## Providers + +| Name | Version | +|------|---------| +| [null](#provider\_null) | ~> 3 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [null_resource.update_app](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [github\_app](#input\_github\_app) | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). |
object({
key_base64 = string
id = string
webhook_secret = string
})
| n/a | yes | +| [webhook\_endpoint](#input\_webhook\_endpoint) | The endpoint to use for the webhook, defaults to the endpoint of the runners module. | `string` | n/a | yes | + +## Outputs + +No outputs. + diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/bin/update-app.sh b/modules/philips-labs-github-runners/modules/webhook-github-app/bin/update-app.sh new file mode 100755 index 000000000..15672ded5 --- /dev/null +++ b/modules/philips-labs-github-runners/modules/webhook-github-app/bin/update-app.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -e + +### CHECKS ### + +function testCommand() { + if ! command -v $1 &> /dev/null + then + echo "$1 could not be found" + exit + fi +} + +testCommand gh + +# create usages function usages mesaages. APP_ID and APP_PRIVATE_KEY_PATH are required as parameter or environment variable +usages() { + echo "Description: Update the GitHub App webhook configuration with terraform output for the webhook output of the module." >&2 + echo " " >&2 + echo "Usage: $0" >&2 + echo "Usage: $0 [-h]" >&2 + echo " Use environment variables" >&2 + echo " -a APP_ID GitHub App ID" >&2 + echo " -k APP_PRIVATE_KEY_BASE64 Base64 encoded private key of the GitHub App" >&2 + echo " -f APP APP_PRIVATE_KEY_FILE Path to the private key of the GitHub App" >&2 + echo " -e WEBHOOK_ENDPOINT Webhook endpoint" >&2 + echo " -s WEBHOOK_SECRET Webhook secret" >&2 + echo " -h Show this help message" >&2 + exit 1 +} + +# hadd h flag to show help +while getopts a:f:k:s:e:h flag +do + case "${flag}" in + a) APP_ID=${OPTARG};; + f) APP_PRIVATE_KEY_FILE=${OPTARG};; + k) APP_PRIVATE_KEY_BASE64=${OPTARG};; + e) WEBHOOK_ENDPOINT=${OPTARG};; + s) WEBHOOK_SECRET=${OPTARG};; + h) usages ;; + esac +done + +if [ -z "$APP_ID" ]; then + echo "APP_ID must be set" + usages +fi + +# check one of variables APP_PRIVATE_KEY_PATH or APP_PRIVATE_KEY are set +if [ -z "$APP_PRIVATE_KEY_BASE64" ] && [ -z "$APP_PRIVATE_KEY_FILE" ]; then + echo "APP_PRIVATE_KEY_BASE64 or APP_PRIVATE_KEY_FILE must be set" + usages +fi + +### Terraform outputs ### + +if [ -z "$WEBHOOK_ENDPOINT" ]; then + testCommand terraform + WEBHOOK_ENDPOINT=$(terraform output --raw webhook_endpoint) +fi + +if [ -z "$WEBHOOK_SECRET" ]; then + testCommand terraform + WEBHOOK_SECRET=$(terraform output --raw webhook_secret) +fi + +### CREATE JWT TOKEN ### + +# Generate the JWT header and payload +HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '\n') +PAYLOAD=$(echo -n "{\"iat\":$(date +%s),\"exp\":$(( $(date +%s) + 600 )),\"iss\":$APP_ID}" | base64 | tr -d '\n') + +# Generate the signature +if [ -z "$APP_PRIVATE_KEY_BASE64" ]; then + APP_PRIVATE_KEY_BASE64=$(cat $APP_PRIVATE_KEY_FILE | base64 | tr -d '\n') +fi + +SIGNATURE=$(echo -n "$HEADER.$PAYLOAD" | openssl dgst -sha256 -sign <(echo "$APP_PRIVATE_KEY_BASE64" | base64 -d) | base64 | tr -d '\n') + +JWT_TOKEN="$HEADER.$PAYLOAD.$SIGNATURE" + + +### UPDATE WEBHOOK ### + +gh api \ + --method PATCH \ + -H "Authorization: Bearer ${JWT_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /app/hook/config \ + -f content_type='json' \ + -f insecure_ssl='0' \ + -f secret=${WEBHOOK_SECRET} \ + -f url=${WEBHOOK_ENDPOINT} diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/main.tf b/modules/philips-labs-github-runners/modules/webhook-github-app/main.tf new file mode 100644 index 000000000..84cce50fa --- /dev/null +++ b/modules/philips-labs-github-runners/modules/webhook-github-app/main.tf @@ -0,0 +1,13 @@ +resource "null_resource" "update_app" { + triggers = { + webhook_endpoint = var.webhook_endpoint + webhook_secret = var.github_app.webhook_secret + always_run = timestamp() + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = "${path.module}/bin/update-app.sh -e ${var.webhook_endpoint} -s ${var.github_app.webhook_secret} -a ${var.github_app.id} -k ${var.github_app.key_base64}" + on_failure = fail + } +} diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/variables.tf b/modules/philips-labs-github-runners/modules/webhook-github-app/variables.tf new file mode 100644 index 000000000..69d404b7b --- /dev/null +++ b/modules/philips-labs-github-runners/modules/webhook-github-app/variables.tf @@ -0,0 +1,13 @@ +variable "github_app" { + description = "GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`)." + type = object({ + key_base64 = string + id = string + webhook_secret = string + }) +} + +variable "webhook_endpoint" { + description = "The endpoint to use for the webhook, defaults to the endpoint of the runners module." + type = string +} diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/versions.tf b/modules/philips-labs-github-runners/modules/webhook-github-app/versions.tf new file mode 100644 index 000000000..e0632ba7d --- /dev/null +++ b/modules/philips-labs-github-runners/modules/webhook-github-app/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + null = { + source = "hashicorp/null" + version = "~> 3" + } + } +} diff --git a/modules/philips-labs-github-runners/outputs.tf b/modules/philips-labs-github-runners/outputs.tf new file mode 100644 index 000000000..d81800398 --- /dev/null +++ b/modules/philips-labs-github-runners/outputs.tf @@ -0,0 +1,19 @@ +output "webhook" { + description = "Information about the webhook to use to register the runner." + value = one(module.github_runner[*].webhook) +} + +output "ssm_parameters" { + description = "Information about the SSM parameters to use to register the runner." + value = one(module.github_runner[*].ssm_parameters) +} + +output "github_runners" { + description = "Information about the GitHub runners." + value = one(module.github_runner[*].runners) +} + +output "queues" { + description = "Information about the GitHub runner queues. Such as `build_queue_arn` the ARN of the SQS queue to use for the build queue." + value = one(module.github_runner[*].queues) +} diff --git a/modules/philips-labs-github-runners/providers.tf b/modules/philips-labs-github-runners/providers.tf new file mode 100644 index 000000000..54257fd20 --- /dev/null +++ b/modules/philips-labs-github-runners/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = module.iam_roles.terraform_role_arn + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/philips-labs-github-runners/remote-state.tf b/modules/philips-labs-github-runners/remote-state.tf new file mode 100644 index 000000000..757ef9067 --- /dev/null +++ b/modules/philips-labs-github-runners/remote-state.tf @@ -0,0 +1,8 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "vpc" + + context = module.this.context +} diff --git a/modules/philips-labs-github-runners/templates/userdata_post_install.sh b/modules/philips-labs-github-runners/templates/userdata_post_install.sh new file mode 100644 index 000000000..b511a316b --- /dev/null +++ b/modules/philips-labs-github-runners/templates/userdata_post_install.sh @@ -0,0 +1,3 @@ + +echo "Installing Custom Packages..." +yum install -y make diff --git a/modules/philips-labs-github-runners/variables.tf b/modules/philips-labs-github-runners/variables.tf new file mode 100644 index 000000000..7824b14e1 --- /dev/null +++ b/modules/philips-labs-github-runners/variables.tf @@ -0,0 +1,60 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "enable_update_github_app_webhook" { + type = bool + description = "Enable updating the github app webhook" + default = false +} + +variable "lambda_repo_url" { + type = string + description = "URL of the lambda repository" + default = "https://github.com/philips-labs/terraform-aws-github-runner" +} + +variable "release_version" { + type = string + description = "Version of the application" + default = "v5.4.0" +} + +variable "github_app_key_ssm_path" { + type = string + description = "Path to the github key in SSM" + default = "/pl-github-runners/key" +} + +variable "github_app_id_ssm_path" { + type = string + description = "Path to the github app id in SSM" + default = "/pl-github-runners/id" +} + +variable "runner_extra_labels" { + description = "Extra (custom) labels for the runners (GitHub). Labels checks on the webhook can be enforced by setting `enable_workflow_job_labels_check`. GitHub read-only labels should not be provided." + type = list(string) + default = ["default"] +} + +variable "scale_up_reserved_concurrent_executions" { + description = "Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations." + type = number + # default from philips labs is 1, which gives an error when creating the lambda Specified ReservedConcurrentExecutions for function decreases account's UnreservedConcurrentExecution below its minimum value of [10] + # https://github.com/philips-labs/terraform-aws-github-runner/issues/1671 + default = -1 +} + +variable "instance_types" { + description = "List of instance types for the action runner. Defaults are based on runner_os (al2023 for linux and Windows Server Core for win)." + type = list(string) + default = ["m5.large", "c5.large"] +} + +variable "repository_white_list" { + description = "List of github repository full names (owner/repo_name) that will be allowed to use the github app. Leave empty for no filtering." + type = list(string) + default = [] +} diff --git a/modules/philips-labs-github-runners/versions.tf b/modules/philips-labs-github-runners/versions.tf new file mode 100644 index 000000000..cc5d839f3 --- /dev/null +++ b/modules/philips-labs-github-runners/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + local = { + source = "hashicorp/local" + version = ">= 2.4.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } + } +} From 025cfaffb97aa704a645e3a3ff1a0e67805bc10c Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Tue, 21 Nov 2023 12:01:37 -0500 Subject: [PATCH 308/501] Update ASG instance_refresh and Terraform Provider Version in ECS Component (#858) Co-authored-by: Dan Miller --- modules/ecs/README.md | 4 ++-- modules/ecs/variables.tf | 12 +++++++----- modules/ecs/versions.tf | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/modules/ecs/README.md b/modules/ecs/README.md index f0997eb34..088f8d093 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -42,7 +42,7 @@ components: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.0 | ## Providers @@ -87,7 +87,7 @@ components: | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [capacity\_providers\_ec2](#input\_capacity\_providers\_ec2) | EC2 autoscale groups capacity providers |
map(object({
instance_type = string
max_size = number
security_group_ids = optional(list(string), [])
min_size = optional(number, 0)
image_id = optional(string)
instance_initiated_shutdown_behavior = optional(string, "terminate")
key_name = optional(string, "")
user_data = optional(string, "")
enable_monitoring = optional(bool, true)
instance_warmup_period = optional(number, 300)
maximum_scaling_step_size = optional(number, 1)
minimum_scaling_step_size = optional(number, 1)
target_capacity_utilization = optional(number, 100)
ebs_optimized = optional(bool, false)
block_device_mappings = optional(list(object({
device_name = string
no_device = bool
virtual_name = string
ebs = object({
delete_on_termination = bool
encrypted = bool
iops = number
kms_key_id = string
snapshot_id = string
volume_size = number
volume_type = string
})
})), [])
instance_market_options = optional(object({
market_type = string
spot_options = object({
block_duration_minutes = number
instance_interruption_behavior = string
max_price = number
spot_instance_type = string
valid_until = string
})
}))
instance_refresh = optional(object({
strategy = string
preferences = object({
instance_warmup = number
min_healthy_percentage = number
})
triggers = list(string)
}))
mixed_instances_policy = optional(object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
}), {
instances_distribution = null
})
placement = optional(object({
affinity = string
availability_zone = string
group_name = string
host_id = string
tenancy = string
}))
credit_specification = optional(object({
cpu_credits = string
}))
elastic_gpu_specifications = optional(object({
type = string
}))
disable_api_termination = optional(bool, false)
default_cooldown = optional(number, 300)
health_check_grace_period = optional(number, 300)
force_delete = optional(bool, false)
termination_policies = optional(list(string), ["Default"])
suspended_processes = optional(list(string), [])
placement_group = optional(string, "")
metrics_granularity = optional(string, "1Minute")
enabled_metrics = optional(list(string), [
"GroupMinSize",
"GroupMaxSize",
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupPendingInstances",
"GroupStandbyInstances",
"GroupTerminatingInstances",
"GroupTotalInstances",
"GroupInServiceCapacity",
"GroupPendingCapacity",
"GroupStandbyCapacity",
"GroupTerminatingCapacity",
"GroupTotalCapacity",
"WarmPoolDesiredCapacity",
"WarmPoolWarmedCapacity",
"WarmPoolPendingCapacity",
"WarmPoolTerminatingCapacity",
"WarmPoolTotalCapacity",
"GroupAndWarmPoolDesiredCapacity",
"GroupAndWarmPoolTotalCapacity",
])
wait_for_capacity_timeout = optional(string, "10m")
service_linked_role_arn = optional(string, "")
metadata_http_endpoint_enabled = optional(bool, true)
metadata_http_put_response_hop_limit = optional(number, 2)
metadata_http_tokens_required = optional(bool, true)
metadata_http_protocol_ipv6_enabled = optional(bool, false)
tag_specifications_resource_types = optional(set(string), ["instance", "volume"])
max_instance_lifetime = optional(number, null)
capacity_rebalance = optional(bool, false)
warm_pool = optional(object({
pool_state = string
min_size = number
max_group_prepared_capacity = number
}))
}))
| `{}` | no | +| [capacity\_providers\_ec2](#input\_capacity\_providers\_ec2) | EC2 autoscale groups capacity providers |
map(object({
instance_type = string
max_size = number
security_group_ids = optional(list(string), [])
min_size = optional(number, 0)
image_id = optional(string)
instance_initiated_shutdown_behavior = optional(string, "terminate")
key_name = optional(string, "")
user_data = optional(string, "")
enable_monitoring = optional(bool, true)
instance_warmup_period = optional(number, 300)
maximum_scaling_step_size = optional(number, 1)
minimum_scaling_step_size = optional(number, 1)
target_capacity_utilization = optional(number, 100)
ebs_optimized = optional(bool, false)
block_device_mappings = optional(list(object({
device_name = string
no_device = bool
virtual_name = string
ebs = object({
delete_on_termination = bool
encrypted = bool
iops = number
kms_key_id = string
snapshot_id = string
volume_size = number
volume_type = string
})
})), [])
instance_market_options = optional(object({
market_type = string
spot_options = object({
block_duration_minutes = number
instance_interruption_behavior = string
max_price = number
spot_instance_type = string
valid_until = string
})
}))
instance_refresh = optional(object({
strategy = string
preferences = optional(object({
instance_warmup = optional(number, null)
min_healthy_percentage = optional(number, null)
skip_matching = optional(bool, null)
auto_rollback = optional(bool, null)
}), null)
triggers = optional(list(string), [])
}))
mixed_instances_policy = optional(object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
}), {
instances_distribution = null
})
placement = optional(object({
affinity = string
availability_zone = string
group_name = string
host_id = string
tenancy = string
}))
credit_specification = optional(object({
cpu_credits = string
}))
elastic_gpu_specifications = optional(object({
type = string
}))
disable_api_termination = optional(bool, false)
default_cooldown = optional(number, 300)
health_check_grace_period = optional(number, 300)
force_delete = optional(bool, false)
termination_policies = optional(list(string), ["Default"])
suspended_processes = optional(list(string), [])
placement_group = optional(string, "")
metrics_granularity = optional(string, "1Minute")
enabled_metrics = optional(list(string), [
"GroupMinSize",
"GroupMaxSize",
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupPendingInstances",
"GroupStandbyInstances",
"GroupTerminatingInstances",
"GroupTotalInstances",
"GroupInServiceCapacity",
"GroupPendingCapacity",
"GroupStandbyCapacity",
"GroupTerminatingCapacity",
"GroupTotalCapacity",
"WarmPoolDesiredCapacity",
"WarmPoolWarmedCapacity",
"WarmPoolPendingCapacity",
"WarmPoolTerminatingCapacity",
"WarmPoolTotalCapacity",
"GroupAndWarmPoolDesiredCapacity",
"GroupAndWarmPoolTotalCapacity",
])
wait_for_capacity_timeout = optional(string, "10m")
service_linked_role_arn = optional(string, "")
metadata_http_endpoint_enabled = optional(bool, true)
metadata_http_put_response_hop_limit = optional(number, 2)
metadata_http_tokens_required = optional(bool, true)
metadata_http_protocol_ipv6_enabled = optional(bool, false)
tag_specifications_resource_types = optional(set(string), ["instance", "volume"])
max_instance_lifetime = optional(number, null)
capacity_rebalance = optional(bool, false)
warm_pool = optional(object({
pool_state = string
min_size = number
max_group_prepared_capacity = number
}))
}))
| `{}` | no | | [capacity\_providers\_fargate](#input\_capacity\_providers\_fargate) | Use FARGATE capacity provider | `bool` | `true` | no | | [capacity\_providers\_fargate\_spot](#input\_capacity\_providers\_fargate\_spot) | Use FARGATE\_SPOT capacity provider | `bool` | `false` | no | | [container\_insights\_enabled](#input\_container\_insights\_enabled) | Whether or not to enable container insights | `bool` | `true` | no | diff --git a/modules/ecs/variables.tf b/modules/ecs/variables.tf index 6c0ccd171..7f0c40eea 100644 --- a/modules/ecs/variables.tf +++ b/modules/ecs/variables.tf @@ -175,11 +175,13 @@ variable "capacity_providers_ec2" { })) instance_refresh = optional(object({ strategy = string - preferences = object({ - instance_warmup = number - min_healthy_percentage = number - }) - triggers = list(string) + preferences = optional(object({ + instance_warmup = optional(number, null) + min_healthy_percentage = optional(number, null) + skip_matching = optional(bool, null) + auto_rollback = optional(bool, null) + }), null) + triggers = optional(list(string), []) })) mixed_instances_policy = optional(object({ instances_distribution = object({ diff --git a/modules/ecs/versions.tf b/modules/ecs/versions.tf index f33ede77f..4c8603db1 100644 --- a/modules/ecs/versions.tf +++ b/modules/ecs/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { From cb2300070f16190e01e2ce61416bb5d7d9a76464 Mon Sep 17 00:00:00 2001 From: Zinovii Dmytriv Date: Tue, 21 Nov 2023 19:16:32 +0200 Subject: [PATCH 309/501] Use absolute links for refarch docs (#770) Co-authored-by: milldr --- deprecated/eks/eks-without-spotinst/README.md | 2 +- modules/account-map/README.md | 2 +- modules/account/README.md | 6 +++--- modules/acm/README.md | 2 +- modules/aws-backup/README.md | 2 +- modules/datadog-monitor/README.md | 12 ++++++------ modules/dns-primary/README.md | 2 +- modules/ecr/README.md | 4 ++-- modules/eks/cluster/README.md | 12 ++++++------ modules/opsgenie-team/README.md | 16 ++++++++-------- modules/tfstate-backend/README.md | 2 +- modules/vpc/README.md | 2 +- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/deprecated/eks/eks-without-spotinst/README.md b/deprecated/eks/eks-without-spotinst/README.md index 1303afb4c..3f0e2344a 100644 --- a/deprecated/eks/eks-without-spotinst/README.md +++ b/deprecated/eks/eks-without-spotinst/README.md @@ -8,7 +8,7 @@ If Spotinst is going to be used, the following course of action needs to be foll 1. Create Spotinst account and subscribe to a Business Plan. 1. Provision [spotinst-integration](https://spot.io/), as documented in the component. 1. Provision EKS with Spotinst Ocean pool only. -1. Deploy core K8s components, including [metrics-server](/components/library/aws/eks/metrics-server), [external-dns](/components/library/aws/eks/external-dns), etc. +1. Deploy core K8s components, including [metrics-server](https://docs.cloudposse.com/components/library/aws/eks/metrics-server), [external-dns](https://docs.cloudposse.com/components/library/aws/eks/external-dns), etc. 1. Deploy Spotinst [ocean-controller](https://docs.spot.io/ocean/tutorials/spot-kubernetes-controller/). ## Usage diff --git a/modules/account-map/README.md b/modules/account-map/README.md index a4ba1c398..a3f5da09a 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -4,7 +4,7 @@ This component is responsible for provisioning information only: it simply popul ## Pre-requisites -- [account](/components/library/aws/account) must be provisioned before [account-map](/components/library/aws/account-map) component +- [account](https://docs.cloudposse.com/components/library/aws/account) must be provisioned before [account-map](https://docs.cloudposse.com/components/library/aws/account-map) component ## Usage diff --git a/modules/account/README.md b/modules/account/README.md index 045e7ca27..8ed60bf4b 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -3,7 +3,7 @@ This component is responsible for provisioning the full account hierarchy along with Organizational Units (OUs). It includes the ability to associate Service Control Policies (SCPs) to the Organization, each Organizational Unit and account. :::info -Part of a [cold start](/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) so it has to be initially run with `SuperAdmin` role. +Part of a [cold start](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) so it has to be initially run with `SuperAdmin` role. ::: @@ -161,7 +161,7 @@ Unfortunately, there are some tasks that need to be done via the console. Log in #### Request an increase in the maximum number of accounts allowed :::caution -Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is necessary to expedite the quota increase requests, which could take several days on a basic support plan. Without it, AWS support will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the requests. AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to your AWS TAM. See [AWS](/reference-architecture/reference/aws). +Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is necessary to expedite the quota increase requests, which could take several days on a basic support plan. Without it, AWS support will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the requests. AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to your AWS TAM. See [AWS](https://docs.cloudposse.com/reference-architecture/reference/aws). ::: @@ -280,7 +280,7 @@ AWS accounts and organizational units are generated dynamically by the `terrafor :::info _**Special note:**_ **** In the rare case where you will need to be enabling non-default AWS Regions, temporarily comment out the `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore it later, after enabling the optional Regions. -See related: [Decide on Opting Into Non-default Regions](/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) +See related: [Decide on Opting Into Non-default Regions](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) ::: diff --git a/modules/acm/README.md b/modules/acm/README.md index 55d0ab71a..7d087ba30 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -2,7 +2,7 @@ This component is responsible for requesting an ACM certificate for a domain and adding a CNAME record to the DNS zone to complete certificate validation. -The ACM component is to manage an unlimited number of certificates, predominantly for vanity domains. While the [dns-primary](/components/library/aws/dns-primary) component has the ability to generate ACM certificates, it is very opinionated and can only manage one zone. In reality, companies have many branded domains associated with a load balancer, so we need to be able to generate more complicated certificates. +The ACM component is to manage an unlimited number of certificates, predominantly for vanity domains. While the [dns-primary](https://docs.cloudposse.com/components/library/aws/dns-primary) component has the ability to generate ACM certificates, it is very opinionated and can only manage one zone. In reality, companies have many branded domains associated with a load balancer, so we need to be able to generate more complicated certificates. We have, as a convenience, the ability to create an ACM certificate as part of creating a DNS zone, whether primary or delegated. That convenience is limited to creating `example.com` and `*.example.com` when creating a zone for `example.com`. For example, Acme has delegated `acct.acme.com` and in addition to `*.acct.acme.com` needed an ACM certificate for `*.usw2.acct.acme.com`, so we use the ACM component to provision that, rather than extend the DNS primary or delegated components to take a list of additional certificates. Both are different views on the Single Responsibility Principle. diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index c0165efd9..07d82c728 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -266,4 +266,4 @@ No resources. ## Related How-to Guides -- [How to Enable Cross-Region Backups in AWS-Backup](/reference-architecture/how-to-guides/tutorials/how-to-enable-cross-region-backups-in-aws-backup) +- [How to Enable Cross-Region Backups in AWS-Backup](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-enable-cross-region-backups-in-aws-backup) diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index ad69c0e8f..9ef9dc6bd 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -24,7 +24,7 @@ components: ``` ## Conventions -- Treat datadog like a separate cloud provider with integrations ([datadog-integration](/components/library/aws/datadog-integration)) into your accounts. +- Treat datadog like a separate cloud provider with integrations ([datadog-integration](https://docs.cloudposse.com/components/library/aws/datadog-integration)) into your accounts. - Use the `catalog` convention to define a step of alerts. You can use ours or define your own. [https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors](https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors) @@ -234,13 +234,13 @@ No resources. ## Related How-to Guides -- [How to Onboard a New Service with Datadog and OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-onboard-a-new-service-with-datadog-and-opsgenie) -- [How to Sign Up for Datadog?](/reference-architecture/how-to-guides/tutorials/how-to-sign-up-for-datadog) -- [How to use Datadog Metrics for Horizontal Pod Autoscaling (HPA)](/reference-architecture/how-to-guides/tutorials/how-to-use-datadog-metrics-for-horizontal-pod-autoscaling-hpa) -- [How to Implement SRE with Datadog](/reference-architecture/how-to-guides/tutorials/how-to-implement-sre-with-datadog) +- [How to Onboard a New Service with Datadog and OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-onboard-a-new-service-with-datadog-and-opsgenie) +- [How to Sign Up for Datadog?](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-sign-up-for-datadog) +- [How to use Datadog Metrics for Horizontal Pod Autoscaling (HPA)](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-use-datadog-metrics-for-horizontal-pod-autoscaling-hpa) +- [How to Implement SRE with Datadog](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-sre-with-datadog) ## Component Dependencies -- [datadog-integration](/reference-architecture/components/datadog-integration) +- [datadog-integration](https://docs.cloudposse.com/components/library/aws/datadog-integration/) ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-monitor) - Cloud Posse's upstream component diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index 9c0023715..46bd8f6e4 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -72,7 +72,7 @@ components: ``` :::info -Use the [acm](/components/library/aws/acm) component for more advanced certificate requirements. +Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component for more advanced certificate requirements. ::: diff --git a/modules/ecr/README.md b/modules/ecr/README.md index 62def8f65..be5d2fc31 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -132,9 +132,9 @@ components: ## Related -- [Decide How to distribute Docker Images](/reference-architecture/design-decisions/foundational-platform/decide-how-to-distribute-docker-images) +- [Decide How to distribute Docker Images](https://docs.cloudposse.com/reference-architecture/design-decisions/foundational-platform/decide-how-to-distribute-docker-images) -- [Decide on ECR Strategy](/reference-architecture/design-decisions/foundational-platform/decide-on-ecr-strategy) +- [Decide on ECR Strategy](https://docs.cloudposse.com/reference-architecture/design-decisions/foundational-platform/decide-on-ecr-strategy) ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecr) - Cloud Posse's upstream component diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 1b3300073..7ea65c62d 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -583,13 +583,13 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor ## Related How-to Guides -- [How to Load Test in AWS](/reference-architecture/how-to-guides/tutorials/how-to-load-test-in-aws) -- [How to Tune EKS with AWS Managed Node Groups](/reference-architecture/how-to-guides/tutorials/how-to-tune-eks-with-aws-managed-node-groups) -- [How to Keep Everything Up to Date](/reference-architecture/how-to-guides/upgrades/how-to-keep-everything-up-to-date) -- [How to Tune SpotInst Parameters for EKS](/reference-architecture/how-to-guides/tutorials/how-to-tune-spotinst-parameters-for-eks) -- [How to Upgrade EKS Cluster Addons](/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons) +- [How to Load Test in AWS](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-load-test-in-aws) +- [How to Tune EKS with AWS Managed Node Groups](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-tune-eks-with-aws-managed-node-groups) +- [How to Keep Everything Up to Date](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-keep-everything-up-to-date) +- [How to Tune SpotInst Parameters for EKS](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-tune-spotinst-parameters-for-eks) +- [How to Upgrade EKS Cluster Addons](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons) +- [How to Upgrade EKS](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks) - [EBS CSI Migration FAQ](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi-migration-faq.html) -- [How to Upgrade EKS](/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks) ## References diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index 9f769d80a..7ed43d5ff 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -373,14 +373,14 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ ## Related How-to Guides -- [How to Add Users to a Team in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-add-users-to-a-team-in-opsgenie) -- [How to Pass Tags Along to Datadog](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-pass-tags-along-to-datadog) -- [How to Onboard a New Service with Datadog and OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-onboard-a-new-service-with-datadog-and-opsgenie) -- [How to Create Escalation Rules in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-escalation-rules-in-opsgenie) -- [How to Setup Rotations in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-setup-rotations-in-opsgenie) -- [How to Create New Teams in OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-new-teams-in-opsgenie) -- [How to Sign Up for OpsGenie?](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-sign-up-for-opsgenie/) -- [How to Implement Incident Management with OpsGenie](/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie) +- [How to Add Users to a Team in OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-add-users-to-a-team-in-opsgenie) +- [How to Pass Tags Along to Datadog](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-pass-tags-along-to-datadog) +- [How to Onboard a New Service with Datadog and OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-onboard-a-new-service-with-datadog-and-opsgenie) +- [How to Create Escalation Rules in OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-escalation-rules-in-opsgenie) +- [How to Setup Rotations in OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-setup-rotations-in-opsgenie) +- [How to Create New Teams in OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-new-teams-in-opsgenie) +- [How to Sign Up for OpsGenie?](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-sign-up-for-opsgenie/) +- [How to Implement Incident Management with OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie) ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/opsgenie-team) - Cloud Posse's upstream component diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 0de1f8eb3..40100f0fe 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -7,7 +7,7 @@ However, perhaps counter-intuitively, all Terraform users require read access to :::info Part of cold start so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and then to move the state into it. -Follow the guide **[here](/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** to get started. +Follow the guide **[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** to get started. ::: ### Access Control diff --git a/modules/vpc/README.md b/modules/vpc/README.md index f464210ba..d8d758578 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -1,6 +1,6 @@ # Component: `vpc` -This component is responsible for provisioning a VPC and corresponding Subnets. Additionally, VPC Flow Logs can optionally be enabled for auditing purposes. See the existing [VPC configuration](./vpc-configuration.md) documentation for the provisioned subnets. +This component is responsible for provisioning a VPC and corresponding Subnets. Additionally, VPC Flow Logs can optionally be enabled for auditing purposes. See the existing VPC configuration documentation for the provisioned subnets. ## Usage From 0854f4c91ee1502681dbf01cb7c847ad6481065b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 21 Nov 2023 14:18:17 -0800 Subject: [PATCH 310/501] Return `local.identity_account_name` for `spacelift/worker-pool` (#912) --- modules/spacelift/worker-pool/iam.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/spacelift/worker-pool/iam.tf b/modules/spacelift/worker-pool/iam.tf index f1df55935..6fc90960a 100644 --- a/modules/spacelift/worker-pool/iam.tf +++ b/modules/spacelift/worker-pool/iam.tf @@ -25,7 +25,8 @@ data "aws_iam_policy_document" "assume_role_policy" { } locals { - role_arn_template = module.account_map.outputs.iam_role_arn_templates[local.identity_account_name] + identity_account_name = module.account_map.outputs.identity_account_account_name + role_arn_template = module.account_map.outputs.iam_role_arn_templates[local.identity_account_name] } data "aws_iam_policy_document" "default" { From f6fd67b56e425908b7e4e5744f0a2799ffc42358 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 23 Nov 2023 04:40:01 -0800 Subject: [PATCH 311/501] fix: `spacelift/worker-pool` AMI User Data Docker Login (#913) --- modules/spacelift/worker-pool/templates/user-data.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/spacelift/worker-pool/templates/user-data.sh b/modules/spacelift/worker-pool/templates/user-data.sh index db8ae1cae..d145a8059 100644 --- a/modules/spacelift/worker-pool/templates/user-data.sh +++ b/modules/spacelift/worker-pool/templates/user-data.sh @@ -3,7 +3,9 @@ spacelift() { ( set -e - $(aws --region ${ecr_region} ecr get-login --registry-ids ${ecr_account_id} --no-include-email) + aws ecr get-login-password --region ${ecr_region} \ + | docker login --username AWS --password-stdin ${ecr_account_id}.dkr.ecr.${ecr_region}.amazonaws.com + docker pull ${spacelift_runner_image} echo "Updating packages (security)" | tee -a /var/log/spacelift/info.log From d42898ab835316f5f04df5f9715281045524b413 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 27 Nov 2023 10:03:04 -0800 Subject: [PATCH 312/501] bugfix: Lambda Fetching for `philips-labs-github-runners` (#911) Co-authored-by: Andriy Knysh Co-authored-by: cloudpossebot --- modules/philips-labs-github-runners/README.md | 2 +- modules/philips-labs-github-runners/main.tf | 35 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/modules/philips-labs-github-runners/README.md b/modules/philips-labs-github-runners/README.md index e38d2ef79..b3533c185 100644 --- a/modules/philips-labs-github-runners/README.md +++ b/modules/philips-labs-github-runners/README.md @@ -84,9 +84,9 @@ This is output by the component, and available via the `webhook` output under `e | Name | Source | Version | |------|--------|---------| -| [fetch\_lambdas](#module\_fetch\_lambdas) | philips-labs/github-runner/aws//modules/download-lambda | 5.4.0 | | [github\_runner](#module\_github\_runner) | philips-labs/github-runner/aws | 5.4.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [module\_artifact](#module\_module\_artifact) | cloudposse/module-artifact/external | 0.8.0 | | [store\_read](#module\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | diff --git a/modules/philips-labs-github-runners/main.tf b/modules/philips-labs-github-runners/main.tf index befa158f2..5dc5ccc7e 100644 --- a/modules/philips-labs-github-runners/main.tf +++ b/modules/philips-labs-github-runners/main.tf @@ -3,20 +3,20 @@ locals { version = var.enabled ? var.release_version : null lambda_repo = "https://github.com/philips-labs/terraform-aws-github-runner" - lambdas = var.enabled ? [ - { - name = "webhook" + lambdas = var.enabled ? { + webhook = { + name = "webhook.zip" tag = local.version }, - { - name = "runners" + runners = { + name = "runners.zip" tag = local.version }, - { - name = "runner-binaries-syncer" + runner-binaries-syncer = { + name = "runner-binaries-syncer.zip" tag = local.version } - ] : [] + } : {} } module "store_read" { @@ -35,13 +35,20 @@ resource "random_id" "webhook_secret" { byte_length = 20 } -module "fetch_lambdas" { - count = local.enabled ? 1 : 0 +module "module_artifact" { + for_each = local.lambdas - source = "philips-labs/github-runner/aws//modules/download-lambda" - version = "5.4.0" + source = "cloudposse/module-artifact/external" + version = "0.8.0" + + filename = each.value.name + module_name = module.this.name + url = "https://github.com/philips-labs/terraform-aws-github-runner/releases/download/${each.value.tag}/${each.key}.zip" + curl_arguments = ["-fsSL"] - lambdas = local.lambdas + module_path = path.module + + context = module.this.context } module "github_runner" { @@ -50,6 +57,8 @@ module "github_runner" { source = "philips-labs/github-runner/aws" version = "5.4.0" + depends_on = [module.module_artifact] + aws_region = var.region vpc_id = module.vpc.outputs.vpc_id subnet_ids = module.vpc.outputs.private_subnet_ids From d19f639be4ff792f142ad69784c3d9e04e320803 Mon Sep 17 00:00:00 2001 From: Brad Janke Date: Thu, 30 Nov 2023 06:32:26 -0600 Subject: [PATCH 313/501] fix(inspector is now enabled on deploy) (#914) Co-authored-by: cloudpossebot --- modules/aws-inspector2/README.md | 2 ++ modules/aws-inspector2/main.tf | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/modules/aws-inspector2/README.md b/modules/aws-inspector2/README.md index 351e97ad7..ba6324d68 100644 --- a/modules/aws-inspector2/README.md +++ b/modules/aws-inspector2/README.md @@ -73,6 +73,8 @@ components: | Name | Type | |------|------| | [aws_inspector2_delegated_admin_account.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/inspector2_delegated_admin_account) | resource | +| [aws_inspector2_enabler.delegated_admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/inspector2_enabler) | resource | +| [aws_inspector2_enabler.member_accounts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/inspector2_enabler) | resource | | [aws_inspector2_member_association.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/inspector2_member_association) | resource | | [aws_inspector2_organization_configuration.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/inspector2_organization_configuration) | resource | | [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | diff --git a/modules/aws-inspector2/main.tf b/modules/aws-inspector2/main.tf index 1f1c26719..1c6b9ff33 100644 --- a/modules/aws-inspector2/main.tf +++ b/modules/aws-inspector2/main.tf @@ -13,6 +13,8 @@ locals { create_org_delegation = local.enabled && local.is_org_management_account create_org_configuration = local.enabled && local.is_org_delegated_administrator_account && var.admin_delegated + + resource_types = compact([var.auto_enable_ec2 ? "EC2" : null, var.auto_enable_ecr ? "ECR" : null, var.auto_enable_lambda ? "Lambda" : null]) } data "aws_caller_identity" "this" { @@ -26,10 +28,19 @@ resource "aws_inspector2_delegated_admin_account" "default" { account_id = local.org_delegated_administrator_account_id } +resource "aws_inspector2_enabler" "delegated_admin" { + count = local.create_org_configuration ? 1 : 0 + + account_ids = [local.org_delegated_administrator_account_id] + resource_types = local.resource_types +} + # If we are are in the AWS Organization designated administrator account, # configure all other accounts to send their Inspector2 findings. resource "aws_inspector2_organization_configuration" "default" { count = local.create_org_configuration ? 1 : 0 + + depends_on = [aws_inspector2_enabler.delegated_admin] auto_enable { ec2 = var.auto_enable_ec2 ecr = var.auto_enable_ecr @@ -37,6 +48,15 @@ resource "aws_inspector2_organization_configuration" "default" { } } +resource "aws_inspector2_enabler" "member_accounts" { + count = local.create_org_configuration ? 1 : 0 + + depends_on = [aws_inspector2_member_association.default] + + account_ids = local.member_account_ids + resource_types = local.resource_types +} + resource "aws_inspector2_member_association" "default" { for_each = local.create_org_configuration ? toset(local.member_account_ids) : [] account_id = each.value From 11e7219f98c31666ad2729ab41ac923336e5d0ac Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Fri, 1 Dec 2023 12:38:09 -0500 Subject: [PATCH 314/501] Update Spacelift components (#915) --- modules/spacelift/admin-stack/README.md | 10 +- modules/spacelift/admin-stack/child-stacks.tf | 112 ++++++++++-------- .../spacelift/admin-stack/root-admin-stack.tf | 61 ++++++---- modules/spacelift/admin-stack/variables.tf | 1 + modules/spacelift/spaces/README.md | 6 +- modules/spacelift/spaces/main.tf | 6 +- 6 files changed, 111 insertions(+), 85 deletions(-) diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index 29a93178f..edd9c60ac 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -1,9 +1,9 @@ # Component: `spacelift/admin-stack` This component is responsible for creating an administrative [stack](https://docs.spacelift.io/concepts/stack/) and its -corresponding child stacks in the spacelift organization. +corresponding child stacks in the Spacelift organization. -The component uses a series of `context_fiters` to select atmos component instances to manage as child stacks. +The component uses a series of `context_filters` to select atmos component instances to manage as child stacks. ## Usage @@ -12,6 +12,7 @@ The component uses a series of `context_fiters` to select atmos component instan The following are example snippets of how to use this component. For more on Spacelift admin stack usage, see the [Spacelift README](https://docs.cloudposse.com/components/library/aws/spacelift/) First define the default configuration for any admin stack: + ```yaml # stacks/catalog/spacelift/admin-stack.yaml components: @@ -54,6 +55,7 @@ components: ``` Then define the root-admin stack: + ```yaml # stacks/orgs/acme/spacelift.yaml import: @@ -96,10 +98,10 @@ components: # this creates policies for the children (admin) stacks child_policy_attachments: - TRIGGER Global administrator - ``` -Finally define any tenant-specific stacks: +Finally, define any tenant-specific stacks: + ```yaml # stacks/orgs/acme/core/spacelift.yaml import: diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index 4b02f69d6..59c181643 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -57,12 +57,25 @@ module "child_stack" { version = "1.4.0" for_each = local.child_stacks - depends_on = [ - null_resource.spaces_precondition, - null_resource.workers_precondition, - spacelift_policy_attachment.root, - null_resource.child_stack_parent_precondition - ] + + # Only the following attributes are available in `each.value` + # component, base_component, stack, imports, deps, deps_all, vars, settings, env, inheritance, metadata, backend_type, backend, workspace, labels + # They are in the outputs from the module https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/main/modules/spacelift-stacks-from-atmos-config + # The rest are configured in `settings.spacelift` or `vars` for each component, and should be accessed by `each.value.settings.spacelift` and `each.value.vars` + + atmos_stack_name = try(each.value.stack, null) + component_env = try(each.value.env, var.component_env) + component_name = try(each.value.component, null) + component_root = try(join("/", [var.component_root, try(each.value.metadata.component, each.value.component)])) + component_vars = try(each.value.vars, var.component_vars) + terraform_workspace = try(each.value.workspace, var.terraform_workspace) + + labels = concat( + try(each.value.labels, []), + try(each.value.vars.labels, []), + ["managed-by:${local.managed_by}"], + local.create_root_admin_stack ? ["depends-on:${local.root_admin_stack_name}", ""] : [] + ) administrative = try(each.value.settings.spacelift.administrative, false) after_apply = try(each.value.settings.spacelift.after_apply, []) @@ -70,63 +83,58 @@ module "child_stack" { after_init = try(each.value.settings.spacelift.after_init, []) after_perform = try(each.value.settings.spacelift.after_perform, []) after_plan = try(each.value.settings.spacelift.after_plan, []) - atmos_stack_name = try(each.value.stack, null) autodeploy = try(each.value.settings.spacelift.autodeploy, var.autodeploy) autoretry = try(each.value.settings.spacelift.autoretry, var.autoretry) - aws_role_enabled = try(each.value.settings.aws_role_enabled, var.aws_role_enabled) - aws_role_arn = try(each.value.settings.aws_role_arn, var.aws_role_arn) - aws_role_external_id = try(each.value.settings.aws_role_external_id, var.aws_role_external_id) - aws_role_generate_credentials_in_worker = try(each.value.settings.aws_role_generate_credentials_in_worker, var.aws_role_generate_credentials_in_worker) + aws_role_enabled = try(each.value.settings.spacelift.aws_role_enabled, var.aws_role_enabled) + aws_role_arn = try(each.value.settings.spacelift.aws_role_arn, var.aws_role_arn) + aws_role_external_id = try(each.value.settings.spacelift.aws_role_external_id, var.aws_role_external_id) + aws_role_generate_credentials_in_worker = try(each.value.settings.spacelift.aws_role_generate_credentials_in_worker, var.aws_role_generate_credentials_in_worker) before_apply = try(each.value.settings.spacelift.before_apply, []) before_destroy = try(each.value.settings.spacelift.before_destroy, []) before_init = try(each.value.settings.spacelift.before_init, []) before_perform = try(each.value.settings.spacelift.before_perform, []) before_plan = try(each.value.settings.spacelift.before_plan, []) - branch = try(each.value.branch, var.branch) - commit_sha = var.commit_sha != null ? var.commit_sha : try(each.value.commit_sha, null) - component_env = try(each.value.env, var.component_env) - component_name = try(each.value.component, null) - component_root = try(join("/", [var.component_root, try(each.value.metadata.component, each.value.component)])) - component_vars = try(each.value.vars, var.component_vars) - context_attachments = try(each.value.context_attachments, var.context_attachments) - description = try(each.value.description, var.description) + branch = try(each.value.settings.spacelift.branch, var.branch) + commit_sha = var.commit_sha != null ? var.commit_sha : try(each.value.settings.spacelift.commit_sha, null) + context_attachments = try(each.value.settings.spacelift.context_attachments, var.context_attachments) + description = try(each.value.settings.spacelift.description, var.description) drift_detection_enabled = try(each.value.settings.spacelift.drift_detection_enabled, var.drift_detection_enabled) drift_detection_reconcile = try(each.value.settings.spacelift.drift_detection_reconcile, var.drift_detection_reconcile) drift_detection_schedule = try(each.value.settings.spacelift.drift_detection_schedule, var.drift_detection_schedule) drift_detection_timezone = try(each.value.settings.spacelift.drift_detection_timezone, var.drift_detection_timezone) - labels = concat( - try(each.value.labels, []), - try(each.value.vars.labels, []), - ["managed-by:${local.managed_by}"], - local.create_root_admin_stack ? ["depends-on:${local.root_admin_stack_name}", ""] : [] - ) - local_preview_enabled = try(each.value.local_preview_enabled, var.local_preview_enabled) - manage_state = try(each.value.manage_state, var.manage_state) - policy_ids = try(local.child_policy_ids, []) - protect_from_deletion = try(each.value.settings.spacelift.protect_from_deletion, var.protect_from_deletion) - repository = var.repository - runner_image = try(each.value.settings.spacelift.runner_image, var.runner_image) - space_id = local.spaces[each.value.settings.spacelift.space_name] - spacelift_run_enabled = try(each.value.settings.spacelift.spacelift_run_enabled, var.spacelift_run_enabled) - spacelift_stack_dependency_enabled = try(each.value.settings.spacelift.spacelift_stack_dependency_enabled, var.spacelift_stack_dependency_enabled) - stack_destructor_enabled = try(each.value.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) - stack_name = try(each.value.settings.spacelift.stack_name, each.key) - terraform_smart_sanitization = try(each.value.terraform_smart_sanitization, var.terraform_smart_sanitization) - terraform_version = lookup(var.terraform_version_map, try(each.value.terraform_version, ""), var.terraform_version) - terraform_workspace = try(each.value.workspace, var.terraform_workspace) - webhook_enabled = try(each.value.webhook_enabled, var.webhook_enabled) - webhook_endpoint = try(each.value.webhook_endpoint, var.webhook_endpoint) - webhook_secret = try(each.value.webhook_secret, var.webhook_secret) - worker_pool_id = try(local.worker_pools[each.value.worker_pool_name], local.worker_pools[var.worker_pool_name]) - - azure_devops = try(each.value.azure_devops, var.azure_devops) - bitbucket_cloud = try(each.value.bitbucket_cloud, var.bitbucket_cloud) - bitbucket_datacenter = try(each.value.bitbucket_datacenter, var.bitbucket_datacenter) - cloudformation = try(each.value.cloudformation, var.cloudformation) - github_enterprise = try(each.value.github_enterprise, var.github_enterprise) - gitlab = try(each.value.gitlab, var.gitlab) - pulumi = try(each.value.pulumi, var.pulumi) - showcase = try(each.value.showcase, var.showcase) + local_preview_enabled = try(each.value.settings.spacelift.local_preview_enabled, var.local_preview_enabled) + manage_state = try(each.value.settings.spacelift.manage_state, var.manage_state) + policy_ids = try(local.child_policy_ids, []) + protect_from_deletion = try(each.value.settings.spacelift.protect_from_deletion, var.protect_from_deletion) + repository = var.repository + runner_image = try(each.value.settings.spacelift.runner_image, var.runner_image) + space_id = local.spaces[try(each.value.settings.spacelift.space_name, var.space_id)] + spacelift_run_enabled = try(each.value.settings.spacelift.spacelift_run_enabled, var.spacelift_run_enabled) + spacelift_stack_dependency_enabled = try(each.value.settings.spacelift.spacelift_stack_dependency_enabled, var.spacelift_stack_dependency_enabled) + stack_destructor_enabled = try(each.value.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) + stack_name = try(each.value.settings.spacelift.stack_name, each.key) + terraform_smart_sanitization = try(each.value.settings.spacelift.terraform_smart_sanitization, var.terraform_smart_sanitization) + terraform_version = lookup(var.terraform_version_map, try(each.value.settings.spacelift.terraform_version, ""), var.terraform_version) + webhook_enabled = try(each.value.settings.spacelift.webhook_enabled, var.webhook_enabled) + webhook_endpoint = try(each.value.settings.spacelift.webhook_endpoint, var.webhook_endpoint) + webhook_secret = try(each.value.settings.spacelift.webhook_secret, var.webhook_secret) + worker_pool_id = try(local.worker_pools[each.value.settings.spacelift.worker_pool_name], local.worker_pools[var.worker_pool_name]) + + azure_devops = try(each.value.settings.spacelift.azure_devops, var.azure_devops) + bitbucket_cloud = try(each.value.settings.spacelift.bitbucket_cloud, var.bitbucket_cloud) + bitbucket_datacenter = try(each.value.settings.spacelift.bitbucket_datacenter, var.bitbucket_datacenter) + cloudformation = try(each.value.settings.spacelift.cloudformation, var.cloudformation) + github_enterprise = try(each.value.settings.spacelift.github_enterprise, var.github_enterprise) + gitlab = try(each.value.settings.spacelift.gitlab, var.gitlab) + pulumi = try(each.value.settings.spacelift.pulumi, var.pulumi) + showcase = try(each.value.settings.spacelift.showcase, var.showcase) + + depends_on = [ + null_resource.spaces_precondition, + null_resource.workers_precondition, + spacelift_policy_attachment.root, + null_resource.child_stack_parent_precondition + ] context = module.this.context } diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf index 0086498ee..24743e264 100644 --- a/modules/spacelift/admin-stack/root-admin-stack.tf +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -18,6 +18,7 @@ module "all_admin_stacks_config" { version = "1.4.0" enabled = local.create_root_admin_stack + context_filters = { administrative = true } @@ -27,8 +28,20 @@ module "root_admin_stack" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" version = "1.4.0" - enabled = local.create_root_admin_stack - depends_on = [null_resource.spaces_precondition, null_resource.workers_precondition] + enabled = local.create_root_admin_stack + + # Only the following attributes are available in `local.root_admin_stack_config` + # component, base_component, stack, imports, deps, deps_all, vars, settings, env, inheritance, metadata, backend_type, backend, workspace, labels + # They are in the outputs from the module https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/tree/main/modules/spacelift-stacks-from-atmos-config + # The rest are configured in `settings.spacelift` or `vars` for each component, and should be accessed by `each.value.settings.spacelift` and `each.value.vars` + + atmos_stack_name = try(local.root_admin_stack_config.stack, null) + component_env = try(local.root_admin_stack_config.env, var.component_env) + component_name = try(local.root_admin_stack_config.component, null) + component_root = try(join("/", [var.component_root, local.root_admin_stack_config.metadata.component]), null) + component_vars = try(local.root_admin_stack_config.vars, var.component_vars) + terraform_workspace = try(local.root_admin_stack_config.workspace, var.terraform_workspace) + labels = concat(try(local.root_admin_stack_config.labels, []), try(var.labels, [])) administrative = true after_apply = try(local.root_admin_stack_config.settings.spacelift.after_apply, []) @@ -36,33 +49,27 @@ module "root_admin_stack" { after_init = try(local.root_admin_stack_config.settings.spacelift.after_init, []) after_perform = try(local.root_admin_stack_config.settings.spacelift.after_perform, []) after_plan = try(local.root_admin_stack_config.settings.spacelift.after_plan, []) - atmos_stack_name = try(local.root_admin_stack_config.stack, null) autodeploy = try(local.root_admin_stack_config.settings.spacelift.autodeploy, var.autodeploy) autoretry = try(local.root_admin_stack_config.settings.spacelift.autoretry, var.autoretry) - aws_role_enabled = try(local.root_admin_stack_config.settings.aws_role_enabled, var.aws_role_enabled) - aws_role_arn = try(local.root_admin_stack_config.settings.aws_role_arn, var.aws_role_arn) - aws_role_external_id = try(local.root_admin_stack_config.settings.aws_role_external_id, var.aws_role_external_id) - aws_role_generate_credentials_in_worker = try(local.root_admin_stack_config.settings.aws_role_generate_credentials_in_worker, var.aws_role_generate_credentials_in_worker) + aws_role_enabled = try(local.root_admin_stack_config.settings.spacelift.aws_role_enabled, var.aws_role_enabled) + aws_role_arn = try(local.root_admin_stack_config.settings.spacelift.aws_role_arn, var.aws_role_arn) + aws_role_external_id = try(local.root_admin_stack_config.settings.spacelift.aws_role_external_id, var.aws_role_external_id) + aws_role_generate_credentials_in_worker = try(local.root_admin_stack_config.settings.spacelift.aws_role_generate_credentials_in_worker, var.aws_role_generate_credentials_in_worker) before_apply = try(local.root_admin_stack_config.settings.spacelift.before_apply, []) before_destroy = try(local.root_admin_stack_config.settings.spacelift.before_destroy, []) before_init = try(local.root_admin_stack_config.settings.spacelift.before_init, []) before_perform = try(local.root_admin_stack_config.settings.spacelift.before_perform, []) before_plan = try(local.root_admin_stack_config.settings.spacelift.before_plan, []) - branch = try(local.root_admin_stack_config.branch, var.branch) - commit_sha = var.commit_sha != null ? var.commit_sha : try(local.root_admin_stack_config.commit_sha, null) - component_env = try(local.root_admin_stack_config.env, var.component_env) - component_name = try(local.root_admin_stack_config.component, null) - component_root = try(join("/", [var.component_root, local.root_admin_stack_config.metadata.component]), null) - component_vars = try(local.root_admin_stack_config.vars, var.component_vars) - context_attachments = try(local.root_admin_stack_config.context_attachments, var.context_attachments) - description = try(local.root_admin_stack_config.description, var.description) + branch = try(local.root_admin_stack_config.settings.spacelift.branch, var.branch) + commit_sha = var.commit_sha != null ? var.commit_sha : try(local.root_admin_stack_config.settings.spacelift.commit_sha, null) + context_attachments = try(local.root_admin_stack_config.settings.spacelift.context_attachments, var.context_attachments) + description = try(local.root_admin_stack_config.settings.spacelift.description, var.description) drift_detection_enabled = try(local.root_admin_stack_config.settings.spacelift.drift_detection_enabled, var.drift_detection_enabled) drift_detection_reconcile = try(local.root_admin_stack_config.settings.spacelift.drift_detection_reconcile, var.drift_detection_reconcile) drift_detection_schedule = try(local.root_admin_stack_config.settings.spacelift.drift_detection_schedule, var.drift_detection_schedule) drift_detection_timezone = try(local.root_admin_stack_config.settings.spacelift.drift_detection_timezone, var.drift_detection_timezone) - labels = concat(try(local.root_admin_stack_config.labels, []), try(var.labels, [])) - local_preview_enabled = try(local.root_admin_stack_config.local_preview_enabled, var.local_preview_enabled) - manage_state = try(local.root_admin_stack_config.manage_state, var.manage_state) + local_preview_enabled = try(local.root_admin_stack_config.settings.spacelift.local_preview_enabled, var.local_preview_enabled) + manage_state = try(local.root_admin_stack_config.settings.spacelift.manage_state, var.manage_state) protect_from_deletion = try(local.root_admin_stack_config.settings.spacelift.protect_from_deletion, var.protect_from_deletion) repository = var.repository runner_image = try(local.root_admin_stack_config.settings.spacelift.runner_image, var.runner_image) @@ -71,12 +78,11 @@ module "root_admin_stack" { spacelift_stack_dependency_enabled = try(local.root_admin_stack_config.settings.spacelift.spacelift_stack_dependency_enabled, var.spacelift_stack_dependency_enabled) stack_destructor_enabled = try(local.root_admin_stack_config.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) stack_name = var.stack_name != null ? var.stack_name : local.root_admin_stack_name - terraform_smart_sanitization = try(local.root_admin_stack_config.terraform_smart_sanitization, var.terraform_smart_sanitization) - terraform_version = lookup(var.terraform_version_map, try(local.root_admin_stack_config.terraform_version, ""), var.terraform_version) - terraform_workspace = try(local.root_admin_stack_config.workspace, var.terraform_workspace) - webhook_enabled = try(local.root_admin_stack_config.webhook_enabled, var.webhook_enabled) - webhook_endpoint = try(local.root_admin_stack_config.webhook_endpoint, var.webhook_endpoint) - webhook_secret = try(local.root_admin_stack_config.webhook_secret, var.webhook_secret) + terraform_smart_sanitization = try(local.root_admin_stack_config.settings.spacelift.terraform_smart_sanitization, var.terraform_smart_sanitization) + terraform_version = lookup(var.terraform_version_map, try(local.root_admin_stack_config.settings.spacelift.terraform_version, ""), var.terraform_version) + webhook_enabled = try(local.root_admin_stack_config.settings.spacelift.webhook_enabled, var.webhook_enabled) + webhook_endpoint = try(local.root_admin_stack_config.settings.spacelift.webhook_endpoint, var.webhook_endpoint) + webhook_secret = try(local.root_admin_stack_config.settings.spacelift.webhook_secret, var.webhook_secret) worker_pool_id = local.worker_pools[var.worker_pool_name] azure_devops = try(local.root_admin_stack_config.settings.spacelift.azure_devops, var.azure_devops) @@ -87,6 +93,13 @@ module "root_admin_stack" { gitlab = try(local.root_admin_stack_config.settings.spacelift.gitlab, var.gitlab) pulumi = try(local.root_admin_stack_config.settings.spacelift.pulumi, var.pulumi) showcase = try(local.root_admin_stack_config.settings.spacelift.showcase, var.showcase) + + depends_on = [ + null_resource.spaces_precondition, + null_resource.workers_precondition + ] + + context = module.this.context } resource "spacelift_policy_attachment" "root" { diff --git a/modules/spacelift/admin-stack/variables.tf b/modules/spacelift/admin-stack/variables.tf index d67ce38e0..9ec895029 100644 --- a/modules/spacelift/admin-stack/variables.tf +++ b/modules/spacelift/admin-stack/variables.tf @@ -9,6 +9,7 @@ variable "allow_public_workers" { description = "Whether to allow public workers to be used for this stack" default = false } + variable "autodeploy" { type = bool description = "Controls the Spacelift 'autodeploy' option for a stack" diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index f1122a01b..1f270932c 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -1,7 +1,7 @@ # Component: `spacelift/spaces` This component is responsible for creating and managing the [spaces](https://docs.spacelift.io/concepts/spaces/) in the -spacelift organization. +Spacelift organization. ## Usage @@ -81,8 +81,8 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.1.0 | -| [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.1.0 | +| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.4.0 | +| [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.4.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/spacelift/spaces/main.tf b/modules/spacelift/spaces/main.tf index bc9f433dd..7a1f289f3 100644 --- a/modules/spacelift/spaces/main.tf +++ b/modules/spacelift/spaces/main.tf @@ -1,5 +1,6 @@ locals { enabled = module.this.enabled + spaces = local.enabled ? { for item in values(module.space)[*].space : item.name => { description = item.description id = item.id @@ -31,12 +32,13 @@ locals { } } } : {} + all_policies_inputs = merge([for k, v in local.policy_inputs : v if length(keys(v)) > 0]...) } module "space" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space" - version = "1.1.0" + version = "1.4.0" # Create a space for each entry in the `spaces` variable, except for the root space which already exists by default # and cannot be deleted. @@ -51,7 +53,7 @@ module "space" { module "policy" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy" - version = "1.1.0" + version = "1.4.0" for_each = local.all_policies_inputs From 2f3b95dba9e07808325933d976169012eb3d00f1 Mon Sep 17 00:00:00 2001 From: Matthias Fuhrmeister Date: Tue, 5 Dec 2023 17:05:26 +0100 Subject: [PATCH 315/501] Add possibility to use the transit gateway as default gateway (#902) Co-authored-by: Martin Nettling Co-authored-by: Andriy Knysh --- modules/tgw/spoke/README.md | 14 ++++++++-- modules/tgw/spoke/main.tf | 27 ++++++++++++++++++- .../modules/standard_vpc_attachment/main.tf | 2 +- .../standard_vpc_attachment/outputs.tf | 2 +- .../standard_vpc_attachment/variables.tf | 11 ++++++++ modules/tgw/spoke/remote-state.tf | 9 +++++++ modules/tgw/spoke/variables.tf | 23 ++++++++++++++++ 7 files changed, 83 insertions(+), 5 deletions(-) diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index 2afd19e68..fd22e0fc0 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -98,7 +98,10 @@ atmos terraform apply tgw/spoke -s -- ## Providers -No providers. +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.1 | +| [aws.tgw-hub](#provider\_aws.tgw-hub) | >= 4.1 | ## Modules @@ -111,10 +114,14 @@ No providers. | [tgw\_hub\_routes](#module\_tgw\_hub\_routes) | cloudposse/transit-gateway/aws | 0.10.0 | | [tgw\_spoke\_vpc\_attachment](#module\_tgw\_spoke\_vpc\_attachment) | ./modules/standard_vpc_attachment | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_route.back_route](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource | +| [aws_route.default_route](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource | ## Inputs @@ -124,6 +131,8 @@ No resources. | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
environment = optional(string, "")
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [default\_route\_enabled](#input\_default\_route\_enabled) | Enable default routing via transit gateway, requires also nat gateway and instance to be disabled in vpc component. Default is disabled. | `bool` | `false` | no | +| [default\_route\_outgoing\_account\_name](#input\_default\_route\_outgoing\_account\_name) | The account name which is used for outgoing traffic, when using the transit gateway as default route. | `string` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -142,6 +151,7 @@ No resources. | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [static\_routes](#input\_static\_routes) | A list of static routes. |
set(object({
blackhole = bool
destination_cidr_block = string
}))
| `[]` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [tgw\_hub\_component\_name](#input\_tgw\_hub\_component\_name) | The name of the transit-gateway component | `string` | `"tgw/hub"` | no | diff --git a/modules/tgw/spoke/main.tf b/modules/tgw/spoke/main.tf index 32ef7792e..f03801aac 100644 --- a/modules/tgw/spoke/main.tf +++ b/modules/tgw/spoke/main.tf @@ -8,6 +8,8 @@ locals { spoke_account = module.this.tenant != null ? format("%s-%s-%s", module.this.tenant, module.this.environment, module.this.stage) : format("%s-%s", module.this.environment, module.this.stage) + // "When default routing via transit gateway is enabled, both nat gateway and nat instance must be disabled" + default_route_enabled_and_nat_disabled = module.this.enabled && var.default_route_enabled && length(module.vpc.outputs.nat_gateway_ids) == 0 && length(module.vpc.outputs.nat_instance_ids) == 0 } module "tgw_hub_routes" { @@ -47,7 +49,30 @@ module "tgw_spoke_vpc_attachment" { connections = var.connections expose_eks_sg = var.expose_eks_sg peered_region = var.peered_region - + static_routes = var.static_routes context = module.this.context } + +resource "aws_route" "default_route" { + count = local.default_route_enabled_and_nat_disabled ? length(module.vpc.outputs.private_route_table_ids) : 0 + + route_table_id = module.vpc.outputs.private_route_table_ids[count.index] + destination_cidr_block = "0.0.0.0/0" + transit_gateway_id = module.tgw_hub.outputs.transit_gateway_id +} + +locals { + outgoing_network_account_name = local.default_route_enabled_and_nat_disabled ? format("%s-%s", var.default_route_outgoing_account_name, var.own_vpc_component_name) : "" + default_route_vpc_public_route_table_ids = local.default_route_enabled_and_nat_disabled ? module.tgw_hub.outputs.vpcs[local.outgoing_network_account_name].outputs.default_route_vpc_public_route_table_ids : [] +} + +resource "aws_route" "back_route" { + provider = aws.tgw-hub + + count = local.default_route_enabled_and_nat_disabled ? length(local.default_route_vpc_public_route_table_ids) : 0 + + route_table_id = local.default_route_vpc_public_route_table_ids[count.index] + destination_cidr_block = module.vpc.outputs.vpc_cidr + transit_gateway_id = module.tgw_hub.outputs.transit_gateway_id +} diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf index 96a28208d..79dda03e7 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf @@ -136,7 +136,7 @@ module "standard_vpc_attachment" { subnet_ids = local.own_vpc.private_subnet_ids subnet_route_table_ids = local.own_vpc.private_route_table_ids route_to = null - static_routes = null + static_routes = var.static_routes transit_gateway_vpc_attachment_id = null route_to_cidr_blocks = [for vpc in local.allowed_vpcs : vpc.cidr if !vpc.cross_region] } diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/outputs.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/outputs.tf index 538a37725..3c21a6725 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/outputs.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/outputs.tf @@ -7,7 +7,7 @@ output "tg_config" { subnet_route_table_ids = null route_to = null route_to_cidr_blocks = null - static_routes = null + static_routes = var.static_routes transit_gateway_vpc_attachment_id = module.standard_vpc_attachment.transit_gateway_vpc_attachment_ids[var.owning_account] } description = "Transit Gateway configuration formatted for handling" diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf index 08f294f5d..080fb3544 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf @@ -50,6 +50,17 @@ variable "connections" { default = [] } +variable "static_routes" { + type = set(object({ + blackhole = bool + destination_cidr_block = string + })) + description = <<-EOT + A list of static routes. + EOT + default = [] +} + variable "expose_eks_sg" { type = bool description = "Set true to allow EKS clusters to accept traffic from source accounts" diff --git a/modules/tgw/spoke/remote-state.tf b/modules/tgw/spoke/remote-state.tf index f6c60563c..41e013679 100644 --- a/modules/tgw/spoke/remote-state.tf +++ b/modules/tgw/spoke/remote-state.tf @@ -15,6 +15,15 @@ module "tgw_hub" { context = module.this.context } +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.own_vpc_component_name + + context = module.this.context +} + module "cross_region_hub_connector" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index a9b66e42f..3f739d692 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -66,3 +66,26 @@ variable "peered_region" { description = "Set `true` if this region is not the primary region" default = false } + +variable "static_routes" { + type = set(object({ + blackhole = bool + destination_cidr_block = string + })) + description = <<-EOT + A list of static routes. + EOT + default = [] +} + +variable "default_route_enabled" { + type = bool + description = "Enable default routing via transit gateway, requires also nat gateway and instance to be disabled in vpc component. Default is disabled." + default = false +} + +variable "default_route_outgoing_account_name" { + type = string + description = "The account name which is used for outgoing traffic, when using the transit gateway as default route." + default = null +} From 05874fd5a954b77b1a0f832a6e9fcfcb6abeb3b1 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 5 Dec 2023 09:26:11 -0800 Subject: [PATCH 316/501] fix: `bastion` userdata volume mount (#917) --- modules/bastion/templates/user-data.sh | 8 -------- 1 file changed, 8 deletions(-) diff --git a/modules/bastion/templates/user-data.sh b/modules/bastion/templates/user-data.sh index 741a38eca..3665a4aed 100644 --- a/modules/bastion/templates/user-data.sh +++ b/modules/bastion/templates/user-data.sh @@ -1,13 +1,5 @@ #!/bin/bash -# Mount additional volume -echo "Mounting additional volume..." -while [ ! -b $(readlink -f /dev/sdh) ]; do echo 'waiting for device /dev/sdh'; sleep 5 ; done -blkid $(readlink -f /dev/sdh) || mkfs -t ext4 $(readlink -f /dev/sdh) -e2label $(readlink -f /dev/sdh) sdh-volume -grep -q ^LABEL=sdh-volume /etc/fstab || echo 'LABEL=sdh-volume /mnt ext4 defaults' >> /etc/fstab -grep -q \"^$(readlink -f /dev/sdh) /mnt \" /proc/mounts || mount /mnt - # Install docker echo "Installing docker..." amazon-linux-extras install docker From 0093ef7b2c89cb76e0bb217e63d55b08f6e217eb Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 5 Dec 2023 11:28:55 -0800 Subject: [PATCH 317/501] Component: `github-runners` update readme (#916) --- modules/github-runners/README.md | 87 +++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index e95e8219a..098a58a63 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -6,6 +6,10 @@ This component is responsible for provisioning EC2 instances for GitHub runners. We also have a similar component based on [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) for Kubernetes. ::: + + +## Requirements + ## Usage **Stack Level**: Regional @@ -144,7 +148,49 @@ In order to use this component, you will have to obtain the `REGISTRATION_TOKEN`
-#### Obtain the Runner Registration Token +### Creating Registration Token + +:::info +We highly recommend using a GitHub Application with the github-action-token-rotator module to generate the Registration Token. This will ensure that the token is rotated and that the token is stored in SSM Parameter Store encrypted with KMS. +::: + +#### GitHub Application + +Follow the quickstart with the upstream module, [cloudposse/terraform-aws-github-action-token-rotator](https://github.com/cloudposse/terraform-aws-github-action-token-rotator#quick-start), or follow the steps below. + +1. Create a new GitHub App +1. Add the following permission: +```diff +# Required Permissions for Repository Runners: +## Repository Permissions ++ Actions (read) ++ Administration (read / write) ++ Metadata (read) + +# Required Permissions for Organization Runners: +## Repository Permissions ++ Actions (read) ++ Metadata (read) + +## Organization Permissions ++ Self-hosted runners (read / write) +``` +1. Generate a Private Key + +If you are working with Cloud Posse, upload this Private Key, GitHub App ID, and Github App Installation ID to 1Password and skip the rest. Otherwise, complete the private key setup in `core--auto`. + +1. Convert the private key to a PEM file using the following command: `openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in {DOWNLOADED_FILE_NAME}.pem -out private-key-pkcs8.key` +1. Upload PEM file key to the specified ssm path: `/github/runners/acme/private-key` in `core--auto` +1. Create another sensitive SSM parameter `/github/runners/acme/registration-token` in `core--auto` with any basic value, such as "foo". This will be overwritten by the rotator. +1. Update the GitHub App ID and Installation ID in the `github-action-token-rotator` catalog. + +:::info + +If you change the Private Key saved in SSM, redeploy `github-action-token-rotator` + +::: + +#### (ClickOps) Obtain the Runner Registration Token 1. Browse to [https://github.com/organizations/{Org}/settings/actions/runners](https://github.com/organizations/{Org}/settings/actions/runners) (Organization) or [https://github.com/{Org}/{Repo}/settings/actions/runners](https://github.com/{Org}/{Repo}/settings/actions/runners) (Repository) 2. Click the **New Runner** button (Organization) or **New Self Hosted Runner** button (Repository) @@ -159,6 +205,45 @@ In order to use this component, you will have to obtain the `REGISTRATION_TOKEN` chamber write github token ``` + +# FAQ + +## The GitHub Registration Token is not updated in SSM + +The `github-action-token-rotator` runs an AWS Lambda function every 30 minutes. This lambda will attempt to use a private key in its environment configuration to generate a GitHub Registration Token, and then store that token to AWS SSM Parameter Store. + +If the GitHub Registration Token parameter, `/github/runners/acme/registration-token`, is not updated, read through the following tips: + +1. The private key is stored at the given parameter path: `parameter_store_private_key_path: /github/runners/acme/private-key` +1. The private key is Base 64 encoded. If you pull the key from SSM and decode it, it should begin with `-----BEGIN PRIVATE KEY-----` +1. If the private key has changed, you must _redeploy_ `github-action-token-rotator`. Run a plan against the component to make sure there are not changes required. + +## The GitHub Registration Token is valid, but the Runners are not registering with GitHub + +If you first deployed the `github-action-token-rotator` component initally with an invalid configuration and then deployed the `github-runners` component, the instance runners will have failed to register with GitHub. + +After you correct `github-action-token-rotator` and have a valid GitHub Registration Token in SSM, _destroy and recreate_ the `github-runners` component. + +If you cannot see the runners registered in GitHub, check the system logs on one of EC2 Instances in AWS in `core--auto`. + +## I cannot assume the role from GitHub Actions after deploying + +The following error is very common if the GitHub workflow is missing proper permission. + +```bash +Error: User: arn:aws:sts::***:assumed-role/acme-core-use1-auto-actions-runner@actions-runner-system/token-file-web-identity is not authorized to perform: sts:TagSession on resource: arn:aws:iam::999999999999:role/acme-plat-use1-dev-gha +``` + +In order to use a web identity, GitHub Action pipelines must have the following permission. +See [GitHub Action documentation for more](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-permissions-settings). + +```yaml +permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout +``` + + ## Requirements From 46cafe53a40fe3eb017c77d385cfb010ad44cb89 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 5 Dec 2023 15:26:37 -0800 Subject: [PATCH 318/501] EKS Actions Runner Controller: Include Context Tags as Labels (#918) --- modules/eks/actions-runner-controller/README.md | 1 + modules/eks/actions-runner-controller/main.tf | 5 +++-- modules/eks/actions-runner-controller/variables.tf | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 08a43b2bb..1dd6d8f5c 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -443,6 +443,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [context\_tags\_enabled](#input\_context\_tags\_enabled) | Whether or not to include all context tags as labels for each runner | `bool` | `false` | no | | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index 11b2c5a35..dcf795dad 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -1,5 +1,6 @@ locals { - enabled = module.this.enabled + enabled = module.this.enabled + context_labels = var.context_tags_enabled ? values(module.this.tags) : [] webhook_enabled = local.enabled ? try(var.webhook.enabled, false) : false webhook_host = local.webhook_enabled ? format(var.webhook.hostname_template, var.tenant, var.stage, var.environment) : "example.com" @@ -223,7 +224,7 @@ module "actions_runner" { service_account_role_arn = module.actions_runner_controller.service_account_role_arn resources = each.value.resources storage = each.value.storage - labels = each.value.labels + labels = concat(each.value.labels, local.context_labels) scale_down_delay_seconds = each.value.scale_down_delay_seconds min_replicas = each.value.min_replicas max_replicas = each.value.max_replicas diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index ccdae7d68..9aa0f7354 100644 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -268,3 +268,9 @@ variable "ssm_github_webhook_secret_token_path" { description = "The path in SSM to the GitHub Webhook Secret token." default = "" } + +variable "context_tags_enabled" { + type = bool + description = "Whether or not to include all context tags as labels for each runner" + default = false +} From 19622bd23d4e7f8b867a9e39c8b0047da3cc84b9 Mon Sep 17 00:00:00 2001 From: Matthias Fuhrmeister Date: Wed, 6 Dec 2023 15:35:41 +0100 Subject: [PATCH 319/501] fix field name for default routing in tgw spoke (#919) --- modules/tgw/spoke/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tgw/spoke/main.tf b/modules/tgw/spoke/main.tf index f03801aac..90218fd7c 100644 --- a/modules/tgw/spoke/main.tf +++ b/modules/tgw/spoke/main.tf @@ -64,7 +64,7 @@ resource "aws_route" "default_route" { locals { outgoing_network_account_name = local.default_route_enabled_and_nat_disabled ? format("%s-%s", var.default_route_outgoing_account_name, var.own_vpc_component_name) : "" - default_route_vpc_public_route_table_ids = local.default_route_enabled_and_nat_disabled ? module.tgw_hub.outputs.vpcs[local.outgoing_network_account_name].outputs.default_route_vpc_public_route_table_ids : [] + default_route_vpc_public_route_table_ids = local.default_route_enabled_and_nat_disabled ? module.tgw_hub.outputs.vpcs[local.outgoing_network_account_name].outputs.public_route_table_ids : [] } resource "aws_route" "back_route" { From a320627b7c4acf18791f7581f7d5fe5883e6a50f Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 6 Dec 2023 11:49:35 -0800 Subject: [PATCH 320/501] ArgoCD Slack Notifications (#905) --- modules/argocd-repo/README.md | 3 + modules/argocd-repo/applicationset.tf | 15 +- modules/argocd-repo/main.tf | 23 +-- .../templates/applicationset.yaml.tpl | 8 + modules/argocd-repo/variables.tf | 18 ++ modules/eks/argocd/CHANGELOG.md | 4 + modules/eks/argocd/README.md | 26 ++- modules/eks/argocd/data.tf | 2 +- modules/eks/argocd/main.tf | 13 +- .../{notifictations.tf => notifications.tf} | 160 ++++++++++++++++-- .../argocd/variables-argocd-notifications.tf | 22 +++ modules/eks/argocd/variables-argocd.tf | 6 - 12 files changed, 252 insertions(+), 48 deletions(-) rename modules/eks/argocd/{notifictations.tf => notifications.tf} (57%) diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index 1cb3fc7bc..260692fb9 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -153,8 +153,11 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [permissions](#input\_permissions) | A list of Repository Permission objects used to configure the team permissions of the repository

`team_slug` should be the name of the team without the `@{org}` e.g. `@cloudposse/team` => `team`
`permission` is just one of the available values listed below |
list(object({
team_slug = string,
permission = string
}))
| `[]` | no | +| [push\_restrictions\_enabled](#input\_push\_restrictions\_enabled) | Enforce who can push to the main branch | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [required\_pull\_request\_reviews](#input\_required\_pull\_request\_reviews) | Enforce restrictions for pull request reviews | `bool` | `true` | no | +| [slack\_notifications\_channel](#input\_slack\_notifications\_channel) | If given, the Slack channel to for deployment notifications. | `string` | `""` | no | | [ssm\_github\_api\_key](#input\_ssm\_github\_api\_key) | SSM path to the GitHub API key | `string` | `"/argocd/github/api_key"` | no | | [ssm\_github\_deploy\_key\_format](#input\_ssm\_github\_deploy\_key\_format) | Format string of the SSM parameter path to which the deploy keys will be written to (%s will be replaced with the environment name) | `string` | `"/argocd/deploy_keys/%s"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/argocd-repo/applicationset.tf b/modules/argocd-repo/applicationset.tf index bdc779c9f..fccb75ec2 100644 --- a/modules/argocd-repo/applicationset.tf +++ b/modules/argocd-repo/applicationset.tf @@ -5,13 +5,14 @@ resource "github_repository_file" "application_set" { branch = local.github_repository.default_branch file = "${each.value.tenant != null ? format("%s/", each.value.tenant) : ""}${each.value.environment}-${each.value.stage}${length(each.value.attributes) > 0 ? format("-%s", join("-", each.value.attributes)) : ""}/${local.manifest_kubernetes_namespace}/applicationset.yaml" content = templatefile("${path.module}/templates/applicationset.yaml.tpl", { - environment = each.key - auto-sync = each.value.auto-sync - ignore-differences = each.value.ignore-differences - name = module.this.namespace - namespace = local.manifest_kubernetes_namespace - ssh_url = local.github_repository.ssh_clone_url - notifications = var.github_default_notifications_enabled + environment = each.key + auto-sync = each.value.auto-sync + ignore-differences = each.value.ignore-differences + name = module.this.namespace + namespace = local.manifest_kubernetes_namespace + ssh_url = local.github_repository.ssh_clone_url + notifications = var.github_default_notifications_enabled + slack_notifications_channel = var.slack_notifications_channel }) commit_message = "Initialize environment: `${each.key}`." commit_author = var.github_user diff --git a/modules/argocd-repo/main.tf b/modules/argocd-repo/main.tf index c21a62ec7..88b24d933 100644 --- a/modules/argocd-repo/main.tf +++ b/modules/argocd-repo/main.tf @@ -8,7 +8,7 @@ locals { env.tenant, env.environment, env.stage, - "${join("-", env.attributes)}" + join("-", env.attributes) )) => env } : {} @@ -72,19 +72,22 @@ resource "github_branch_protection" "default" { repository_id = local.github_repository.name - pattern = join("", github_branch_default.default.*.branch) + pattern = join("", github_branch_default.default[*].branch) enforce_admins = false # needs to be false in order to allow automation user to push allows_deletions = true - required_pull_request_reviews { - dismiss_stale_reviews = true - restrict_dismissals = true - require_code_owner_reviews = true + dynamic "required_pull_request_reviews" { + for_each = var.required_pull_request_reviews ? [0] : [] + content { + dismiss_stale_reviews = true + restrict_dismissals = true + require_code_owner_reviews = true + } } - push_restrictions = [ - join("", data.github_user.automation_user.*.node_id), - ] + push_restrictions = var.push_restrictions_enabled ? [ + join("", data.github_user.automation_user[*].node_id), + ] : [] } data "github_team" "default" { @@ -112,7 +115,7 @@ resource "github_repository_deploy_key" "default" { for_each = local.environments title = "Deploy key for ArgoCD environment: ${each.key} (${local.github_repository.default_branch} branch)" - repository = join("", github_repository.default.*.name) + repository = join("", github_repository.default[*].name) key = tls_private_key.default[each.key].public_key_openssh read_only = true } diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index b3b389386..f4aec1768 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -55,6 +55,14 @@ spec: notifications.argoproj.io/subscribe.on-deploy-succeded.argocd-repo-github-commit-status: "" notifications.argoproj.io/subscribe.on-deploy-failed.app-repo-github-commit-status: "" notifications.argoproj.io/subscribe.on-deploy-failed.argocd-repo-github-commit-status: "" +%{ endif ~} +%{if length(slack_notifications_channel) > 0 ~} + notifications.argoproj.io/subscribe.on-created.slack: ${slack_notifications_channel} + notifications.argoproj.io/subscribe.on-deleted.slack: ${slack_notifications_channel} + notifications.argoproj.io/subscribe.on-success.slack: ${slack_notifications_channel} + notifications.argoproj.io/subscribe.on-health-degraded.slack: ${slack_notifications_channel} + notifications.argoproj.io/subscribe.on-failure.slack: ${slack_notifications_channel} + notifications.argoproj.io/subscribe.on-started.slack: ${slack_notifications_channel} %{ endif ~} name: '{{name}}' spec: diff --git a/modules/argocd-repo/variables.tf b/modules/argocd-repo/variables.tf index 1a8c0bd5b..ac6e22187 100644 --- a/modules/argocd-repo/variables.tf +++ b/modules/argocd-repo/variables.tf @@ -138,3 +138,21 @@ variable "create_repo" { description = "Whether or not to create the repository or use an existing one" default = true } + +variable "required_pull_request_reviews" { + type = bool + description = "Enforce restrictions for pull request reviews" + default = true +} + +variable "push_restrictions_enabled" { + type = bool + description = "Enforce who can push to the main branch" + default = true +} + +variable "slack_notifications_channel" { + type = string + default = "" + description = "If given, the Slack channel to for deployment notifications." +} diff --git a/modules/eks/argocd/CHANGELOG.md b/modules/eks/argocd/CHANGELOG.md index c4a90f49a..df97d2e81 100644 --- a/modules/eks/argocd/CHANGELOG.md +++ b/modules/eks/argocd/CHANGELOG.md @@ -1,3 +1,7 @@ +## Components PR [#905](https://github.com/cloudposse/terraform-aws-components/pull/905) + +The `notifictations.tf` file has been renamed to `notifications.tf`. Delete `notifictations.tf` after vendoring these changes. + ## Components PR [#851](https://github.com/cloudposse/terraform-aws-components/pull/851) This is a bug fix and feature enhancement update. diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index d43fe1464..456c7b772 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -348,6 +348,24 @@ components: github_webhook_enabled: true ``` +### Slack Notifications + +ArgoCD supports Slack notifications on application deployments. + +1. In order to enable Slack notifications, first create a Slack Application following the [ArgoCD documentation](https://argocd-notifications.readthedocs.io/en/stable/services/slack/). +1. Create an OAuth token for the new Slack App +1. Save the OAuth token to AWS SSM Parameter Store in the same account and region as Github tokens. For example, `core-use2-auto` +1. Add the app to the chosen Slack channel. _If not added, notifications will not work_ +1. For this component, enable Slack integrations for each Application with `var.slack_notifications_enabled` and `var.slack_notifications`: + +```yaml + slack_notifications_enabled: true + slack_notifications: + channel: argocd-updates +``` + +6. In the `argocd-repo` component, set `var.slack_notifications_channel` to the name of the Slack notification channel to add the relevant ApplicationSet annotations + ## Troubleshooting ## Login to ArgoCD admin UI @@ -404,8 +422,8 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | Name | Source | Version | |------|--------|---------| -| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.10.0 | -| [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.10.0 | +| [argocd](#module\_argocd) | cloudposse/helm-release/aws | 0.10.1 | +| [argocd\_apps](#module\_argocd\_apps) | cloudposse/helm-release/aws | 0.10.1 | | [argocd\_repo](#module\_argocd\_repo) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | @@ -425,6 +443,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [aws_ssm_parameter.github_deploy_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.oidc_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.oidc_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.slack_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameters_by_path.argocd_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameters_by_path) | data source | | [kubernetes_resources.crd](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/resources) | data source | @@ -438,7 +457,6 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [alb\_logs\_bucket](#input\_alb\_logs\_bucket) | The name of the bucket for ALB access logs. The bucket must have policy allowing the ELB logging principal | `string` | `""` | no | | [alb\_logs\_prefix](#input\_alb\_logs\_prefix) | `alb_logs_bucket` s3 bucket prefix | `string` | `""` | no | | [alb\_name](#input\_alb\_name) | The name of the ALB (e.g. `argocd`) provisioned by `alb-controller`. Works together with `var.alb_group_name` | `string` | `null` | no | -| [argo\_enable\_workflows\_auth](#input\_argo\_enable\_workflows\_auth) | Allow argo-workflows to use Dex instance for SAML auth | `bool` | `false` | no | | [argocd\_apps\_chart](#input\_argocd\_apps\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"argocd-apps"` | no | | [argocd\_apps\_chart\_description](#input\_argocd\_apps\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"A Helm chart for managing additional Argo CD Applications and Projects"` | no | | [argocd\_apps\_chart\_repository](#input\_argocd\_apps\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://argoproj.github.io/argo-helm"` | no | @@ -506,6 +524,8 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [saml\_enabled](#input\_saml\_enabled) | Toggles SAML integration in the deployed chart | `bool` | `false` | no | | [saml\_rbac\_scopes](#input\_saml\_rbac\_scopes) | SAML RBAC scopes to request | `string` | `"[email,groups]"` | no | | [saml\_sso\_providers](#input\_saml\_sso\_providers) | SAML SSO providers components |
map(object({
component = string
environment = optional(string, null)
}))
| `{}` | no | +| [slack\_notifications](#input\_slack\_notifications) | ArgoCD Slack notification configuration. Requires Slack Bot created with token stored at the given SSM Parameter path.

See: https://argocd-notifications.readthedocs.io/en/stable/services/slack/ |
object({
token_ssm_path = optional(string, "/argocd/notifications/notifiers/slack/token")
api_url = optional(string, null)
username = optional(string, "ArgoCD")
icon = optional(string, null)
})
| `{}` | no | +| [slack\_notifications\_enabled](#input\_slack\_notifications\_enabled) | Whether or not to enable Slack notifications. See `var.slack_notifications.` | `bool` | `false` | no | | [ssm\_github\_api\_key](#input\_ssm\_github\_api\_key) | SSM path to the GitHub API key | `string` | `"/argocd/github/api_key"` | no | | [ssm\_oidc\_client\_id](#input\_ssm\_oidc\_client\_id) | The SSM Parameter Store path for the ID of the IdP client | `string` | `"/argocd/oidc/client_id"` | no | | [ssm\_oidc\_client\_secret](#input\_ssm\_oidc\_client\_secret) | The SSM Parameter Store path for the secret of the IdP client | `string` | `"/argocd/oidc/client_secret"` | no | diff --git a/modules/eks/argocd/data.tf b/modules/eks/argocd/data.tf index b745c8f33..212c20e51 100644 --- a/modules/eks/argocd/data.tf +++ b/modules/eks/argocd/data.tf @@ -35,7 +35,7 @@ data "aws_ssm_parameter" "github_deploy_key" { module.this.tenant, module.this.environment, module.this.stage, - "${join("-", module.this.attributes)}" + join("-", module.this.attributes) ) ) : null diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 679598adb..6781c7605 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -43,6 +43,13 @@ locals { value = nonsensitive(local.webhook_github_secret) type = "string" } + ] : [], + local.slack_notifications_enabled ? [ + { + name = "notifications.secret.items.slack-token" + value = data.aws_ssm_parameter.slack_notifications[0].value + type = "string" + } ] : [] )) regional_service_discovery_domain = "${module.this.environment}.${module.dns_gbl_delegated.outputs.default_domain_name}" @@ -102,7 +109,7 @@ locals { module "argocd" { source = "cloudposse/helm-release/aws" - version = "0.10.0" + version = "0.10.1" name = "argocd" # avoids hitting length restrictions on IAM Role names chart = var.chart @@ -188,7 +195,7 @@ data "kubernetes_resources" "crd" { module "argocd_apps" { source = "cloudposse/helm-release/aws" - version = "0.10.0" + version = "0.10.1" name = "" # avoids hitting length restrictions on IAM Role names chart = var.argocd_apps_chart @@ -215,7 +222,7 @@ module "argocd_apps" { stage = var.stage attributes = var.attributes } - ), + ) ]) depends_on = [ diff --git a/modules/eks/argocd/notifictations.tf b/modules/eks/argocd/notifications.tf similarity index 57% rename from modules/eks/argocd/notifictations.tf rename to modules/eks/argocd/notifications.tf index e806e8ff6..0e9c2ebf3 100644 --- a/modules/eks/argocd/notifictations.tf +++ b/modules/eks/argocd/notifications.tf @@ -4,10 +4,19 @@ data "aws_ssm_parameters_by_path" "argocd_notifications" { with_decryption = true } +data "aws_ssm_parameter" "slack_notifications" { + provider = aws.config_secrets + count = local.slack_notifications_enabled ? 1 : 0 + + name = var.slack_notifications.token_ssm_path + with_decryption = true +} + locals { github_default_notifications_enabled = local.enabled && var.github_default_notifications_enabled + slack_notifications_enabled = local.enabled && var.slack_notifications_enabled - notification_default_notifier_github_commot_status = { + notification_default_notifier_github_commit_status = { url = "https://api.github.com" headers = [ { @@ -17,14 +26,25 @@ locals { ] } - notification_default_notifiers = local.github_default_notifications_enabled ? { - webhook = { - app-repo-github-commit-status = local.notification_default_notifier_github_commot_status - argocd-repo-github-commit-status = local.notification_default_notifier_github_commot_status - } - } : {} + notification_slack_service = { + apiURL = var.slack_notifications.api_url + token = "$slack-token" + username = var.slack_notifications.username + icon = var.slack_notifications.icon + } - notifications_notifiers = merge(var.notifications_notifiers, local.notification_default_notifiers) + notifications_notifiers = merge( + var.notifications_notifiers, + local.github_default_notifications_enabled ? { + webhook = { + app-repo-github-commit-status = local.notification_default_notifier_github_commit_status + argocd-repo-github-commit-status = local.notification_default_notifier_github_commit_status + } + } : {}, + local.slack_notifications_enabled ? { + slack = local.notification_slack_service + } : {} + ) ## Get list of notifiers services notifications_notifiers_variables = merge( @@ -68,9 +88,7 @@ locals { [for name in value.names : format("$%s_%s", key, trimprefix(name, local.notifications_notifiers_ssm_path[key]))] ) } -} -locals { notifications_template_github_commit_status = { method = "POST" body = { @@ -88,7 +106,7 @@ locals { path = "/repos/{{call .repo.FullNameByRepoURL .app.spec.source.repoURL}}/statuses/{{.app.status.operationState.operation.sync.revision}}" }) - notifications_default_templates = local.github_default_notifications_enabled ? { + notifications_default_templates = merge(local.github_default_notifications_enabled ? { app-deploy-succeded = { message = "Application {{ .app.metadata.name }} is now running new version of deployments manifests." webhook = { @@ -128,13 +146,74 @@ locals { } } } - } : {} + } : {}, + local.slack_notifications_enabled ? { + app-created = { + message = "Application {{ .app.metadata.name }} has been created." + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#00ff00" + } + ) + } + }, + app-deleted = { + message = "Application {{ .app.metadata.name }} was deleted." + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#FFA500" + } + ) + } + }, + app-success = { + message = "Application {{ .app.metadata.name }} deployment was successful!" + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#00ff00" + } + ) + } + }, + app-failure = { + message = "Application {{ .app.metadata.name }} deployment failed!" + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#FF0000" + } + ) + } + }, + app-started = { + message = "Application {{ .app.metadata.name }} started deployment..." + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#0000ff" + } + ) + } + }, + app-health-degraded = { + message = "Application {{ .app.metadata.name }} health has degraded!" + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#FF0000" + } + ) + } + } + } : {} + ) notifications_templates = merge(var.notifications_templates, local.notifications_default_templates) -} -locals { - notifications_default_triggers = local.github_default_notifications_enabled ? { + notifications_default_triggers = merge(local.github_default_notifications_enabled ? { on-deploy-started = [ { when = "app.status.operationState.phase in ['Running'] or ( app.status.operationState.phase == 'Succeeded' and app.status.health.status == 'Progressing' )" @@ -156,12 +235,57 @@ locals { send = ["app-deploy-failed"] } ] - } : {} + } : {}, + local.slack_notifications_enabled ? { + # Full catalog of notification triggers as default + # https://github.com/argoproj/argo-cd/tree/master/notifications_catalog/triggers + on-created = [ + { + when = "true" + send = ["app-created"] + oncePer = "app.metadata.name" + } + ], + on-deleted = [ + { + when = "app.metadata.deletionTimestamp != nil" + send = ["app-deleted"] + oncePer = "app.metadata.deletionTimestamp" + } + ], + on-success = [ + { + when = "app.status.operationState != nil and app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'" + send = ["app-success"] + oncePer = "app.status.operationState?.syncResult?.revision" + } + ], + on-failure = [ + { + when = "app.status.operationState != nil and (app.status.operationState.phase in ['Error', 'Failed'] or app.status.sync.status == 'Unknown')" + send = ["app-failure"] + oncePer = "app.status.operationState?.syncResult?.revision" + } + ], + on-health-degraded = [ + { + when = "app.status.health.status == 'Degraded'" + send = ["app-health-degraded"] + oncePer = "app.status.operationState?.syncResult?.revision" + } + ], + on-started = [ + { + when = "app.status.operationState != nil and app.status.operationState.phase in ['Running']" + send = ["app-started"] + oncePer = "app.status.operationState?.syncResult?.revision" + } + ] + } : {} + ) notifications_triggers = merge(var.notifications_triggers, local.notifications_default_triggers) -} -locals { notifications = { notifications = { templates = { for key, value in local.notifications_templates : format("template.%s", key) => yamlencode(value) } diff --git a/modules/eks/argocd/variables-argocd-notifications.tf b/modules/eks/argocd/variables-argocd-notifications.tf index 681eebaae..4e5f838be 100644 --- a/modules/eks/argocd/variables-argocd-notifications.tf +++ b/modules/eks/argocd/variables-argocd-notifications.tf @@ -79,3 +79,25 @@ variable "notifications_notifiers" { EOT default = {} } + +variable "slack_notifications_enabled" { + type = bool + default = false + description = "Whether or not to enable Slack notifications. See `var.slack_notifications." +} + +variable "slack_notifications" { + type = object({ + token_ssm_path = optional(string, "/argocd/notifications/notifiers/slack/token") + api_url = optional(string, null) + username = optional(string, "ArgoCD") + icon = optional(string, null) + }) + description = <<-EOT + ArgoCD Slack notification configuration. Requires Slack Bot created with token stored at the given SSM Parameter path. + + See: https://argocd-notifications.readthedocs.io/en/stable/services/slack/ + EOT + + default = {} +} diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index 5a7861e6a..3c0ddf6a5 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -143,12 +143,6 @@ variable "saml_rbac_scopes" { default = "[email,groups]" } -variable "argo_enable_workflows_auth" { - type = bool - default = false - description = "Allow argo-workflows to use Dex instance for SAML auth" -} - variable "argocd_rbac_policies" { type = list(string) default = [] From eb4c7b335b9ff2933d5a7947f07af7c452eda9fd Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 6 Dec 2023 13:58:45 -0800 Subject: [PATCH 321/501] fix: docker on philips labs (#920) Co-authored-by: cloudpossebot --- modules/philips-labs-github-runners/main.tf | 1 + .../templates/userdata_pre_install.sh | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 modules/philips-labs-github-runners/templates/userdata_pre_install.sh diff --git a/modules/philips-labs-github-runners/main.tf b/modules/philips-labs-github-runners/main.tf index 5dc5ccc7e..b79b4420c 100644 --- a/modules/philips-labs-github-runners/main.tf +++ b/modules/philips-labs-github-runners/main.tf @@ -86,6 +86,7 @@ module "github_runner" { # this variable is substituted in the user-data.sh startup script. It cannot point to another script if using a base ami. # instead this will just run after the runner is installed. Hence we use `file` to read the contents of the file which is injected into the user-data.sh userdata_post_install = file("${path.module}/templates/userdata_post_install.sh") + userdata_pre_install = file("${path.module}/templates/userdata_pre_install.sh") runner_extra_labels = var.runner_extra_labels diff --git a/modules/philips-labs-github-runners/templates/userdata_pre_install.sh b/modules/philips-labs-github-runners/templates/userdata_pre_install.sh new file mode 100644 index 000000000..96bd32681 --- /dev/null +++ b/modules/philips-labs-github-runners/templates/userdata_pre_install.sh @@ -0,0 +1,8 @@ +# From https://github.com/aws-observability/aws-otel-test-framework/pull/1425/files +## Fixes: Error loading Python lib '/tmp/_MEIaR70C0/libpython3.7m.so.1.0': dlopen: libcrypt.so.1: cannot open shared object file: No such file or directory + +echo "Custom Pre-Install Script" +sudo yum update -y +sudo yum install -y libxcrypt-compat +sudo yum install -y docker +sudo ln -s /usr/lib/libcrypt.so /usr/lib/libcrypt.so.1 From 38604f0f5aaf08d73ecbbf317c1ebf821efcc3e8 Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Wed, 6 Dec 2023 18:29:05 -0500 Subject: [PATCH 322/501] feature(tgw) add support for external ram sharing (#921) Co-authored-by: cloudpossebot --- modules/tgw/hub/README.md | 2 ++ modules/tgw/hub/main.tf | 2 ++ modules/tgw/hub/variables.tf | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 482c49b4d..1c39d71d9 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -111,6 +111,7 @@ No resources. | [account\_map\_stage\_name](#input\_account\_map\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | | [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [allow\_external\_principals](#input\_allow\_external\_principals) | Set true to allow the TGW to be RAM shared with external principals specified in ram\_principals | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
environment = optional(string, "")
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | @@ -126,6 +127,7 @@ No resources. | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [ram\_principals](#input\_ram\_principals) | A list of AWS account IDs to share the TGW with outside the organization | `list(string)` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/tgw/hub/main.tf b/modules/tgw/hub/main.tf index f62ff26cc..ff08867e0 100644 --- a/modules/tgw/hub/main.tf +++ b/modules/tgw/hub/main.tf @@ -11,6 +11,8 @@ module "tgw_hub" { version = "0.11.0" ram_resource_share_enabled = true + ram_principals = var.ram_principals + allow_external_principals = var.allow_external_principals route_keys_enabled = true create_transit_gateway = true diff --git a/modules/tgw/hub/variables.tf b/modules/tgw/hub/variables.tf index 0bf4d7c71..13bae4927 100644 --- a/modules/tgw/hub/variables.tf +++ b/modules/tgw/hub/variables.tf @@ -48,3 +48,15 @@ variable "account_map_tenant_name" { EOT default = null } + +variable "ram_principals" { + type = list(string) + description = "A list of AWS account IDs to share the TGW with outside the organization" + default = [] +} + +variable "allow_external_principals" { + type = bool + description = "Set true to allow the TGW to be RAM shared with external principals specified in ram_principals" + default = false +} From 4cb4c36c4a2cb0bf7e7ee38314f3a290feee093b Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Wed, 6 Dec 2023 19:27:49 -0500 Subject: [PATCH 323/501] feature(tgw): allow specifying of arbitrary destinations on the tgw spoke (#922) Co-authored-by: cloudpossebot --- modules/tgw/spoke/README.md | 3 ++- modules/tgw/spoke/main.tf | 1 + .../tgw/spoke/modules/standard_vpc_attachment/main.tf | 2 +- .../spoke/modules/standard_vpc_attachment/variables.tf | 6 ++++++ modules/tgw/spoke/variables.tf | 10 +++++++--- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index fd22e0fc0..f2e80dd65 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -151,7 +151,8 @@ atmos terraform apply tgw/spoke -s -- | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [static\_routes](#input\_static\_routes) | A list of static routes. |
set(object({
blackhole = bool
destination_cidr_block = string
}))
| `[]` | no | +| [static\_routes](#input\_static\_routes) | A list of static routes to add to the transit gateway, pointing at this VPC as a destination. |
set(object({
blackhole = bool
destination_cidr_block = string
}))
| `[]` | no | +| [static\_tgw\_routes](#input\_static\_tgw\_routes) | A list of static routes to add to the local routing table with the transit gateway as a destination. | `list(string)` | `[]` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [tgw\_hub\_component\_name](#input\_tgw\_hub\_component\_name) | The name of the transit-gateway component | `string` | `"tgw/hub"` | no | diff --git a/modules/tgw/spoke/main.tf b/modules/tgw/spoke/main.tf index 90218fd7c..42ad86c58 100644 --- a/modules/tgw/spoke/main.tf +++ b/modules/tgw/spoke/main.tf @@ -50,6 +50,7 @@ module "tgw_spoke_vpc_attachment" { expose_eks_sg = var.expose_eks_sg peered_region = var.peered_region static_routes = var.static_routes + static_tgw_routes = var.static_tgw_routes context = module.this.context } diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf index 79dda03e7..634975bc3 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/main.tf @@ -138,7 +138,7 @@ module "standard_vpc_attachment" { route_to = null static_routes = var.static_routes transit_gateway_vpc_attachment_id = null - route_to_cidr_blocks = [for vpc in local.allowed_vpcs : vpc.cidr if !vpc.cross_region] + route_to_cidr_blocks = concat([for vpc in local.allowed_vpcs : vpc.cidr if !vpc.cross_region], var.static_tgw_routes) } } diff --git a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf index 080fb3544..8722bbd02 100644 --- a/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf +++ b/modules/tgw/spoke/modules/standard_vpc_attachment/variables.tf @@ -61,6 +61,12 @@ variable "static_routes" { default = [] } +variable "static_tgw_routes" { + type = list(string) + description = "A list of static routes to add to the local routing table with the transit gateway as a destination." + default = [] +} + variable "expose_eks_sg" { type = bool description = "Set true to allow EKS clusters to accept traffic from source accounts" diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index 3f739d692..752a9e607 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -72,9 +72,13 @@ variable "static_routes" { blackhole = bool destination_cidr_block = string })) - description = <<-EOT - A list of static routes. - EOT + description = "A list of static routes to add to the transit gateway, pointing at this VPC as a destination." + default = [] +} + +variable "static_tgw_routes" { + type = list(string) + description = "A list of static routes to add to the local routing table with the transit gateway as a destination." default = [] } From 589d875e6cd0e41a3bde67350ca0182b5e3cc5ab Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Thu, 7 Dec 2023 16:43:33 -0500 Subject: [PATCH 324/501] feature(tgw): add support for multiple cross-region connections (#923) Co-authored-by: cloudpossebot --- modules/tgw/spoke/README.md | 1 + modules/tgw/spoke/remote-state.tf | 8 +------- modules/tgw/spoke/variables.tf | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index f2e80dd65..ee096dfad 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -131,6 +131,7 @@ atmos terraform apply tgw/spoke -s -- | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
environment = optional(string, "")
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [cross\_region\_hub\_connector\_components](#input\_cross\_region\_hub\_connector\_components) | A map of cross-region hub connector components that provide this spoke with the appropriate Transit Gateway attachments IDs.
- The key should be the environment that the remote VPC is located in.
- The component is the name of the compoent in the remote region (e.g. `tgw/cross-region-hub-connector`)
- The environment is the region that the cross-region-hub-connector is deployed in.
e.g. the following would configure a component called `tgw/cross-region-hub-connector/use1` that is deployed in the
If use2 is the primary region, the following would be its configuration:
use1:
component: "tgw/cross-region-hub-connector"
environment: "use1" (the remote region)
and in the alternate region, the following would be its configuration:
use2:
component: "tgw/cross-region-hub-connector"
environment: "use1" (our own region) | `map(object({ component = string, environment = string }))` | `{}` | no | | [default\_route\_enabled](#input\_default\_route\_enabled) | Enable default routing via transit gateway, requires also nat gateway and instance to be disabled in vpc component. Default is disabled. | `bool` | `false` | no | | [default\_route\_outgoing\_account\_name](#input\_default\_route\_outgoing\_account\_name) | The account name which is used for outgoing traffic, when using the transit gateway as default route. | `string` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | diff --git a/modules/tgw/spoke/remote-state.tf b/modules/tgw/spoke/remote-state.tf index 41e013679..8edd8f2de 100644 --- a/modules/tgw/spoke/remote-state.tf +++ b/modules/tgw/spoke/remote-state.tf @@ -1,9 +1,3 @@ -locals { - # Any cross region connection requires a TGW Hub connector deployed - # If any connections given are cross-region, get the `tgw/cross-region-hub-connector` component from that region - connected_environments = distinct(compact(concat([for c in var.connections : c.account.environment], [module.this.environment]))) -} - module "tgw_hub" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" @@ -28,7 +22,7 @@ module "cross_region_hub_connector" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" - for_each = toset(local.connected_environments) + for_each = var.cross_region_hub_connector_components component = "tgw/cross-region-hub-connector" tenant = length(var.tgw_hub_tenant_name) > 0 ? var.tgw_hub_tenant_name : module.this.tenant diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index 752a9e607..c87a0efaf 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -93,3 +93,23 @@ variable "default_route_outgoing_account_name" { description = "The account name which is used for outgoing traffic, when using the transit gateway as default route." default = null } + +variable "cross_region_hub_connector_components" { + type = map(object({ component = string, environment = string })) + description = <<-EOT + A map of cross-region hub connector components that provide this spoke with the appropriate Transit Gateway attachments IDs. + - The key should be the environment that the remote VPC is located in. + - The component is the name of the compoent in the remote region (e.g. `tgw/cross-region-hub-connector`) + - The environment is the region that the cross-region-hub-connector is deployed in. + e.g. the following would configure a component called `tgw/cross-region-hub-connector/use1` that is deployed in the + If use2 is the primary region, the following would be its configuration: + use1: + component: "tgw/cross-region-hub-connector" + environment: "use1" (the remote region) + and in the alternate region, the following would be its configuration: + use2: + component: "tgw/cross-region-hub-connector" + environment: "use1" (our own region) + EOT + default = {} +} From feae60c70eae751e192058affc9625a25533e5a4 Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Thu, 7 Dec 2023 18:09:40 -0500 Subject: [PATCH 325/501] bug(tgw): fix bug with upstreaming (#924) --- modules/tgw/spoke/remote-state.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/tgw/spoke/remote-state.tf b/modules/tgw/spoke/remote-state.tf index 8edd8f2de..709e9222d 100644 --- a/modules/tgw/spoke/remote-state.tf +++ b/modules/tgw/spoke/remote-state.tf @@ -24,10 +24,10 @@ module "cross_region_hub_connector" { for_each = var.cross_region_hub_connector_components - component = "tgw/cross-region-hub-connector" + component = each.value.component tenant = length(var.tgw_hub_tenant_name) > 0 ? var.tgw_hub_tenant_name : module.this.tenant stage = length(var.tgw_hub_stage_name) > 0 ? var.tgw_hub_stage_name : module.this.stage - environment = each.value + environment = each.value.environment # Ignore if hub connector doesnt exist (it doesnt exist in primary region) ignore_errors = true From b562b67be0821d71ff5e1622e049af2650a8c579 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Fri, 8 Dec 2023 13:03:02 -0500 Subject: [PATCH 326/501] Add `msk` component (#925) Co-authored-by: cloudpossebot --- README.md | 121 ++++------ modules/msk/README.md | 216 ++++++++++++++++++ modules/msk/context.tf | 279 ++++++++++++++++++++++++ modules/msk/main.tf | 68 ++++++ modules/msk/outputs.tf | 99 +++++++++ modules/msk/providers.tf | 19 ++ modules/msk/remote-state.tf | 18 ++ modules/msk/security-group-variables.tf | 184 ++++++++++++++++ modules/msk/variables.tf | 256 ++++++++++++++++++++++ modules/msk/versions.tf | 10 + 10 files changed, 1186 insertions(+), 84 deletions(-) create mode 100644 modules/msk/README.md create mode 100644 modules/msk/context.tf create mode 100644 modules/msk/main.tf create mode 100644 modules/msk/outputs.tf create mode 100644 modules/msk/providers.tf create mode 100644 modules/msk/remote-state.tf create mode 100644 modules/msk/security-group-variables.tf create mode 100644 modules/msk/variables.tf create mode 100644 modules/msk/versions.tf diff --git a/README.md b/README.md index fdfe316c3..91619a01e 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,6 @@ This is a collection of reusable Terraform components for provisioning infrastru This project is part of our comprehensive ["SweetOps"](https://cpco.io/sweetops) approach towards DevOps. - - It's 100% Open Source and licensed under the [APACHE2](LICENSE). @@ -87,8 +85,6 @@ make rebuild-docs - - ## Usage @@ -116,13 +112,6 @@ Available targets: - -## Share the Love - -Like this project? Please give it a ★ on [our GitHub](https://github.com/cloudposse/terraform-aws-components)! (it helps us **a lot**) - - - ## Related Projects Check out these related projects. @@ -139,54 +128,8 @@ For additional context, refer to some of these links. - [Reference Architectures](https://cloudposse.com/) - Launch effortlessly with our turnkey reference architectures, built either by your team or ours. -## Help - -**Got a question?** We got answers. - -File a GitHub [issue](https://github.com/cloudposse/terraform-aws-components/issues), send us an [email][email] or join our [Slack Community][slack]. - -[![README Commercial Support][readme_commercial_support_img]][readme_commercial_support_link] - -## DevOps Accelerator for Startups - - -We are a [**DevOps Accelerator**][commercial_support]. We'll help you build your cloud infrastructure from the ground up so you can own it. Then we'll show you how to operate it and stick around for as long as you need us. - -[![Learn More](https://img.shields.io/badge/learn%20more-success.svg?style=for-the-badge)][commercial_support] - -Work directly with our team of DevOps experts via email, slack, and video conferencing. - -We deliver 10x the value for a fraction of the cost of a full-time engineer. Our track record is not even funny. If you want things done right and you need it done FAST, then we're your best bet. - -- **Reference Architecture.** You'll get everything you need from the ground up built using 100% infrastructure as code. -- **Release Engineering.** You'll have end-to-end CI/CD with unlimited staging environments. -- **Site Reliability Engineering.** You'll have total visibility into your apps and microservices. -- **Security Baseline.** You'll have built-in governance with accountability and audit logs for all changes. -- **GitOps.** You'll be able to operate your infrastructure via Pull Requests. -- **Training.** You'll receive hands-on training so your team can operate what we build. -- **Questions.** You'll have a direct line of communication between our teams via a Shared Slack channel. -- **Troubleshooting.** You'll get help to triage when things aren't working. -- **Code Reviews.** You'll receive constructive feedback on Pull Requests. -- **Bug Fixes.** We'll rapidly work with you to fix any bugs in our projects. - -## Slack Community - -Join our [Open Source Community][slack] on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure. - -## Newsletter - -Sign up for [our newsletter][newsletter] that covers everything on our technology radar. Receive updates on what we're up to on GitHub as well as awesome new projects we discover. - -## Office Hours - -[Join us every Wednesday via Zoom][office_hours] for our weekly "Lunch & Learn" sessions. It's **FREE** for everyone! - -[![zoom](https://img.cloudposse.com/fit-in/200x200/https://cloudposse.com/wp-content/uploads/2019/08/Powered-by-Zoom.png")][office_hours] - ## ✨ Contributing - - This project is under active development, and we encourage contributions from our community. Many thanks to our outstanding contributors: @@ -194,13 +137,11 @@ Many thanks to our outstanding contributors: - - -### Bug Reports & Feature Requests +### 🐛 Bug Reports & Feature Requests Please use the [issue tracker](https://github.com/cloudposse/terraform-aws-components/issues) to report any bugs or file feature requests. -### Developing +### 💻 Developing If you are interested in being a contributor and want to get involved in developing this project or [help out](https://cpco.io/help-out) with our other projects, we would love to hear from you! Shoot us an [email][email]. @@ -214,13 +155,43 @@ In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. **NOTE:** Be sure to merge the latest changes from "upstream" before making a pull request! +### 🌎 Slack Community + +Join our [Open Source Community][slack] on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure. -## Copyright +### 📰 Newsletter -Copyright © 2017-2023 [Cloud Posse, LLC](https://cpco.io/copyright) +Sign up for [our newsletter][newsletter] that covers everything on our technology radar. Receive updates on what we're up to on GitHub as well as awesome new projects we discover. + +### 📆 Office Hours + +[Join us every Wednesday via Zoom][office_hours] for our weekly "Lunch & Learn" sessions. It's **FREE** for everyone! + +## About + +This project is maintained and funded by [Cloud Posse, LLC][website]. + +We are a [**DevOps Accelerator**][commercial_support]. We'll help you build your cloud infrastructure from the ground up so you can own it. Then we'll show you how to operate it and stick around for as long as you need us. + +[![Learn More](https://img.shields.io/badge/learn%20more-success.svg?style=for-the-badge)][commercial_support] +Work directly with our team of DevOps experts via email, slack, and video conferencing. + +We deliver 10x the value for a fraction of the cost of a full-time engineer. Our track record is not even funny. If you want things done right and you need it done FAST, then we're your best bet. +- **Reference Architecture.** You'll get everything you need from the ground up built using 100% infrastructure as code. +- **Release Engineering.** You'll have end-to-end CI/CD with unlimited staging environments. +- **Site Reliability Engineering.** You'll have total visibility into your apps and microservices. +- **Security Baseline.** You'll have built-in governance with accountability and audit logs for all changes. +- **GitOps.** You'll be able to operate your infrastructure via Pull Requests. +- **Training.** You'll receive hands-on training so your team can operate what we build. +- **Questions.** You'll have a direct line of communication between our teams via a Shared Slack channel. +- **Troubleshooting.** You'll get help to triage when things aren't working. +- **Code Reviews.** You'll receive constructive feedback on Pull Requests. +- **Bug Fixes.** We'll rapidly work with you to fix any bugs in our projects. + +[![README Commercial Support][readme_commercial_support_img]][readme_commercial_support_link] ## License [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) @@ -246,29 +217,12 @@ specific language governing permissions and limitations under the License. ``` - - - - - - - - ## Trademarks All other trademarks referenced herein are the property of their respective owners. - -## About - -This project is maintained and funded by [Cloud Posse, LLC][website]. Like it? Please let us know by [leaving a testimonial][testimonial]! - -[![Cloud Posse][logo]][website] - -We're a [DevOps Professional Services][hire] company based in Los Angeles, CA. We ❤️ [Open Source Software][we_love_open_source]. - -We offer [paid support][commercial_support] on all of our projects. - -Check out [our other projects][github], [follow us on twitter][twitter], [apply for a job][jobs], or [hire us][hire] to help with your cloud strategy and implementation.[![README Footer][readme_footer_img]][readme_footer_link] +--- +Copyright © 2017-2023 [Cloud Posse, LLC](https://cpco.io/copyright) +[![README Footer][readme_footer_img]][readme_footer_link] [![Beacon][beacon]][website] [logo]: https://cloudposse.com/logo-300x69.svg @@ -279,7 +233,6 @@ Check out [our other projects][github], [follow us on twitter][twitter], [apply [hire]: https://cpco.io/hire?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=hire [slack]: https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=slack [twitter]: https://cpco.io/twitter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=twitter - [testimonial]: https://cpco.io/leave-testimonial?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=testimonial [office_hours]: https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=office_hours [newsletter]: https://cpco.io/newsletter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=newsletter [email]: https://cpco.io/email?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=email diff --git a/modules/msk/README.md b/modules/msk/README.md new file mode 100644 index 000000000..f517bcf6b --- /dev/null +++ b/modules/msk/README.md @@ -0,0 +1,216 @@ +# Component: `msk/cluster` + +This component is responsible for provisioning [Amazon Managed Streaming](https://aws.amazon.com/msk/) +clusters for [Apache Kafka](https://aws.amazon.com/msk/what-is-kafka/). + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + + msk: + metadata: + component: "msk" + vars: + enabled: true + name: "msk" + vpc_component_name: "vpc" + dns_delegated_component_name: "dns-delegated" + dns_delegated_environment_name: "gbl" + # https://docs.aws.amazon.com/msk/latest/developerguide/supported-kafka-versions.html + kafka_version: "3.4.0" + public_access_enabled: false + # https://aws.amazon.com/msk/pricing/ + broker_instance_type: "kafka.m5.large" + # Number of brokers per AZ + broker_per_zone: 1 + # `broker_dns_records_count` specifies how many DNS records to create for the broker endpoints in the DNS zone provided in the `zone_id` variable. + # This corresponds to the total number of broker endpoints created by the module. + # Calculate this number by multiplying the `broker_per_zone` variable by the subnet count. + broker_dns_records_count: 3 + broker_volume_size: 500 + client_broker: "TLS_PLAINTEXT" + encryption_in_cluster: true + encryption_at_rest_kms_key_arn: "" + enhanced_monitoring: "DEFAULT" + certificate_authority_arns: [] + + # Authentication methods + client_allow_unauthenticated: true + client_sasl_scram_enabled: false + client_sasl_scram_secret_association_enabled: false + client_sasl_scram_secret_association_arns: [] + client_sasl_iam_enabled: false + client_tls_auth_enabled: false + + jmx_exporter_enabled: false + node_exporter_enabled: false + cloudwatch_logs_enabled: false + firehose_logs_enabled: false + firehose_delivery_stream: "" + s3_logs_enabled: false + s3_logs_bucket: "" + s3_logs_prefix: "" + properties: {} + autoscaling_enabled: true + storage_autoscaling_target_value: 60 + storage_autoscaling_max_capacity: null + storage_autoscaling_disable_scale_in: false + create_security_group: true + security_group_rule_description: "Allow inbound %s traffic" + # A list of IDs of Security Groups to allow access to the cluster security group + allowed_security_group_ids: [] + # A list of IPv4 CIDRs to allow access to the cluster security group + allowed_cidr_blocks: [] +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [kafka](#module\_kafka) | cloudposse/msk-apache-kafka-cluster/aws | 2.3.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_security\_group\_rules](#input\_additional\_security\_group\_rules) | A list of Security Group rule objects to add to the created security group, in addition to the ones
this module normally creates. (To suppress the module's rules, set `create_security_group` to false
and supply your own security group(s) via `associated_security_group_ids`.)
The keys and values of the objects are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique and known at "plan" time.
For more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule
and https://github.com/cloudposse/terraform-aws-security-group. | `list(any)` | `[]` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [allow\_all\_egress](#input\_allow\_all\_egress) | If `true`, the created security group will allow egress on all ports and protocols to all IP addresses.
If this is false and no egress rules are otherwise specified, then no egress will be allowed. | `bool` | `true` | no | +| [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | A list of IPv4 CIDRs to allow access to the security group created by this module.
The length of this list must be known at "plan" time. | `list(string)` | `[]` | no | +| [allowed\_security\_group\_ids](#input\_allowed\_security\_group\_ids) | A list of IDs of Security Groups to allow access to the security group created by this module.
The length of this list must be known at "plan" time. | `list(string)` | `[]` | no | +| [associated\_security\_group\_ids](#input\_associated\_security\_group\_ids) | A list of IDs of Security Groups to associate the created resource with, in addition to the created security group.
These security groups will not be modified and, if `create_security_group` is `false`, must have rules providing the desired access. | `list(string)` | `[]` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [autoscaling\_enabled](#input\_autoscaling\_enabled) | To automatically expand your cluster's storage in response to increased usage, you can enable this. [More info](https://docs.aws.amazon.com/msk/latest/developerguide/msk-autoexpand.html) | `bool` | `true` | no | +| [broker\_dns\_records\_count](#input\_broker\_dns\_records\_count) | This variable specifies how many DNS records to create for the broker endpoints in the DNS zone provided in the `zone_id` variable.
This corresponds to the total number of broker endpoints created by the module.
Calculate this number by multiplying the `broker_per_zone` variable by the subnet count.
This variable is necessary to prevent the Terraform error:
The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. | `number` | `0` | no | +| [broker\_instance\_type](#input\_broker\_instance\_type) | The instance type to use for the Kafka brokers | `string` | n/a | yes | +| [broker\_per\_zone](#input\_broker\_per\_zone) | Number of Kafka brokers per zone | `number` | `1` | no | +| [broker\_volume\_size](#input\_broker\_volume\_size) | The size in GiB of the EBS volume for the data drive on each broker node | `number` | `1000` | no | +| [certificate\_authority\_arns](#input\_certificate\_authority\_arns) | List of ACM Certificate Authority Amazon Resource Names (ARNs) to be used for TLS client authentication | `list(string)` | `[]` | no | +| [client\_allow\_unauthenticated](#input\_client\_allow\_unauthenticated) | Enable unauthenticated access | `bool` | `false` | no | +| [client\_broker](#input\_client\_broker) | Encryption setting for data in transit between clients and brokers. Valid values: `TLS`, `TLS_PLAINTEXT`, and `PLAINTEXT` | `string` | `"TLS"` | no | +| [client\_sasl\_iam\_enabled](#input\_client\_sasl\_iam\_enabled) | Enable client authentication via IAM policies. Cannot be set to `true` at the same time as `client_tls_auth_enabled` | `bool` | `false` | no | +| [client\_sasl\_scram\_enabled](#input\_client\_sasl\_scram\_enabled) | Enable SCRAM client authentication via AWS Secrets Manager. Cannot be set to `true` at the same time as `client_tls_auth_enabled` | `bool` | `false` | no | +| [client\_sasl\_scram\_secret\_association\_arns](#input\_client\_sasl\_scram\_secret\_association\_arns) | List of AWS Secrets Manager secret ARNs for SCRAM authentication | `list(string)` | `[]` | no | +| [client\_sasl\_scram\_secret\_association\_enabled](#input\_client\_sasl\_scram\_secret\_association\_enabled) | Enable the list of AWS Secrets Manager secret ARNs for SCRAM authentication | `bool` | `true` | no | +| [client\_tls\_auth\_enabled](#input\_client\_tls\_auth\_enabled) | Set `true` to enable the Client TLS Authentication | `bool` | `false` | no | +| [cloudwatch\_logs\_enabled](#input\_cloudwatch\_logs\_enabled) | Indicates whether you want to enable or disable streaming broker logs to Cloudwatch Logs | `bool` | `false` | no | +| [cloudwatch\_logs\_log\_group](#input\_cloudwatch\_logs\_log\_group) | Name of the Cloudwatch Log Group to deliver logs to | `string` | `null` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_security\_group](#input\_create\_security\_group) | Set `true` to create and configure a new security group. If false, `associated_security_group_ids` must be provided. | `bool` | `true` | no | +| [custom\_broker\_dns\_name](#input\_custom\_broker\_dns\_name) | Custom Route53 DNS hostname for MSK brokers. Use `%%ID%%` key to specify brokers index in the hostname. Example: `kafka-broker%%ID%%.example.com` | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | The component name of `dns-delegated` | `string` | `"dns-delegated"` | no | +| [dns\_delegated\_environment\_name](#input\_dns\_delegated\_environment\_name) | The environment name of `dns-delegated` | `string` | `"gbl"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [encryption\_at\_rest\_kms\_key\_arn](#input\_encryption\_at\_rest\_kms\_key\_arn) | You may specify a KMS key short ID or ARN (it will always output an ARN) to use for encrypting your data at rest | `string` | `""` | no | +| [encryption\_in\_cluster](#input\_encryption\_in\_cluster) | Whether data communication among broker nodes is encrypted | `bool` | `true` | no | +| [enhanced\_monitoring](#input\_enhanced\_monitoring) | Specify the desired enhanced MSK CloudWatch monitoring level. Valid values: `DEFAULT`, `PER_BROKER`, and `PER_TOPIC_PER_BROKER` | `string` | `"DEFAULT"` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [firehose\_delivery\_stream](#input\_firehose\_delivery\_stream) | Name of the Kinesis Data Firehose delivery stream to deliver logs to | `string` | `""` | no | +| [firehose\_logs\_enabled](#input\_firehose\_logs\_enabled) | Indicates whether you want to enable or disable streaming broker logs to Kinesis Data Firehose | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [inline\_rules\_enabled](#input\_inline\_rules\_enabled) | NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources.
See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules.
See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) for details on the difference between inline rules and rule resources. | `bool` | `false` | no | +| [jmx\_exporter\_enabled](#input\_jmx\_exporter\_enabled) | Set `true` to enable the JMX Exporter | `bool` | `false` | no | +| [kafka\_version](#input\_kafka\_version) | The desired Kafka software version.
Refer to https://docs.aws.amazon.com/msk/latest/developerguide/supported-kafka-versions.html for more details | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [node\_exporter\_enabled](#input\_node\_exporter\_enabled) | Set `true` to enable the Node Exporter | `bool` | `false` | no | +| [preserve\_security\_group\_id](#input\_preserve\_security\_group\_id) | When `false` and `security_group_create_before_destroy` is `true`, changes to security group rules
cause a new security group to be created with the new rules, and the existing security group is then
replaced with the new one, eliminating any service interruption.
When `true` or when changing the value (from `false` to `true` or from `true` to `false`),
existing security group rules will be deleted before new ones are created, resulting in a service interruption,
but preserving the security group itself.
**NOTE:** Setting this to `true` does not guarantee the security group will never be replaced,
it only keeps changes to the security group rules from triggering a replacement.
See the [terraform-aws-security-group README](https://github.com/cloudposse/terraform-aws-security-group) for further discussion. | `bool` | `false` | no | +| [properties](#input\_properties) | Contents of the server.properties file. Supported properties are documented in the [MSK Developer Guide](https://docs.aws.amazon.com/msk/latest/developerguide/msk-configuration-properties.html) | `map(string)` | `{}` | no | +| [public\_access\_enabled](#input\_public\_access\_enabled) | Enable public access to MSK cluster (given that all of the requirements are met) | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS region | `string` | n/a | yes | +| [s3\_logs\_bucket](#input\_s3\_logs\_bucket) | Name of the S3 bucket to deliver logs to | `string` | `""` | no | +| [s3\_logs\_enabled](#input\_s3\_logs\_enabled) | Indicates whether you want to enable or disable streaming broker logs to S3 | `bool` | `false` | no | +| [s3\_logs\_prefix](#input\_s3\_logs\_prefix) | Prefix to append to the S3 folder name logs are delivered to | `string` | `""` | no | +| [security\_group\_create\_before\_destroy](#input\_security\_group\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We only recommend setting this `false` if you are importing an existing security group
that you do not want replaced and therefore need full control over its name.
Note that changing this value will always cause the security group to be replaced. | `bool` | `true` | no | +| [security\_group\_create\_timeout](#input\_security\_group\_create\_timeout) | How long to wait for the security group to be created. | `string` | `"10m"` | no | +| [security\_group\_delete\_timeout](#input\_security\_group\_delete\_timeout) | How long to retry on `DependencyViolation` errors during security group deletion from
lingering ENIs left by certain AWS services such as Elastic Load Balancing. | `string` | `"15m"` | no | +| [security\_group\_description](#input\_security\_group\_description) | The description to assign to the created Security Group.
Warning: Changing the description causes the security group to be replaced. | `string` | `"Managed by Terraform"` | no | +| [security\_group\_name](#input\_security\_group\_name) | The name to assign to the created security group. Must be unique within the VPC.
If not provided, will be derived from the `null-label.context` passed in.
If `create_before_destroy` is true, will be used as a name prefix. | `list(string)` | `[]` | no | +| [security\_group\_rule\_description](#input\_security\_group\_rule\_description) | The description to place on each security group rule. The %s will be replaced with the protocol name | `string` | `"Allow inbound %s traffic"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [storage\_autoscaling\_disable\_scale\_in](#input\_storage\_autoscaling\_disable\_scale\_in) | If the value is true, scale in is disabled and the target tracking policy won't remove capacity from the scalable resource | `bool` | `false` | no | +| [storage\_autoscaling\_max\_capacity](#input\_storage\_autoscaling\_max\_capacity) | Maximum size the autoscaling policy can scale storage. Defaults to `broker_volume_size` | `number` | `null` | no | +| [storage\_autoscaling\_target\_value](#input\_storage\_autoscaling\_target\_value) | Percentage of storage used to trigger autoscaled storage increase | `number` | `60` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | The name of the Atmos VPC component | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [bootstrap\_brokers](#output\_bootstrap\_brokers) | Comma separated list of one or more hostname:port pairs of Kafka brokers suitable to bootstrap connectivity to the Kafka cluster | +| [bootstrap\_brokers\_public\_sasl\_iam](#output\_bootstrap\_brokers\_public\_sasl\_iam) | Comma separated list of one or more DNS names (or IP addresses) and SASL IAM port pairs for public access to the Kafka cluster using SASL/IAM | +| [bootstrap\_brokers\_public\_sasl\_scram](#output\_bootstrap\_brokers\_public\_sasl\_scram) | Comma separated list of one or more DNS names (or IP addresses) and SASL SCRAM port pairs for public access to the Kafka cluster using SASL/SCRAM | +| [bootstrap\_brokers\_public\_tls](#output\_bootstrap\_brokers\_public\_tls) | Comma separated list of one or more DNS names (or IP addresses) and TLS port pairs for public access to the Kafka cluster using TLS | +| [bootstrap\_brokers\_sasl\_iam](#output\_bootstrap\_brokers\_sasl\_iam) | Comma separated list of one or more DNS names (or IP addresses) and SASL IAM port pairs for access to the Kafka cluster using SASL/IAM | +| [bootstrap\_brokers\_sasl\_scram](#output\_bootstrap\_brokers\_sasl\_scram) | Comma separated list of one or more DNS names (or IP addresses) and SASL SCRAM port pairs for access to the Kafka cluster using SASL/SCRAM | +| [bootstrap\_brokers\_tls](#output\_bootstrap\_brokers\_tls) | Comma separated list of one or more DNS names (or IP addresses) and TLS port pairs for access to the Kafka cluster using TLS | +| [broker\_endpoints](#output\_broker\_endpoints) | List of broker endpoints | +| [cluster\_arn](#output\_cluster\_arn) | Amazon Resource Name (ARN) of the MSK cluster | +| [cluster\_name](#output\_cluster\_name) | The cluster name of the MSK cluster | +| [config\_arn](#output\_config\_arn) | Amazon Resource Name (ARN) of the MSK configuration | +| [current\_version](#output\_current\_version) | Current version of the MSK Cluster | +| [hostnames](#output\_hostnames) | List of MSK Cluster broker DNS hostnames | +| [latest\_revision](#output\_latest\_revision) | Latest revision of the MSK configuration | +| [security\_group\_arn](#output\_security\_group\_arn) | The ARN of the created security group | +| [security\_group\_id](#output\_security\_group\_id) | The ID of the created security group | +| [security\_group\_name](#output\_security\_group\_name) | The name of the created security group | +| [storage\_mode](#output\_storage\_mode) | Storage mode for supported storage tiers | +| [zookeeper\_connect\_string](#output\_zookeeper\_connect\_string) | Comma separated list of one or more hostname:port pairs to connect to the Apache Zookeeper cluster | +| [zookeeper\_connect\_string\_tls](#output\_zookeeper\_connect\_string\_tls) | Comma separated list of one or more hostname:port pairs to connect to the Apache Zookeeper cluster via TLS | + + +## References + +- https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/msk_cluster +- https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/msk_serverless_cluster +- https://aws.amazon.com/blogs/big-data/securing-apache-kafka-is-easy-and-familiar-with-iam-access-control-for-amazon-msk/ +- https://docs.aws.amazon.com/msk/latest/developerguide/security-iam.html +- https://docs.aws.amazon.com/msk/latest/developerguide/iam-access-control.html +- https://docs.aws.amazon.com/msk/latest/developerguide/kafka_apis_iam.html +- https://github.com/aws/aws-msk-iam-auth +- https://www.cloudthat.com/resources/blog/a-guide-to-create-aws-msk-cluster-with-iam-based-authentication +- https://blog.devops.dev/how-to-use-iam-auth-with-aws-msk-a-step-by-step-guide-2023-eb8291781fcb +- https://www.kai-waehner.de/blog/2022/08/30/when-not-to-choose-amazon-msk-serverless-for-apache-kafka/ +- https://stackoverflow.com/questions/72508438/connect-python-to-msk-with-iam-role-based-authentication +- https://github.com/aws/aws-msk-iam-auth/issues/10 +- https://aws.amazon.com/msk/faqs/ +- https://aws.amazon.com/blogs/big-data/secure-connectivity-patterns-to-access-amazon-msk-across-aws-regions/ +- https://docs.aws.amazon.com/msk/latest/developerguide/client-access.html +- https://repost.aws/knowledge-center/msk-broker-custom-ports + +[](https://cpco.io/component) diff --git a/modules/msk/context.tf b/modules/msk/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/msk/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/msk/main.tf b/modules/msk/main.tf new file mode 100644 index 000000000..6f538a990 --- /dev/null +++ b/modules/msk/main.tf @@ -0,0 +1,68 @@ +locals { + enabled = module.this.enabled + vpc_outputs = module.vpc.outputs +} + +module "kafka" { + source = "cloudposse/msk-apache-kafka-cluster/aws" + version = "2.3.0" + + # VPC and subnets + vpc_id = local.vpc_outputs.vpc_id + subnet_ids = local.vpc_outputs.private_subnet_ids + + # Cluster config + kafka_version = var.kafka_version + broker_per_zone = var.broker_per_zone + broker_instance_type = var.broker_instance_type + broker_volume_size = var.broker_volume_size + client_broker = var.client_broker + encryption_in_cluster = var.encryption_in_cluster + encryption_at_rest_kms_key_arn = var.encryption_at_rest_kms_key_arn + enhanced_monitoring = var.enhanced_monitoring + certificate_authority_arns = var.certificate_authority_arns + client_allow_unauthenticated = var.client_allow_unauthenticated + client_sasl_scram_enabled = var.client_sasl_scram_enabled + client_sasl_scram_secret_association_enabled = var.client_sasl_scram_secret_association_enabled + client_sasl_scram_secret_association_arns = var.client_sasl_scram_secret_association_arns + client_sasl_iam_enabled = var.client_sasl_iam_enabled + client_tls_auth_enabled = var.client_tls_auth_enabled + jmx_exporter_enabled = var.jmx_exporter_enabled + node_exporter_enabled = var.node_exporter_enabled + cloudwatch_logs_enabled = var.cloudwatch_logs_enabled + cloudwatch_logs_log_group = var.cloudwatch_logs_log_group + firehose_logs_enabled = var.firehose_logs_enabled + firehose_delivery_stream = var.firehose_delivery_stream + s3_logs_enabled = var.s3_logs_enabled + s3_logs_bucket = var.s3_logs_bucket + s3_logs_prefix = var.s3_logs_prefix + properties = var.properties + autoscaling_enabled = var.autoscaling_enabled + storage_autoscaling_target_value = var.storage_autoscaling_target_value + storage_autoscaling_max_capacity = var.storage_autoscaling_max_capacity + storage_autoscaling_disable_scale_in = var.storage_autoscaling_disable_scale_in + security_group_rule_description = var.security_group_rule_description + public_access_enabled = var.public_access_enabled + + # DNS hostname records + zone_id = module.dns_delegated.outputs.default_dns_zone_id + broker_dns_records_count = var.broker_dns_records_count + custom_broker_dns_name = var.custom_broker_dns_name + + # Cluster Security Group + allowed_security_group_ids = var.allowed_security_group_ids + allowed_cidr_blocks = var.allowed_cidr_blocks + associated_security_group_ids = var.associated_security_group_ids + create_security_group = var.create_security_group + security_group_name = var.security_group_name + security_group_description = var.security_group_description + security_group_create_before_destroy = var.security_group_create_before_destroy + preserve_security_group_id = var.preserve_security_group_id + security_group_create_timeout = var.security_group_create_timeout + security_group_delete_timeout = var.security_group_delete_timeout + allow_all_egress = var.allow_all_egress + additional_security_group_rules = var.additional_security_group_rules + inline_rules_enabled = var.inline_rules_enabled + + context = module.this.context +} diff --git a/modules/msk/outputs.tf b/modules/msk/outputs.tf new file mode 100644 index 000000000..ffa71a1e5 --- /dev/null +++ b/modules/msk/outputs.tf @@ -0,0 +1,99 @@ +output "cluster_name" { + value = module.kafka.cluster_name + description = "The cluster name of the MSK cluster" +} + +output "cluster_arn" { + value = module.kafka.cluster_arn + description = "Amazon Resource Name (ARN) of the MSK cluster" +} + +output "storage_mode" { + value = module.kafka.storage_mode + description = "Storage mode for supported storage tiers" +} + +output "bootstrap_brokers" { + value = module.kafka.bootstrap_brokers + description = "Comma separated list of one or more hostname:port pairs of Kafka brokers suitable to bootstrap connectivity to the Kafka cluster" +} + +output "bootstrap_brokers_tls" { + value = module.kafka.bootstrap_brokers_tls + description = "Comma separated list of one or more DNS names (or IP addresses) and TLS port pairs for access to the Kafka cluster using TLS" +} + +output "bootstrap_brokers_public_tls" { + value = module.kafka.bootstrap_brokers_public_tls + description = "Comma separated list of one or more DNS names (or IP addresses) and TLS port pairs for public access to the Kafka cluster using TLS" +} + +output "bootstrap_brokers_sasl_scram" { + value = module.kafka.bootstrap_brokers_sasl_scram + description = "Comma separated list of one or more DNS names (or IP addresses) and SASL SCRAM port pairs for access to the Kafka cluster using SASL/SCRAM" +} + +output "bootstrap_brokers_public_sasl_scram" { + value = module.kafka.bootstrap_brokers_public_sasl_scram + description = "Comma separated list of one or more DNS names (or IP addresses) and SASL SCRAM port pairs for public access to the Kafka cluster using SASL/SCRAM" +} + +output "bootstrap_brokers_sasl_iam" { + value = module.kafka.bootstrap_brokers_sasl_iam + description = "Comma separated list of one or more DNS names (or IP addresses) and SASL IAM port pairs for access to the Kafka cluster using SASL/IAM" +} + +output "bootstrap_brokers_public_sasl_iam" { + value = module.kafka.bootstrap_brokers_public_sasl_iam + description = "Comma separated list of one or more DNS names (or IP addresses) and SASL IAM port pairs for public access to the Kafka cluster using SASL/IAM" +} + +output "zookeeper_connect_string" { + value = module.kafka.zookeeper_connect_string + description = "Comma separated list of one or more hostname:port pairs to connect to the Apache Zookeeper cluster" +} + +output "zookeeper_connect_string_tls" { + value = module.kafka.zookeeper_connect_string_tls + description = "Comma separated list of one or more hostname:port pairs to connect to the Apache Zookeeper cluster via TLS" +} + +output "broker_endpoints" { + value = module.kafka.broker_endpoints + description = "List of broker endpoints" +} + +output "current_version" { + value = module.kafka.current_version + description = "Current version of the MSK Cluster" +} + +output "config_arn" { + value = module.kafka.config_arn + description = "Amazon Resource Name (ARN) of the MSK configuration" +} + +output "latest_revision" { + value = module.kafka.latest_revision + description = "Latest revision of the MSK configuration" +} + +output "hostnames" { + value = module.kafka.hostnames + description = "List of MSK Cluster broker DNS hostnames" +} + +output "security_group_id" { + value = module.kafka.security_group_id + description = "The ID of the created security group" +} + +output "security_group_arn" { + value = module.kafka.security_group_arn + description = "The ARN of the created security group" +} + +output "security_group_name" { + value = module.kafka.security_group_name + description = "The name of the created security group" +} diff --git a/modules/msk/providers.tf b/modules/msk/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/msk/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/msk/remote-state.tf b/modules/msk/remote-state.tf new file mode 100644 index 000000000..26780aa76 --- /dev/null +++ b/modules/msk/remote-state.tf @@ -0,0 +1,18 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.vpc_component_name + + context = module.this.context +} + +module "dns_delegated" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.dns_delegated_component_name + environment = var.dns_delegated_environment_name + + context = module.this.context +} diff --git a/modules/msk/security-group-variables.tf b/modules/msk/security-group-variables.tf new file mode 100644 index 000000000..bd52e3f58 --- /dev/null +++ b/modules/msk/security-group-variables.tf @@ -0,0 +1,184 @@ +# security-group-variables Version: 3 +# +# Copy this file from https://github.com/cloudposse/terraform-aws-security-group/blob/master/exports/security-group-variables.tf +# and EDIT IT TO SUIT YOUR PROJECT. Update the version number above if you update this file from a later version. +# Unlike null-label context.tf, this file cannot be automatically updated +# because of the tight integration with the module using it. +## +# Delete this top comment block, except for the first line (version number), +# REMOVE COMMENTS below that are intended for the initial implementor and not maintainers or end users. +# +# This file provides the standard inputs that all Cloud Posse Open Source +# Terraform module that create AWS Security Groups should implement. +# This file does NOT provide implementation of the inputs, as that +# of course varies with each module. +# +# This file declares some standard outputs modules should create, +# but the declarations should be moved to `outputs.tf` and of course +# may need to be modified based on the module's use of security-group. +# + + +variable "create_security_group" { + type = bool + description = "Set `true` to create and configure a new security group. If false, `associated_security_group_ids` must be provided." + default = true +} + +variable "associated_security_group_ids" { + type = list(string) + description = <<-EOT + A list of IDs of Security Groups to associate the created resource with, in addition to the created security group. + These security groups will not be modified and, if `create_security_group` is `false`, must have rules providing the desired access. + EOT + default = [] +} + +## +## allowed_* inputs are optional, because the same thing can be accomplished by +## providing `additional_security_group_rules`. However, if the rules this +## module creates are non-trivial (for example, opening ports based on +## feature settings, see https://github.com/cloudposse/terraform-aws-msk-apache-kafka-cluster/blob/3fe23c402cc420799ae721186812482335f78d24/main.tf#L14-L53 ) +## then it makes sense to include these. +## Reasons not to include some or all of these inputs include +## - too hard to implement +## - does not make sense (particularly the IPv6 inputs if the underlying resource does not yet support IPv6) +## - likely to confuse users +## - likely to invite count/for_each issues +variable "allowed_security_group_ids" { + type = list(string) + description = <<-EOT + A list of IDs of Security Groups to allow access to the security group created by this module. + The length of this list must be known at "plan" time. + EOT + default = [] +} + +variable "allowed_cidr_blocks" { + type = list(string) + description = <<-EOT + A list of IPv4 CIDRs to allow access to the security group created by this module. + The length of this list must be known at "plan" time. + EOT + default = [] +} +## End of optional allowed_* ########### + +variable "security_group_name" { + type = list(string) + description = <<-EOT + The name to assign to the created security group. Must be unique within the VPC. + If not provided, will be derived from the `null-label.context` passed in. + If `create_before_destroy` is true, will be used as a name prefix. + EOT + default = [] +} + +variable "security_group_description" { + type = string + description = <<-EOT + The description to assign to the created Security Group. + Warning: Changing the description causes the security group to be replaced. + EOT + default = "Managed by Terraform" +} + +variable "security_group_create_before_destroy" { + type = bool + description = <<-EOT + Set `true` to enable terraform `create_before_destroy` behavior on the created security group. + We only recommend setting this `false` if you are importing an existing security group + that you do not want replaced and therefore need full control over its name. + Note that changing this value will always cause the security group to be replaced. + EOT + default = true +} + +variable "preserve_security_group_id" { + type = bool + description = <<-EOT + When `false` and `security_group_create_before_destroy` is `true`, changes to security group rules + cause a new security group to be created with the new rules, and the existing security group is then + replaced with the new one, eliminating any service interruption. + When `true` or when changing the value (from `false` to `true` or from `true` to `false`), + existing security group rules will be deleted before new ones are created, resulting in a service interruption, + but preserving the security group itself. + **NOTE:** Setting this to `true` does not guarantee the security group will never be replaced, + it only keeps changes to the security group rules from triggering a replacement. + See the [terraform-aws-security-group README](https://github.com/cloudposse/terraform-aws-security-group) for further discussion. + EOT + default = false +} + +variable "security_group_create_timeout" { + type = string + description = "How long to wait for the security group to be created." + default = "10m" +} + +variable "security_group_delete_timeout" { + type = string + description = <<-EOT + How long to retry on `DependencyViolation` errors during security group deletion from + lingering ENIs left by certain AWS services such as Elastic Load Balancing. + EOT + default = "15m" +} + +variable "allow_all_egress" { + type = bool + description = <<-EOT + If `true`, the created security group will allow egress on all ports and protocols to all IP addresses. + If this is false and no egress rules are otherwise specified, then no egress will be allowed. + EOT + default = true +} + +variable "additional_security_group_rules" { + type = list(any) + description = <<-EOT + A list of Security Group rule objects to add to the created security group, in addition to the ones + this module normally creates. (To suppress the module's rules, set `create_security_group` to false + and supply your own security group(s) via `associated_security_group_ids`.) + The keys and values of the objects are fully compatible with the `aws_security_group_rule` resource, except + for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique and known at "plan" time. + For more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule + and https://github.com/cloudposse/terraform-aws-security-group. + EOT + default = [] +} + +#### We do not expose an `additional_security_group_rule_matrix` input for a few reasons: +# - It is a convenience and ultimately provides no rules that cannot be provided via `additional_security_group_rules` +# - It is complicated and can, in some situations, create problems for Terraform `for_each` +# - It is difficult to document and easy to make mistakes using it + + +# +# +#### The variables below (but not the outputs) can be omitted if not needed, and may need their descriptions modified +# +# + +############################################################################################# +## Special note about inline_rules_enabled and revoke_rules_on_delete +## +## The security-group inputs inline_rules_enabled and revoke_rules_on_delete should not +## be exposed in other modules unless there is a strong reason for them to be used. +## We discourage the use of inline_rules_enabled and we rarely need or want +## revoke_rules_on_delete, so we do not want to clutter our interface with those inputs. +## +## If someone wants to enable either of those options, they have the option +## of creating a security group configured as they like +## and passing it in as the target security group. +############################################################################################# + +variable "inline_rules_enabled" { + type = bool + description = <<-EOT + NOT RECOMMENDED. Create rules "inline" instead of as separate `aws_security_group_rule` resources. + See [#20046](https://github.com/hashicorp/terraform-provider-aws/issues/20046) for one of several issues with inline rules. + See [this post](https://github.com/hashicorp/terraform-provider-aws/pull/9032#issuecomment-639545250) for details on the difference between inline rules and rule resources. + EOT + default = false +} diff --git a/modules/msk/variables.tf b/modules/msk/variables.tf new file mode 100644 index 000000000..afdbb877a --- /dev/null +++ b/modules/msk/variables.tf @@ -0,0 +1,256 @@ +variable "region" { + type = string + description = "AWS region" + nullable = false +} + +variable "vpc_component_name" { + type = string + description = "The name of the Atmos VPC component" +} + +variable "kafka_version" { + type = string + description = <<-EOT + The desired Kafka software version. + Refer to https://docs.aws.amazon.com/msk/latest/developerguide/supported-kafka-versions.html for more details + EOT + nullable = false +} + +variable "broker_instance_type" { + type = string + description = "The instance type to use for the Kafka brokers" + nullable = false +} + +variable "broker_per_zone" { + type = number + default = 1 + description = "Number of Kafka brokers per zone" + validation { + condition = var.broker_per_zone > 0 + error_message = "The broker_per_zone value must be at least 1." + } + nullable = false +} + +variable "broker_volume_size" { + type = number + default = 1000 + description = "The size in GiB of the EBS volume for the data drive on each broker node" + nullable = false +} + +variable "client_broker" { + type = string + default = "TLS" + description = "Encryption setting for data in transit between clients and brokers. Valid values: `TLS`, `TLS_PLAINTEXT`, and `PLAINTEXT`" + nullable = false +} + +variable "encryption_in_cluster" { + type = bool + default = true + description = "Whether data communication among broker nodes is encrypted" + nullable = false +} + +variable "encryption_at_rest_kms_key_arn" { + type = string + default = "" + description = "You may specify a KMS key short ID or ARN (it will always output an ARN) to use for encrypting your data at rest" +} + +variable "enhanced_monitoring" { + type = string + default = "DEFAULT" + description = "Specify the desired enhanced MSK CloudWatch monitoring level. Valid values: `DEFAULT`, `PER_BROKER`, and `PER_TOPIC_PER_BROKER`" + nullable = false +} + +variable "certificate_authority_arns" { + type = list(string) + default = [] + description = "List of ACM Certificate Authority Amazon Resource Names (ARNs) to be used for TLS client authentication" + nullable = false +} + +variable "client_allow_unauthenticated" { + type = bool + default = false + description = "Enable unauthenticated access" + nullable = false +} + +variable "client_sasl_scram_enabled" { + type = bool + default = false + description = "Enable SCRAM client authentication via AWS Secrets Manager. Cannot be set to `true` at the same time as `client_tls_auth_enabled`" + nullable = false +} + +variable "client_sasl_scram_secret_association_enabled" { + type = bool + default = true + description = "Enable the list of AWS Secrets Manager secret ARNs for SCRAM authentication" + nullable = false +} + +variable "client_sasl_scram_secret_association_arns" { + type = list(string) + default = [] + description = "List of AWS Secrets Manager secret ARNs for SCRAM authentication" + nullable = false +} + +variable "client_sasl_iam_enabled" { + type = bool + default = false + description = "Enable client authentication via IAM policies. Cannot be set to `true` at the same time as `client_tls_auth_enabled`" + nullable = false +} + +variable "client_tls_auth_enabled" { + type = bool + default = false + description = "Set `true` to enable the Client TLS Authentication" + nullable = false +} + +variable "jmx_exporter_enabled" { + type = bool + default = false + description = "Set `true` to enable the JMX Exporter" + nullable = false +} + +variable "node_exporter_enabled" { + type = bool + default = false + description = "Set `true` to enable the Node Exporter" + nullable = false +} + +variable "cloudwatch_logs_enabled" { + type = bool + default = false + description = "Indicates whether you want to enable or disable streaming broker logs to Cloudwatch Logs" + nullable = false +} + +variable "cloudwatch_logs_log_group" { + type = string + default = null + description = "Name of the Cloudwatch Log Group to deliver logs to" +} + +variable "firehose_logs_enabled" { + type = bool + default = false + description = "Indicates whether you want to enable or disable streaming broker logs to Kinesis Data Firehose" + nullable = false +} + +variable "firehose_delivery_stream" { + type = string + default = "" + description = "Name of the Kinesis Data Firehose delivery stream to deliver logs to" +} + +variable "s3_logs_enabled" { + type = bool + default = false + description = " Indicates whether you want to enable or disable streaming broker logs to S3" + nullable = false +} + +variable "s3_logs_bucket" { + type = string + default = "" + description = "Name of the S3 bucket to deliver logs to" +} + +variable "s3_logs_prefix" { + type = string + default = "" + description = "Prefix to append to the S3 folder name logs are delivered to" +} + +variable "properties" { + type = map(string) + default = {} + description = "Contents of the server.properties file. Supported properties are documented in the [MSK Developer Guide](https://docs.aws.amazon.com/msk/latest/developerguide/msk-configuration-properties.html)" + nullable = false +} + +variable "autoscaling_enabled" { + type = bool + default = true + description = "To automatically expand your cluster's storage in response to increased usage, you can enable this. [More info](https://docs.aws.amazon.com/msk/latest/developerguide/msk-autoexpand.html)" + nullable = false +} + +variable "storage_autoscaling_target_value" { + type = number + default = 60 + description = "Percentage of storage used to trigger autoscaled storage increase" +} + +variable "storage_autoscaling_max_capacity" { + type = number + default = null + description = "Maximum size the autoscaling policy can scale storage. Defaults to `broker_volume_size`" +} + +variable "storage_autoscaling_disable_scale_in" { + type = bool + default = false + description = "If the value is true, scale in is disabled and the target tracking policy won't remove capacity from the scalable resource" + nullable = false +} + +variable "security_group_rule_description" { + type = string + default = "Allow inbound %s traffic" + description = "The description to place on each security group rule. The %s will be replaced with the protocol name" + nullable = false +} + +variable "public_access_enabled" { + type = bool + default = false + description = "Enable public access to MSK cluster (given that all of the requirements are met)" + nullable = false +} + +variable "dns_delegated_component_name" { + type = string + description = "The component name of `dns-delegated`" + default = "dns-delegated" +} + +variable "dns_delegated_environment_name" { + type = string + description = "The environment name of `dns-delegated`" + default = "gbl" +} + +variable "broker_dns_records_count" { + type = number + description = <<-EOT + This variable specifies how many DNS records to create for the broker endpoints in the DNS zone provided in the `zone_id` variable. + This corresponds to the total number of broker endpoints created by the module. + Calculate this number by multiplying the `broker_per_zone` variable by the subnet count. + This variable is necessary to prevent the Terraform error: + The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. + EOT + default = 0 + nullable = false +} + +variable "custom_broker_dns_name" { + type = string + description = "Custom Route53 DNS hostname for MSK brokers. Use `%%ID%%` key to specify brokers index in the hostname. Example: `kafka-broker%%ID%%.example.com`" + default = null +} diff --git a/modules/msk/versions.tf b/modules/msk/versions.tf new file mode 100644 index 000000000..cc73ffd35 --- /dev/null +++ b/modules/msk/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From f911bf8ddfceb657561697c389c7489487aaaa45 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 11 Dec 2023 13:27:28 -0800 Subject: [PATCH 327/501] upstream `s3-bucket/policy` (#927) --- .../s3-bucket/github-actions-iam-policy.tf | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 mixins/github-actions-iam-policy/s3-bucket/github-actions-iam-policy.tf diff --git a/mixins/github-actions-iam-policy/s3-bucket/github-actions-iam-policy.tf b/mixins/github-actions-iam-policy/s3-bucket/github-actions-iam-policy.tf new file mode 100644 index 000000000..08c50f33a --- /dev/null +++ b/mixins/github-actions-iam-policy/s3-bucket/github-actions-iam-policy.tf @@ -0,0 +1,23 @@ +variable "github_actions_iam_actions" { + type = list(string) + default = [ + "s3:CreateMultipartUpload", + "s3:PutObject", + "s3:PutObjectAcl" + ] + description = "List of actions to permit `GitHub OIDC authenticated users` to perform on bucket and bucket prefixes" +} + + +locals { + github_actions_iam_policy = data.aws_iam_policy_document.github_actions_iam_policy.json +} + +data "aws_iam_policy_document" "github_actions_iam_policy" { + statement { + sid = "AllowS3UploadPermissions" + effect = "Allow" + actions = var.github_actions_iam_actions + resources = [module.s3_bucket.bucket_arn, "${module.s3_bucket.bucket_arn}/*"] + } +} From e4c5f6ea7d261fb84b273af2aaa69ad4561105ec Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 11 Dec 2023 14:26:03 -0800 Subject: [PATCH 328/501] dynamic GHA role (#929) --- .../github-actions-iam-policy.tf | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 mixins/github-actions-iam-policy/github-actions-iam-policy.tf diff --git a/mixins/github-actions-iam-policy/github-actions-iam-policy.tf b/mixins/github-actions-iam-policy/github-actions-iam-policy.tf new file mode 100644 index 000000000..deb44132a --- /dev/null +++ b/mixins/github-actions-iam-policy/github-actions-iam-policy.tf @@ -0,0 +1,34 @@ +## Custom IAM Policy for GitHub Actions +## Requires GitHub OIDC Component be deployed +## Usage: +## in your stack configuration: +# components: +# terraform: +# foo: +# vars: +# github_actions_iam_role_enabled: true +# github_actions_allowed_repos: +# - MyOrg/MyRepo +# github_actions_iam_policy_statements: +# - Sid: "AllowAll" +# Action: [ +# "lambda:*", +# ] +# Effect: "Allow" +# Resource: ["*"] +# + + +variable "github_actions_iam_policy_statements" { + type = list(any) + default = [] +} + +locals { + enabled = module.this.enabled + policy = jsonencode({ + Version = "2012-10-17", + Statement = var.github_actions_iam_policy_statements + }) + github_actions_iam_policy = local.policy +} From 5f0abd4a5df3d24b8e704ad251cd0f785718018c Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 11 Dec 2023 18:21:18 -0800 Subject: [PATCH 329/501] feat: create `aws-sso` groups for google workspaces (#928) --- modules/aws-sso/README.md | 26 +++++++++ modules/aws-sso/main.tf | 101 +++++++++++++++++++++-------------- modules/aws-sso/outputs.tf | 5 ++ modules/aws-sso/variables.tf | 10 ++++ 4 files changed, 103 insertions(+), 39 deletions(-) diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index aab18d348..bcdb80a90 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -26,6 +26,28 @@ delegating SSO to the `identity` account is lost. Nevertheless, it is also not worth the effort to remove the delegation. If you have already delegated SSO to the `identity`, continue on, leaving the stack configuration in the `gbl-identity` stack rather than the currently recommended `gbl-root` stack. +### Google Workspace + +:::important + +> Your identity source is currently configured as 'External identity provider'. To add new groups or edit their memberships, you must do this using your external identity provider. + +Groups _cannot_ be created with ClickOps in the AWS console and instead must be created with AWS API. + +::: + +Google Workspace is now supported by AWS Identity Center, but Group creation is not automatically handled. After [configuring SAML and SCIM with Google Workspace and IAM Identity Center following the AWS documentation](https://docs.aws.amazon.com/singlesignon/latest/userguide/gs-gwp.html), add any Group name to `var.groups` to create the Group with Terraform. Once the setup steps as described in the AWS documentation have been completed and the Groups are created with Terraform, Users should automatically populate each created Group. + +```yaml +components: + terraform: + aws-sso: + vars: + groups: + - "Developers" + - "Dev Ops" +``` + ### Atmos **Stack Level**: Global @@ -187,11 +209,13 @@ components: | Name | Type | |------|------| +| [aws_identitystore_group.manual](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/identitystore_group) | resource | | [aws_iam_policy_document.assume_aws_team](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.dns_administrator_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.eks_read_only](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.terraform_update_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_ssoadmin_instances.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssoadmin_instances) | data source | ## Inputs @@ -206,6 +230,7 @@ components: | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [groups](#input\_groups) | List of AWS Identity Center Groups to be created with the AWS API.

When provisioning the Google Workspace Integration with AWS, Groups need to be created with API in order for automatic provisioning to work as intended. | `list(string)` | `[]` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | @@ -225,6 +250,7 @@ components: | Name | Description | |------|-------------| +| [group\_ids](#output\_group\_ids) | Group IDs created for Identity Center | | [permission\_sets](#output\_permission\_sets) | Permission sets | | [sso\_account\_assignments](#output\_sso\_account\_assignments) | SSO account assignments | diff --git a/modules/aws-sso/main.tf b/modules/aws-sso/main.tf index 7f71b703b..55d82ba3d 100644 --- a/modules/aws-sso/main.tf +++ b/modules/aws-sso/main.tf @@ -1,42 +1,3 @@ -module "permission_sets" { - source = "cloudposse/sso/aws//modules/permission-sets" - version = "1.1.1" - - permission_sets = concat( - local.overridable_additional_permission_sets, - local.administrator_access_permission_set, - local.billing_administrator_access_permission_set, - local.billing_read_only_access_permission_set, - local.dns_administrator_access_permission_set, - local.identity_access_permission_sets, - local.poweruser_access_permission_set, - local.read_only_access_permission_set, - local.terraform_update_access_permission_set, - ) - - context = module.this.context -} - -module "sso_account_assignments" { - source = "cloudposse/sso/aws//modules/account-assignments" - version = "1.1.1" - - account_assignments = local.account_assignments - context = module.this.context -} - -module "sso_account_assignments_root" { - source = "cloudposse/sso/aws//modules/account-assignments" - version = "1.1.1" - - providers = { - aws = aws.root - } - - account_assignments = local.account_assignments_root - context = module.this.context -} - locals { enabled = module.this.enabled @@ -99,4 +60,66 @@ locals { aws_partition = data.aws_partition.current.partition } +data "aws_ssoadmin_instances" "this" {} + data "aws_partition" "current" {} + +resource "aws_identitystore_group" "manual" { + for_each = toset(var.groups) + + display_name = each.key + description = "Group created with Terraform" + + identity_store_id = tolist(data.aws_ssoadmin_instances.this.identity_store_ids)[0] +} + +module "permission_sets" { + source = "cloudposse/sso/aws//modules/permission-sets" + version = "1.1.1" + + permission_sets = concat( + local.overridable_additional_permission_sets, + local.administrator_access_permission_set, + local.billing_administrator_access_permission_set, + local.billing_read_only_access_permission_set, + local.dns_administrator_access_permission_set, + local.identity_access_permission_sets, + local.poweruser_access_permission_set, + local.read_only_access_permission_set, + local.terraform_update_access_permission_set, + ) + + context = module.this.context + + depends_on = [ + aws_identitystore_group.manual + ] +} + +module "sso_account_assignments" { + source = "cloudposse/sso/aws//modules/account-assignments" + version = "1.1.1" + + account_assignments = local.account_assignments + context = module.this.context + + depends_on = [ + aws_identitystore_group.manual + ] +} + +module "sso_account_assignments_root" { + source = "cloudposse/sso/aws//modules/account-assignments" + version = "1.1.1" + + providers = { + aws = aws.root + } + + account_assignments = local.account_assignments_root + context = module.this.context + + depends_on = [ + aws_identitystore_group.manual + ] +} diff --git a/modules/aws-sso/outputs.tf b/modules/aws-sso/outputs.tf index dbb76ef5b..e0c154510 100644 --- a/modules/aws-sso/outputs.tf +++ b/modules/aws-sso/outputs.tf @@ -7,3 +7,8 @@ output "sso_account_assignments" { value = module.sso_account_assignments.assignments description = "SSO account assignments" } + +output "group_ids" { + value = { for group_key, group_output in aws_identitystore_group.manual : group_key => group_output.group_id } + description = "Group IDs created for Identity Center" +} diff --git a/modules/aws-sso/variables.tf b/modules/aws-sso/variables.tf index 70c5993c2..b8b41210e 100644 --- a/modules/aws-sso/variables.tf +++ b/modules/aws-sso/variables.tf @@ -42,3 +42,13 @@ variable "aws_teams_accessible" { EOT default = [] } + +variable "groups" { + type = list(string) + description = <<-EOT + List of AWS Identity Center Groups to be created with the AWS API. + + When provisioning the Google Workspace Integration with AWS, Groups need to be created with API in order for automatic provisioning to work as intended. + EOT + default = [] +} From d2cae969c0d74e441e066d25aa6b490895d90066 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 14 Dec 2023 19:29:36 -0800 Subject: [PATCH 330/501] [eks/github-actions-runner] New component deploying GitHub Action Runner Scale Sets (#935) --- modules/aurora-postgres-resources/README.md | 22 +- .../eks/github-actions-runner/CHANGELOG.md | 165 +++++++ modules/eks/github-actions-runner/README.md | 452 ++++++++++++++++++ modules/eks/github-actions-runner/context.tf | 279 +++++++++++ modules/eks/github-actions-runner/main.tf | 172 +++++++ modules/eks/github-actions-runner/outputs.tf | 25 + .../github-actions-runner/provider-helm.tf | 166 +++++++ .../eks/github-actions-runner/provider-ssm.tf | 15 + .../eks/github-actions-runner/providers.tf | 19 + .../eks/github-actions-runner/remote-state.tf | 8 + modules/eks/github-actions-runner/runners.tf | 185 +++++++ .../eks/github-actions-runner/variables.tf | 223 +++++++++ modules/eks/github-actions-runner/versions.tf | 18 + 13 files changed, 1748 insertions(+), 1 deletion(-) create mode 100644 modules/eks/github-actions-runner/CHANGELOG.md create mode 100644 modules/eks/github-actions-runner/README.md create mode 100644 modules/eks/github-actions-runner/context.tf create mode 100644 modules/eks/github-actions-runner/main.tf create mode 100644 modules/eks/github-actions-runner/outputs.tf create mode 100644 modules/eks/github-actions-runner/provider-helm.tf create mode 100644 modules/eks/github-actions-runner/provider-ssm.tf create mode 100644 modules/eks/github-actions-runner/providers.tf create mode 100644 modules/eks/github-actions-runner/remote-state.tf create mode 100644 modules/eks/github-actions-runner/runners.tf create mode 100644 modules/eks/github-actions-runner/variables.tf create mode 100644 modules/eks/github-actions-runner/versions.tf diff --git a/modules/aurora-postgres-resources/README.md b/modules/aurora-postgres-resources/README.md index 8c4aa09e1..9744ebce6 100644 --- a/modules/aurora-postgres-resources/README.md +++ b/modules/aurora-postgres-resources/README.md @@ -22,9 +22,26 @@ components: - grant: [ "ALL" ] db: example object_type: database - schema: null + schema: "" ``` +## PostgreSQL Quick Reference on Grants + +GRANTS can be on database, schema, role, table, and other database objects (e.g. columns in a table for fine control). Database and schema do not have much to grant. The `object_type` field in the input determines which kind of object the grant is being applied to. The `db` field is always required. The `schema` field is required unless the `object_type` is `db`, in which case it should be set to the empty string (`""`). + +The keyword PUBLIC indicates that the privileges are to be granted to all roles, including those that might be created later. PUBLIC can be thought of as an implicitly defined group that always includes all roles. Any particular role will have the sum of privileges granted directly to it, privileges granted to any role it is presently a member of, and privileges granted to PUBLIC. + +When an object is created, it is assigned an owner. The owner is normally the role that executed the creation statement. For most kinds of objects, the initial state is that only the owner (or a superuser) can do anything with the object. To allow other roles to use it, privileges must be granted. (When using AWS managed RDS, you cannot have access to any superuser roles; superuser is reserved for AWS to use to manage the cluster.) + +PostgreSQL grants privileges on some types of objects to PUBLIC by default when the objects are created. No privileges are granted to PUBLIC by default on tables, table columns, sequences, foreign data wrappers, foreign servers, large objects, schemas, or tablespaces. For other types of objects, the default privileges granted to PUBLIC are as follows: CONNECT and TEMPORARY (create temporary tables) privileges for databases; EXECUTE privilege for functions and procedures; and USAGE privilege for languages and data types (including domains). The object owner can, of course, REVOKE both default and expressly granted privileges. (For maximum security, issue the REVOKE in the same transaction that creates the object; then there is no window in which another user can use the object.) Also, these default privilege settings can be overridden using the ALTER DEFAULT PRIVILEGES command. + +The CREATE privilege: +- For databases, allows new schemas and publications to be created within the database, and allows trusted extensions to be installed within the database. +- For schemas, allows new objects to be created within the schema. To rename an existing object, you must own the object and have this privilege for the containing schema. + +For databases and schemas, there are not a lot of other privileges to grant, and all but CREATE are granted by default, so you might as well grant "ALL". For tables etc., the creator has full control. You grant access to other users via explicit grants. This component does not allow fine-grained grants. You have to specify the database, and unless the grant is on the database, you have to specify the schema. For any other object type (table, sequence, function, procedure, routine, foreign_data_wrapper, foreign_server, column), the component applies the grants to all objects of that type in the specified schema. + + ## Requirements @@ -109,5 +126,8 @@ components: ## References * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aurora-postgres-resources) - Cloud Posse's upstream component +* PostgreSQL references (select the correct version of PostgreSQL at the top of the page): + * [GRANT command](https://www.postgresql.org/docs/14/sql-grant.html) + * [Privileges that can be GRANTed](https://www.postgresql.org/docs/14/ddl-priv.html) [](https://cpco.io/component) diff --git a/modules/eks/github-actions-runner/CHANGELOG.md b/modules/eks/github-actions-runner/CHANGELOG.md new file mode 100644 index 000000000..9ff3d2182 --- /dev/null +++ b/modules/eks/github-actions-runner/CHANGELOG.md @@ -0,0 +1,165 @@ +## Initial Release + +This release has been tested and used in production, but testing has not covered +all available features. Please use with caution and report any issues you +encounter. + +### Migration from `actions-runner-controller` + +GitHub has released its own official self-hosted GitHub Actions Runner support, +replacing the `actions-runner-controller` implementation developed by Summerwind. +(See the [announcement from GitHub](https://github.com/actions/actions-runner-controller/discussions/2072).) +Accordingly, this component is a replacement for the [`actions-runner-controller`](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) +component. Although there are different defaults for some of the configuration options, if +you are already using `actions-runner-controller` you should be able to reuse +the GitHub app or PAT and image pull secret you are already using, making +migration relatively straightforward. + +We recommend deploying this component into a separate namespace (or namespaces) +than `actions-runner-controller` and get the new runners sets running before +you remove the old ones. You can then migrate your workflows to use the new +runners sets and have zero downtime. + +Major differences: +- Self-hosted runners, such as those deployed with the `actions-runner-controller` + component, are targeted by a set of labels indicated by a workflow's `runs-on` + array, of which the first must be "self-hosted". Runner Sets, such as are + deployed with this component, are targeted by a single label, which is the + name of the Runner Set. This means that you will need to update your workflows + to target the new Runner Set label. See [here](https://github.com/actions/actions-runner-controller/discussions/2921#discussioncomment-7501051) + for the reasoning behind GitHub's decision to use a single label instead of a set. +- The `actions-runner-controller` component uses the published Helm chart for the + controller, but there is none for the runners, so it includes a custom Helm chart + for them. However, for Runner Sets, GitHub has published 2 charts, one for the controller + and one for the runners (runner sets). This means that this component requires + configuration (e.g. version numbers) of 2 charts, although both should be + kept at the same version. +- The `actions-runner-controller` component has a `resources/values.yaml` file + that provided defaults for the controller Helm chart. This component does not have + files like that by default, but supports a `resources/values-controller.yaml` file + for the "gha-runner-scale-set-controller" chart and a `resources/values-runner.yaml` + file for the "gha-runner-scale-set" chart. +- The default values for the SSM paths for the GitHub auth secret and the imagePullSecret + have changed. Specify the old values explicitly to keep using the same secrets. +- The `actions-runner-controller` component creates an IAM Role (IRSA) for the runners + to use. This component does not create an IRSA, because the chart does not support + using one while in "dind" mode. Use GitHub OIDC authentication inside your workflows instead. +- The Runner Sets deployed by this component use a different autoscaling mechanism, + so most of the `actions-runner-controller` configuration options related to + autoscaling are not applicable. +- For the same reason, this component does not deploy a webhook listener or Ingress and + does not require configuration of a GitHub webhook. +- The `actions-runner-controller` component has an input named `existing_kubernetes_secret_name`. + The equivalent input for this component is `github_kubernetes_secret_name`, + in order to clearly distinguish it from the `image_pull_kubernetes_secret_name` input. + +### Translating configuration from `actions-runner-controller` + +Here is an example configuration for the `github-actions-runner` controller, +with comments indicating where in the `actions-runner-controller` configuration +the corresponding configuration option can be copied from. + +```yaml +components: + terraform: + eks/github-actions-runner: + vars: + # This first set of values you can just copy from here. + # However, if you had customized the standard Helm configuration + # (such things as `cleanup_on_fail`, `atmoic`, or `timeout`), you + # now need to do that per chart under the `charts` input. + enabled: true + name: "gha-runner-controller" + charts: + controller: + # As of the time of the creation of this component, 0.7.0 is the latest version + # of the chart. If you use a newer version, check for breaking changes + # and any updates to this component that may be required. + # Find the latest version at https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set-controller/Chart.yaml#L18 + chart_version: "0.7.0" + runner_sets: + # We expect that the runner set chart will always be at the same version as the controller chart, + # but the charts are still in pre-release so that may change. + # Find the latest version at https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/Chart.yaml#L18 + chart_version: "0.7.0" + controller: + # These inputs from `actions-runner-controller` are now parts of the controller configuration input + kubernetes_namespace: "gha-runner-controller" + create_namespace: true + replicas: 1 # From `actions-runner-controller` file `resources/values.yaml`, value `replicaCount` + # resources from var.resources + + + + # These values can be copied directly from the `actions-runner-controller` configuration + ssm_github_secret_path: "/github_runners/controller_github_app_secret" + github_app_id: "250828" + github_app_installation_id: "30395627" + + + # These values require some converstion from the `actions-runner-controller` configuration + # Set `create_github_kubernetes_secret` to `true` if `existing_kubernetes_secret_name` was not set, `false` otherwise. + create_github_kubernetes_secret: true + # If `existing_kubernetes_secret_name` was set, copy the setting to `github_kubernetes_secret_name` here. + # github_kubernetes_secret_name: + + # To configure imagePullSecrets: + # Set `image_pull_secret_enabled` to the value of `docker_config_json_enabled` in `actions-runner-controller` configuration. + image_pull_secret_enabled: true + # Set `ssm_image_pull_secret_path` to the value of `ssm_docker_config_json_path` in `actions-runner-controller` configuration. + ssm_image_pull_secret_path: "/github_runners/docker/config-json" + + # To configure the runner sets, there is still a map of `runners`, but most + # of the configuration options from `actions-runner-controller` are not applicable. + # Most of the applicable configuration options are the same as for `actions-runner-controller`. + runners: + # The name of the runner set is the key of the map. The name is now the only label + # that is used to target the runner set. + self-hosted-default: + # Namespace is new. The `actions-runner-controller` always deployed the runners to the same namespace as the controller. + # Runner sets support deploying the runners in a namespace other than the controller, + # and it is recommended to do so. If you do not set kubernetes_namespace, the runners will be deployed + # in the same namespace as the controller. + kubernetes_namespace: "gha-runner-private" + # Set create_namespace to false if the namespace has been created by another component. + create_namespace: true + + # `actions-runner-controller` had a `dind_enabled` input that was switch between "kubernetes" and "dind" mode. + # This component has a `mode` input that can be set to "kubernetes" or "dind". + mode: "dind" + + # Where the `actions-runner-controller` configuration had `type` and `scope`, + # the runner set has `github_url`. For organization scope runners, use https://github.com/myorg + # (or, if you are using Enterprise GitHub, your GitHub Enterprise URL). + # For repo runners, use the repo URL, e.g. https://github.com/myorg/myrepo + github_url: https://github.com/cloudposse + + # These configuration options are the same as for `actions-runner-controller` + # group: "default" + # node_selector: + # kubernetes.io/os: "linux" + # kubernetes.io/arch: "arm64" + # tolerations: + # - key: "kubernetes.io/arch" + # operator: "Equal" + # value: "arm64" + # effect: "NoSchedule" + # If min_replicas > 0 and you also have do-not-evict: "true" set + # then the idle/waiting runner will keep Karpenter from deprovisioning the node + # until a job runs and the runner is deleted. So we do not set it by default. + # pod_annotations: + # karpenter.sh/do-not-evict: "true" + min_replicas: 1 + max_replicas: 12 + resources: + limits: + cpu: 1100m + memory: 1024Mi + ephemeral-storage: 5Gi + requests: + cpu: 500m + memory: 256Mi + ephemeral-storage: 1Gi + # The rest of the `actions-runner-controller` configuration is not applicable. + # This includes `labels` as well as anything to do with autoscaling. +``` diff --git a/modules/eks/github-actions-runner/README.md b/modules/eks/github-actions-runner/README.md new file mode 100644 index 000000000..7d84a897b --- /dev/null +++ b/modules/eks/github-actions-runner/README.md @@ -0,0 +1,452 @@ +# Component: `github-actions-runner` + +This component deploys self-hosted GitHub Actions Runners and a [Controller](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller#introduction) +on an EKS cluster, using "[runner scale sets](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#runner-scale-set)". + +This solution is supported by GitHub and supersedes the [actions-runner-controller](https://github.com/actions/actions-runner-controller/blob/master/docs/about-arc.md) developed +by Summerwind and deployed by Cloud Posse's [actions-runner-controller](https://docs.cloudposse.com/components/library/aws/eks/actions-runner-controller/) component. + +### Current limitations + +In the current version of this component, only "dind" (Docker in Docker) mode has been tested. +Support for "kubernetes" mode is provided, but has not been validated. + +Many elements in the Controller chart are not directly configurable by named inputs. +To configure them, you can use the `controller.chart_values` input or create a +`resources/values-controller.yaml` file in the component to supply values. + +Almost all the features of the Runner Scale Set chart are configurable by named inputs. +The exceptions are: +- There is no specific input for specifying an outbound HTTP proxy. +- There is no specific input for supplying a [custom certificate authority (CA) certificate](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#custom-tls-certificates) + to use when connecting to GitHub Enterprise Server. + +You can specify these values by creating a `resources/values-runner.yaml` file +in the component and setting values as shown by the default Helm [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml), +and they will be applied to all runners. + +Currently, this component has some additional limitations. In particular: +- The controller and all runners and listeners share the Image Pull Secrets. +You cannot use different ones for different runners. +- All the runners use the same GitHub secret (app or PAT). Using a GitHub app + is preferred anyway, and the single GitHub app serves the entire organization. +- Only one controller is supported per cluster, though it can have multiple replicas. + +These limitations could be addressed if there is demand. Contact [Cloud Posse Professional Services](https://cloudposse.com/professional-services/) +if you would be interested in sponsoring the development of any of these features. + +### Ephemeral work storage + +The runners are configured to use ephemeral storage for workspaces, but the +details and defaults can be a bit confusing. + +When running in "dind" ("Docker in Docker") mode, the default is to use `emptyDir`, which +means space on the `kubelet` base directory, which is usually the root disk. You +can manage the amount of storage allowed to be used with `ephemeral_storage` requests and limits, +or you can just let it use whatever free space there is on the root disk. + +When running in `kubernetes` mode, the only supported local disk storage is an +ephemeral `PersistentVolumeClaim`, which causes a separate disk to be allocated +for the runner pod. This disk is ephemeral, and will be deleted when the runner +pod is deleted. When combined with the recommended ephemeral runner +configuration, this means that a new disk will be created for each job, and +deleted when the job is complete. That is a lot of overhead and will slow things +down somewhat. + + +The size of the attached PersistentVolume is controlled +by `ephemeral_pvc_storage` (a Kubernetes size string like "1G") and the kind of +storage is controlled by `ephemeral_pvc_storage_class` +(which can be omitted to use the cluster default storage class). + +This mode is also optionally available when using `dind`. To enable it, set +`ephemeral_pvc_storage` to the desired size. Leave `ephemeral_pvc_storage` at +the default value of `null` to use `emptyDir` storage (recommended). + +Beware that using a PVC may significantly increase the startup of the runner. +If you are using a PVC, you may want to keep idle runners available so that +jobs can be started without waiting for a new runner to start. + +## Usage + +**Stack Level**: Regional + +Once the catalog file is created, the file can be imported as follows. + +```yaml +import: + - catalog/eks/github-actions-runner + ... +``` + +The default catalog values `e.g. stacks/catalog/eks/github-actions-runner.yaml` + +```yaml +components: + terraform: + eks/github-actions-runner: + vars: + enabled: true + ssm_region: "us-east-2" + name: "gha-runner-controller" + charts: + controller: + chart_version: "0.7.0" + runner_sets: + chart_version: "0.7.0" + controller: + kubernetes_namespace: "gha-runner-controller" + create_namespace: true + + create_github_kubernetes_secret: true + ssm_github_secret_path: "/github-action-runners/github-auth-secret" + github_app_id: "123456" + github_app_installation_id: "12345678" + runners: + config-default: &runner-default + enabled: false + github_url: https://github.com/cloudposse + # group: "default" + # kubernetes_namespace: "gha-runner-private" + create_namespace: true + # If min_replicas > 0 and you also have do-not-evict: "true" set + # then the idle/waiting runner will keep Karpenter from deprovisioning the node + # until a job runs and the runner is deleted. + # override by setting `pod_annotations: {}` + pod_annotations: + karpenter.sh/do-not-evict: "true" + min_replicas: 0 + max_replicas: 8 + resources: + limits: + cpu: 1100m + memory: 1024Mi + ephemeral-storage: 5Gi + requests: + cpu: 500m + memory: 256Mi + ephemeral-storage: 1Gi + self-hosted-default: + <<: *runner-default + enabled: true + kubernetes_namespace: "gha-runner-private" + # If min_replicas > 0 and you also have do-not-evict: "true" set + # then the idle/waiting runner will keep Karpenter from deprovisioning the node + # until a job runs and the runner is deleted. So we override the default. + pod_annotations: {} + min_replicas: 1 + max_replicas: 12 + resources: + limits: + cpu: 1100m + memory: 1024Mi + ephemeral-storage: 5Gi + requests: + cpu: 500m + memory: 256Mi + ephemeral-storage: 1Gi + self-hosted-large: + <<: *runner-default + enabled: true + resources: + limits: + cpu: 6000m + memory: 7680Mi + ephemeral-storage: 90G + requests: + cpu: 4000m + memory: 7680Mi + ephemeral-storage: 40G + +``` + +## TODO pick up from here + +### Authentication and Secrets + +The GitHub Action Runners need to authenticate to GitHub in order to do such +things as register runners and pickup jobs. You can authenticate using either +a [GitHub App](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-github-app) +or a [Personal Access Token (classic)](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-personal-access-token-classic). +The preferred way to authenticate is by _creating_ and _installing_ a GitHub +App. This is the recommended approach as it allows for much more restricted +access than using a Personal Access Token (classic), and the Action Runners do +not currently support using a fine-grained Personal Access Token. + + +#### Site note about SSM and Regions + +This component supports using AWS SSM to store and retrieve secrets. SSM +parameters are regional, so if you want to deploy to multiple regions +you have 2 choices: + +1. Create the secrets in each region. This is the most robust approach, but + requires you to create the secrets in each region and keep them in sync. +2. Create the secrets in one region and use the `ssm_region` input to specify + the region where they are stored. This is the easiest approach, but does add + some obstacles to managing deployments during a region outage. If the region + where the secrets are stored goes down, there will be no impact on runners in + other regions, but you will not be able to deploy new runners or modify + existing runners until the SSM region is restored or until you set up SSM + parameters in a new region. + +Alternatively, you can create Kubernetes secrets outside of this component +(perhaps using [SOPS](https://github.com/getsops/sops)) and reference them by +name. We describe here how to save the secrets to SSM, but you can save the +secrets wherever and however you want to, as long as you deploy them as +Kubernetes secret the runners can reference. If you store them in SSM, this +component will take care of the rest, but the standard Terraform caveat applies: +any secrets referenced by Terraform will be stored unencrypted in the Terraform +state file. + +#### Creating and Using a GitHub App + +Follow the instructions [here](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-github-app) to create and install a GitHub App +for the runners to use for authentication. + +At the App creation stage, you will be asked to generate a private key. This is +the private key that will be used to authenticate the Action Runner. Download +the file and store the contents in SSM using the following command, adjusting +the profile, region, and file name. The profile should be the `terraform` role in the +account to which you are deploying the runner controller. The region should be +the region where you are deploying the primary runner controller. If you are +deploying runners to multiple regions, they can all reference the same SSM +parameter by using the `ssm_region` input to specify the region where they are +stored. The file name (argument to `cat`) should be the name of the private key +file you downloaded. + +``` +# Adjust profile name and region to suit your environment, use file name you chose for key +AWS_PROFILE=acme-core-gbl-auto-terraform AWS_REGION=us-west-2 chamber write github-action-runners github-auth-secret -- "$(cat APP_NAME.DATE.private-key.pem)" +``` + +You can verify the file was correctly written to SSM by matching the private key fingerprint reported by GitHub with: + +``` +AWS_PROFILE=acme-core-gbl-auto-terraform AWS_REGION=us-west-2 chamber read -q github-action-runners github-auth-secret | openssl rsa -in - -pubout -outform DER | openssl sha256 -binary | openssl base64 +``` + +## TODO pick up from here + +At this stage, record the Application ID and the private key fingerprint in your secrets manager (e.g. 1Password). +You may want to record the private key as well, or you may consider it sufficient to have it in SSM. +You will need the Application ID to configure the runner controller, and want the fingerprint to verify the private key. +(You can see the fingerprint in the GitHub App settings, under "Private keys".) + +Proceed to install the GitHub App in the organization or repository you want to use the runner controller for, +and record the Installation ID (the final numeric part of the URL, as explained in the instructions +linked above) in your secrets manager. You will need the Installation ID to configure the runner controller. + +In your stack configuration, set the following variables, making sure to quote the values so they are +treated as strings, not numbers. + +``` +github_app_id: "12345" +github_app_installation_id: "12345" +``` + +#### OR (obsolete): Creating and Using a Personal Access Token (classic) + +Though not recommended, you can use a Personal Access Token (classic) to +authenticate the runners. To do so, create a PAT (classic) as described in the [GitHub Documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-personal-access-token-classic). +Save this to the value specified by `ssm_github_token_path` using the following command, adjusting the + AWS profile and region as explained above: + +``` +AWS_PROFILE=acme-core-gbl-auto-terraform AWS_REGION=us-west-2 chamber write github-action-runners github-auth-secret -- "" +``` + +### Using Runner Groups + +GitHub supports grouping runners into distinct [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups), which allow you to have different access controls +for different runners. Read the linked documentation about creating and configuring Runner Groups, which you must do +through the GitHub Web UI. If you choose to create Runner Groups, you can assign one or more Runner Sets (from the +`runners` map) to groups (only one group per runner set, but multiple sets can be in the same group) by including +`group: ` in the runner configuration. We recommend including it immediately after `github_url`. + + +### Interaction with Karpenter or other EKS autoscaling solutions + +Kubernetes cluster autoscaling solutions generally expect that a Pod runs a service that can be terminated on one +Node and restarted on another with only a short duration needed to finish processing any in-flight requests. When +the cluster is resized, the cluster autoscaler will do just that. However, GitHub Action Runner Jobs do not fit this +model. If a Pod is terminated in the middle of a job, the job is lost. The likelihood of this happening is increased +by the fact that the Action Runner Controller Autoscaler is expanding and contracting the size of the Runner Pool on +a regular basis, causing the cluster autoscaler to more frequently want to scale up or scale down the EKS cluster, +and, consequently, to move Pods around. + +To handle these kinds of situations, Karpenter respects an annotation on the Pod: + +```yaml +spec: + template: + metadata: + annotations: + karpenter.sh/do-not-evict: "true" +``` + +When you set this annotation on the Pod, Karpenter will not voluntarily evict it. This means that the Pod will stay on the Node +it is on, and the Node it is on will not be considered for deprovisioning (scale down). This is good because it means that the Pod +will not be terminated in the middle of a job. However, it also means that the Node the Pod is on will remain running +until the Pod is terminated, even if the node is underutilized and Karpenter would like to get rid of it. + +Since the Runner Pods terminate at the end of the job, this is not a problem for the Pods actually running jobs. +However, if you have set `minReplicas > 0`, then you have some Pods that are just idling, waiting for jobs to be +assigned to them. These Pods are exactly the kind of Pods you want terminated and moved when the cluster is underutilized. +Therefore, when you set `minReplicas > 0`, you should **NOT** set `karpenter.sh/do-not-evict: "true"` on the Pod. + + +### Updating CRDs + +When updating the chart or application version +of `gha-runner-scale-set-controller`, it is possible you will need to install +new CRDs. Such a requirement should be indicated in +the `gha-runner-scale-set-controller` release notes and may require some +adjustment to this component. + +This component uses `helm` to manage the deployment, and `helm` will not auto-update CRDs. +If new CRDs are needed, follow the instructions in the release notes for the Helm chart +or `gha-runner-scale-set-controller` itself. + + + +### Useful Reference + +- Runner Scale Set Controller's Helm chart [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set-controller/values.yaml) +- Runner Scale Set's Helm chart [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml) + +When reviewing documentation, code, issues, etc. for self-hosted GitHub action runners +or the Actions Runner Controller (ARC), keep in mind that there are 2 implementations +going by that name. The original implementation, which is now deprecated, uses +the `actions.summerwind.dev` API group, and is at times called the Summerwind +or Legacy implementation. It is primarily described by documentation in the +[actions/actions-runner-controller](https://github.com/actions/actions-runner-controller) +GitHub repository itself. + +The new implementation, which is the one this component +uses, uses the `actions.github.com` API group, and is at times called the GitHub +implementation or "Runner Scale Sets" implementation. The new implementation is +described in the official [GitHub documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller). + +Feature requests about the new implementation are officially +directed to the [Actions category of GitHub community discussion](https://github.com/orgs/community/discussions/categories/actions). +However, Q&A and community support is directed to the `actions/actions-runner-controller` +repo's [Discussion section](https://github.com/actions/actions-runner-controller/discussions), +though beware that discussions about the old implementation are mixed in with +discussions about the new implementation. + +Bug reports for the new implementation are still filed under the `actions/actions-runner-controller` +repo's [Issues](https://github.com/actions/actions-runner-controller/issues) tab, +though again, these are mixed in with bug reports for the old implementation. +Look for the `gha-runner-scale-set` label to find issues specific to the new +implementation. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.0, != 2.21.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | +| [aws.ssm](#provider\_aws.ssm) | >= 4.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.0, != 2.21.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [gha\_runner\_controller](#module\_gha\_runner\_controller) | cloudposse/helm-release/aws | 0.10.0 | +| [gha\_runners](#module\_gha\_runners) | cloudposse/helm-release/aws | 0.10.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_namespace.controller](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_namespace.runner](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_secret_v1.controller_image_pull_secret](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret_v1) | resource | +| [kubernetes_secret_v1.controller_ns_github_secret](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret_v1) | resource | +| [kubernetes_secret_v1.github_secret](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret_v1) | resource | +| [kubernetes_secret_v1.image_pull_secret](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret_v1) | resource | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_ssm_parameter.github_token](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.image_pull_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [charts](#input\_charts) | Map of Helm charts to install. Keys are "controller" and "runner\_sets". |
map(object({
chart_version = string
chart = optional(string, null) # defaults according to the key to "gha-runner-scale-set-controller" or "gha-runner-scale-set"
chart_description = optional(string, null) # visible in Helm history
chart_repository = optional(string, "oci://ghcr.io/actions/actions-runner-controller-charts")
wait = optional(bool, true)
atomic = optional(bool, true)
cleanup_on_fail = optional(bool, true)
timeout = optional(number, null)
}))
| n/a | yes | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [controller](#input\_controller) | Configuration for the controller. |
object({
image = optional(object({
repository = optional(string, null)
tag = optional(string, null) # Defaults to the chart appVersion
pull_policy = optional(string, null)
}), null)
replicas = optional(number, 1)
kubernetes_namespace = string
create_namespace = optional(bool, true)
chart_values = optional(any, null)
affinity = optional(map(string), {})
labels = optional(map(string), {})
node_selector = optional(map(string), {})
priority_class_name = optional(string, "")
resources = optional(object({
limits = optional(object({
cpu = optional(string, null)
memory = optional(string, null)
}), null)
requests = optional(object({
cpu = optional(string, null)
memory = optional(string, null)
}), null)
}), null)
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
log_level = optional(string, "info")
log_format = optional(string, "json")
update_strategy = optional(string, "immediate")
})
| n/a | yes | +| [create\_github\_kubernetes\_secret](#input\_create\_github\_kubernetes\_secret) | If `true`, this component will create the Kubernetes Secret that will be used to get
the GitHub App private key or GitHub PAT token, based on the value retrieved
from SSM at the `var.ssm_github_secret_path`. WARNING: This will cause
the secret to be stored in plaintext in the Terraform state.
If `false`, this component will not create a secret and you must create it
(with the name given by `var.github_kubernetes_secret_name`) in every
namespace where you are deploying runners (the controller does not need it). | `bool` | `true` | no | +| [create\_image\_pull\_kubernetes\_secret](#input\_create\_image\_pull\_kubernetes\_secret) | If `true` and `image_pull_secret_enabled` is `true`, this component will create the Kubernetes image pull secret resource,
using the value in SSM at the path specified by `ssm_image_pull_secret_path`.
WARNING: This will cause the secret to be stored in plaintext in the Terraform state.
If `false`, this component will not create a secret and you must create it
(with the name given by `var.github_kubernetes_secret_name`) in every
namespace where you are deploying controllers or runners. | `bool` | `true` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github\_app\_id](#input\_github\_app\_id) | The ID of the GitHub App to use for the runner controller. Leave empty if using a GitHub PAT. | `string` | `null` | no | +| [github\_app\_installation\_id](#input\_github\_app\_installation\_id) | The "Installation ID" of the GitHub App to use for the runner controller. Leave empty if using a GitHub PAT. | `string` | `null` | no | +| [github\_kubernetes\_secret\_name](#input\_github\_kubernetes\_secret\_name) | Name of the Kubernetes Secret that will be used to get the GitHub App private key or GitHub PAT token. | `string` | `"gha-github-secret"` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [image\_pull\_kubernetes\_secret\_name](#input\_image\_pull\_kubernetes\_secret\_name) | Name of the Kubernetes Secret that will be used as the imagePullSecret. | `string` | `"gha-image-pull-secret"` | no | +| [image\_pull\_secret\_enabled](#input\_image\_pull\_secret\_enabled) | Whether to configure the controller and runners with an image pull secret. | `bool` | `false` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region. | `string` | n/a | yes | +| [runners](#input\_runners) | Map of Runner Scale Set configurations, with the key being the name of the runner set.
Please note that the name must be in kebab-case (no underscores).

For example:
hcl
organization-runner = {
# Specify the scope (organization or repository) and the target
# of the runner via the `github_url` input.
# ex: https://github.com/myorg/myrepo or https://github.com/myorg
github_url = https://github.com/myorg
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
min_replicas = 1
max_replicas = 5
}
|
map(object({
# we allow a runner to be disabled because Atmos cannot delete an inherited map object
enabled = optional(bool, true)
github_url = string
group = optional(string, null)
kubernetes_namespace = optional(string, null) # defaults to the controller's namespace
create_namespace = optional(bool, true)
image = optional(string, "ghcr.io/actions/actions-runner:latest") # repo and tag
mode = optional(string, "dind") # Optional. Can be "dind" or "kubernetes".
pod_labels = optional(map(string), {})
pod_annotations = optional(map(string), {})
affinity = optional(map(string), {})
node_selector = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
# tolerationSeconds is not supported, because Terraform requires all objects in a list to have the same keys,
# but tolerationSeconds must be omitted to get the default behavior of "tolerate forever".
# If really needed, could use a default value of 1,000,000,000 (one billion seconds = about 32 years).
})), [])
min_replicas = number
max_replicas = number

# ephemeral_pvc_storage and _class are ignored for "dind" mode but required for "kubernetes" mode
ephemeral_pvc_storage = optional(string, null) # ex: 10Gi
ephemeral_pvc_storage_class = optional(string, null)

kubernetes_mode_service_account_annotations = optional(map(string), {})

resources = optional(object({
limits = optional(object({
cpu = optional(string, null)
memory = optional(string, null)
ephemeral-storage = optional(string, null)
}), null)
requests = optional(object({
cpu = optional(string, null)
memory = optional(string, null)
ephemeral-storage = optional(string, null)
}), null)
}), null)
}))
| `{}` | no | +| [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `"/github-action-runners/github-auth-secret"` | no | +| [ssm\_image\_pull\_secret\_path](#input\_ssm\_image\_pull\_secret\_path) | SSM path to the base64 encoded `dockercfg` image pull secret. | `string` | `"/github-action-runners/image-pull-secrets"` | no | +| [ssm\_region](#input\_ssm\_region) | AWS Region where SSM secrets are stored. Defaults to `var.region`. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [metadata](#output\_metadata) | Block status of the deployed release | +| [runners](#output\_runners) | Human-readable summary of the deployed runners | + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/actions-runner-controller) - Cloud Posse's upstream component +- [alb-controller](https://artifacthub.io/packages/helm/aws/aws-load-balancer-controller) - Helm Chart +- [alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) - AWS Load Balancer Controller +- [actions-runner-controller Webhook Driven Scaling](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/docs/detailed-docs.md#webhook-driven-scaling) +- [actions-runner-controller Chart Values](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/charts/actions-runner-controller/values.yaml) +- [How to set service account for workers spawned in Kubernetes mode](https://github.com/actions/actions-runner-controller/issues/2992#issuecomment-1764855221) + +[](https://cpco.io/component) diff --git a/modules/eks/github-actions-runner/context.tf b/modules/eks/github-actions-runner/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/github-actions-runner/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/github-actions-runner/main.tf b/modules/eks/github-actions-runner/main.tf new file mode 100644 index 000000000..6f4e74b92 --- /dev/null +++ b/modules/eks/github-actions-runner/main.tf @@ -0,0 +1,172 @@ +locals { + enabled = module.this.enabled + enabled_runners = { for k, v in var.runners : k => v if v.enabled && local.enabled } + + # Default chart names + controller_chart_name = "gha-runner-scale-set-controller" + runner_chart_name = "gha-runner-scale-set" + + image_pull_secret_enabled = local.enabled && var.image_pull_secret_enabled + create_image_pull_secret = local.image_pull_secret_enabled && var.create_image_pull_kubernetes_secret + image_pull_secret = one(data.aws_ssm_parameter.image_pull_secret[*].value) + image_pull_secret_name = var.image_pull_kubernetes_secret_name + + controller_namespace = var.controller.kubernetes_namespace + controller_namespace_set = toset([local.controller_namespace]) + runner_namespaces = toset([for v in values(local.enabled_runners) : coalesce(v.kubernetes_namespace, local.controller_namespace)]) + runner_only_namespaces = setsubtract(local.runner_namespaces, local.controller_namespace_set) + + # We have the possibility of several deployments to the same namespace, + # with some deployments configured to create the namespace and others not. + # We choose to create any namespace that is asked to be created, even if + # other deployments to the same namespace do not ask for it to be created. + all_runner_namespaces_to_create = local.enabled ? toset([ + for v in values(local.enabled_runners) : coalesce(v.kubernetes_namespace, local.controller_namespace) if v.create_namespace + ]) : [] + + # Potentially, the configuration calls for the controller's namespace to be created for the runner, + # even if the controller does not specify that its namespace be created. As before, + # we create the namespace if any deployment to the namespace asks for it to be created. + # Here, however, we have to be careful to create the controller's namespace + # using the controller's namespace resource, even if the request came from the runner. + create_controller_namespace = local.enabled && (var.controller.create_namespace || contains(local.all_runner_namespaces_to_create, local.controller_namespace)) + runner_namespaces_to_create = setsubtract(local.all_runner_namespaces_to_create, local.controller_namespace_set) + + # github_secret_namespaces = local.enabled ? local.runner_namespaces : [] + # image_pull_secret_namespaces = setunion(local.controller_namespace, local.runner_namespaces) + +} + +data "aws_ssm_parameter" "image_pull_secret" { + count = local.create_image_pull_secret ? 1 : 0 + + name = var.ssm_image_pull_secret_path + with_decryption = true + provider = aws.ssm +} + +# We want to completely deploy the controller before deploying the runners, +# so we need separate resources for the controller and the runners, or +# else there will be a circular dependency as the runners depend on the controller +# and the controller resources are mixed in with the runners. +resource "kubernetes_namespace" "controller" { + for_each = local.create_controller_namespace ? local.controller_namespace_set : [] + + metadata { + name = each.value + } + + # During destroy, we may need the IAM role preserved in order to run finalizers + # which remove resources. This depends_on ensures that the IAM role is not + # destroyed until after the namespace is destroyed. + depends_on = [module.gha_runner_controller.service_account_role_unique_id] +} + + +resource "kubernetes_secret_v1" "controller_image_pull_secret" { + for_each = local.create_image_pull_secret ? local.controller_namespace_set : [] + + metadata { + name = local.image_pull_secret_name + namespace = each.value + } + + binary_data = { ".dockercfg" = local.image_pull_secret } + + type = "kubernetes.io/dockercfg" + + depends_on = [kubernetes_namespace.controller] +} + +resource "kubernetes_secret_v1" "controller_ns_github_secret" { + for_each = local.create_github_secret && contains(local.runner_namespaces, local.controller_namespace) ? local.controller_namespace_set : [] + + metadata { + name = local.github_secret_name + namespace = each.value + } + + data = local.github_secrets[local.github_app_enabled ? "app" : "pat"] + + depends_on = [kubernetes_namespace.controller] +} + + +module "gha_runner_controller" { + source = "cloudposse/helm-release/aws" + version = "0.10.0" + + chart = coalesce(var.charts["controller"].chart, local.controller_chart_name) + repository = var.charts["controller"].chart_repository + description = var.charts["controller"].chart_description + chart_version = var.charts["controller"].chart_version + wait = var.charts["controller"].wait + atomic = var.charts["controller"].atomic + cleanup_on_fail = var.charts["controller"].cleanup_on_fail + timeout = var.charts["controller"].timeout + + # We need the module to wait for the namespace to be created before creating + # resources in the namespace, but we need it to create the IAM role first, + # so we cannot directly depend on the namespace resources, because that + # would create a circular dependency. So instead we make the kubernetes + # namespace depend on the resource, while the service_account_namespace + # (which is used to create the IAM role) does not. + kubernetes_namespace = try(kubernetes_namespace.controller[local.controller_namespace].metadata[0].name, local.controller_namespace) + create_namespace_with_kubernetes = false + + eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer + + service_account_name = module.this.name + service_account_namespace = local.controller_namespace + + iam_role_enabled = false + + values = compact([ + # hardcoded values + try(file("${path.module}/resources/values-controller.yaml"), null), + # standard k8s object settings + yamlencode({ + fullnameOverride = module.this.name, + serviceAccount = { + name = module.this.name + }, + affinity = var.controller.affinity, + labels = var.controller.labels, + nodeSelector = var.controller.node_selector, + priorityClassName = var.controller.priority_class_name, + replicaCount = var.controller.replicas, + tolerations = var.controller.tolerations, + flags = { + logLevel = var.controller.log_level + logFormat = var.controller.log_format + updateStrategy = var.controller.update_strategy + } + }), + # filter out null values + var.controller.resources == null ? null : yamlencode({ + resources = merge( + try(var.controller.resources.requests, null) == null ? {} : { requests = { for k, v in var.controller.resources.requests : k => v if v != null } }, + try(var.controller.resources.limits, null) == null ? {} : { limits = { for k, v in var.controller.resources.limits : k => v if v != null } }, + ) + }), + var.controller.image == null ? null : yamlencode(merge( + try(var.controller.image.repository, null) == null ? {} : { repository = var.controller.image.repository }, + try(var.controller.image.tag, null) == null ? {} : { tag = var.controller.image.tag }, + try(var.controller.image.pull_policy, null) == null ? {} : { pullPolicy = var.controller.image.pull_policy }, + )), + local.image_pull_secret_enabled ? yamlencode({ + # We need to wait until the secret is created before creating the controller, + # but we cannot explicitly make the whole module depend on the secret, because + # the secret depends on the namespace, and the namespace depends on the IAM role created by the module, + # even if no IAM role is created (because Terraform uses static dependencies). + imagePullSecrets = [{ name = try(kubernetes_secret_v1.controller_image_pull_secret[local.controller_namespace].metadata[0].name, var.image_pull_kubernetes_secret_name) }] + }) : null, + # additional values + yamlencode(var.controller.chart_values) + ]) + + context = module.this.context + + # Cannot depend on the namespace directly, because that would create a circular dependency (see above) + # depends_on = [kubernetes_namespace.default] +} diff --git a/modules/eks/github-actions-runner/outputs.tf b/modules/eks/github-actions-runner/outputs.tf new file mode 100644 index 000000000..22f614166 --- /dev/null +++ b/modules/eks/github-actions-runner/outputs.tf @@ -0,0 +1,25 @@ +output "metadata" { + value = module.gha_runner_controller.metadata + description = "Block status of the deployed release" +} + +output "runners" { + value = { for k, v in local.enabled_runners : k => merge({ + "1) Kubernetes namespace" = coalesce(v.kubernetes_namespace, local.controller_namespace) + "2) Runner Group" = v.group + "3) Min Runners" = v.min_replicas + "4) Max Runners" = v.max_replicas + }, + length(v.node_selector) > 0 ? { + "?) Node Selector" = v.node_selector + } : {}, + length(v.tolerations) > 0 ? { + "?) Tolerations" = v.tolerations + } : {}, + length(v.affinity) > 0 ? { + "?) Affinity" = v.affinity + } : {}, + ) + } + description = "Human-readable summary of the deployed runners" +} diff --git a/modules/eks/github-actions-runner/provider-helm.tf b/modules/eks/github-actions-runner/provider-helm.tf new file mode 100644 index 000000000..64459d4f4 --- /dev/null +++ b/modules/eks/github-actions-runner/provider-helm.tf @@ -0,0 +1,166 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/github-actions-runner/provider-ssm.tf b/modules/eks/github-actions-runner/provider-ssm.tf new file mode 100644 index 000000000..04e8b1d65 --- /dev/null +++ b/modules/eks/github-actions-runner/provider-ssm.tf @@ -0,0 +1,15 @@ +provider "aws" { + region = coalesce(var.ssm_region, var.region) + alias = "ssm" + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} diff --git a/modules/eks/github-actions-runner/providers.tf b/modules/eks/github-actions-runner/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/eks/github-actions-runner/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/eks/github-actions-runner/remote-state.tf b/modules/eks/github-actions-runner/remote-state.tf new file mode 100644 index 000000000..c1ec8226d --- /dev/null +++ b/modules/eks/github-actions-runner/remote-state.tf @@ -0,0 +1,8 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + context = module.this.context +} diff --git a/modules/eks/github-actions-runner/runners.tf b/modules/eks/github-actions-runner/runners.tf new file mode 100644 index 000000000..5e21cf195 --- /dev/null +++ b/modules/eks/github-actions-runner/runners.tf @@ -0,0 +1,185 @@ +locals { + github_app_enabled = var.github_app_id != null && var.github_app_installation_id != null + create_github_secret = local.enabled && var.create_github_kubernetes_secret + github_secret_name = var.github_kubernetes_secret_name + + github_secrets = { + app = { + github_app_id = var.github_app_id + github_app_installation_id = var.github_app_installation_id + github_app_private_key = one(data.aws_ssm_parameter.github_token[*].value) + } + pat = { + github_token = one(data.aws_ssm_parameter.github_token[*].value) + } + } +} + +data "aws_ssm_parameter" "github_token" { + count = local.create_github_secret ? 1 : 0 + + name = var.ssm_github_secret_path + with_decryption = true + provider = aws.ssm +} + +resource "kubernetes_namespace" "runner" { + for_each = local.runner_namespaces_to_create + + metadata { + name = each.value + } + + # During destroy, we may need the IAM role preserved in order to run finalizers + # which remove resources. This depends_on ensures that the IAM role is not + # destroyed until after the namespace is destroyed. + depends_on = [module.gha_runners.service_account_role_unique_id] +} + +resource "kubernetes_secret_v1" "github_secret" { + for_each = local.create_github_secret ? local.runner_only_namespaces : [] + + metadata { + name = local.github_secret_name + namespace = each.value + } + + data = local.github_secrets[local.github_app_enabled ? "app" : "pat"] + + depends_on = [kubernetes_namespace.runner] +} + +resource "kubernetes_secret_v1" "image_pull_secret" { + for_each = local.create_image_pull_secret ? local.runner_only_namespaces : [] + + metadata { + name = local.image_pull_secret_name + namespace = each.value + } + + binary_data = { ".dockercfg" = local.image_pull_secret } + + type = "kubernetes.io/dockercfg" + + depends_on = [kubernetes_namespace.runner] +} + +module "gha_runners" { + for_each = local.enabled ? local.enabled_runners : {} + + source = "cloudposse/helm-release/aws" + version = "0.10.0" + + name = each.key + chart = coalesce(var.charts["runner_sets"].chart, local.runner_chart_name) + repository = var.charts["runner_sets"].chart_repository + description = var.charts["runner_sets"].chart_description + chart_version = var.charts["runner_sets"].chart_version + wait = var.charts["runner_sets"].wait + atomic = var.charts["runner_sets"].atomic + cleanup_on_fail = var.charts["runner_sets"].cleanup_on_fail + timeout = var.charts["runner_sets"].timeout + + kubernetes_namespace = coalesce(each.value.kubernetes_namespace, local.controller_namespace) + create_namespace = false # will be created above to manage duplicate namespaces + + eks_cluster_oidc_issuer_url = module.eks.outputs.eks_cluster_identity_oidc_issuer + + iam_role_enabled = false + + values = compact([ + # hardcoded values + try(file("${path.module}/resources/values-runner.yaml"), null), + yamlencode({ + githubConfigUrl = each.value.github_url + maxRunners = each.value.max_replicas + minRunners = each.value.min_replicas + runnerGroup = each.value.group + + # Create an explicit dependency on the secret to be sure it is created first. + githubConfigSecret = coalesce(each.value.kubernetes_namespace, local.controller_namespace) == local.controller_namespace ? ( + try(kubernetes_secret_v1.controller_ns_github_secret[local.controller_namespace].metadata[0].name, local.github_secret_name) + ) : ( + try(kubernetes_secret_v1.github_secret[each.value.kubernetes_namespace].metadata[0].name, local.github_secret_name) + ) + + containerMode = { + type = each.value.mode + kubernetesModeWorkVolumeClaim = { + accessModes = ["ReadWriteOnce"] + storageClassName = each.value.ephemeral_pvc_storage_class + resources = { + requests = { + storage = each.value.ephemeral_pvc_storage + } + } + } + kubernetesModeServiceAccount = { + annotations = each.value.kubernetes_mode_service_account_annotations + } + } + template = { + metadata = { + annotations = each.value.pod_annotations + labels = each.value.pod_labels + } + spec = merge( + local.image_pull_secret_enabled ? { + # We want to wait until the secret is created before creating the runner, + # but the secret might be the `controller_image_pull_secret`. That is O.K. + # because we separately depend on the controller, which depends on the secret. + imagePullSecrets = [{ name = try(kubernetes_secret_v1.image_pull_secret[each.value.kubernetes_namespace].metadata[0].name, var.image_pull_kubernetes_secret_name) }] + } : {}, + try(length(each.value.ephemeral_pvc_storage), 0) > 0 ? { + volumes = [{ + name = "work" + ephemeral = { + volumeClaimTemplate = { + spec = merge( + try(length(each.value.ephemeral_pvc_storage_class), 0) > 0 ? { + storageClassName = each.value.ephemeral_pvc_storage_class + } : {}, + { + accessModes = ["ReadWriteOnce"] + resources = { + requests = { + storage = each.value.ephemeral_pvc_storage + } + } + }) + } + } + }] + } : {}, + { + affinity = each.value.affinity + nodeSelector = each.value.node_selector + tolerations = each.value.tolerations + containers = [merge({ + name = "runner" + image = each.value.image + # command from https://github.com/actions/actions-runner-controller/blob/0bfa57ac504dfc818128f7185fc82830cbdb83f1/charts/gha-runner-scale-set/values.yaml#L193 + command = ["/home/runner/run.sh"] + }, + each.value.resources == null ? {} : { + resources = merge( + try(each.value.resources.requests, null) == null ? {} : { requests = { for k, v in each.value.resources.requests : k => v if v != null } }, + try(each.value.resources.limits, null) == null ? {} : { limits = { for k, v in each.value.resources.limits : k => v if v != null } }, + ) + }, + )] + } + ) + } + }), + local.image_pull_secret_enabled ? yamlencode({ + listenerTemplate = { + spec = { + imagePullSecrets = [{ name = try(kubernetes_secret_v1.image_pull_secret[each.value.kubernetes_namespace].metadata[0].name, var.image_pull_kubernetes_secret_name) }] + containers = [] + } } }) : null + ]) + + # Cannot depend on the namespace directly, because that would create a circular dependency (see above). + depends_on = [module.gha_runner_controller, kubernetes_secret_v1.controller_ns_github_secret] +} diff --git a/modules/eks/github-actions-runner/variables.tf b/modules/eks/github-actions-runner/variables.tf new file mode 100644 index 000000000..ee29149e3 --- /dev/null +++ b/modules/eks/github-actions-runner/variables.tf @@ -0,0 +1,223 @@ +variable "region" { + description = "AWS Region." + type = string +} + +variable "ssm_region" { + description = "AWS Region where SSM secrets are stored. Defaults to `var.region`." + type = string + default = null +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +######## Helm Chart configurations + +variable "charts" { + description = "Map of Helm charts to install. Keys are \"controller\" and \"runner_sets\"." + type = map(object({ + chart_version = string + chart = optional(string, null) # defaults according to the key to "gha-runner-scale-set-controller" or "gha-runner-scale-set" + chart_description = optional(string, null) # visible in Helm history + chart_repository = optional(string, "oci://ghcr.io/actions/actions-runner-controller-charts") + wait = optional(bool, true) + atomic = optional(bool, true) + cleanup_on_fail = optional(bool, true) + timeout = optional(number, null) + })) + validation { + condition = length(keys(var.charts)) == 2 && contains(keys(var.charts), "controller") && contains(keys(var.charts), "runner_sets") + error_message = "Must have exactly two charts: \"controller\" and \"runner_sets\"." + } +} + +######## ImagePullSecret settings + +variable "image_pull_secret_enabled" { + type = bool + description = "Whether to configure the controller and runners with an image pull secret." + default = false +} + +variable "image_pull_kubernetes_secret_name" { + type = string + description = "Name of the Kubernetes Secret that will be used as the imagePullSecret." + default = "gha-image-pull-secret" + nullable = false +} + +variable "create_image_pull_kubernetes_secret" { + type = bool + description = <<-EOT + If `true` and `image_pull_secret_enabled` is `true`, this component will create the Kubernetes image pull secret resource, + using the value in SSM at the path specified by `ssm_image_pull_secret_path`. + WARNING: This will cause the secret to be stored in plaintext in the Terraform state. + If `false`, this component will not create a secret and you must create it + (with the name given by `var.github_kubernetes_secret_name`) in every + namespace where you are deploying controllers or runners. + EOT + default = true + nullable = false +} + +variable "ssm_image_pull_secret_path" { + type = string + description = "SSM path to the base64 encoded `dockercfg` image pull secret." + default = "/github-action-runners/image-pull-secrets" + nullable = false +} + +######## Controller-specific settings + +variable "controller" { + type = object({ + image = optional(object({ + repository = optional(string, null) + tag = optional(string, null) # Defaults to the chart appVersion + pull_policy = optional(string, null) + }), null) + replicas = optional(number, 1) + kubernetes_namespace = string + create_namespace = optional(bool, true) + chart_values = optional(any, null) + affinity = optional(map(string), {}) + labels = optional(map(string), {}) + node_selector = optional(map(string), {}) + priority_class_name = optional(string, "") + resources = optional(object({ + limits = optional(object({ + cpu = optional(string, null) + memory = optional(string, null) + }), null) + requests = optional(object({ + cpu = optional(string, null) + memory = optional(string, null) + }), null) + }), null) + tolerations = optional(list(object({ + key = string + operator = string + value = optional(string, null) + effect = string + })), []) + log_level = optional(string, "info") + log_format = optional(string, "json") + update_strategy = optional(string, "immediate") + }) + description = "Configuration for the controller." +} + + +######## Runner-specific settings + +variable "github_app_id" { + type = string + description = "The ID of the GitHub App to use for the runner controller. Leave empty if using a GitHub PAT." + default = null +} + +variable "github_app_installation_id" { + type = string + description = "The \"Installation ID\" of the GitHub App to use for the runner controller. Leave empty if using a GitHub PAT." + default = null +} + +variable "ssm_github_secret_path" { + type = string + description = "The path in SSM to the GitHub app private key file contents or GitHub PAT token." + default = "/github-action-runners/github-auth-secret" + nullable = false +} + +variable "create_github_kubernetes_secret" { + type = bool + description = <<-EOT + If `true`, this component will create the Kubernetes Secret that will be used to get + the GitHub App private key or GitHub PAT token, based on the value retrieved + from SSM at the `var.ssm_github_secret_path`. WARNING: This will cause + the secret to be stored in plaintext in the Terraform state. + If `false`, this component will not create a secret and you must create it + (with the name given by `var.github_kubernetes_secret_name`) in every + namespace where you are deploying runners (the controller does not need it). + EOT + default = true +} + +variable "github_kubernetes_secret_name" { + type = string + description = "Name of the Kubernetes Secret that will be used to get the GitHub App private key or GitHub PAT token." + default = "gha-github-secret" + nullable = false +} + + +variable "runners" { + description = <<-EOT + Map of Runner Scale Set configurations, with the key being the name of the runner set. + Please note that the name must be in kebab-case (no underscores). + + For example: + + ```hcl + organization-runner = { + # Specify the scope (organization or repository) and the target + # of the runner via the `github_url` input. + # ex: https://github.com/myorg/myrepo or https://github.com/myorg + github_url = https://github.com/myorg + group = "core-automation" # Optional. Assigns the runners to a runner group, for access control. + min_replicas = 1 + max_replicas = 5 + } + ``` + EOT + + type = map(object({ + # we allow a runner to be disabled because Atmos cannot delete an inherited map object + enabled = optional(bool, true) + github_url = string + group = optional(string, null) + kubernetes_namespace = optional(string, null) # defaults to the controller's namespace + create_namespace = optional(bool, true) + image = optional(string, "ghcr.io/actions/actions-runner:latest") # repo and tag + mode = optional(string, "dind") # Optional. Can be "dind" or "kubernetes". + pod_labels = optional(map(string), {}) + pod_annotations = optional(map(string), {}) + affinity = optional(map(string), {}) + node_selector = optional(map(string), {}) + tolerations = optional(list(object({ + key = string + operator = string + value = optional(string, null) + effect = string + # tolerationSeconds is not supported, because Terraform requires all objects in a list to have the same keys, + # but tolerationSeconds must be omitted to get the default behavior of "tolerate forever". + # If really needed, could use a default value of 1,000,000,000 (one billion seconds = about 32 years). + })), []) + min_replicas = number + max_replicas = number + + # ephemeral_pvc_storage and _class are ignored for "dind" mode but required for "kubernetes" mode + ephemeral_pvc_storage = optional(string, null) # ex: 10Gi + ephemeral_pvc_storage_class = optional(string, null) + + kubernetes_mode_service_account_annotations = optional(map(string), {}) + + resources = optional(object({ + limits = optional(object({ + cpu = optional(string, null) + memory = optional(string, null) + ephemeral-storage = optional(string, null) + }), null) + requests = optional(object({ + cpu = optional(string, null) + memory = optional(string, null) + ephemeral-storage = optional(string, null) + }), null) + }), null) + })) + default = {} +} diff --git a/modules/eks/github-actions-runner/versions.tf b/modules/eks/github-actions-runner/versions.tf new file mode 100644 index 000000000..f4e52c7b2 --- /dev/null +++ b/modules/eks/github-actions-runner/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.0, != 2.21.0" + } + } +} From af8a534e03c9b21568d62d46c4f7c1d6b58adb33 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 15 Dec 2023 09:56:41 -0800 Subject: [PATCH 331/501] chore: upstream `aws-backup` (#932) --- modules/aws-backup/README.md | 4 ++++ modules/aws-backup/main.tf | 8 +++++++- modules/aws-backup/outputs.tf | 5 +++++ modules/aws-backup/remote-state.tf | 11 +++++++++++ modules/aws-backup/variables.tf | 12 ++++++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 modules/aws-backup/remote-state.tf diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index 07d82c728..5e21b670c 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -200,6 +200,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [backup](#module\_backup) | cloudposse/backup/aws | 0.14.0 | +| [copy\_destination\_vault](#module\_copy\_destination\_vault) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -223,6 +224,8 @@ No resources. | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [destination\_vault\_arn](#input\_destination\_vault\_arn) | An Amazon Resource Name (ARN) that uniquely identifies the destination backup vault for the copied backup | `string` | `null` | no | +| [destination\_vault\_component\_name](#input\_destination\_vault\_component\_name) | The name of the component to be used to look up the destination vault | `string` | `"aws-backup/common"` | no | +| [destination\_vault\_region](#input\_destination\_vault\_region) | The short region of the destination backup vault | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [iam\_role\_enabled](#input\_iam\_role\_enabled) | Whether or not to create a new IAM Role and Policy Attachment | `bool` | `true` | no | @@ -255,6 +258,7 @@ No resources. | [backup\_selection\_id](#output\_backup\_selection\_id) | Backup Selection ID | | [backup\_vault\_arn](#output\_backup\_vault\_arn) | Backup Vault ARN | | [backup\_vault\_id](#output\_backup\_vault\_id) | Backup Vault ID | +| [copy\_destination\_backup\_vault\_arn](#output\_copy\_destination\_backup\_vault\_arn) | ARN of the destination Backup Vault copy | diff --git a/modules/aws-backup/main.tf b/modules/aws-backup/main.tf index e44cde2ac..bb371da65 100644 --- a/modules/aws-backup/main.tf +++ b/modules/aws-backup/main.tf @@ -1,3 +1,9 @@ +locals { + copy_action_enabled = module.this.enabled && (var.destination_vault_arn != null || (var.destination_vault_component_name != null && var.destination_vault_region != null)) + copy_destination_arn_selection = var.destination_vault_arn != null ? var.destination_vault_arn : try(module.copy_destination_vault[0].outputs.backup_vault_arn, null) + copy_destination_arn = local.copy_action_enabled ? local.copy_destination_arn_selection : null +} + module "backup" { source = "cloudposse/backup/aws" version = "0.14.0" @@ -18,7 +24,7 @@ module "backup" { kms_key_arn = var.kms_key_arn # Copy config to new region - destination_vault_arn = var.destination_vault_arn + destination_vault_arn = local.copy_destination_arn copy_action_cold_storage_after = var.copy_action_cold_storage_after copy_action_delete_after = var.copy_action_delete_after diff --git a/modules/aws-backup/outputs.tf b/modules/aws-backup/outputs.tf index 69fa779e5..2ac27e34a 100644 --- a/modules/aws-backup/outputs.tf +++ b/modules/aws-backup/outputs.tf @@ -22,3 +22,8 @@ output "backup_selection_id" { value = module.backup.backup_selection_id description = "Backup Selection ID" } + +output "copy_destination_backup_vault_arn" { + value = local.copy_destination_arn + description = "ARN of the destination Backup Vault copy" +} diff --git a/modules/aws-backup/remote-state.tf b/modules/aws-backup/remote-state.tf new file mode 100644 index 000000000..c4661026e --- /dev/null +++ b/modules/aws-backup/remote-state.tf @@ -0,0 +1,11 @@ +module "copy_destination_vault" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.copy_action_enabled ? 1 : 0 + + component = var.destination_vault_component_name + environment = var.destination_vault_region + + context = module.this.context +} diff --git a/modules/aws-backup/variables.tf b/modules/aws-backup/variables.tf index 1ec30ed17..c43515f5a 100644 --- a/modules/aws-backup/variables.tf +++ b/modules/aws-backup/variables.tf @@ -45,6 +45,18 @@ variable "destination_vault_arn" { default = null } +variable "destination_vault_component_name" { + type = string + description = "The name of the component to be used to look up the destination vault" + default = "aws-backup/common" +} + +variable "destination_vault_region" { + type = string + description = "The short region of the destination backup vault" + default = null +} + variable "copy_action_cold_storage_after" { type = number description = "For copy operation, specifies the number of days after creation that a recovery point is moved to cold storage" From 3ea68c4835ada377d70060a04a209f4092d80d08 Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 19 Dec 2023 19:08:42 -0800 Subject: [PATCH 332/501] [eks/github-actions-runner] Explain about tools missing from runners (#937) --- .../eks/github-actions-runner/CHANGELOG.md | 11 +++++++ modules/eks/github-actions-runner/README.md | 31 ++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/modules/eks/github-actions-runner/CHANGELOG.md b/modules/eks/github-actions-runner/CHANGELOG.md index 9ff3d2182..bb7b5e33b 100644 --- a/modules/eks/github-actions-runner/CHANGELOG.md +++ b/modules/eks/github-actions-runner/CHANGELOG.md @@ -21,6 +21,17 @@ you remove the old ones. You can then migrate your workflows to use the new runners sets and have zero downtime. Major differences: +- The official GitHub runners deployed are different from the GitHub hosted + runners and the Summerwind self-hosted runners in that [they have very few tools installed](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image). You will need to + install any tools you need in your workflows, either as part of your workflow + (recommended) or by maintaining a [custom runner image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image), or by running + such steps in a [separate container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) + that has the tools pre-installed. Many tools have publicly available actions + to install them, such as `actions/setup-node` to install NodeJS or `dcarbone/install-jq-action` + to install `jq`. You can also install packages using `awalsh128/cache-apt-pkgs-action`, + which has the advantage of being able to skip the installation if the package + is already installed, so you can more efficiently run the same workflow on + GitHub hosted as well as self-hosted runners. - Self-hosted runners, such as those deployed with the `actions-runner-controller` component, are targeted by a set of labels indicated by a workflow's `runs-on` array, of which the first must be "self-hosted". Runner Sets, such as are diff --git a/modules/eks/github-actions-runner/README.md b/modules/eks/github-actions-runner/README.md index 7d84a897b..037a2fd4c 100644 --- a/modules/eks/github-actions-runner/README.md +++ b/modules/eks/github-actions-runner/README.md @@ -8,6 +8,32 @@ by Summerwind and deployed by Cloud Posse's [actions-runner-controller](https:// ### Current limitations +The runner image used by Runner Sets contains [no more packages than are necessary](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image) +to run the runner. This is in contrast to the Summerwind implementation, which +contains some commonly needed packages like `build-essential`, `curl`, `wget`, +`git`, and `jq`, and the GitHub hosted images which contain a robust set of tools. +(This is a limitation of the official Runner Sets implementation, not this +component per se.) You will need to +install any tools you need in your workflows, either as part of your workflow +(recommended), by maintaining a [custom runner image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image), or by running +such steps in a [separate container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) +that has the tools pre-installed. Many tools have publicly available actions +to install them, such as `actions/setup-node` to install NodeJS or `dcarbone/install-jq-action` +to install `jq`. You can also install packages using `awalsh128/cache-apt-pkgs-action`, +which has the advantage of being able to skip the installation if the package +is already installed, so you can more efficiently run the same workflow on +GitHub hosted as well as self-hosted runners. + +:::info +There are (as of this writing) open feature requests to add some +commonly needed packages to the official Runner Sets runner image. You can +upvote these +requests [here](https://github.com/actions/actions-runner-controller/discussions/3168) +and [here](https://github.com/orgs/community/discussions/80868) to help get them +implemented. + +::: + In the current version of this component, only "dind" (Docker in Docker) mode has been tested. Support for "kubernetes" mode is provided, but has not been validated. @@ -160,8 +186,6 @@ components: ``` -## TODO pick up from here - ### Authentication and Secrets The GitHub Action Runners need to authenticate to GitHub in order to do such @@ -226,8 +250,6 @@ You can verify the file was correctly written to SSM by matching the private key AWS_PROFILE=acme-core-gbl-auto-terraform AWS_REGION=us-west-2 chamber read -q github-action-runners github-auth-secret | openssl rsa -in - -pubout -outform DER | openssl sha256 -binary | openssl base64 ``` -## TODO pick up from here - At this stage, record the Application ID and the private key fingerprint in your secrets manager (e.g. 1Password). You may want to record the private key as well, or you may consider it sufficient to have it in SSM. You will need the Application ID to configure the runner controller, and want the fingerprint to verify the private key. @@ -314,6 +336,7 @@ or `gha-runner-scale-set-controller` itself. - Runner Scale Set Controller's Helm chart [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set-controller/values.yaml) - Runner Scale Set's Helm chart [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml) +- Runner Scale Set's [Docker image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image) and [how to create your own](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image) When reviewing documentation, code, issues, etc. for self-hosted GitHub action runners or the Actions Runner Controller (ARC), keep in mind that there are 2 implementations From 867a2dfabdb62fcff0090c366f74ec384d092b82 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 20 Dec 2023 15:33:17 -0800 Subject: [PATCH 333/501] `github-oidc-role` component (#936) Co-authored-by: Dan Miller Co-authored-by: Nuru Co-authored-by: cloudpossebot --- modules/github-oidc-role/README.md | 239 +++++++++++++++ .../github-oidc-role/additional-policy-map.tf | 11 + modules/github-oidc-role/context.tf | 279 ++++++++++++++++++ modules/github-oidc-role/main.tf | 50 ++++ modules/github-oidc-role/outputs.tf | 9 + modules/github-oidc-role/policy_gitops.tf | 113 +++++++ .../github-oidc-role/policy_lambda-cicd.tf | 113 +++++++ modules/github-oidc-role/providers.tf | 19 ++ modules/github-oidc-role/variables.tf | 56 ++++ modules/github-oidc-role/versions.tf | 10 + 10 files changed, 899 insertions(+) create mode 100644 modules/github-oidc-role/README.md create mode 100644 modules/github-oidc-role/additional-policy-map.tf create mode 100644 modules/github-oidc-role/context.tf create mode 100644 modules/github-oidc-role/main.tf create mode 100644 modules/github-oidc-role/outputs.tf create mode 100644 modules/github-oidc-role/policy_gitops.tf create mode 100644 modules/github-oidc-role/policy_lambda-cicd.tf create mode 100644 modules/github-oidc-role/providers.tf create mode 100644 modules/github-oidc-role/variables.tf create mode 100644 modules/github-oidc-role/versions.tf diff --git a/modules/github-oidc-role/README.md b/modules/github-oidc-role/README.md new file mode 100644 index 000000000..886f880ca --- /dev/null +++ b/modules/github-oidc-role/README.md @@ -0,0 +1,239 @@ +# Component: `github-oidc-role` + +This component is responsible for creating IAM roles for GitHub Actions to assume. + +## Usage + +**Stack Level**: Global + +Here's an example snippet for how to use this component. + +```yaml +# stacks/catalog/github-oidc-role/defaults.yaml +components: + terraform: + github-oidc-role/defaults: + metadata: + type: abstract + vars: + enabled: true + name: gha-iam + # Note: inherited lists are not merged, they are replaced + github_actions_allowed_repos: + - MyOrg/* ## allow all repos in MyOrg +``` + +Example using for gitops (predefined policy): + +```yaml +# stacks/catalog/github-oidc-role/gitops.yaml +import: + - catalog/github-oidc-role/defaults + +components: + terraform: + github-oidc-role/gitops: + metadata: + component: github-oidc-role + inherits: + - github-oidc-role/defaults + vars: + enabled: true + # Note: inherited lists are not merged, they are replaced + github_actions_allowed_repos: + - "MyOrg/infrastructure" + attributes: [ "gitops" ] + iam_policies: + - gitops + gitops_policy_configuration: + s3_bucket_component_name: gitops/s3-bucket + dynamodb_component_name: gitops/dynamodb +``` + +Example using for lambda-cicd (predefined policy): + +```yaml +# stacks/catalog/github-oidc-role/lambda-cicd.yaml +import: + - catalog/github-oidc-role/defaults + +components: + terraform: + github-oidc-role/lambda-cicd: + metadata: + component: github-oidc-role + inherits: + - github-oidc-role/defaults + vars: + enabled: true + github_actions_allowed_repos: + - MyOrg/example-app-on-lambda-with-gha + attributes: [ "lambda-cicd" ] + iam_policies: + - lambda-cicd + lambda_cicd_policy_configuration: + enable_ssm_access: true + enable_s3_access: true + s3_bucket_component_name: s3-bucket/github-action-artifacts + s3_bucket_environment_name: gbl + s3_bucket_stage_name: artifacts + s3_bucket_tenant_name: core +``` + +Example Using an AWS Managed policy and a custom inline policy: + +```yaml +# stacks/catalog/github-oidc-role/custom.yaml +import: + - catalog/github-oidc-role/defaults + +components: + terraform: + github-oidc-role/custom: + metadata: + component: github-oidc-role + inherits: + - github-oidc-role/defaults + vars: + enabled: true + github_actions_allowed_repos: + - MyOrg/example-app-on-lambda-with-gha + attributes: [ "custom" ] + iam_policies: + - arn:aws:iam::aws:policy/AdministratorAccess + iam_policy: + - version: "2012-10-17" + statements: + - effect: "Allow" + actions: + - "ec2:*" + resources: + - "*" +``` + +### Adding Custom Policies + +There are two methods for adding custom policies to the IAM role. + +1. Through the `iam_policy` input which you can use to add inline policies to the IAM role. +2. By defining policies in Terraform and then attaching them to roles by name. + +#### Defining Custom Policies in Terraform + +1. Give the policy a unique name, e.g. `docker-publish`. We will use `NAME` as a placeholder for the name in the instructions below. +2. Create a file in the component directory (i.e. `github-oidc-role`) with the name `policy_NAME.tf`. +3. In that file, conditionally (based on need) create a policy document as follows: + + ```hcl + locals { + NAME_policy_enabled = contains(var.iam_policies, "NAME") + NAME_policy = local.NAME_policy_enabled ? one(data.aws_iam_policy_document.NAME.*.json) : null + } + + data "aws_iam_policy_document" "NAME" { + count = local.NAME_policy_enabled ? 1 : 0 + + # Define the policy here + } + ``` + + Note that you can also add input variables and outputs to this file if desired. Just make sure that all inputs are optional. +4. Create a file named `additional-policy-map_override.tf` in the component directory (if it does not already exist). + This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), meaning its + contents will be merged with the main terraform file, and any locals defined in it will override locals defined in other files. + Having your code in this separate override file makes it possible for the component to provide a placeholder local variable + so that it works without customization, while allowing you to customize the component and still update it without losing your customizations. +5. In that file, redefine the local variable `overridable_additional_custom_policy_map` map as follows: + + ```hcl + locals { + overridable_additional_custom_policy_map = { + "NAME" = local.NAME_policy + } + } + ``` + + If you have multiple custom policies, using just this one file, add each policy document to the map in the form `NAME = local.NAME_policy`. +6. With that done, you can now attach that policy by adding the name to the `iam_policies` list. For example: + + ```yaml + iam_policies: + - "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" + - "NAME" + ``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [dynamodb](#module\_dynamodb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | +| [iam\_policy](#module\_iam\_policy) | cloudposse/iam-policy/aws | 2.0.1 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [s3\_artifacts\_bucket](#module\_s3\_artifacts\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [s3\_bucket](#module\_s3\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_policy_document.gitops_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_cicd_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github\_actions\_allowed\_repos](#input\_github\_actions\_allowed\_repos) | A list of the GitHub repositories that are allowed to assume this role from GitHub Actions. For example,
["cloudposse/infra-live"]. Can contain "*" as wildcard.
If org part of repo name is omitted, "cloudposse" will be assumed. | `list(string)` | `[]` | no | +| [gitops\_policy\_configuration](#input\_gitops\_policy\_configuration) | Configuration for the GitOps IAM Policy, valid keys are
- `s3_bucket_component_name` - Component Name of where to store the TF Plans in S3, defaults to `gitops/s3-bucket`
- `dynamodb_component_name` - Component Name of where to store the TF Plans in Dynamodb, defaults to `gitops/dynamodb`
- `s3_bucket_environment_name` - Environment name for the S3 Bucket, defaults to current environment
- `dynamodb_environment_name` - Environment name for the Dynamodb Table, defaults to current environment |
object({
s3_bucket_component_name = optional(string, "gitops/s3-bucket")
dynamodb_component_name = optional(string, "gitops/dynamodb")
s3_bucket_environment_name = optional(string)
dynamodb_environment_name = optional(string)
})
| `{}` | no | +| [iam\_policies](#input\_iam\_policies) | List of policies to attach to the IAM role, should be either an ARN of an AWS Managed Policy or a name of a custom policy e.g. `gitops` | `list(string)` | `[]` | no | +| [iam\_policy](#input\_iam\_policy) | IAM policy as list of Terraform objects, compatible with Terraform `aws_iam_policy_document` data source
except that `source_policy_documents` and `override_policy_documents` are not included.
Use inputs `iam_source_policy_documents` and `iam_override_policy_documents` for that. |
list(object({
policy_id = optional(string, null)
version = optional(string, null)
statements = list(object({
sid = optional(string, null)
effect = optional(string, null)
actions = optional(list(string), null)
not_actions = optional(list(string), null)
resources = optional(list(string), null)
not_resources = optional(list(string), null)
conditions = optional(list(object({
test = string
variable = string
values = list(string)
})), [])
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
not_principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
}))
}))
| `[]` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lambda\_cicd\_policy\_configuration](#input\_lambda\_cicd\_policy\_configuration) | Configuration for the lambda-cicd policy. The following keys are supported:
- `enable_kms_access` - (bool) - Whether to allow access to KMS. Defaults to false.
- `enable_ssm_access` - (bool) - Whether to allow access to SSM. Defaults to false.
- `enable_s3_access` - (bool) - Whether to allow access to S3. Defaults to false.
- `s3_bucket_component_name` - (string) - The name of the component to use for the S3 bucket. Defaults to `s3-bucket/github-action-artifacts`.
- `s3_bucket_environment_name` - (string) - The name of the environment to use for the S3 bucket. Defaults to the environment of the current module.
- `s3_bucket_tenant_name` - (string) - The name of the tenant to use for the S3 bucket. Defaults to the tenant of the current module.
- `s3_bucket_stage_name` - (string) - The name of the stage to use for the S3 bucket. Defaults to the stage of the current module.
- `enable_lambda_update` - (bool) - Whether to allow access to update lambda functions. Defaults to false. |
object({
enable_kms_access = optional(bool, false)
enable_ssm_access = optional(bool, false)
enable_s3_access = optional(bool, false)
s3_bucket_component_name = optional(string, "s3-bucket/github-action-artifacts")
s3_bucket_environment_name = optional(string)
s3_bucket_tenant_name = optional(string)
s3_bucket_stage_name = optional(string)
enable_lambda_update = optional(bool, false)
})
| `{}` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [github\_actions\_iam\_role\_arn](#output\_github\_actions\_iam\_role\_arn) | ARN of IAM role for GitHub Actions | +| [github\_actions\_iam\_role\_name](#output\_github\_actions\_iam\_role\_name) | Name of IAM role for GitHub Actions | + + +## References +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/github-oidc-role) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/github-oidc-role/additional-policy-map.tf b/modules/github-oidc-role/additional-policy-map.tf new file mode 100644 index 000000000..2d0aea69b --- /dev/null +++ b/modules/github-oidc-role/additional-policy-map.tf @@ -0,0 +1,11 @@ +locals { + # If you have custom policies, override this declaration by creating + # a file called `additional-policy-map_override.tf`. + # Then add the custom policies to the overridable_additional_custom_policy_map in that file. + # The key should be the policy you want to override, the value is the json policy document. + # See the README in `github-oidc-role` for more details. + overridable_additional_custom_policy_map = { + # Example: + # gitops = aws_iam_policy.my_custom_gitops_policy.policy + } +} diff --git a/modules/github-oidc-role/context.tf b/modules/github-oidc-role/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/github-oidc-role/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/github-oidc-role/main.tf b/modules/github-oidc-role/main.tf new file mode 100644 index 000000000..7ad3e55b1 --- /dev/null +++ b/modules/github-oidc-role/main.tf @@ -0,0 +1,50 @@ +locals { + enabled = module.this.enabled + managed_policies = [for arn in var.iam_policies : arn if can(regex("^arn:aws[^:]*:iam::aws:policy/", arn))] + policies = length(local.managed_policies) > 0 ? local.managed_policies : null + policy_document_map = { + "gitops" = local.gitops_policy + "lambda_cicd" = local.lambda_cicd_policy + "inline_policy" = one(module.iam_policy.*.json) + } + custom_policy_map = merge(local.policy_document_map, local.overridable_additional_custom_policy_map) + + # Ignore empty policies of the form `"{}"` as well as null policies + active_policy_map = { for k, v in local.custom_policy_map : k => v if try(length(v), 0) > 3 } +} + +module "iam_policy" { + enabled = local.enabled && length(var.iam_policy) > 0 + + source = "cloudposse/iam-policy/aws" + version = "2.0.1" + + iam_policy = var.iam_policy + + context = module.this.context +} + +module "gha_assume_role" { + source = "../account-map/modules/team-assume-role-policy" + + trusted_github_repos = var.github_actions_allowed_repos + + context = module.this.context +} + +resource "aws_iam_role" "github_actions" { + count = local.enabled ? 1 : 0 + + name = module.this.id + assume_role_policy = module.gha_assume_role.github_assume_role_policy + + managed_policy_arns = local.policies + + dynamic "inline_policy" { + for_each = local.active_policy_map + content { + name = inline_policy.key + policy = inline_policy.value + } + } +} diff --git a/modules/github-oidc-role/outputs.tf b/modules/github-oidc-role/outputs.tf new file mode 100644 index 000000000..20d0b1503 --- /dev/null +++ b/modules/github-oidc-role/outputs.tf @@ -0,0 +1,9 @@ +output "github_actions_iam_role_arn" { + value = one(aws_iam_role.github_actions[*].arn) + description = "ARN of IAM role for GitHub Actions" +} + +output "github_actions_iam_role_name" { + value = one(aws_iam_role.github_actions[*].name) + description = "Name of IAM role for GitHub Actions" +} diff --git a/modules/github-oidc-role/policy_gitops.tf b/modules/github-oidc-role/policy_gitops.tf new file mode 100644 index 000000000..5c91edc88 --- /dev/null +++ b/modules/github-oidc-role/policy_gitops.tf @@ -0,0 +1,113 @@ +variable "gitops_policy_configuration" { + type = object({ + s3_bucket_component_name = optional(string, "gitops/s3-bucket") + dynamodb_component_name = optional(string, "gitops/dynamodb") + s3_bucket_environment_name = optional(string) + dynamodb_environment_name = optional(string) + }) + default = {} + nullable = false + description = <<-EOT + Configuration for the GitOps IAM Policy, valid keys are + - `s3_bucket_component_name` - Component Name of where to store the TF Plans in S3, defaults to `gitops/s3-bucket` + - `dynamodb_component_name` - Component Name of where to store the TF Plans in Dynamodb, defaults to `gitops/dynamodb` + - `s3_bucket_environment_name` - Environment name for the S3 Bucket, defaults to current environment + - `dynamodb_environment_name` - Environment name for the Dynamodb Table, defaults to current environment + EOT +} + +locals { + gitops_policy_enabled = contains(var.iam_policies, "gitops") + gitops_policy = local.gitops_policy_enabled ? one(data.aws_iam_policy_document.gitops_iam_policy.*.json) : null + + s3_bucket_arn = one(module.s3_bucket[*].outputs.bucket_arn) + dynamodb_table_arn = one(module.dynamodb[*].outputs.table_arn) +} + +module "s3_bucket" { + count = local.gitops_policy_enabled ? 1 : 0 + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = lookup(var.gitops_policy_configuration, "s3_bucket_component_name", "gitops/s3-bucket") + environment = lookup(var.gitops_policy_configuration, "s3_bucket_environment_name", module.this.environment) + + context = module.this.context +} + +module "dynamodb" { + count = local.gitops_policy_enabled ? 1 : 0 + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = lookup(var.gitops_policy_configuration, "dynamodb_component_name", module.this.environment) + environment = lookup(var.gitops_policy_configuration, "dynamodb_environment_name", module.this.environment) + + context = module.this.context +} + +data "aws_iam_policy_document" "gitops_iam_policy" { + count = local.gitops_policy_enabled ? 1 : 0 + + # Allow access to the Dynamodb table used to store TF Plans + # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_dynamodb_specific-table.html + statement { + sid = "AllowDynamodbAccess" + effect = "Allow" + actions = [ + "dynamodb:List*", + "dynamodb:DescribeReservedCapacity*", + "dynamodb:DescribeLimits", + "dynamodb:DescribeTimeToLive" + ] + resources = [ + "*" + ] + } + statement { + sid = "AllowDynamodbTableAccess" + effect = "Allow" + actions = [ + "dynamodb:BatchGet*", + "dynamodb:DescribeStream", + "dynamodb:DescribeTable", + "dynamodb:Get*", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWrite*", + "dynamodb:CreateTable", + "dynamodb:Delete*", + "dynamodb:Update*", + "dynamodb:PutItem" + ] + resources = [ + local.dynamodb_table_arn, + "${local.dynamodb_table_arn}/*" + ] + } + + # Allow access to the S3 Bucket used to store TF Plans + # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html + statement { + sid = "AllowS3Actions" + effect = "Allow" + actions = [ + "s3:ListBucket" + ] + resources = [ + local.s3_bucket_arn + ] + } + statement { + sid = "AllowS3ObjectActions" + effect = "Allow" + actions = [ + "s3:*Object" + ] + resources = [ + "${local.s3_bucket_arn}/*" + ] + } +} diff --git a/modules/github-oidc-role/policy_lambda-cicd.tf b/modules/github-oidc-role/policy_lambda-cicd.tf new file mode 100644 index 000000000..b11efdc91 --- /dev/null +++ b/modules/github-oidc-role/policy_lambda-cicd.tf @@ -0,0 +1,113 @@ +variable "lambda_cicd_policy_configuration" { + type = object({ + enable_kms_access = optional(bool, false) + enable_ssm_access = optional(bool, false) + enable_s3_access = optional(bool, false) + s3_bucket_component_name = optional(string, "s3-bucket/github-action-artifacts") + s3_bucket_environment_name = optional(string) + s3_bucket_tenant_name = optional(string) + s3_bucket_stage_name = optional(string) + enable_lambda_update = optional(bool, false) + }) + default = {} + nullable = false + description = <<-EOT + Configuration for the lambda-cicd policy. The following keys are supported: + - `enable_kms_access` - (bool) - Whether to allow access to KMS. Defaults to false. + - `enable_ssm_access` - (bool) - Whether to allow access to SSM. Defaults to false. + - `enable_s3_access` - (bool) - Whether to allow access to S3. Defaults to false. + - `s3_bucket_component_name` - (string) - The name of the component to use for the S3 bucket. Defaults to `s3-bucket/github-action-artifacts`. + - `s3_bucket_environment_name` - (string) - The name of the environment to use for the S3 bucket. Defaults to the environment of the current module. + - `s3_bucket_tenant_name` - (string) - The name of the tenant to use for the S3 bucket. Defaults to the tenant of the current module. + - `s3_bucket_stage_name` - (string) - The name of the stage to use for the S3 bucket. Defaults to the stage of the current module. + - `enable_lambda_update` - (bool) - Whether to allow access to update lambda functions. Defaults to false. + EOT +} + +locals { + lambda_cicd_policy_enabled = contains(var.iam_policies, "lambda-cicd") + lambda_cicd_policy = local.lambda_cicd_policy_enabled ? one(data.aws_iam_policy_document.lambda_cicd_policy.*.json) : null + + lambda_bucket_arn = try(module.s3_artifacts_bucket[0].outputs.bucket_arn, null) +} + +module "s3_artifacts_bucket" { + count = lookup(var.lambda_cicd_policy_configuration, "enable_s3_access", false) ? 1 : 0 + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = lookup(var.lambda_cicd_policy_configuration, "s3_bucket_component_name", "s3-bucket/github-action-artifacts") + environment = lookup(var.lambda_cicd_policy_configuration, "s3_bucket_environment_name", module.this.environment) + tenant = lookup(var.lambda_cicd_policy_configuration, "s3_bucket_tenant_name", module.this.tenant) + stage = lookup(var.lambda_cicd_policy_configuration, "s3_bucket_stage_name", module.this.stage) + + context = module.this.context +} + +data "aws_iam_policy_document" "lambda_cicd_policy" { + count = local.lambda_cicd_policy_enabled ? 1 : 0 + + dynamic "statement" { + for_each = lookup(var.lambda_cicd_policy_configuration, "enable_kms_access", false) ? [1] : [] + content { + sid = "AllowKMSAccess" + effect = "Allow" + actions = [ + "kms:DescribeKey", + "kms:Encrypt", + ] + resources = [ + "*" + ] + } + } + + dynamic "statement" { + for_each = lookup(var.lambda_cicd_policy_configuration, "enable_ssm_access", false) ? [1] : [] + content { + effect = "Allow" + actions = [ + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:GetParametersByPath", + "ssm:DescribeParameters", + "ssm:PutParameter" + ] + resources = [ + "arn:aws:ssm:*:*:parameter/lambda/*" + ] + } + } + + dynamic "statement" { + for_each = lookup(var.lambda_cicd_policy_configuration, "enable_s3_access", false) && local.lambda_bucket_arn != null ? [1] : [] + content { + effect = "Allow" + actions = [ + "s3:HeadObject", + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket", + "s3:GetBucketLocation" + ] + resources = [ + local.lambda_bucket_arn, + ] + } + } + + dynamic "statement" { + for_each = lookup(var.lambda_cicd_policy_configuration, "enable_lambda_update", false) ? [1] : [] + content { + effect = "Allow" + actions = [ + "lambda:UpdateFunctionCode", + "lambda:UpdateFunctionConfiguration" + ] + resources = [ + "*" + ] + } + } +} diff --git a/modules/github-oidc-role/providers.tf b/modules/github-oidc-role/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/github-oidc-role/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/github-oidc-role/variables.tf b/modules/github-oidc-role/variables.tf new file mode 100644 index 000000000..29d0561f5 --- /dev/null +++ b/modules/github-oidc-role/variables.tf @@ -0,0 +1,56 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "iam_policies" { + type = list(string) + description = "List of policies to attach to the IAM role, should be either an ARN of an AWS Managed Policy or a name of a custom policy e.g. `gitops`" + default = [] +} + +variable "iam_policy" { + type = list(object({ + policy_id = optional(string, null) + version = optional(string, null) + statements = list(object({ + sid = optional(string, null) + effect = optional(string, null) + actions = optional(list(string), null) + not_actions = optional(list(string), null) + resources = optional(list(string), null) + not_resources = optional(list(string), null) + conditions = optional(list(object({ + test = string + variable = string + values = list(string) + })), []) + principals = optional(list(object({ + type = string + identifiers = list(string) + })), []) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + })), []) + })) + })) + description = <<-EOT + IAM policy as list of Terraform objects, compatible with Terraform `aws_iam_policy_document` data source + except that `source_policy_documents` and `override_policy_documents` are not included. + Use inputs `iam_source_policy_documents` and `iam_override_policy_documents` for that. + EOT + default = [] + nullable = false +} + + +variable "github_actions_allowed_repos" { + type = list(string) + description = < Date: Thu, 21 Dec 2023 10:55:03 -0800 Subject: [PATCH 334/501] fix `spacelift/admin-stack`: changed resource type from set to list (#940) --- modules/spacelift/admin-stack/README.md | 12 ++++++------ modules/spacelift/admin-stack/child-stacks.tf | 4 ++-- modules/spacelift/admin-stack/root-admin-stack.tf | 6 +++--- modules/spacelift/admin-stack/variables.tf | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index edd9c60ac..1e47dee35 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -157,11 +157,11 @@ components: | Name | Source | Version | |------|--------|---------| -| [all\_admin\_stacks\_config](#module\_all\_admin\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.4.0 | -| [child\_stack](#module\_child\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.4.0 | -| [child\_stacks\_config](#module\_child\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.4.0 | -| [root\_admin\_stack](#module\_root\_admin\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.4.0 | -| [root\_admin\_stack\_config](#module\_root\_admin\_stack\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.4.0 | +| [all\_admin\_stacks\_config](#module\_all\_admin\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.5.0 | +| [child\_stack](#module\_child\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.5.0 | +| [child\_stacks\_config](#module\_child\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.5.0 | +| [root\_admin\_stack](#module\_root\_admin\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.5.0 | +| [root\_admin\_stack\_config](#module\_root\_admin\_stack\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.5.0 | | [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -203,7 +203,7 @@ components: | [component\_root](#input\_component\_root) | The path, relative to the root of the repository, where the component can be found | `string` | n/a | yes | | [component\_vars](#input\_component\_vars) | All Terraform values to be applied to the stack via a mounted file | `any` | `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [context\_attachments](#input\_context\_attachments) | A list of context IDs to attach to this stack | `set(string)` | `[]` | no | +| [context\_attachments](#input\_context\_attachments) | A list of context IDs to attach to this stack | `list(string)` | `[]` | no | | [context\_filters](#input\_context\_filters) | Context filters to select atmos stacks matching specific criteria to create as children. |
object({
namespaces = optional(list(string), [])
environments = optional(list(string), [])
tenants = optional(list(string), [])
stages = optional(list(string), [])
tags = optional(map(string), {})
administrative = optional(bool)
root_administrative = optional(bool)
})
| n/a | yes | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [description](#input\_description) | Specify description of stack | `string` | `null` | no | diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index 59c181643..d4105a0cd 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -44,7 +44,7 @@ resource "null_resource" "child_stack_parent_precondition" { # for each one. module "child_stacks_config" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" - version = "1.4.0" + version = "1.5.0" context_filters = var.context_filters excluded_context_filters = var.excluded_context_filters @@ -54,7 +54,7 @@ module "child_stacks_config" { module "child_stack" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" - version = "1.4.0" + version = "1.5.0" for_each = local.child_stacks diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf index 24743e264..e262d9db6 100644 --- a/modules/spacelift/admin-stack/root-admin-stack.tf +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -3,7 +3,7 @@ # such stack is allowed in the Spacelift organization. module "root_admin_stack_config" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" - version = "1.4.0" + version = "1.5.0" enabled = local.create_root_admin_stack @@ -15,7 +15,7 @@ module "root_admin_stack_config" { # This gets the atmos stack config for all of the administrative stacks module "all_admin_stacks_config" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config" - version = "1.4.0" + version = "1.5.0" enabled = local.create_root_admin_stack @@ -26,7 +26,7 @@ module "all_admin_stacks_config" { module "root_admin_stack" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" - version = "1.4.0" + version = "1.5.0" enabled = local.create_root_admin_stack diff --git a/modules/spacelift/admin-stack/variables.tf b/modules/spacelift/admin-stack/variables.tf index 9ec895029..561bf6a85 100644 --- a/modules/spacelift/admin-stack/variables.tf +++ b/modules/spacelift/admin-stack/variables.tf @@ -106,7 +106,7 @@ variable "component_vars" { } variable "context_attachments" { - type = set(string) + type = list(string) description = "A list of context IDs to attach to this stack" default = [] } From 85a33501540c1c5b39356dfc9ede35755ddeff2f Mon Sep 17 00:00:00 2001 From: Hans Donner Date: Thu, 21 Dec 2023 21:05:17 +0100 Subject: [PATCH 335/501] chore(ecs): Remove unused variables (#938) --- modules/ecs/README.md | 7 ++----- modules/ecs/variables.tf | 31 ++----------------------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/modules/ecs/README.md b/modules/ecs/README.md index 088f8d093..fe269d05b 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -84,8 +84,8 @@ components: | [alb\_configuration](#input\_alb\_configuration) | Map of multiple ALB configurations. | `map(any)` | `{}` | no | | [alb\_ingress\_cidr\_blocks\_http](#input\_alb\_ingress\_cidr\_blocks\_http) | List of CIDR blocks allowed to access environment over HTTP | `list(string)` |
[
"0.0.0.0/0"
]
| no | | [alb\_ingress\_cidr\_blocks\_https](#input\_alb\_ingress\_cidr\_blocks\_https) | List of CIDR blocks allowed to access environment over HTTPS | `list(string)` |
[
"0.0.0.0/0"
]
| no | -| [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | -| [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | +| [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the ECS cluster | `list(string)` | `[]` | no | +| [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the ECS cluster | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [capacity\_providers\_ec2](#input\_capacity\_providers\_ec2) | EC2 autoscale groups capacity providers |
map(object({
instance_type = string
max_size = number
security_group_ids = optional(list(string), [])
min_size = optional(number, 0)
image_id = optional(string)
instance_initiated_shutdown_behavior = optional(string, "terminate")
key_name = optional(string, "")
user_data = optional(string, "")
enable_monitoring = optional(bool, true)
instance_warmup_period = optional(number, 300)
maximum_scaling_step_size = optional(number, 1)
minimum_scaling_step_size = optional(number, 1)
target_capacity_utilization = optional(number, 100)
ebs_optimized = optional(bool, false)
block_device_mappings = optional(list(object({
device_name = string
no_device = bool
virtual_name = string
ebs = object({
delete_on_termination = bool
encrypted = bool
iops = number
kms_key_id = string
snapshot_id = string
volume_size = number
volume_type = string
})
})), [])
instance_market_options = optional(object({
market_type = string
spot_options = object({
block_duration_minutes = number
instance_interruption_behavior = string
max_price = number
spot_instance_type = string
valid_until = string
})
}))
instance_refresh = optional(object({
strategy = string
preferences = optional(object({
instance_warmup = optional(number, null)
min_healthy_percentage = optional(number, null)
skip_matching = optional(bool, null)
auto_rollback = optional(bool, null)
}), null)
triggers = optional(list(string), [])
}))
mixed_instances_policy = optional(object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
}), {
instances_distribution = null
})
placement = optional(object({
affinity = string
availability_zone = string
group_name = string
host_id = string
tenancy = string
}))
credit_specification = optional(object({
cpu_credits = string
}))
elastic_gpu_specifications = optional(object({
type = string
}))
disable_api_termination = optional(bool, false)
default_cooldown = optional(number, 300)
health_check_grace_period = optional(number, 300)
force_delete = optional(bool, false)
termination_policies = optional(list(string), ["Default"])
suspended_processes = optional(list(string), [])
placement_group = optional(string, "")
metrics_granularity = optional(string, "1Minute")
enabled_metrics = optional(list(string), [
"GroupMinSize",
"GroupMaxSize",
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupPendingInstances",
"GroupStandbyInstances",
"GroupTerminatingInstances",
"GroupTotalInstances",
"GroupInServiceCapacity",
"GroupPendingCapacity",
"GroupStandbyCapacity",
"GroupTerminatingCapacity",
"GroupTotalCapacity",
"WarmPoolDesiredCapacity",
"WarmPoolWarmedCapacity",
"WarmPoolPendingCapacity",
"WarmPoolTerminatingCapacity",
"WarmPoolTotalCapacity",
"GroupAndWarmPoolDesiredCapacity",
"GroupAndWarmPoolTotalCapacity",
])
wait_for_capacity_timeout = optional(string, "10m")
service_linked_role_arn = optional(string, "")
metadata_http_endpoint_enabled = optional(bool, true)
metadata_http_put_response_hop_limit = optional(number, 2)
metadata_http_tokens_required = optional(bool, true)
metadata_http_protocol_ipv6_enabled = optional(bool, false)
tag_specifications_resource_types = optional(set(string), ["instance", "volume"])
max_instance_lifetime = optional(number, null)
capacity_rebalance = optional(bool, false)
warm_pool = optional(object({
pool_state = string
min_size = number
max_group_prepared_capacity = number
}))
}))
| `{}` | no | | [capacity\_providers\_fargate](#input\_capacity\_providers\_fargate) | Use FARGATE capacity provider | `bool` | `true` | no | @@ -102,13 +102,10 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [internal\_enabled](#input\_internal\_enabled) | Whether to create an internal load balancer for services in this cluster | `bool` | `false` | no | -| [kms\_key\_id](#input\_kms\_key\_id) | The AWS Key Management Service key ID to encrypt the data between the local client and the container. | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [log\_configuration](#input\_log\_configuration) | The log configuration for the results of the execute command actions Required when logging is OVERRIDE |
object({
cloud_watch_encryption_enabled = string
cloud_watch_log_group_name = string
s3_bucket_name = string
s3_key_prefix = string
})
| `null` | no | -| [logging](#input\_logging) | The AWS Key Management Service key ID to encrypt the data between the local client and the container. (Valid values: 'NONE', 'DEFAULT', 'OVERRIDE') | `string` | `"DEFAULT"` | no | | [maintenance\_page\_path](#input\_maintenance\_page\_path) | The path from this directory to the text/html page to use as the maintenance page. Must be within 1024 characters | `string` | `"templates/503_example.html"` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | diff --git a/modules/ecs/variables.tf b/modules/ecs/variables.tf index 7f0c40eea..0c8a80d7c 100644 --- a/modules/ecs/variables.tf +++ b/modules/ecs/variables.tf @@ -84,40 +84,13 @@ variable "dns_delegated_component_name" { variable "allowed_security_groups" { type = list(string) default = [] - description = "List of Security Group IDs to be allowed to connect to the EKS cluster" + description = "List of Security Group IDs to be allowed to connect to the ECS cluster" } variable "allowed_cidr_blocks" { type = list(string) default = [] - description = "List of CIDR blocks to be allowed to connect to the EKS cluster" -} - -variable "kms_key_id" { - description = "The AWS Key Management Service key ID to encrypt the data between the local client and the container." - type = string - default = null -} - -variable "logging" { - description = "The AWS Key Management Service key ID to encrypt the data between the local client and the container. (Valid values: 'NONE', 'DEFAULT', 'OVERRIDE')" - type = string - default = "DEFAULT" - validation { - condition = contains(["NONE", "DEFAULT", "OVERRIDE"], var.logging) - error_message = "The 'logging' value must be one of 'NONE', 'DEFAULT', 'OVERRIDE'" - } -} - -variable "log_configuration" { - description = "The log configuration for the results of the execute command actions Required when logging is OVERRIDE" - type = object({ - cloud_watch_encryption_enabled = string - cloud_watch_log_group_name = string - s3_bucket_name = string - s3_key_prefix = string - }) - default = null + description = "List of CIDR blocks to be allowed to connect to the ECS cluster" } variable "capacity_providers_fargate" { From b694da541796a6a0d9d106d0bf9e56bf78f79413 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 26 Dec 2023 19:45:32 -0800 Subject: [PATCH 336/501] feat: documentation for `aws-ssosync` (#941) --- modules/aws-ssosync/README.md | 217 +++++++++--------- modules/aws-ssosync/docs/img/admin_sdk.png | Bin 0 -> 103765 bytes .../docs/img/create_service_account.png | Bin 0 -> 124952 bytes .../docs/img/dl_service_account_creds.png | Bin 0 -> 141836 bytes modules/aws-ssosync/iam.tf | 5 +- modules/aws-ssosync/outputs.tf | 6 +- modules/aws-ssosync/providers.tf | 16 -- 7 files changed, 118 insertions(+), 126 deletions(-) create mode 100644 modules/aws-ssosync/docs/img/admin_sdk.png create mode 100644 modules/aws-ssosync/docs/img/create_service_account.png create mode 100644 modules/aws-ssosync/docs/img/dl_service_account_creds.png diff --git a/modules/aws-ssosync/README.md b/modules/aws-ssosync/README.md index 4fc72c9bb..3a6515deb 100644 --- a/modules/aws-ssosync/README.md +++ b/modules/aws-ssosync/README.md @@ -4,13 +4,10 @@ Deploys [AWS ssosync](https://github.com/awslabs/ssosync) to sync Google Groups AWS `ssosync` is a Lambda application that regularly manages Identity Store users. -This component requires manual deployment by a privileged user because it deploys a role in the identity account. - -You need to have set up AWS SSO in root account and delegated to the identity account as your SSO administrator. +This component requires manual deployment by a privileged user because it deploys a role in the root or identity management account. ## Usage -You should be able to deploy the `aws-ssosync` component to the `[core-]gbl-identity` stack -with `atmos terraform deploy aws-ssosync -s gbl-identity`. +You should be able to deploy the `aws-ssosync` component to the same account as `aws-sso`. Typically that is the `core-gbl-root` or `gbl-root` stack. **Stack Level**: Global **Deployment**: Must be deployed by `managers` or SuperAdmin using `atmos` CLI @@ -22,12 +19,6 @@ The following is an example snippet for how to use this component: components: terraform: aws-ssosync: - backend: - s3: - role_arn: null - settings: - spacelift: - workspace_enabled: false vars: enabled: true name: aws-ssosync @@ -43,79 +34,135 @@ components: We recommend following a similar process to what the [AWS ssosync](https://github.com/awslabs/ssosync) documentation recommends. -### Clickops +### Deployment Overview of steps: -1. Deploy the `aws-sso` component -1. Configure GSuite -1. Deploy the `aws-ssosync` component to the `gbl-identity` stack -#### Deploy the `aws-sso` component +1. Configure AWS IAM Identity Center +1. Configure Google Cloud console +1. Configure Google Admin console +1. Deploy the `aws-ssosync` component +1. Deploy the `aws-sso` component -Follow the [aws-sso](../aws-sso/) component documentation to deploy the `aws-sso` component. -Once this is done, you'll want to grab a few pieces of information. -Go to the AWS Single Sign-On console in the region you have set up AWS SSO and -select `Settings`. Click `Enable automatic provisioning`. +#### 1. Configure AWS IAM Identity Center (AWS SSO) -A pop up will appear with URL and the Access Token. The Access Token will only -appear at this stage. You want to copy both of these as a parameter to the ssosync command. +Follow [AWS documentation to configure SAML and SCIM with Google Workspace and IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/gs-gwp.html). -To pass parameters to the `ssosync` command, you'll need to decide on a path -in SSM Parameter Store, `google_credentials_ssm_path`. +As part of this process, save the SCIM endpoint token and URL. Then in AWS SSM Parameter Store, +create two `SecureString` parameters in the same account used for AWS SSO. +This is usually the root account in the primary region. -In SSM Parameter Store on your `identity` account, create a parameter with the -name `/scim_endpoint_url` and the value of the -URL from the previous step. Also create a parameter with the name -`/scim_endpoint_access_token` and the value of the -Access Token from the previous step. +``` +/ssosync/scim_endpoint_access_token +/ssosync/scim_endpoint_url +``` One more parameter you'll need is your Identity Store ID. -To obtain your Identity store ID, go to the AWS Identity Center console and +To obtain your Identity Store ID, go to the AWS Identity Center console and select `Settings`. Under the `Identity Source` section, copy the Identity Store ID. -Back in the `identity` account, create a parameter with the name -`/identity_store_id`. +In the same account used for AWS SSO, create the following parameter: + +``` +/ssosync/identity_store_id +``` + +#### 2. Configure Google Cloud console + +Within the Google Cloud console, we need to create a new Google Project and Service Account and enable the Admin SDK API. +Follow these steps: -Lastly, go ahead and [delegate administration](https://docs.aws.amazon.com/singlesignon/latest/userguide/delegated-admin.html) -from the `root` account to the `identity` account +1. Open the Google Cloud
console: https://console.cloud.google.com +2. Create a new project. Give the project a descriptive name such as `AWS SSO Sync` +3. Enable Admin SDK in APIs: `APIs & Services > Enabled APIs & Services > + ENABLE APIS AND SERVICES` -#### Configure GSuite +![Enable Admin SDK](./docs/img/admin_sdk.png) -_steps taken directly from [ssosync README.md](https://github.com/awslabs/ssosync/blob/master/README.md#google)_ +4. Create Service Account: `IAM & Admin > Service Accounts > Create Service Account` [(ref)](https://cloud.google.com/iam/docs/service-accounts-create). -First, you have to setup your API. In the project you want to use go to the -[Console](https://console.developers.google.com/apis) and select *API & Service * > -*Enable APIs and Services*. Search for *Admin SDK* and *Enable* the API. +![Create Service Account](./docs/img/create_service_account.png) + +5. Download credentials for the new Service Account: `IAM & Admin > Service Accounts > select Service Account > Keys > ADD KEY > Create new key > JSON` + +![Download Credentials](./docs/img/dl_service_account_creds.png) + +6. Save the JSON credentials as a new `SecureString` AWS SSM parameter in the same account used for AWS SSO. Use the full JSON string as the value for the parameter. + +``` +/ssosync/google_credentials +``` + +#### 3. Configure Google Admin console + +- Open the Google Admin console +- From your domain’s Admin console, go to `Main menu menu > Security > Access and data control > API controls` [(ref)](https://developers.google.com/cloud-search/docs/guides/delegation) +- In the Domain wide delegation pane, select `Manage Domain Wide Delegation`. +- Click `Add new`. +- In the Client ID field, enter the client ID obtained from the service account creation steps above. +- In the OAuth Scopes field, enter a comma-delimited list of the scopes required for your application. Use the scope `https://www.googleapis.com/auth/cloud_search.query` for search applications using the Query API. +- Add the following permission: [(ref)](https://github.com/awslabs/ssosync?tab=readme-ov-file#google) + +```console +https://www.googleapis.com/auth/admin.directory.group.readonly +https://www.googleapis.com/auth/admin.directory.group.member.readonly +https://www.googleapis.com/auth/admin.directory.user.readonly +``` -You have to perform this -[tutorial](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) -to create a service account that you use to sync your users. Save -the `JSON file` you create during the process and rename it to `google_credentials.json`. -Head back in to your `identity` account in AWS and create a parameter in SSM -Parameter Store with the name `/google_credentials` and -give it the contents of the `google_credentials.json` file. +#### 4. Deploy the `aws-ssosync` component -In the domain-wide delegation for the Admin API, you have to specify the -following scopes for the user. +Make sure that all four of the following SSM parameters exist in the target account and region: -* https://www.googleapis.com/auth/admin.directory.group.readonly -* https://www.googleapis.com/auth/admin.directory.group.member.readonly -* https://www.googleapis.com/auth/admin.directory.user.readonly +* `/ssosync/scim_endpoint_url` +* `/ssosync/scim_endpoint_access_token` +* `/ssosync/identity_store_id` +* `/ssosync/google_credentials` -Back in the Console go to the Dashboard for the API & Services and select -`Enable API and Services`. -In the Search box type `Admin` and select the `Admin SDK` option. Click the -`Enable` button. +If deployed successfully, Groups and Users should be programmatically copied from the Google Workspace into AWS IAM Identity Center on the given schedule. -#### Deploy the `aws-ssosync` component +If these Groups are not showing up, check the CloudWatch logs for the new Lambda function and refer the [FAQs](#FAQ) included below. -Make sure that all four of the following SSM parameters exist in the `identity` account: -* `/scim_endpoint_url` -* `/scim_endpoint_access_token` -* `/identity_store_id` -* `/google_credentials` +#### 5. Deploy the `aws-sso` component +Use the names of the Groups now provisioned programmatically in the `aws-sso` component catalog. Follow the [aws-sso](../aws-sso/) component documentation to deploy the `aws-sso` component. + +### FAQ + +#### Why is the tool forked by `Benbentwo`? + +The `awslabs` tool requires AWS Secrets Managers for the Google Credentials. However, we would prefer to use AWS SSM to store all credentials consistency and not require AWS Secrets Manager. Therefore we've created a Pull Request and will point to a fork until the PR is merged. + +Ref: +- https://github.com/awslabs/ssosync/pull/133 +- https://github.com/awslabs/ssosync/issues/93 + +#### What should I use for the Google Admin Email Address? + +The Service Account created will assume the User given by `--google-admin` / `SSOSYNC_GOOGLE_ADMIN` / `var.google_admin_email`. Therefore, this user email must be a valid Google admin user in your organization. + +This is not the same email as the Service Account. + +If Google fails to query Groups, you may see the following error: + +```console +Notifying Lambda and mark this execution as Failure: googleapi: Error 404: Domain not found., notFound +``` + +#### Common Group Name Query Error + +If filtering group names using query strings, make sure the provided string is valid. For example, `google_group_match: "name:aws*"` is incorrect. Instead use `google_group_match: "Name:aws*"` + +If not, you may again see the same error message: + +```console +Notifying Lambda and mark this execution as Failure: googleapi: Error 404: Domain not found., notFound +``` + +Ref: + +> The specific error you are seeing is because the google api doesn't like the query string you provided for the -g parameter. try -g "Name:Fuel*" + +https://github.com/awslabs/ssosync/issues/91 @@ -140,7 +187,6 @@ Make sure that all four of the following SSM parameters exist in the `identity` | Name | Source | Version | |------|--------|---------| -| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [ssosync\_artifact](#module\_ssosync\_artifact) | cloudposse/module-artifact/external | 0.8.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -211,47 +257,6 @@ Make sure that all four of the following SSM parameters exist in the `identity` ## References -- [cloudposse/terraform-aws-sso][39] - -[][40] - -[1]: https://docs.aws.amazon.com/singlesignon/latest/userguide/permissionsetsconcept.html -[2]: #requirement%5C_terraform -[3]: #requirement%5C_aws -[4]: #requirement%5C_external -[5]: #requirement%5C_local -[6]: #requirement%5C_template -[7]: #requirement%5C_utils -[8]: #provider%5C_aws -[9]: #module%5C_account%5C_map -[10]: #module%5C_permission%5C_sets -[11]: #module%5C_role%5C_prefix -[12]: #module%5C_sso%5C_account%5C_assignments -[13]: #module%5C_this -[14]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document -[15]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document -[16]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document -[17]: #input%5C_account%5C_assignments -[18]: #input%5C_additional%5C_tag%5C_map -[19]: #input%5C_attributes -[20]: #input%5C_context -[21]: #input%5C_delimiter -[22]: #input%5C_enabled -[23]: #input%5C_environment -[24]: #input%5C_global%5C_environment%5C_name -[25]: #input%5C_iam%5C_primary%5C_roles%5C_stage%5C_name -[26]: #input%5C_id%5C_length%5C_limit -[27]: #input%5C_identity%5C_roles%5C_accessible -[28]: #input%5C_label%5C_key%5C_case -[29]: #input%5C_label%5C_order -[30]: #input%5C_label%5C_value%5C_case -[31]: #input%5C_name -[32]: #input%5C_namespace -[33]: #input%5C_privileged -[34]: #input%5C_regex%5C_replace%5C_chars -[35]: #input%5C_region -[36]: #input%5C_root%5C_account%5C_stage%5C_name -[37]: #input%5C_stage -[38]: #input%5C_tags -[39]: https://github.com/cloudposse/terraform-aws-sso -[40]: https://cpco.io/component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-ssosync) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/aws-ssosync/docs/img/admin_sdk.png b/modules/aws-ssosync/docs/img/admin_sdk.png new file mode 100644 index 0000000000000000000000000000000000000000..6ece1a68ddf2f242c61d243bf5b00980f146ab72 GIT binary patch literal 103765 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%0D`%p)z3Xnr%bf@|*u;H#90Y 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

-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 E-v$=v;s5{u literal 0 HcmV?d00001 diff --git a/modules/aws-ssosync/docs/img/create_service_account.png b/modules/aws-ssosync/docs/img/create_service_account.png new file mode 100644 index 0000000000000000000000000000000000000000..1653f47f979127a5cb17ceeb61aaaf9b60aa7367 GIT binary patch literal 124952 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≫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 literal 0 HcmV?d00001 diff --git a/modules/aws-ssosync/iam.tf b/modules/aws-ssosync/iam.tf index c6eecd62c..d5ecaf3f0 100644 --- a/modules/aws-ssosync/iam.tf +++ b/modules/aws-ssosync/iam.tf @@ -25,7 +25,10 @@ data "aws_iam_policy_document" "ssosync_lambda_identity_center" { "identitystore:DeleteGroupMembership", "identitystore:DeleteGroup", "secretsmanager:GetSecretValue", - "kms:Decrypt" + "kms:Decrypt", + "logs:PutLogEvents", + "logs:CreateLogStream", + "logs:CreateLogGroup" ] resources = ["*"] } diff --git a/modules/aws-ssosync/outputs.tf b/modules/aws-ssosync/outputs.tf index 2e9f84bd1..5f4ba5421 100644 --- a/modules/aws-ssosync/outputs.tf +++ b/modules/aws-ssosync/outputs.tf @@ -1,14 +1,14 @@ output "arn" { description = "ARN of the lambda function" - value = aws_lambda_function.ssosync.arn + value = one(aws_lambda_function.ssosync[*].arn) } output "invoke_arn" { description = "Invoke ARN of the lambda function" - value = aws_lambda_function.ssosync.invoke_arn + value = one(aws_lambda_function.ssosync[*].invoke_arn) } output "qualified_arn" { description = "ARN identifying your Lambda Function Version (if versioning is enabled via publish = true)" - value = aws_lambda_function.ssosync.qualified_arn + value = one(aws_lambda_function.ssosync[*].qualified_arn) } diff --git a/modules/aws-ssosync/providers.tf b/modules/aws-ssosync/providers.tf index ef923e10a..dc58d9a25 100644 --- a/modules/aws-ssosync/providers.tf +++ b/modules/aws-ssosync/providers.tf @@ -1,19 +1,3 @@ provider "aws" { region = var.region - - # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. - profile = module.iam_roles.terraform_profile_name - - dynamic "assume_role" { - # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. - for_each = compact([module.iam_roles.terraform_role_arn]) - content { - role_arn = assume_role.value - } - } -} - -module "iam_roles" { - source = "../account-map/modules/iam-roles" - context = module.this.context } From 4ca0f035e76215ca2085033eee5d53bb5dbce143 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 28 Dec 2023 16:15:41 -0800 Subject: [PATCH 337/501] fix: image links for website (#942) --- modules/aws-ssosync/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/aws-ssosync/README.md b/modules/aws-ssosync/README.md index 3a6515deb..f02653193 100644 --- a/modules/aws-ssosync/README.md +++ b/modules/aws-ssosync/README.md @@ -76,15 +76,15 @@ Follow these steps: 2. Create a new project. Give the project a descriptive name such as `AWS SSO Sync` 3. Enable Admin SDK in APIs: `APIs & Services > Enabled APIs & Services > + ENABLE APIS AND SERVICES` -![Enable Admin SDK](./docs/img/admin_sdk.png) +![Enable Admin SDK](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/admin_sdk.png) # use raw URL so that this works in both GitHub and docusaurus 4. Create Service Account: `IAM & Admin > Service Accounts > Create Service Account` [(ref)](https://cloud.google.com/iam/docs/service-accounts-create). -![Create Service Account](./docs/img/create_service_account.png) +![Create Service Account](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/create_service_account.png) # use raw URL so that this works in both GitHub and docusaurus 5. Download credentials for the new Service Account: `IAM & Admin > Service Accounts > select Service Account > Keys > ADD KEY > Create new key > JSON` -![Download Credentials](./docs/img/dl_service_account_creds.png) +![Download Credentials](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/dl_service_account_creds.png) # use raw URL so that this works in both GitHub and docusaurus 6. Save the JSON credentials as a new `SecureString` AWS SSM parameter in the same account used for AWS SSO. Use the full JSON string as the value for the parameter. From fb83749d6270803808769e944f28e5754776e1ea Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 8 Jan 2024 13:51:41 -0800 Subject: [PATCH 338/501] `spa-s3-cloudfront`: Variable for Lambda Runtime (#946) --- modules/spa-s3-cloudfront/README.md | 1 + modules/spa-s3-cloudfront/main.tf | 2 ++ .../modules/lambda-edge-preview/main.tf | 2 +- .../modules/lambda-edge-preview/variables.tf | 9 +++++++++ .../modules/lambda_edge_redirect_404/main.tf | 2 +- .../modules/lambda_edge_redirect_404/variables.tf | 9 +++++++++ modules/spa-s3-cloudfront/variables.tf | 9 +++++++++ 7 files changed, 32 insertions(+), 2 deletions(-) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 006e4f714..9d8b7279f 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -189,6 +189,7 @@ an extensive explanation for how these preview environments work. | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |

[
"default"
]
| no | | [lambda\_edge\_redirect\_404\_enabled](#input\_lambda\_edge\_redirect\_404\_enabled) | Enable or disable SPA 404 redirects via Lambda@Edge - returns a 302 and a location of `/` if the request returned 404. | `bool` | `false` | no | +| [lambda\_runtime](#input\_lambda\_runtime) | Identifier of the function's runtime. See Runtimes for valid values.
https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime | `string` | `"nodejs16.x"` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [ordered\_cache](#input\_ordered\_cache) | An ordered list of [cache behaviors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#cache-behavior-arguments) resource for this distribution.
List in order of precedence (first match wins). This is in addition to the default cache policy.
Set `target_origin_id` to `""` to specify the S3 bucket origin created by this module. |
list(object({
target_origin_id = string
path_pattern = string

allowed_methods = list(string)
cached_methods = list(string)
compress = bool
trusted_signers = list(string)
trusted_key_groups = list(string)

cache_policy_id = string
origin_request_policy_id = string

viewer_protocol_policy = string
min_ttl = number
default_ttl = number
max_ttl = number
response_headers_policy_id = string

forward_query_string = bool
forward_header_values = list(string)
forward_cookies = string
forward_cookies_whitelisted_names = list(string)

lambda_function_association = list(object({
event_type = string
include_body = bool
lambda_arn = string
}))

function_association = list(object({
event_type = string
function_arn = string
}))
}))
| `[]` | no | diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf index 4298b7ee7..fd6cd47d8 100644 --- a/modules/spa-s3-cloudfront/main.tf +++ b/modules/spa-s3-cloudfront/main.tf @@ -156,6 +156,7 @@ module "lambda_edge_preview" { cloudfront_distribution_hosted_zone_id = module.spa_web.cf_hosted_zone_id site_fqdn = local.site_fqdn parent_zone_name = local.parent_zone_name + runtime = var.lambda_runtime context = module.this.context @@ -173,6 +174,7 @@ module "lambda_edge_redirect_404" { cloudfront_distribution_hosted_zone_id = module.spa_web.cf_hosted_zone_id site_fqdn = local.site_fqdn parent_zone_name = local.parent_zone_name + runtime = var.lambda_runtime context = module.this.context diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf index 3ee3f61c6..1fc6b98dd 100644 --- a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf +++ b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf @@ -1,7 +1,7 @@ # https://levelup.gitconnected.com/preview-environments-in-aws-with-cloudfront-and-lambda-edge-7acccb0b67d1 locals { - lambda_runtime = "nodejs12.x" + lambda_runtime = var.runtime lambda_handler = "index.handler" } diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf index 92d9d1313..1b3ea97eb 100644 --- a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf +++ b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf @@ -17,3 +17,12 @@ variable "site_fqdn" { type = string description = "The fully qualified alias for the CloudFront Distribution." } + +variable "runtime" { + type = string + description = <<-EOT + Identifier of the function's runtime. See Runtimes for valid values. + https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime + EOT + default = "nodejs16.x" +} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf index 36603d5d6..659d6880a 100644 --- a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf +++ b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf @@ -1,7 +1,7 @@ # https://levelup.gitconnected.com/preview-environments-in-aws-with-cloudfront-and-lambda-edge-7acccb0b67d1 locals { - lambda_runtime = "nodejs12.x" + lambda_runtime = var.runtime lambda_handler = "index.handler" } diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf index 92d9d1313..1b3ea97eb 100644 --- a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf +++ b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf @@ -17,3 +17,12 @@ variable "site_fqdn" { type = string description = "The fully qualified alias for the CloudFront Distribution." } + +variable "runtime" { + type = string + description = <<-EOT + Identifier of the function's runtime. See Runtimes for valid values. + https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime + EOT + default = "nodejs16.x" +} diff --git a/modules/spa-s3-cloudfront/variables.tf b/modules/spa-s3-cloudfront/variables.tf index 1e50f7725..a99568240 100644 --- a/modules/spa-s3-cloudfront/variables.tf +++ b/modules/spa-s3-cloudfront/variables.tf @@ -489,3 +489,12 @@ variable "github_runners_tenant_name" { description = "The tenant name where the GitHub Runners are provisioned" default = null } + +variable "lambda_runtime" { + type = string + description = <<-EOT + Identifier of the function's runtime. See Runtimes for valid values. + https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime + EOT + default = "nodejs16.x" +} From 855356cc552f9891d92264f5beeecb758797d061 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 8 Jan 2024 14:06:38 -0800 Subject: [PATCH 339/501] Component Update: `rds` - Module Version (#947) Co-authored-by: cloudpossebot --- modules/rds/README.md | 2 +- modules/rds/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/rds/README.md b/modules/rds/README.md index a4a3ccd17..e4b44817c 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -112,7 +112,7 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [kms\_key\_rds](#module\_kms\_key\_rds) | cloudposse/kms-key/aws | 0.12.1 | | [rds\_client\_sg](#module\_rds\_client\_sg) | cloudposse/security-group/aws | 2.2.0 | -| [rds\_instance](#module\_rds\_instance) | cloudposse/rds/aws | 0.38.5 | +| [rds\_instance](#module\_rds\_instance) | cloudposse/rds/aws | 1.1.0 | | [rds\_monitoring\_role](#module\_rds\_monitoring\_role) | cloudposse/iam-role/aws | 0.17.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | diff --git a/modules/rds/main.tf b/modules/rds/main.tf index d89218c5e..4f08e901c 100644 --- a/modules/rds/main.tf +++ b/modules/rds/main.tf @@ -35,7 +35,7 @@ module "rds_client_sg" { module "rds_instance" { source = "cloudposse/rds/aws" - version = "0.38.5" + version = "1.1.0" allocated_storage = var.allocated_storage allow_major_version_upgrade = var.allow_major_version_upgrade From 1b5ccf4fbcc697aba32a41b873effab9215c3855 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 9 Jan 2024 14:56:38 -0800 Subject: [PATCH 340/501] update ecs cluster and services to support vanity domians (#949) Co-authored-by: cloudpossebot --- modules/ecs-service/main.tf | 2 +- modules/ecs-service/remote-state.tf | 2 +- modules/ecs/README.md | 17 ++++++++++++++++- modules/ecs/main.tf | 22 ++++++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 920d84657..58f9aefbd 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -284,7 +284,7 @@ module "alb_ingress" { vpc_id = local.vpc_id unauthenticated_listener_arns = [local.lb_listener_https_arn] - unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", var.vanity_domain), local.full_domain] : [local.full_domain] + unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", var.vanity_domain), local.full_domain] : concat([local.full_domain], var.vanity_alias) unauthenticated_paths = flatten(var.unauthenticated_paths) # When set to catch-all, make priority super high to make sure last to match unauthenticated_priority = var.lb_catch_all ? 99 : var.unauthenticated_priority diff --git a/modules/ecs-service/remote-state.tf b/modules/ecs-service/remote-state.tf index 3c11291c1..4ca467c7f 100644 --- a/modules/ecs-service/remote-state.tf +++ b/modules/ecs-service/remote-state.tf @@ -20,7 +20,7 @@ locals { http_protocol = coalesce(local.requested_protocol, local.lb_protocol) lb_arn = try(coalesce(local.nlb.nlb_arn, ""), coalesce(local.alb.alb_arn, ""), null) - lb_name = try(coalesce(local.nlb.nlb_name, ""), coalesce(local.alb.alb_name, ""), null) + lb_name = try(coalesce(local.nlb.nlb_name, ""), coalesce(local.alb.alb_dns_name, ""), null) lb_listener_http_is_redirect = try(length(local.is_nlb ? "" : local.alb.http_redirect_listener_arn) > 0, false) lb_listener_https_arn = try(coalesce(local.nlb.default_listener_arn, ""), coalesce(local.alb.https_listener_arn, ""), null) lb_sg_id = try(local.is_nlb ? null : local.alb.security_group_id, null) diff --git a/modules/ecs/README.md b/modules/ecs/README.md index fe269d05b..c38cb3cbc 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -34,7 +34,20 @@ components: capacity_providers_ec2: default: instance_type: t3.medium - max_size: 2 + max_size: 2 + + alb_configuration: + public: + internal_enabled: false + # resolves to *.public-platform..... + route53_record_name: "*.public-platform" + additional_certs: + - "my-vanity-domain.com" + private: + internal_enabled: true + route53_record_name: "*.private-platform" + additional_certs: + - "my-vanity-domain.com" ``` @@ -67,11 +80,13 @@ components: | Name | Type | |------|------| +| [aws_lb_listener_certificate.additional_certs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener_certificate) | resource | | [aws_route53_record.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | | [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group_rule.egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.ingress_cidr](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.ingress_security_groups](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_acm_certificate.additional_certs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/acm_certificate) | data source | | [aws_acm_certificate.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/acm_certificate) | data source | ## Inputs diff --git a/modules/ecs/main.tf b/modules/ecs/main.tf index 97d38cfda..5d58bbcc2 100644 --- a/modules/ecs/main.tf +++ b/modules/ecs/main.tf @@ -225,3 +225,25 @@ module "alb" { context = module.this.context } + +locals { + # formats the loadbalancer configuration data to be: + # { "${alb_configuration key}_${additional_cert_entry}" => "additional_cert_entry" } + certificate_domains = merge([ + for config_key, config in var.alb_configuration : + { for domain in config.additional_certs : + "${config_key}_${domain}" => domain } + ]...) +} + +resource "aws_lb_listener_certificate" "additional_certs" { + for_each = local.certificate_domains + + listener_arn = module.alb[split("_", each.key)[0]].https_listener_arn + certificate_arn = data.aws_acm_certificate.additional_certs[each.key].arn +} +data "aws_acm_certificate" "additional_certs" { + for_each = local.certificate_domains + + domain = each.value +} From d36df8d8a80f60105ee1bde74425750e62d4808a Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Tue, 9 Jan 2024 22:20:36 -0700 Subject: [PATCH 341/501] =?UTF-8?q?feat:=20adds=20passing=20terraform=5Fwo?= =?UTF-8?q?rkflow=5Ftool=20to=20spacelift-stack=20module=20=F0=9F=8D=9C=20?= =?UTF-8?q?(#950)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/spacelift/admin-stack/README.md | 1 + modules/spacelift/admin-stack/child-stacks.tf | 1 + modules/spacelift/admin-stack/root-admin-stack.tf | 1 + modules/spacelift/admin-stack/variables.tf | 11 +++++++++++ 4 files changed, 14 insertions(+) diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index 1e47dee35..025d88299 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -250,6 +250,7 @@ components: | [terraform\_smart\_sanitization](#input\_terraform\_smart\_sanitization) | Whether or not to enable [Smart Sanitization](https://docs.spacelift.io/vendors/terraform/resource-sanitization) which will only sanitize values marked as sensitive. | `bool` | `false` | no | | [terraform\_version](#input\_terraform\_version) | Specify the version of Terraform to use for the stack | `string` | `null` | no | | [terraform\_version\_map](#input\_terraform\_version\_map) | A map to determine which Terraform patch version to use for each minor version | `map(string)` | `{}` | no | +| [terraform\_workflow\_tool](#input\_terraform\_workflow\_tool) | Defines the tool that will be used to execute the workflow. This can be one of OPEN\_TOFU, TERRAFORM\_FOSS or CUSTOM. Defaults to TERRAFORM\_FOSS. | `string` | `"TERRAFORM_FOSS"` | no | | [terraform\_workspace](#input\_terraform\_workspace) | Specify the Terraform workspace to use for the stack | `string` | `null` | no | | [webhook\_enabled](#input\_webhook\_enabled) | Flag to enable/disable the webhook endpoint to which Spacelift sends the POST requests about run state changes | `bool` | `false` | no | | [webhook\_endpoint](#input\_webhook\_endpoint) | Webhook endpoint to which Spacelift sends the POST requests about run state changes | `string` | `null` | no | diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index d4105a0cd..3195e283b 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -115,6 +115,7 @@ module "child_stack" { stack_name = try(each.value.settings.spacelift.stack_name, each.key) terraform_smart_sanitization = try(each.value.settings.spacelift.terraform_smart_sanitization, var.terraform_smart_sanitization) terraform_version = lookup(var.terraform_version_map, try(each.value.settings.spacelift.terraform_version, ""), var.terraform_version) + terraform_workflow_tool = try(each.value.settings.spacelift.terraform_workflow_tool, var.terraform_workflow_tool) webhook_enabled = try(each.value.settings.spacelift.webhook_enabled, var.webhook_enabled) webhook_endpoint = try(each.value.settings.spacelift.webhook_endpoint, var.webhook_endpoint) webhook_secret = try(each.value.settings.spacelift.webhook_secret, var.webhook_secret) diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf index e262d9db6..9c2e56823 100644 --- a/modules/spacelift/admin-stack/root-admin-stack.tf +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -80,6 +80,7 @@ module "root_admin_stack" { stack_name = var.stack_name != null ? var.stack_name : local.root_admin_stack_name terraform_smart_sanitization = try(local.root_admin_stack_config.settings.spacelift.terraform_smart_sanitization, var.terraform_smart_sanitization) terraform_version = lookup(var.terraform_version_map, try(local.root_admin_stack_config.settings.spacelift.terraform_version, ""), var.terraform_version) + terraform_workflow_tool = try(local.root_admin_stack_config.settings.spacelift.terraform_workflow_tool, var.terraform_workflow_tool) webhook_enabled = try(local.root_admin_stack_config.settings.spacelift.webhook_enabled, var.webhook_enabled) webhook_endpoint = try(local.root_admin_stack_config.settings.spacelift.webhook_endpoint, var.webhook_endpoint) webhook_secret = try(local.root_admin_stack_config.settings.spacelift.webhook_secret, var.webhook_secret) diff --git a/modules/spacelift/admin-stack/variables.tf b/modules/spacelift/admin-stack/variables.tf index 561bf6a85..eb8e3af91 100644 --- a/modules/spacelift/admin-stack/variables.tf +++ b/modules/spacelift/admin-stack/variables.tf @@ -309,6 +309,17 @@ variable "terraform_version_map" { default = {} } +variable "terraform_workflow_tool" { + type = string + description = "Defines the tool that will be used to execute the workflow. This can be one of OPEN_TOFU, TERRAFORM_FOSS or CUSTOM. Defaults to TERRAFORM_FOSS." + default = "TERRAFORM_FOSS" + + validation { + condition = contains(["OPEN_TOFU", "TERRAFORM_FOSS", "CUSTOM"], var.terraform_workflow_tool) + error_message = "Valid values for terraform_workflow_tool are (OPEN_TOFU, TERRAFORM_FOSS, CUSTOM)." + } +} + variable "terraform_workspace" { type = string description = "Specify the Terraform workspace to use for the stack" From 2677de4764af234f2a64196bfb1079633bf747aa Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Tue, 9 Jan 2024 23:14:27 -0700 Subject: [PATCH 342/501] feat: adds passing terraform_workflow_tool to spacelift-stack module (#951) --- modules/spacelift/admin-stack/README.md | 4 ++-- modules/spacelift/admin-stack/child-stacks.tf | 2 +- modules/spacelift/admin-stack/root-admin-stack.tf | 2 +- modules/spacelift/spaces/README.md | 2 +- modules/spacelift/spaces/main.tf | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index 025d88299..6251199fa 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -158,9 +158,9 @@ components: | Name | Source | Version | |------|--------|---------| | [all\_admin\_stacks\_config](#module\_all\_admin\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.5.0 | -| [child\_stack](#module\_child\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.5.0 | +| [child\_stack](#module\_child\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.6.0 | | [child\_stacks\_config](#module\_child\_stacks\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.5.0 | -| [root\_admin\_stack](#module\_root\_admin\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.5.0 | +| [root\_admin\_stack](#module\_root\_admin\_stack) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack | 1.6.0 | | [root\_admin\_stack\_config](#module\_root\_admin\_stack\_config) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stacks-from-atmos-config | 1.5.0 | | [spaces](#module\_spaces) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index 3195e283b..a7734f87d 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -54,7 +54,7 @@ module "child_stacks_config" { module "child_stack" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" - version = "1.5.0" + version = "1.6.0" for_each = local.child_stacks diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf index 9c2e56823..c35e0ab27 100644 --- a/modules/spacelift/admin-stack/root-admin-stack.tf +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -26,7 +26,7 @@ module "all_admin_stacks_config" { module "root_admin_stack" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-stack" - version = "1.5.0" + version = "1.6.0" enabled = local.create_root_admin_stack diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index 1f270932c..5db0f2244 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -82,7 +82,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.4.0 | -| [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.4.0 | +| [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.6.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources diff --git a/modules/spacelift/spaces/main.tf b/modules/spacelift/spaces/main.tf index 7a1f289f3..92c525185 100644 --- a/modules/spacelift/spaces/main.tf +++ b/modules/spacelift/spaces/main.tf @@ -38,7 +38,7 @@ locals { module "space" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space" - version = "1.4.0" + version = "1.6.0" # Create a space for each entry in the `spaces` variable, except for the root space which already exists by default # and cannot be deleted. From 0673589f516b8d21e61d9c6013ad92b13ca770a5 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 11 Jan 2024 06:29:23 -0800 Subject: [PATCH 343/501] bugfix: `ECS` additional_certs is empty (#952) --- modules/ecs/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ecs/main.tf b/modules/ecs/main.tf index 5d58bbcc2..1912df97e 100644 --- a/modules/ecs/main.tf +++ b/modules/ecs/main.tf @@ -227,12 +227,12 @@ module "alb" { } locals { - # formats the loadbalancer configuration data to be: + # formats the load-balancer configuration data to be: # { "${alb_configuration key}_${additional_cert_entry}" => "additional_cert_entry" } certificate_domains = merge([ for config_key, config in var.alb_configuration : { for domain in config.additional_certs : - "${config_key}_${domain}" => domain } + "${config_key}_${domain}" => domain } if lookup(config, "additional_certs", []) != [] ]...) } From 15e2860f08f5d81acd5d7443adec45443e99d4ee Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Fri, 12 Jan 2024 10:01:45 -0700 Subject: [PATCH 344/501] fix: usage of spacelift admin-stack w/o worker pools (#953) --- modules/spacelift/admin-stack/child-stacks.tf | 2 +- modules/spacelift/admin-stack/root-admin-stack.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index a7734f87d..c237cc8f2 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -119,7 +119,7 @@ module "child_stack" { webhook_enabled = try(each.value.settings.spacelift.webhook_enabled, var.webhook_enabled) webhook_endpoint = try(each.value.settings.spacelift.webhook_endpoint, var.webhook_endpoint) webhook_secret = try(each.value.settings.spacelift.webhook_secret, var.webhook_secret) - worker_pool_id = try(local.worker_pools[each.value.settings.spacelift.worker_pool_name], local.worker_pools[var.worker_pool_name]) + worker_pool_id = try(local.worker_pools[each.value.settings.spacelift.worker_pool_name], local.worker_pools[var.worker_pool_name], null) azure_devops = try(each.value.settings.spacelift.azure_devops, var.azure_devops) bitbucket_cloud = try(each.value.settings.spacelift.bitbucket_cloud, var.bitbucket_cloud) diff --git a/modules/spacelift/admin-stack/root-admin-stack.tf b/modules/spacelift/admin-stack/root-admin-stack.tf index c35e0ab27..2e2b4b38c 100644 --- a/modules/spacelift/admin-stack/root-admin-stack.tf +++ b/modules/spacelift/admin-stack/root-admin-stack.tf @@ -84,7 +84,7 @@ module "root_admin_stack" { webhook_enabled = try(local.root_admin_stack_config.settings.spacelift.webhook_enabled, var.webhook_enabled) webhook_endpoint = try(local.root_admin_stack_config.settings.spacelift.webhook_endpoint, var.webhook_endpoint) webhook_secret = try(local.root_admin_stack_config.settings.spacelift.webhook_secret, var.webhook_secret) - worker_pool_id = local.worker_pools[var.worker_pool_name] + worker_pool_id = try(local.worker_pools[var.worker_pool_name], null) azure_devops = try(local.root_admin_stack_config.settings.spacelift.azure_devops, var.azure_devops) bitbucket_cloud = try(local.root_admin_stack_config.settings.spacelift.bitbucket_cloud, var.bitbucket_cloud) From 58e8474b7849005e7a0fed90ab32c1c4090ab083 Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Sun, 14 Jan 2024 17:14:51 -0700 Subject: [PATCH 345/501] feat: adds support for passing stack specific policies (#954) --- modules/spacelift/admin-stack/child-stacks.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index c237cc8f2..6bad152c7 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -104,7 +104,7 @@ module "child_stack" { drift_detection_timezone = try(each.value.settings.spacelift.drift_detection_timezone, var.drift_detection_timezone) local_preview_enabled = try(each.value.settings.spacelift.local_preview_enabled, var.local_preview_enabled) manage_state = try(each.value.settings.spacelift.manage_state, var.manage_state) - policy_ids = try(local.child_policy_ids, []) + policy_ids = try(concat(each.value.settings.spacelift.policies, local.child_policy_ids), local.child_policy_ids, []) protect_from_deletion = try(each.value.settings.spacelift.protect_from_deletion, var.protect_from_deletion) repository = var.repository runner_image = try(each.value.settings.spacelift.runner_image, var.runner_image) From df68693c6c4bca8bf8c190783b4e00dc669f3fd5 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 19 Jan 2024 10:08:36 -0500 Subject: [PATCH 346/501] Lambda Component Support CICD (#956) --- modules/lambda/README.md | 3 +++ modules/lambda/main.tf | 12 +++++++++++- modules/lambda/variables.tf | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/modules/lambda/README.md b/modules/lambda/README.md index 570fb0ee6..172c3327b 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -107,6 +107,7 @@ components: |------|------| | [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [archive_file.lambdazip](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | +| [aws_ssm_parameter.cicd_ssm_param](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs @@ -115,6 +116,8 @@ components: | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [architectures](#input\_architectures) | Instruction set architecture for your Lambda function. Valid values are ["x86\_64"] and ["arm64"].
Default is ["x86\_64"]. Removing this attribute, function's architecture stay the same. | `list(string)` | `null` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [cicd\_s3\_key\_format](#input\_cicd\_s3\_key\_format) | The format of the S3 key to store the latest version/sha of the Lambda function. This is used with cicd\_ssm\_param\_name. Defaults to 'stage/{stage}/lambda/{function\_name}/%s.zip' | `string` | `null` | no | +| [cicd\_ssm\_param\_name](#input\_cicd\_ssm\_param\_name) | The name of the SSM parameter to store the latest version/sha of the Lambda function. This is used with cicd\_s3\_key\_format | `string` | `null` | no | | [cloudwatch\_event\_rules](#input\_cloudwatch\_event\_rules) | Creates EventBridge (CloudWatch Events) rules for invoking the Lambda Function along with the required permissions. | `map(any)` | `{}` | no | | [cloudwatch\_lambda\_insights\_enabled](#input\_cloudwatch\_lambda\_insights\_enabled) | Enable CloudWatch Lambda Insights for the Lambda Function. | `bool` | `false` | no | | [cloudwatch\_log\_subscription\_filters](#input\_cloudwatch\_log\_subscription\_filters) | CloudWatch Logs subscription filter resources. Currently supports only Lambda functions as destinations. | `map(any)` | `{}` | no | diff --git a/modules/lambda/main.tf b/modules/lambda/main.tf index 29d808b00..897976789 100644 --- a/modules/lambda/main.tf +++ b/modules/lambda/main.tf @@ -2,6 +2,16 @@ locals { enabled = module.this.enabled iam_policy_enabled = local.enabled && (try(length(var.iam_policy), 0) > 0 || var.policy_json != null) s3_bucket_full_name = var.s3_bucket_name != null ? format("%s-%s-%s-%s-%s", module.this.namespace, module.this.tenant, module.this.environment, module.this.stage, var.s3_bucket_name) : null + + cicd_s3_key_format = var.cicd_s3_key_format != null ? var.cicd_s3_key_format : "stage/${module.this.stage}/lambda/${var.function_name}/%s" + s3_key = var.s3_bucket_name == null ? null : (var.s3_key != null ? var.s3_key : format(local.cicd_s3_key_format, coalesce(one(data.aws_ssm_parameter.cicd_ssm_param[*].value), "example"))) + +} + +data "aws_ssm_parameter" "cicd_ssm_param" { + count = local.enabled && var.cicd_ssm_param_name != null ? 1 : 0 + + name = var.cicd_ssm_param_name } module "label" { @@ -52,7 +62,7 @@ module "lambda" { filename = var.filename s3_bucket = local.s3_bucket_full_name - s3_key = var.s3_key + s3_key = local.s3_key s3_object_version = var.s3_object_version architectures = var.architectures diff --git a/modules/lambda/variables.tf b/modules/lambda/variables.tf index 2dbcd85cb..f2f1ccc7b 100644 --- a/modules/lambda/variables.tf +++ b/modules/lambda/variables.tf @@ -312,3 +312,16 @@ variable "zip" { description = "Zip Configuration for local file deployments" default = {} } + + +variable "cicd_ssm_param_name" { + type = string + description = "The name of the SSM parameter to store the latest version/sha of the Lambda function. This is used with cicd_s3_key_format" + default = null +} + +variable "cicd_s3_key_format" { + type = string + description = "The format of the S3 key to store the latest version/sha of the Lambda function. This is used with cicd_ssm_param_name. Defaults to 'stage/{stage}/lambda/{function_name}/%s.zip'" + default = null +} From ffe2322db0a87cf09b295925a2e621f7430eeee7 Mon Sep 17 00:00:00 2001 From: "Erik Osterman (CEO @ Cloud Posse)" Date: Fri, 19 Jan 2024 17:05:58 -0600 Subject: [PATCH 347/501] Add banner (#957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: cloudpossebot Co-authored-by: screenshot-action 📷 --- .github/banner.png | Bin 0 -> 1045919 bytes README.md | 31 +++++++++++-------------------- 2 files changed, 11 insertions(+), 20 deletions(-) create mode 100644 .github/banner.png diff --git a/.github/banner.png b/.github/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..ede1e98e0d6e21b0ef2e6536ad113251313b2473 GIT binary patch literal 1045919 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

$HGR)54{|I z`)vnmsX1ecUk|&i{m{LZUwLHMNj$c%e5OG6%?DPPptv*5Vl2JD0zSfO7@uD!-ILtp z@Epf95>{4s!?Er1)0D(ut?%9VU?AfGdbr_bGQ-DdqdehwADTqP{av*!|2x~RN^qK+ zkUPY9)YpGEC{r(ppox@(QNq{C&}~bKfpcC~gZ|Jx4JqEvKT1>!A6xI=;~D$wP@!b^ zgTfeZuRZv&(>%}p4)~e^z=8a>uxz7V2KV##qpNnmYHh+-?Jlc%?fz;Wk)ro!fKQ76 zN&c0RHQ&^p+X#r&cL!ehXrnT+!znh`wj<*<09VuoWgD+@6irR8bVwXm&dKi7 zl6704_pD2?-BmIv5}m#-h!x%(N%47dUKU%hYyhaFyo`Z=b+8h6ikDW{7NZ){7Ro z(L24mO^5sb%vvF3w65{-IdH?iD)dB&X_IIxyE7!e_T`S-nhFaeAtv%QH}{jxq^fi0 zb-ZAH8@JvtT!@RwZyi0bp+>EI9PTn$*R0lTx*5~Q4Mp=dIiQ_sii=X4i=sd=*X#QD zM@<;RITl+At>dDh$Ib`!yZ7i$=5>U7R+2V!znI@C+@G~c#c9#Hc-(#6`{T9`UJf9MJs+H2l0T^Jf^Wm;o}&XHZNMtu#RQp^&<&G6zXfjU(pVBDkjnBlQl8% zwb(SF718LG%o$3gE{J7GHzyoSca7bwI09x}T!YRTGhDi=4oLm}dkcTs)=f04uv)@j z6LgqlRtCmeBN@h<3chaSnP5gMnuSX}Ug>cTr6Cd1JjzIBnKs}CZ3d4*PkX9moePZc z80d$9pGf-=Ot%oTgKsgf!aakOC+d}SSC+b7TA_YQh)vJ|?PfE5X`Qj9PH*B@(2=E+ zhcU6>xi9}UDV|(3B~8LZ6COTC<*OV_+!NBq1ym(TzBdDV4cxs0HeJggXIKdWX&-72 z7BTk@zE+h}ky*bQEjJWVHkN8{80|0obusZ-RpJLxP=Zx$s-EAQAEgGv&>DVya_r9K zjR7&L&d=hiR5m4&qf_eo!RP~|l%V9o~m#ObyY?Pn^-aGTw8i8= z&$zWhg=c3&xZSKmFpzrOl^GCfPTBB(V)!uip79AwYQ;AjR3pkhT{zhDc0hJ5Wq1He zCvd&6%LXC0lrH$)jW~+gpI7+hT9h&uTwq`?dDkK^5uP1crW4np9$9|P0E@-i{lZYO zdW5l#Pbj}boF%gUiH`wSfj|I7OvF7hbOD35f}O-a<7!+%@&U2hq_E& z>ry&t#wH_LO;3-#8ijw7P&Yv0eUb0QC*&%(FgvVMZ#_m$!!=JNsBrI=AE?8)G4Af$ z=hWPY&n>)oY$?TdV&jCjR6{Rq1e_7ei*MvkYTJj$ncuaXF-Jr_N#-oeX`T`9KE($e z>A`kRf+?-&T(1dI>WB&+i0nc(Gz3jeqdcH_D?uF<%!gWB*up!e_(vm_4iB-c&tuC2 zcVjZ^!1I;UEhHKZlQMtTg%niOm>h6!{&|1QQNOL^ZxHXAWdE@}^l#g4VrS4-&I@Px z3rn(~`b*_vd`L&-Pa@3x%^Y^l0kM$ZoAbV=az%%)!1}dHs`sFG&W4Rmx;0kpD!eIo z{Fr5+b#ueW?Vdl2*cVy1@H)kvN1>tSUN{RE|4-m9Vn(z63g9q&B=P6;#Q;0w%bL{% z`pxT(ll?WT$g%30^uKo4TK?!-`)v+baJ*^IRJ}nt1B^p*6G#&CCD;=ChNiVNW_2?B0wx%XOjxnS{zVF?Te$1Um!h+R zD#lS4)w*#j;1H*`gU)@$rvJ+I=_XB3vYo>$C)WSW9hw&}8x}1T4!%GUJ8XY&a6#5s z%Gh!>gfU;TyyF~Yl5?l!DXtT2ofeL&7MPpVQEDiCs`ti4`A6}*-RzQ4vYp8?;++O_ zZVR(n6cqUe8dL|1NN}5xaLuiki5v;iFo#URg z0*S}PfravOfww)&o1Tff&UTg;=#OH~7t6O}?g6gEoeONeQ2IopEq#rRm-dq4M*Tfh zCLWRFbtO*(Bx*}gRlNwUsZ&L7TM14MM(0M{27mQ;fi>|7KGn;%u8#v~Nju(F5B{mP z|5kW8Z7JZ--LN!@_H`EzeaLzi8lNYiMhAAolC}qD>ZWP-I7sH3t81C_dwCc3{gS%7enU?iChmKUEa)cxAg|Rg0o|?DK}uf*5zxl4Szw*QiT!KQ~<| z9I}hS)35dV^(CL9l1>h#nAh2X&MY(@#=B(ctoh47|9}%~+@FcJm$@a>Q<82+DOuv7 zi@#H}Bo@KJ;`J$L+J4WZL#h~Zs2|Z(=^Aoo)l)xh1#*~i2Qj!^IbeADehoD8tjl|F zAw3=Vf*Aip)s%(U_9@`86im}v1b-xGMu>iXzm!-5;{!~x*eMGdYu7<8JJN(kiv&!1 zWE=LYkrSOsuhfM)Xv|;y5_UB@AZ%X4?nbZDr9!nKgp6mNew_bPX{F;BT*&S@>;C*- z{acXau+LYL(K2*^2F?H7l%#z@rR68z%mbx^Y1Cb|?js?w9PAJc`q^}N1*3T<#n|0z zI24;A3>t3i{j+cv6yE7*Ec_z_l6{Ku>JQ$FpEvy)+#PAeQVN%M3)1*xDBBm{irlr~ zXe2$fjR3B}oe7uJcBTtk2)HEuGK3v9V&ezsU&syj^i9U@H>e7ODj9}HYlHH7D=E9! zwhTAw_Y3jL6a+f>H{vH+lUX|Q==F-)H1@_l%{hkKV6(?$o`br7`p72x0*@tUs`xtg zX8LQ6w)4^ywk>b!E$e#veK&9HOH+l=A|cU0bLr+!EBl7hM19S(P+gK7F|`91y6V%I z^_Nqs^y9m-9}s~$FW%xxW*w;3Ya1cw*Q7ub7sQ*W~@L)tqGnn<8Of%=V%kXah~}WcBk0On)lj*C79(V$i|r zU3~__wSXR*LY;K;17H$0QH&G0&ce#`2uIC5Z&uW!ZFmdl*9Y^abfP_CJ+Fg<-NvjH zNj5-RP4~O6#Tn+cZ6)qlcfZS{yA@1-nuhy%;NNgJ^7Uspr+h6ER0CqEGlWFZ^SD^I z7EIfYrx{*p%3L+rGG{(4Eh=IYDg?W$NHO>|2R`_3CwTr9RgtGC{8XV1X22(5GkCtP zI%1XnRWL!m&bdxEBtSDMGH)!2^_VO;Oif^YF%7)Pdz4hoM=VAPr0Uc4h6aG(Gi4LB)d4y@b=-+0q~ERAqP(@o>U zY^&zlbBVSdI|kR4*I?_?cxic+MhcyEjI{aj$Nh;k1L*ti6ha0V+d ziZ0EX$)!%lIP*sHb-`Gn9__K`M9f@G@%Z3ojGCrCHJNeoTN-m0G*RAX!E{4*DCnvB z9^gVkWkTi-8x=f_+sRw+^@Ycwb2|Zlm$ZPi<_Lcz?uanhuMnEe4#Gu z0ZGZB{yNgRmfV8H%bXCq?G7Ru9&-i%O?mUEze0ZaaEpZLx24!cve^Il-yhjk#duWM zi^|Uoz5ObyK=V$nQ_72l_DoKmn-XwySOOMTBB65w8;Qse9)dOZKP`Z0c!X6rJ1^PS z;+%E@dHJC0Q$3Do30iA3Fz>L;Bx|%eM>$uAj(SpL_|x)%wnZb;Z=MFKVR9li*zNxK zNTXK{Vz$Z4V1x9?!B;V;e&vO?6Va^gAsAbr7U@SDHnbr~5HInJoY2DHi&E4XWg?k zv+b4lFRgdEo#a|U;7uPOEl2^3h>?vIP3l4vijpt}p@(`FJtoO}UbhCf_YPEV#ou(< z^Y)(D)%w~Bn;uTkd(mFZaUMm|S>59$RNCj=j$B1zY^`O1Qd z6~x|cmnb8eAWgau*O3l}GnZVUx_ZD~)sX$LO8Y{qUCS+IL9bW*D!O5wi>t2r;I%J2 zWH!9t;x!f)WRU&@kBJLYt4&10HVNKb17=j75*wXcoThD5wUZMnUB|I8RfU2+&R_0n z`!Z$MoZw#B+k|*iMOAxQB8}(i=7diP4BkXaB$^-d$@4NfZvsb480=DV*b3(lpt^`b zC;C@cudIl~-t+-mEOev&qxFB&PJ?iDJaqw?k$^}}9-^cWQ5zMjRRGYE{wQII1bRH0 zmBySMnmebwxtjS9JJ9d7F}*nJtYNX5^&y9d9deVX_#hJagcb;fdS;5#oEBuB@X~h~ ziYN+WdN6`kEPu%3`xs?AHen}HotRZEC4)FV!B6y>vHTM5Cgz#|CQ^9**(AOkDR#vH zSdQJck?4dJJZLF1lPK!UM47I^%>y@agF_pCRJvK`VVWor5bH2r&ytn-Qpp4iQ_KLv zB#=kbT8pq=^aMA^w1oPQ^}Ob3!qM^af1DI~mUp3ZW5`cemOx~28a!U66rMN^%~;UE zoWKaKy;Nl7GbT3t%M_k5cP03h;)lxa>@)0&z^A4@N_^1%%g-^sZ?Mj3>6Ew2G&o9_ ztjwKqdhOVe9&a(8a0&lRdm!qDr>$ogYd9$%{>%j@>A8Z1_-VNJ?u1?xb$#6RY#0B} zHj_eoYtf%)aEX68DpZuUA`tiy8w&1vOi1?)aUhMA4dPX=wy=3PoeA%AhqdSrf6%)) zJ{Rz_H_NG|(YwbYCa@s$<;K>ofZ@gL(Xzr=Ciytc6DCt6xB)8i2ChL&IxxeYv6!@h zDExm2<;hb=7#V-)SbkBrA9-DG$2P3?kEGsbtBD<+7dH2QaCjc3DXx06G$^cQc9nbsSzh*>tkiqBB*|CH50a0h zsFp9EwAz7qp^-l3d0nJBEps8w!MCK+d-vBN^20-TOklpG<Jw@v3fIk}kdHg)i z;rp*aaLY=LSy569_bw%5byfgP6>{cYR6`zK?F$V!KW%`vcD-FLHn|?Ue+N=F;AyA9 zli3v_08qXT~kHeCoV|ib0a3HT`F7Zr+F%ZYCtZ7suGu=$U6P z?&Vj81LTko%BeHNbsL5IL?+ki#hvjBlpa(q#&aiJ9V{8kQ+b+^PS?G?a*J~izq%u! zhkH+DIe?U-0&N1JbD`l?>&LqczT7JFr84x?2OCdL>s~Fdb9B7HF(}5T-yIw=%o;YV zyYW78L6v=rg$)}w#~lRO>p3r$3HEn^J8R8o zWT|pC3ds;0%)iNGY;c^_9 zRKXi1x@~O>)z^IGyVn;**4_ytSem=0HhB-|J!o$9AqG(2&5o1sT8r`$c2pHxy5v$i zZeDk5>kHJaDY}r}xf9fw46(Kp?w)|TftwkE?YVjHVOQgViLCW&u$RFq2pOC_n2o(( zAM7FQuuV|yZ4@Pk#IW2ke>$z>l7n?#3Qz;+2}JRPs<2t;fhy6vp4>soww+jcL%x;u zCa6Y}FTegDipI~V>6=HiCZ>CC%0%ZcDDag6yBw5=zqv8EF0jIMhhWzPZjVUSX~3#a z_6#(i)OB#-fp7ZBiMPv9=q&a{j_WX7;T;5+NqQ0F4WVLWL#_{aGmQ#A==~5sSUy^? zz2a`x(e(O|xeb7yF8}hPRY&4vQGda-Rs=gg(gv?`c>{IuV2*q^oOYOXx9BDpPPKVBOvg$mSMWRUMx;cpK3`?0L%l|)gOJI1H*wwk zy!CFpnfM&<@oLhuyNa~f>2|^$+Dca5tn&(R12kMXkT+E&TwPBC#!FjJ6Rl_a4+CBr zE?jh1)C`8ceQ8uf>0!SOG63r%a(oJaIo0i+*I}2&kEc`L21C6x!i^i<3XxGzPs{af zV=(fOgilc9wRJ+a^`e7Stu*7~6QHj2TTRvSpip&EhnvK_j5^#<(0S1EIQFFZG(CK4 zOJnRWER?aWmb=Lkjri~WufsX}o_icMj6QIm#Yxz?Gsbf5(CC-A5sV+wcq%Dgb<4j~ z!{Z9OC+ozD2|Mr;J5s<(K)D{Rf8@m_I`?Vv4l$%w@>3*jk3M# z`zCnCN-P`e4#*ZYw22?2I>c-^bdhDkK#i-4KW|H&ibt20SHT0L7x~{iPSCE3?jh%7aP3A~TZ?uv9>)z36czlxcBKcpv!jS8?C^yUT6v*pq6kpk0 zS#vip9RNlqy6q4sg~JdntUnPUXWd?aEjqTqF(5_c@lEVA_lYUnHTLR_0e7EUBPZ&i z6z#<+O~))NNp#@a$3ozoX%u7+ac7O?*R*9_@s-ru`$&zE;D(=l(v5gKdlp*nwd{J0 z{t7hdZR*<^7HqPrg|)9q zDu$zkC;_^zp;vh=PWYp&R^bIh{pqy!YK{3l%>Y8H096kv>Z)dl>7~nK8?VsKISJAKtzjB}{Cv_E)mp`GMa~DVS3fKR z*6EzCxl;t)=$*r!jNR%mp*Se^&=_QuA`5&jVy+oGLN}*PEE^G;cQJo)tFPVRJe5?n zPtjlT{VyU#nq=4o$AP7d?KHm$;3U}Kph#Dw$G?79r4(*yVa`0at9oH@R5?zRH;TF0 z2wRTL@Yf{SPd;@1rn}Z#&iB)!seiAZazr;krqr7Esllc7QLZ^-;h zDRn4n|25%xX63}aQqlG>Z2RVBa1fqyyF!bo!HGePw8dg>$@jhgx6FRjkeJZITm7SF zimejez{rnhtl&$ejM+pF-nZ%o>?uSYDTyk4FR1Jxr> z)thu)+63=d3mh|9z5Q}qI^Uz;VQuXylxL8RG#_0`V95pxlSHlQT#brE>1dDHt+`XVX@bG%YjSec_POl(kblYb(YPMc4L*puR(PZ&v2WA+5y+YW&fhSw-`cB6a27>r ziNbUmM(UXEdR91I??Nse6T=>P-*39jkSZlZ1YhEg5!yoC24j$qdTF0nb}y3*l&+!f ze;W2L9Q3;}R?j+Uc-?#AX_26)4QZ&fQ?zdbTRp{_Sy^HU@Um`_n+t@2W5lV!%ovTq zFD}JeGSWPCuPNDx^;r~iz~%x(iRt!ufs-Rn8d>{&)6>8b)AuTuQ0!z^50t292~eoq?RIQm!Gx2&kL_+ZLp<#gNA zSKMc=!jJP(4)ZC%{Uq6MHU2-hmalEWRv~8h?$2!Jm-&YAqokCn8kFXeSLj}!adpil zq&XB`31=K0@|yYg;Mx2>2eFm9BV1wQMK0_h-l0zgUAbRt`k%H}{XV-1mAOQ!XzW?T z(jPp}>{j5b4mFV2f;&Qk`lXLH1bmf0Saiak5lTLGc~`28bv#V)xoIYW*H|%AF*1M% z_|^F@X}c;V-=}OjYXQReNJZwkDUAQ-_lsU^==UMxl}FX)#XUM*ekq3n&9DVKHrp5O z*?=*|xL=V<=&h@VUa2m_BbQF|5JQaygU5SKUf{I-j+`2^{-z7N{Q}<`@|0M#YEP^; zt$C5$uyBK&KzAJh4q)DZWDA4WP%QSXF41AwiLWz7Ecxhf!cq}%8PvKFd}3vH2tm_F zS-eYZ9HYwq>0`f~)rSX99%t=g{qe{258$$GiUt^YD?^dSUB?H{pCR5%bHE1ouErCN zc|G^n_lsQ!U+7WEujE10q~g!&UpqQdI7UWXPN_2zjmDTS*Rx*wh8GCzx-O+&)a`=f*v*_L)snyR>pHV3 zg8!nxjq<@G7tP(xc>>Ln3mk5gAZhi*hW6c{^K>0aJ;tQPm3e66C_xV)xv#-pzj#;V z@vY_6W%IA-FVSq9YrxNnXyScTlh_U~IbC$t3U$Vf%--1yp6OT6q^_HiMgJJ)Gmd)_ zZGzvCekAx4Tdi|rA0Ne>adns2Nn-vJln?xfL>-xkBym{zwyn~L#xrTDIJaa2%=_eP zK~BjZabFpGfhyxBKqu%+R*2?J$hYO&m|}_<#-+uD{yw?#3+0U}L2u#Zz`x6?rQ{ow ze(ujE?L8pcpJ@#$u|Mit;RRH-;GyeVY@M6<&cuDxwTdIDfJH%$UbfG9MeDboT~7vd zD-6I1S$(4H9^JK@Td@(qw+M^GI?9dYpLKcbK#?~ucHN7ZEihsI=tE2XY=YW~b4eqr z?+PagQ(%_X!q(}1yvfo<;kI}S`SHo0;#qTRH)DZ@QCRek?w~$s$=;&x(L53RHF9@_ z+WEm_)TYyG+$Na=6$hcs{|3IcyGneq{u94R7U@IaBk9!=S93@4pXCLL{{XjK@OFHcH$c~~G4u?+Si2_{`{ZxE*!741knVr3{ zj;9kl-oLU|Sjq3~qKT~Hi;j)1>}H+{ZZbX6X*C6@XBn7=6Kq+PZ+>vM)@+%THj=&i zj@gI2HWxTyg97f@(gZ0lq|68R*)$F==KADxDx0g+&~{Eesl0*;d1RjHvvoO>%T``t zcNZubZx+99<>DpNk~#|=S0eoI=Ldp+lIn=br%o2%mORrI10B_C2bABSiM5ZGs64>2 z$vDWqaq zb#ENHT|14Jt)YJrX3@{zzl%DM{L60cRGg8$cytXG7uG)AKqJutrye(|I5mxX%ZBl- zTsG)uMC5-9c#8p}0P{~#VF4sL&Y&zkGSU){n`_kkxdu*-6rSQ%QVqFR4rtWv(CKji zxys3Ke>~c#nzX9lKK);ocSx{#iHR(h7Zu3=bsqUG+e=Wa+PgP+&UlBU2X+Y1?JBU* zSK1lFeTO)XhtFS71TR|~zGw*=Fv=awGXeiB%dXl&X8XR_$f76DJ*oG+18XXo8BFoG z$?0>LN*0wM?uxVo8u}kh^2|E)Pa;5DtqL=845Y}e|2%5OEIW&U4A^OMZ^cHQF z2>9~m9>C2c)>J8nCAs5jw(y=tm}9$4g3h=i1oQTTwu;Bt=is`o)uXT<>)z1IM5puu z_;OG8TEV^qiv&%?kBBN$1O!x8X`B!S(Od{ng=Yv3bdJHR60f_ut~k=7NYL)1)h>=u zZ~9GP5o*ToenP&v}AcCdKNfSHV%Xe;{aw@tS@m4 z-&o}sB>6rVH3+KOfEuq&l#qU>3qyCoa1B+I6p}A^w%3(Ul#wG4NxaE_{Zm3 z18+Ay+xMeq;7Fu;1T9Hk1FJRs7=DQ%!}PZgXeti52GXB>|o?EG}i$HndeW27TK7P)sxz_ zPFnzQu7+(hZeqq))3i#@q{wH~5pN-OEbSgxg6zi$oKj)I+?BghY<1#29ZlGcNJban z6!)bYUcaQcI7~B5-Bh7R&{9I2f?VMJb zG#MH2)vPH({auyZjp=5%{#vcklvAhjdKf&~3EK4Eh8Y#sDT-;x6TaXQL!FuOS2ZOv z%j;9EidJiDu3B1$_Nv|34G08TK#IImZt-3lGp+h1t!`_CzLPa@Lmy-8SWk5zY{I->JQKSRe znJXLodAYkWzhC+elPwmjgyl-0E*(sj)>ETN~-xZ?JgtuU8RynS6rr;0#BZqOumAc=&ZsfNse9#z3ZaFpl=kEOFn~Y@KyP z9%AkV9X*ISk^?Cy+B)T(q*PXWk<}gKQn%T~U^1J$zvx5DY*|Zt5ci{lPHrownIvCnZSm!zp78iZ8o8gvqz3i+iG?4@b|8%Us zYhSu2L2Vu2w+)l2`3~YJ+OzI*;>=5b}K2#O^K4aEn-Of=r6^ z`kqKP&U@FYvb~nFoPxf4&4HKckxU!HvF=9}yH}xeZ&)R>_<_XvvrRcX%W)cG5S_+2 zwg94Ag9=`;V@(^;|NT|okgbhh+6fx|gqztmcX&BAlN16yec1lVYQ4Ei7YOd}YK?Vt zh!Rc_OL>PTY}|gZD&}l2gxuyRP8^p+11>>dqJlLe*>3{0=YBnyc!z%rjmFL(a&%q* z=CYbSxr*ZeB-yJDF_Sk1W6DY?bOzz@JFX?>0p@IY)TP zPnqaQd?(()NqX>7kb|$~B)^BVW;*c_W+q)@bW&n?$CKYS-J zhO0y@L?(nDn2tqmCS(YX&r=)Lf`QY)3*5#Hl>y5Xp$GIN#4}51oX-=y+lsZ5pR;Uq zzMP?RU~nI0$mop&p1G;QFG43X2hmTq(eY2@FSC_BENn?3_kj&3%-4P$D7kf;-mUOFQ-FP&$ z8cJgZnWhbNPT_aWu8cBg>ac%{3PcT#i6rj^RvP`Oe_Eqy?!uy?lu`N59GmC77w}tj zGjtV~3!V>iY{eIHL5cdr%HiD$6j!)WQAApOa5`jqaH+X&A0@vpnGKE@ubDK{=AOL} zP;NGKEMed?*c(6rNp-0Z0Wtp@sK6 zImTZ^7_bIUZYLZEq8Tmx8qjE_C+jR65aOjxTE(axoGVhf@ z9}@F8s#A#wf0@R%O~wUlnlaC9{Ag@>f3Lwz*%8BA5+^c$Cui!xObgJF-c4^AYkfv) z2QrPm>%Sbo%GEWz^3U-N{W_v6zLcC$nqiu|-aD?$_gr6l74%_@V?JLrIOkN{4c#EM zg{2J5BA={L0>00>ZvYp>t@`v!jxC>&y8sWIPn~-EvQG@Xk-)9)Ym9g}>sq*6I7ly$Upa+i#*AzU%p!9B;A z-TX72XDj>$86gqlkM3;3@T$w(Dpy=+^+0ZC zG2}^@(3!PhoV|9gw!1MnWVyU>fFSVDi^LGf<;Gtm09&i2SWFmXM2jL8>LZk-eaR+xSo`VCB~Oc$ zT9=};=BaDIb7gLN4M!aQmFh^oOKWcZKe6TeP{;bk!Qqi>Uy0C_xzDVB`;|I;Wi`W< zN78YAq#vh5xfo1_T%~XO!H_#i5me{Q0+EVSGMhUattnTMXGC;e@asb#SCOC!}oqMUn z?AQ_}Coso`pi;JNxXAL~W)F;+y=G>z);(`rdo8ouET@+`4_#A!w0J)5Aw^+ZF|B6N z5%=4_=Yb1G1FO7mqiS!iAXDMZ3CF>YW+;RCu^@3xZ;gR8-nU`=h45Rvy<9r}KdbB{ zJ;x^>dUw2);ZKH<8J?t$hQ%q8D=7BY`(0H#m(ryfK%e@MUc9ZGLawIYjw;*II-n~Z z-?zz$sr8V{Yb0;C$9sHcGp8#%RMS;Fcm6r;e}nxub}Dzk7cUAW#7cLOA5q>plb`~7 z>5~(7a!KMTHqNw?24~V6pQ~T@@PZWDmhQ7;tK;2_cc-3|4LsDz5spy}WS|AqCvua% zJT(MG!7_W-g%E;M9I=Z3jwHtyMAtY>x(sMvm$*XyDu-3EIh%q8o(nNslijKeA%~Bz2)TKmY{c!7p zG|Y&?cDEd8IsI-($O-J^!SmT3A-eFOk%LXT@&;)Zs1?yd^bz)!$Er;VH@`-SYx+gxWzQSUEYCT}~l?=P;Esmjb)2g@lH4_(t$u4&0J z`@YAlSaE6Z>^c!fsquZ5vFT~)*txnK@-RI@0~-1l+#<^uoH!O$%fGSWoZ(H1!c?MO|ypNl{cKmuz z-IXYtp{lt{K1Bs_bu-Gj%1`U%(7lp_>!JOmcOf==!L*VQ&zr~FvNqCJN0=jFTBw6b z0H(*{1T!Cqu=7x4wDWHq|N>UVi>gLgSk7V?&Nt>b2Jn5|t zCNW|U`&4cU@@-zIQLb~9UNnezDD*EqGzX!4z`cd6rbe!SM~CF1vc;cGypzoP_t75n zl*MZNgJl;#$S6%)ER&q!`SvGShlc(E2@z5)qXfR5-2nG!7W3TTr?#qvUkYybh(&-K z3?uk2$!DzJhQc`-=r&Jt{1z`cvl=D~ZWzzSA>4Y^UW3tr#Ug4MM$gJk%cY7Z;e>X1e5rV z#zZ~fEO@1w{5+b!Wk#R#;g|e?(YEdb_3ixVRawbBDoEsBwY2JqzsZbTJ;1PRElKE5JL>8VY-jxzJ zj$bpMxpPU_#3xBRr0)kZm42N1-_ryA(y=J(WoL^Xorapb3lGwl$jFmvS?H*(E2PM> zaP_6*NAKLP9Y`hZAP-NGh|U6^MIa&k{wVYZadS3S#lVnQ1e(Rv)k=xU;4tDY*JPMv zLiEek#4jXLLYJlAJGw;~X78vI?#mz~#VeJJKhQ}vzsdcV8P&@Rxjo=6`gZaBg7(wZ z*LIy&8RW;LayoBg=6?a=iltuep(hI$GhXFtfEJdx`EugZP3sI?f<&j`bqjfUWJKji z@UClt?srB)N|UnSk@xdCRbvzv_S0BAdBcfgIxFvF#4h53;j;3}gIvimS?bnf6QszN z&2SLBxY82`<`tGW-YP$|$QmJ$q`qI%0b@o(EFO`L^nw?ILt>!*%P=dM4wy?9n#i&`{>9 z#FWAg&2{}H$%no_&vD7BJFsdgN5K}J0+B6d=|KgRy4zndwks(ZxvPg$6)tVByGD2# za6TuLXM<$%D-sK~r;5UP^}9K}-CKf-gAU17*FY9}6x_Dr#*q0^?PoFOv}(!6l)inj zXgqh*Io!ys#*eOK@f}ycjacC}1MgR23I(lT%wdJ230_^SSA>L}@)AdHR-KH#>l$vP)G!mJAZv0er4|05hJ9@zLqNXK+qg8u`G zAUqNWdQzy#r2mfn!`9FB{cvKvJ|eVXiW`D^FB+d0Wz{B8e8+LI(qgQQi#gh`)0khj zS+4(aS7tfgMtH%|K6^P`1l59H?jY$ZKSKM+HO-$K15gDpV7f*_yxt4An zKZwctZj*a^Iv5&I`ox6n_hI-P9by?QdhW-He6jBVD@A>|Fm{`NSpMa^t}Z%=|vfM$_Qzk-a1dI{Nc|qLb!0}0GhVeinG@e9uWCrtBaPZu3;mLi4s7K?}1 zB~L23tcSef9Vw}p5^FQBWPaVlgKzA6D>wmxiE{@(j21LBPiOixPUA;?`!sT@KLT98 z#a~;!j(wG{-oEAfiwF_hI#R39P7b{e_L0k4MS!GVdfZlreAe228ptlR;3%6T5_?Z~ zFaE(sQ$3qa;Uf-KgmXcW=_m<7j3fmR~-20_mGVML7OC#AnmpOm>fti48Ap4=gruC1YQ2D0zN%J+Y5>Ns; z_`E25kKx+xaeR@X@*rui1S%K+HI6h~VIr~tRcsBWwoq;Pn1I~&S)8!!Uh%p%49kH&Sy&&$9V(%fPhm!4JO2IKV1PGlltsI%~ zdXOn+#<|P9#CuRhvPT_NsXA7A7uV(8Z`3-z82)Pi>a9H04n^^{G3P1DMj1wHNj@KT z4DXNA(`B{WOvIX}dQH>-Z3oE8b6zmRFwfeuRixO8lUTbu_>p0D@Wn4`EVw0ooB7$Qj9R9gdxUh?UV^va3lC5cCGE0A8SM(?a`IR8)?47Q7ovNllURG2 z^o&Fh-FJ(W$;($pe-cHNx;Mh;Diih~)IMMaV%zzR@1!D99)o^L43Z`|ahf+f>992ka#u&*|GtgBU-)b=-cI6};pu*gH7?yJ(wHvkKSz5c z+L1!tFk-7QeJLZa$#YB zR%#%9BmU9sU}D#=+3LX6^xwfTZ^gtAQ}^W3v8L!w2 zjy^2t@kIMKu)&kDg2{#qL2OJsaTbk2zr*!dezi)r5~j4piZ>0m>*D^7rl+`Q(Je?2 zJUjDU=o_6a*-<#Xr%hwH%gkuH<=D4U8djfO+9#XrScLG*y+g=nZK@oF`iuCTYwy{; z{Ffa)>hNDMS==W!#d+O6KwoDo-CkVy%`yLlAGVV3g5HYVkBbps%C#AbjU;_eZ*=8? zHwsXVW?T6#nBOZU3p-c4H<#@5l&g&@bF6Am!-rfK^!AM?vlo)pjKU zjC&c5ZdN^@&fWBFVQD!3mp$h;p0+C^VLRb(J0??FWlq8M2}Ok6@YD2}kQ1+jQT69T z9JNyrQCYKZ?(bcIOj(t!@g3YB)@uk1?dDcb>@HPC#I=;dSDFUPNO@sTMTb|EH0LHY zE}G92l*y9|me`0eGsaJmDQ!J=DR4=&uz1vO^0En`t-M)#Ne`Lfg3%uQt-^+S66|MP zz82i}{-&~9b>~U|uY9Lb!`{?tU$bnKZ@8CB$%bdAc@@kPzB%FF>Q+_Bw@GF-nq48P zpjF)NRAF$bVYGE_+L3FMIwYfKel*E$vE|rN(PU$EhS{!1C#y_`2d=_`;2lyCQ0jA+ zlYl%${jg~+TZ9F9?EIJV>vga5jgjK*ev-$Qw!@ke!$|N+$q||Pxxb6PC$5c&${)_L z2PFBHtVL~$XAiO0%RD64?_aLS;qwNt8WZ?1G18s4tK*oKUG;~Dj_%@vgdB0P4DJDm zP~2fu=`~&<9?U^#e^CY0Qf9ylN6}%p^q$!zz14^YN$vp{tjB*4gRXj?`9H#+M2X;{ z1;6g5Oy$nWuhJJx4Iuy*vTYMX_rL-j1F;B?3(oMyz_>|3z)E(5u zu)lc9Yk}yL4A3r*X*0AdXq+i?v0Aeg*l>Sq*n)Zc`#jM4$pg%b7dvy!(y#vrdn=Vq ziDM)e<2^{XMn+VJc9p~Wl6)hF`RAk?&x4~PO(YfWAT_-PUEDOQ!n$y3K?T;w{o3O> zJ?hmGyFUad!!siVja4this*kdnSkmit=C;BoAr@?~(P3F@bNknt5 z`X4W%N11Z|!PhbFt>&lTI!Fxn3A5$p)W)c0t97L8OB08sq2()B^AY}kvSSSRd!iNZdyK_TZ+-VX8_HJw8N|AYo`0DF7 zox%~)3Gf|0KV?Qk53F@br>^b`m_NQ3X3?^0dZjUfaUOU74s_AVpLEun1hft*77TzD zn+%s@g=#db0b}bIJEv58uF8%{#oI9F>vd??SXcpFAx!gSYdGMRW-=R0~dRa~XV;pPrw3@Q$05|DIK8 zm=2RX?nDCL5pLQp*$dj8{0Eq*OifXx>>dev1vc$1tocF=M^MWB=CCu^ znMFUf^Q%SZnsR*$ruf(KCq?9H^0jcjq~_U(WIpJ>H`Jf*&DBBw7sP-U#51S%q#KG&WT5ja!M08{AW7`nhjIi{7^2-vM?HR);vB-&A($dkcJ? zF3Sd-enEQT5%FY^A-5hheo(1SVNoSAD8IMlg~n()7F$ODpQl2aqnfcBX=t0}kI^-a z%0mXD#lQG5O`j<)|In2+@N2GEPoL_w+EHgk$1aZ7KN}NR{6!9jx0wbofTaXz`CoGaTaIcm|Pq5tM{}I>5}D$HXI>7HqSDl<)^{Y2?#z`il)7Xx;;YI z(>9N+VbwYk*WwYgn%Q`u!J|VECu;C2^}Pbf(!j1?PYQ%sOw>G~{OS&F4tW)P7lf6~ zHMT3Q8@yD#xnV92hPj9`s5L97ttOYLFiNXR8=W07vP}?os)UlyM|Sf*70>P8>|x^d z*F_xZOYG{2O%En$vgB6?uUB^SsM}?eHC?`dwm1{dUQjUSW`U$AZIAJJ1<*Gp!!!F0 zrES!Q-DD7_JjayZJ2Zs<0N)SEd5wZiJeme%_t9i;n|cR`;NRlnW#Q_ z3H|IX6vBht3Kg~lZv;imZQ-(2LzE$Bg{Nfgh0bv{^fv>z;17dwPt!tYj@Vv_u`cZ= z-^di|pUbX9?TQh7fo9+R-XTT~OXFfyTJ z7M-3$$0|J3&9LueomBuylO2XmPg8lgGTd5rY@vO&xO@ftf zG%ijZ>F>_tbyjH#lpr|Tj(<^Ur3k&-8vDG*j8l_rvPBvTz05CfJszlEkKw-!v0=X~ zgEcXg7m?e>UwI>@)VpPYy}Gl9c>2>P7Iv&_DpTtA0g8&}U@Y|(=GW22x!!l?|77I$a8)O`yRY}NB8vTau0 zg$8sZRKpmD)~H4J3&_KU$^lK#j~q+Xtj}DP{xa+U_-knE=}qyYx%SrZ`yp+Dp@oRw z&sPMx0XJlH5VP^ZG`3RCV7EN{cGVZdu38G7@M5PhWz(o+v+}(S$esSE--Mt?q>!iX zk6$;7uT=>9)Nmo>h^M0TqI(6dQ%Syal@Bey}VxurfYaq{wu4fYKv7-)-Uj3mLBXEjQ737XEJJX={WvfVG-ehyXD7htCgw} zpR1OugqB2MjxhE-t-KqwfN4(@Yw!8=zZP-vN}{Bp4NM!F-`O9Kjx>UJZVwxjXz)LW z%T_r;%C=m*C49aM*-yWPR3s#z1%lIbqb+%cA1t?qj|SYYix4kyzw%NsT23Oczm16| z`@1jGkl?np4SRkFj+SCQioTESk5LkDI+o|(D1GxA!26%hbZkbpwQ!#}w>+$sXH^2l2 zOx~A<#i-nMAe+JPf;Ywv()RuJ5iE>58A0Vak|gb4t$r}KX8kPgTYJzbs*)ShDL1s| zb~7hRaMN+MZ_hF7ek$j^l&wINhKmvqQk{y_P-XZsZ{X|lNam7Rz<&m*zg|Noj~@Q* zu-Dm{SvJQ8=oF8k?&C1HZ|j1 zy@r#fMYKn|JBsP1c%;|ZL z3wSoJ12c4c%1T1O|7`9XCv)Gz(Dh$5Q>qU6#hlhw>??+)-(5FCiwlX*v1&rt)X(}0 z&-?c&Ex3Hr*`zSsX%A2FyCth*Aib1fV`6Wg>RxJaVk7qDs^}+7Dk!HCx=BQ?7*tLpnFvgGAAO|VC+ZBCvJK1>p z|7!s%XT}*sAT5g7F!iY+E%?hc3RgFh|3V{tu(n~!JXD}si>$Jw8T+3Nk!<1a;&lbh zhQxO2l9ZsY!<4jp_2*A6SzVO=MZ0i`L4?T*+V(C4E1Bq3Q2|qH+8yP-u$$WO@SEPw zfJ^Rwg|cK?!-}Pr#dUMI6U-alPSm)lvG`mnI~Rm|7~`X^QTv>~#r+7?o#SQmde6&+ zKsgvesR?>U`-cE|87!UDtZw*rI8uqdY~;oGxnWEx%hE2)Rs%Spzx&%>Fq<1wLTr zxsq0SF)~VHwW(*bJGLj7yC0^8tr)x8%kU%*1usrwCIhBkns~UoS=;~n5b}DTl#1O%qIm#Q9%@8L+NgC-k09Lm_B84& zRfPKI{Qwa=^-Lfa0a!TZi~7m%`BrR9F$YG4y1JJL-5VP5-Cq5aAa`8#gm?C&s6EP< z3Pv|9FN+FBvr|Lyv2`q2z%qKl5thWfaX`p2lD59b_2Qs{janVV1W>%;7@%k0%qpy; zO8;qQ(Yf*H&EIv)0RE>GQ`Zo+@T}pyfHvtG`&CrWA(`qbt!<;)Lg$aqQrR^z6ux%c z^Dy61-WCEMbNfwfDArFt^C-bQyL2^Es&`N7*!H9x@3J106zSj%_#Tj~r%vjx0NJ^O z4Lh$;oCb7;bN@$QKuk$cmQC;TUl> zd+e~-e#g@Tqo+r%K98EWTi>c&?AtcFqy`5D-aRy$@O&^P4C8N$`}uuyz;wR$>Vz9V z_Ji^ID90><=`zKG_TZ-u|7sVtLC*P1Uv9~=U>&WSEoZ(^db#*H6~zp}cnKIL*w;-B3iJN;RU1yCQfp^6Up(~0$%56Hu$Ei2bIpiZMHK37i;3!!&^ zq*mG!syhuEZh;%dYMum+hZ%@UoIsTMgXhIN#l7)1D!}X~dKCt3dvz|RujdM{zI9Vd z|2r+1soex*Scje(RLBkZb^7)nkmfiqT>6zIyrNyx+p4|+Je2ts7pT%fP~WVOJiBWJ z%C-p|cL`@r{gUnaH@!!?v$g2JoH+ipK7kpr`@O9sLc!a%UGt>1^`6|JLiEl&i%SLS z9%sPns|D$j_8E5TFWgCM_PeCQ@G=9d5pf$xw)uYXs6p)}trM@no#v?}McAq3Y{Dm9 zho>#G`hOS<4Ot#c0ABxJbA!>diAQkfdZ@f6G7hj2Jr}(fFX~WV9mIeLdswtlj28xR zPTAkEF}QgsTd#tq-be<~Y#Y_??=~1ixtS>Y4{-ZyUl31_=+mTb|MUm+>EcJ~$IA&0 zTBn}KeK?n5irIO8*&epe zXP}&E*v9uHo6HQa&tB_{?oEnkVY+bZMCknT&)}=v`9PfSD)p;lcI0D#IJtWbK`7*f z;Ic6FgXU|a49pEcWMBKSq_@*CMBj|nubQ+g=ZnmiK6ULW6W>=q6 z48e%U3@+8+-!tMRH0UKQ(6?^I?3o;zT(&X}+Tpebm64BEJb9plWwxYOP z`O^x=L_l8Tn$6wZz$yeIzvA|$E~J{M@|s;y`#@6m$9s!a4TKx{@c?mB`MUm%s&dID z>=It}9`AFW8aN)D#iqyJOu(FBD;d1oMvTeBfq&2K>v7A^;%bS&t)Gc6d8A& zp3EUcX01Z|P1$A(!~us^-R0Byc>R<-R$7M!`!;&OE6>j;mD^ECYLujKi)tST@6Q@R zA<<@+18!=&gq)BC^dD*e(n3K^NgOdu$uIwxKd$5sw;B)lwz%*xaLU)Lsy0V1uEcfb z#L)VNE!r#FaE{PvzMQFJJZ(Jpp3^40*Zzh%1p2B6^MuKoyf4B0&yHnt_j7>sf-wCC ztP8w$Y*XKLM>qR_T|p_1a;5n_x4~a)C27zZ?=W!<-gKoQ$wIvj@a!=C!2IOQP1WPWCfuo{ca1 zhulOHbqJiEQW$nf&^J;Ro~^R5un)USO^Ho{}*{lFjTBS`g$UN$)p+ zyct0kSC>(+4xDzM9@U(Xt&cZxC(TG65aS`~*-9q$MXmwa4pkqP;}62abj+k5>|$z{ zS9h#7ZTxqvAF}|;h{cT8^%%h~VxuvN>%h+Ezqd)>1FVg=f6i<9N3Aeix^ltFtRc|- zil|s}_KZ~TnP{Bzh91!H)tz&;bj;<@P?07Pe*l{l={;Snbcv_YXf3z=TAD_;##;qw zzqN0S_s@DN?SRdGQTzSmcftChW&3LC8Z{`#zaqHFNCw>Ko!(JIAgo0FI3Hdi=`h># z8_{}f@%v4kG+t++JmEEO0sI-jY!qE|KNzYhDg&diXWoorhR=V1@*=y0Xhg_8yG z%Xip0>C}^$;zx1^X18GC*72s*g7v2dt9Gp?7GL0$kL39OED*2D(f^x7$gCxOM%w+I z1#`}1r)&Si%{20YWA#LDPd1^eJFP1(WaAcfvW51e(l>N!^VFZOtVxJc-K4cU1rb&q zGRHeN#jZ@rf4A)%0S>}W_VDOG{d87h!62-j=Gu4$+PWhpA4%XdQvO4TcGgL1aKp=_ z>mOsGpIa|HmB~gvt~5~{>W`z07l|^CK<+Ry&OB1u$(#P?eeY9P6aKew1~4zzYThbU zCCoQ-ZtRz1Ap*Z(nopx^^0FX_Bo+yaZEP85Tj)=|3pqIpP_KTsV^8Xl3sz!@8rgG2 zU|>pF`W+@CTNr2Y`cVi@5t1UVPXOBew>`95qij2irVHdAnCXLCnG;jy%fe!UQ+thn z^bp)aI~~!T9?+0mEX`2JCG(t@FIlQRr&q&=2MO#<*-2JPXrwE-t}8{OwfVi|wQgJ0 zcZ;-O*qYL0-Tfc(w{6U)jX??qyBlOD_lKA)NnVgU&aoS3FpWBXKe~5?9E?g`pK?1N zdM+kQ^(ll~vdeHzc~JuxrhXJ2@2?gufn@;4hE>BcF=;P|I*&`0YxkB^WG;YUU z|LO}-c;@8u&??eJ;YhnWEYwq5L-p-A>AKlVD5bs;z_;W+5783dEIUHlfYzyuv7|H0 zzUfaW4xb&S->vNk-)kWi+~&n9-e1)ncKV4orkUAAtNIM@JgD3WpSabA-_=Ar?g*U3*! z#akV_%^%_S>Vew>-el@E6d97Q5XadCv%VJkz^GIN{oDd|=ENWB+NPgA0bt55drLpz z2KUS3uO7vA)vD7f)`H#;3U~cjMEyPNOi6Ts6nPZWyEfsI5qNu!csKpuK@S}-i}7Js znYTRO8u)#Txe&e#3x9t|{+=6-7GfQ{LM$&SL`iz7Mx}KSgzVkn%t;SStvH?>o{)JB zKl>~LR}#JlJi4JDqlsWIcRTFJ|50p4BFq1S{+3Vb1#kw93|BVBDcpwd?;H9<)p^O! zyoF}x^Kvf*Wdkmc9}vHigIG2frv?#6W>`W8W26hgI7DT|w9T^u&hyz_pMoN8OTF(= zwF)QSUvFZq*v8Wh@%5$`$F!Mk#npz}P3~Srj0Q6xT^nlwB6Xb{acC#~6BhO>0?6r^ zBx-nDVqhIjQ1+RP0yN*Ot;2wIHMPf-%;8iUZ@gA{jSp29M`*d=MaYE90joLu~d*6%+RQf$p4l{(fi5 zG-mry*W6pkg6&pPl*su-mQ|a1*Ukq4cLoN?wxx-G%~5Ao`Dtjajt(*vE48VQ&}x|K z!p37q1cJuIXI98C&_SC&J{M5?oLa4u^o5zBM{x`9SmV0gx!sZed^JjSb#&UYSCH~ahuTV|I)smNrb&1#xb z@m~q;jD^TUUiH1zS885_%AFAwhPI!UCVcF{eaK<{aC{xw7$_vP2~`%kJ+be3hJQkf zKo~X?LSpn^lu#_tN)*3!OUo%?=o&w zB8T3HeV=l#5*THNcLO;?w*{24f6=#duf}=ny#*rsYoObsqv<&sHrc!rg^SI(Wx zAq&h$y<9$>ow|A2P-@27X~7mmPqa4;f#UhSyOMoWXU7&j?-M0 zaOJ!lK&GRo!OjkKVmNW#x`Ki_)POGR3rCfbfDj1bqQ~OVGeS3K!81*vyfLnE^kh=D=NNrvtvs1pzN={;wx)-VF zpUmGPvPS%fwd0AmP?QTb2MMn`;sMR!hs3Xynxr~;zbTE5v&9_cxi*PH#E|r>ccY4x zqlQ3ZU9ETJ%!W4I8nrDThkM54<>AuIxjCyC_04pE@{tD;^~surSpE*X|3w}e<| z0lhgXk9{3&pG42LG6jS~6`~*h-Y~UWCPilcZWcX;?cnI})wGjNztaQZ?oG6m?QcKU zL1xAZzn8LvWS0vOYhEkJ)w;>U&rr>xov0T9NddWwqbu3`-HY)7(E$c;Ccb9s zypi6Jy>9irZIpJabv3E&ay)^~GWmcF*pe4AV8~RF|{Mci5QFN?~ z&;i-czM1VIy@+zv0*%`DY==+Uk0@Qrej2b#RK2!WDke*hzxgmP4YfM%IuO>d!F|$_ zT$LShw;@~RN|Hg~2+3;g3C!SKc>ihk13co`-(zdqtZ1Y@h=}6wK?{EurC{129o$l6 zUkH*S4z*&$lJ;^v1oZ2z-BNW$n6~>Eg^mYH^^+seUeLkRf-yHNRIId)^PH2`>BC$b zV|_O&{%(|e)cf9prCg*JbtG|N^Rr2*w=#nrqr?%o*79q<3uY-8!n zfQdKn3V*%C0)!|n$S(X#i(OU(rn2)H)Ehz%shT}oUzM2liBb{ z^lJDf`Sy?L>G%IBk*q|kL(#fjz%ZWK#4iV<3p(w8h{#p-()Eu=NXoy%bN{zBElq2q z{hI|m3~;Qg`>EL)>(Gp()vtX(2^kjC(Rpt^7c(&}JeqdjKdP^eUxJz_8f6X-b{OhA zA6wkBEUyoDpD~|Rl+2yU2909i1ysP&ygxy#!#O~67nB2lE+BplEjCvPPQf~gNL=X^@LJ;20G%qFJ&XFoejpHIGw@X}ot$*WhsR}K ziXhKC$B)Ni)K>+TlCSh++?i%{3Qrk@=k19#KFNiY8eSJb7d3^Wd2d5~>L=(6{0+9A z-4oq1T#lq&5_aVqBcj0>&l0@v9U7G6jO?w-vo)mxRsphs(4`Z(!deH5sy`c*ywQKl z8loowhv^FzDdP>Q+`)pcV7_%0K`upDK$~gGeqE))e^npiu5~EotXz5Nw0L3@o=^1S zJ2X2oe%bTxM8t%jPI$JT=g39ZlTz2UnSa{zE>hDmM3^dnPiDeyPW8BSy$Ye_d*&9eS#lEsf@Yz)%C3=!®!_HwDOA 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

5}D1II^9ctN>WV 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∨S0qwwr;eC{D}1rekI$_NoD^SrV4*1 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

#kd_8=XqOcrye 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

gi*XKB^Or0;y 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�FviG@ZSTInbKXCmbDlq+xX*pv*Yzpq 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 z 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໯s&IPpo94;%Df>+;wbm&u>%zh$ zw29w_aECS#ERKXI*-jPMu3|{!+ry>qSlo7!!}}!YR(vBRjdUBNycG6lax(Z|5zt?W zzD6IPmBLo%aDyn^|6lg!Eg}IL5f@c3q3jh+hsySW6XlHuQXwD4cEc3SJeSMMuhS=- zh9h~*ch*8Hy#E;7#IRaT1~HK~7I3}&iHXx8`(HwrC*rE}cjh5D<_KiHJ05(xih*@4c5OC6O8d0qH`3 z0HK8tApOh#UF$n|*2+N+W+u;?duHdl_XZGOCop>H-P@pb|Gk)~_=oKXx{m%6u9xb^ zS;lta^I~r4p1=(ICeL*Z2BzQN1?0lk2Q?k;ebYl% zeY{y1{wvu%rBFeKo(7es$9*8DM}7;-7Q#~SE(OAAI`?B_T`C5*D`a0>$E}*~=A=F= z3k%PBUQvEaMD&g1hx;Zk+*S-Xg_^Y#%g3r8T#igxc3MLY-WR%oPlG#%g_u|!(354h zA|iv32jMpkD3S5lMekryyQ3rTpE|vyT)Ka-S3x4EJr~LH^z4MViZ$vvh!h=f25MvvQ(1HJ#gkwB1-mFy1&# zcDsHV{y9yn#ZE|0C{aH;6PfM#-QHR5_Z!jS(hw=j?A=9Sqi5U(o%)F)mTnk2f=4N3 zUqv0zcx!tZK7n@K*V*BRNZS3zJQlj0^h-BPCA&AKx zpRq6ou@e!H9pCGw4qLxn%JhaW_kk?44P8nxCaRkr+QyX z<&At`|9dpJUaLkN5;NQLX@%j(p2Ozz5XgPZ{bRD`(3kbLSnjL4Fv7;;nT7Aj$eGC9 z!b+uIr@49HfV~OG7vwC`K=N|_KIc-qCf4LA+c_st$5*7qnw;1Q>!WjJGI7wNV%RS2 z2es&eE|&djPZL$Mo;&8NY_;9yWR280RrY60{bxE{(Cd}PUnIte*Jj(`!8W@zp@o_m zYf^`UUEw#PWHl*ey&}@2N&tUgP2rc0t^(PM!+%zTYANu0Y1S+6P|96uTeI=I?QQEl zq}cpFS@1~wY|TOlZ)?QZ*^ix`FsjwACR_))Y@Ye~#JxuW^`_zF`uccN!1gh0GLvma zWHIQ+aN>%YFF!aH7g9=uGGj15mRuS89rvNzU@f&b_V&CRCvP}OG?MS;qZ#%ng_o#r zK2!Jp9sbvGFG7v&cAp;5MiLaK{pOPQPN$fZn#8a_{z&Ps)`(rln_sg>%<_;@Os31G z$-+^553TOU#2*H>)3wJ!Y$hO?A8KCNXR~pkrr>hyq0ba1n+8?!BKi1`OaLJ>$;B;M zXO*d5z2vcox{aen-1vYF3`00;^@b`G;K*gDAp>Pj41Y2x;D+LU7c;%AG1a!e5vZjN z=W`*h5t~>WUkiE9$ZbBqZh7HGFnhc)aH9gf8U+ZsRY1urTaz#Vv7-y23gb$c2fIsM zOvhiyDME;U$pixei^6uE z8w~gr(%z|3vN`{KceQz6W_SEdb-T|mFawC)nvhxan^kW|kH@@4?6LnV#T@jt%q}Kr zvhgl2omAJ~zQQwx%ivQ)o*rdDzM;+CcPW#;x>UcLYHAzYt_9Kbs=6KYlesYN|2`1q z@BJWf<~0=+|DfwhpK`t0Z!6jaig|UedfE7Gcq);%Pz&W?!2Z6*+`*~4-}eaox}$^| zu6pY51E(4Xy|~05c5~W%EK}Vh^PS=wimZeFQDb~&`Pdy+S_1zOJq~{0>0=X07Fg)D zJ6r#jVc|*i?lPSP@{6@itkv0Vw$(@#rL)?e83|~g8uIhGgi6H44JdO}F z&U~*g$^aTAg2bOXu|Lb0&w0yVbxdM4f-3+3ZcmoW6h0b;C+{8SRI&Y%JSc^^x<` zh%8~I3~7;|5BBw1l?6?EM!5$K!Uh)x4O2h5-J^l!H-7tn?9c=&W)A)H0v=O7IC91P zNfs2V$i7~ZC(ZG7=X4sCgMqG2-TTp0c=|4Ein$=dJ7|20E*BSmkRM;1i+>Ws#t+}4 z8+k2wbu+gu2PxKetZDG7zU4}DP_rsQM>>4HD_U%YoIZ%oBE_taK}^s$^A6HvS%8d{ z_xY2~SGB6-{}F{4QVm#i9*p-nDOH}hZ>qYC3sU&QLo=#JVBuQ^1}H=M}!~hZBiEtWZVv9S-9G?hhQFafj^c_PS&jLh&J7aCCHym z;6oOhpoz4Z0%j2Lmt)Y`x5%KwWrlWCX_pUr>~!tq_CX(&#NUczof7!&ojsu|m+Q~f z?C@J@fim;J-awkg(&+d-sqUYN{!+Ya&p+;NTe@^Ngp z=wE5T4XAOLgf5p;4!*A_!+xNu);`Xf+e_miRr1SY?cXRlM`Dq(p9sce+Xg9)!Y1$^ zNLe5>GGpah^tcR*kAW9p)jqki?o;8%IBZ%il9Ih=AKRk3pt)C;ec6!EQc4Mit4t!f zHB>3aAM^`8_J1+*F-{otDrj(kOy<_r_(AX_?PM8~SMFOavI<0kLA-?FW?Tz2c?>P@ zFvH>USb`8HV4$~Xr2CUfs|BXotYD?I!k0T!V~)CYP&Eaa_z|fq20I<)$^vh3J-g6q z+M6~6Ci&l^<#AaViLB*6#*Mw;D}prCDP< zqAN@=McK(?XE7NYHn28??Lq`tv6|B&?1)n&u4Y~{&!$^MNCNj124?@3n(Ar!wVtSFj&-?_(D;1z$oW zzQ?zL$7{uo$$hQIrrN_#S~m8RaK=E3s5WOYoxRua+8LQ(P_4OfC=V$Nuhd4~3(DW*lVP{>>R4k4N@9vj4o_gHFg zt?;Q~Do;9HeU7A(hG?|9j@`2CmYTLOEVwXOxg;(klJVwDyxE0FOKs7@(Yj z5`>cZY)zr=hgLrRrz&slw;9g#3$(X8&xc0GGT#gPuYj(Q`cOHL@QSkh4^#Rp@h191 z;Xsyabopv^E5%XC_n0e*f3D9lCYc_4$PW)c<@-Tp;(zkA@=>b|#B;ZDHdklQ@E`=2 zD-v>#W|_2o$ZGdh^Y&B~-@umkgZGmuv0;LDLJp|dpO||di_TK zdAuf0Fnx4NZMRRgxV2MnXvkZ6cdyskyd~c-=2PKicVx3x{HiTa5W*+~^?V6v|Q?@r2_te^PW}ygC@BfaOE@Kb=jA2{- zM^&f4=~;A-Yuk`zIYMrU~+ftjqF4{@qDZ zg*0=3^Y~AWf3^vnZ|+FaC*?2|mc!@FJiNl@0~X?MEx#E`*0I^~v~rimO@8F{E~Qdj z7gZAjJx}*me8SdOrh@dAR;p}TQ&lwnyV=gs|iVRUv}Lj!v{3~EX}6jd(p0@Fa|xC$!uAE-sEL)A|b8-u;t@Pl570n z-fc!c0W8(-sNMDo#Tw5B`aaBl#q^bLr7CFd5jm`MB01wjAjNnExixumkOyg|+@J0z zu>u8t<1Hc0q9Ltdk!@{-hqK(?;QTpsR(8nms5dZ-H@nrSL2>()2Wn`D*5Tx z=~Wa;$gBrTd|!>8E$91IpZ;K#oTe(Tz~-*9YXD$@d7j8pYc3*nb)^OCx#;6XZPKT3 zwjl@=&cu3?a7iB%k}?s7=%E4n65Wkqqh}4avQ>NB!#TK2VsIl$toO>u(l78=M-Hk( zxQcZ-8)hvrLu!VhGo)R+_Z9VE6DXD&7P`?%=^R0IEQGX`_MYy#C?*#yosLkumr36^ z_SiGim(Y_JH})m|j*s|zjl>vLe?0U=5=mOORGcPZ^3wUOl4&8D*)zv3w`ePJoWaVD zq|!UI@n0LPpLY^dlft;a5DzX1VRIcb7JG);_>^CVS)JmJ;JJ}*dL6yX`$Lr5Gga#} zXup^aQ2)Vl!SWk@?>1L+y)Ikl*{*HHmxPj*Lw(Bbby%;YzI1|=nWg+z&F#}KJ>Qy_ zlz@T!J#)1RCQmISX0s4R)1g37MY>d26Scv*!PADQ_~Kn;)6!ABTO6#mE%DMmn4<^I z3ViPVi$T@T)_9JA9vt&*xVZfv{U%gsSii;WxKZ|%ICdr@Q&-h)m1K60A!F|uH z!sbZ15;N7T=Mg8Yfk^UKtRy@^Pww1u-~oj0PjRL*7Nm?I&xSVTQ0Q3zP3HG|QVQMV z6m`>ig>@N6oiMVbsH4p=7hx$DqZf=~={`Jh5k=RKLUQGwb56J<%X|1^s7jcXn5?!7 z>hDLvpTI7#qIJQ)xGE~M6s9TXd=0-hiTiIg%`wKP|9-%QVyzu>`G+CFZxa+8W(Urt zUrGCIkrkHeP|U3UYBa$m$|tej#(*opy%biSm3Bi&FWw%0+~%TsTkh54|KxJ7$!EBn zNhL^6+-KN9FBv7OCp5@PU)1HWP}}lxjV^M^vdH3on>T#0P1Xv(B_p}nX5}5@3w7vS z4N%@xn5bIxCTGJx{QSER<62#r?b@jeGI8j+6D^M%;E3~){6ZD#xfA*dE^cq^;L=qg zi%)cNHEcgt+;;8&$TJd(RB%hGrn`Imbxf6^84-~~#I!Y#Sk_eQ{mNvW`~y|-xK`4UK?ilWrIme54{dR&k-q+x#|xT#dbvoq5WAKcfsYL80yTtMZdg`kd*_gMv@Cc%@PQWz?^?jwp>?m>~Kc<+RHXX?`EP8GF z!^Ve<+PfOT zJOiro&u65|sp*-`TaTLeA1LSj!GXV|+3&|@kNpjHTqYpUx@rR!#DlNAvN>P(IdXq?(y&049-gclHiE!3;Yp%-}fiue*t9H);uFtv(H{gt+f=WK(+GdxAsT#T^2BLR)M{h;@S&Ur*}; zmg|{$#3w^*>uL?s!^LXf&v3x__wlcPh8*bEj~ZMeg)4}7&o_jOm+yAUw2?RyU6^Dv?;R9uYylf+w(4D4$*(@b6FpDiW0*n7 zp6uB!Jq+b(N1Kl8EQT2MB`gJebi}w^TWGV9iC$kmI9=-_hgEW9O4`6o+aIx=pu~Cb zJ8;xZrS2_DDCoR{jbCn?nNW&&O1%{OmVuQrdkWQ{?cmo%G;yCLGg{^g0I~Zp#P{)| zkcdTaU20ua@Z*v9b*Wvn+sdaFIuSfasHkw9`A}P=Ev+WKu=vcWLX*=mhO)nb`R9+{ zdA91m7oY;Mza12{VzmIMJ*-;5VeEQygDh>TZV9TMY*e;;tOZyDJKfcb>P?q_+R4lq zNd=>PZjRqP)@8EPd-=-Wqpq|^(QZBL;aot;gP6CyPr<$1s?Z0aUn8tVtoUYrEVM6G zE(C7KLneP^O4QFP9?*TSf8!w<$iSNJ908T?VWeAE*>kl=f&=fH#vd`jDErXFCAIqu z{{Rf1W84=Ev;ji`DSYiX)&Oe5!K-{43X<~NvKL~n&=_=IB%PPhy6Y|@b7**sn~fnU z-}ecc(|PgwN6WftundUPobWKl6GMJiD$Tz|m5e`HggN_NIp3K56jr8peJ4{mp3(Rl z^-Ru7HL#V)oYgi}8FBfN%+s3+x#?+CQ4xXW#Y)HCum$9l=fkw?p43=y*04+M40vpJ z_LEBA*;5u_PnZ8DKs_B#S%q90?O?xMIER9o_N?qYHT;PYA7_U!B9;X$dHu0E%< z>)yV`povoX6d2aU8NT#tgkM}~$>q~X6Gwh5-=81&QaWOS-7Y-N3uEWQJQ)HX*Q=^(syu`gr zO~Ub;c@ylg*k7pgEpw&E!P+jWEs!m2)$Zjk;%?JhKw4d zK+nv8f+|#8{))y;11aFe1ek$|n;0o^=x{GqQw^NlL@dEc?wXAmk?Heh4FZ0}ycTDV8|G~wrB-9Xv; zCiAeD4#U8`%HTKu)_)9KaP=ZfROIaCtZh4C%65YcePkRJaKrcfDbH(>WyM$9PZZ(c zszPHh%B_TecXr_yb(`aBmFvtL7vG+w>HOKFr%Q2VK&@uyk zT@4KD_N@pK{+#yNo5eL7gMX8Y{~)IeQ8K6AAUhx0%K&h)JSKnQHeeQ4GJs8B zDcj2=pN)On+<4JS>!Q)_h!s&nxFP zs)1aU|EkLL8z+iY=v3+|hBOgPtNaNf>$(mJ=|0WKI*|&ah5UlD%n*4Yr6W!z!`+#L zDQ%cBqN|pF>`cTP@pdm0tVpamuIlXhgsger!c$ikD31|*qv#nN_^2kP%iPwamDslQ zEpgvAIE!j-F&xr6N4@yuAvCvt9H2!);~RPep2QrS_XaJvL1{Y#^2wvRq*GD0Mpzrw z7aP(M?Dcv?ZwQ;anc?31`-g&ECUJ|@!plvAK1ayx{bjV1TCAzitfG98Uqs#U9z*Z= zS4a8Y1Xv}MkKvE_er1ZkuUnA}4(0Q1+vrM6o(u6hu zUJ~$zHR<*yf$@;wYyrjowkzH$`|l)wIov*dpwEnY&6hl~cMFcY^jGV`q^drTV~eVp zuUYW)yWc~vh~(0xn)(H^0E44>Z;S3<2fM!yYB1()=vlw1F9_DeiCCSFnTQt2Iiw@F z_EXoBk%@t&pky2U`1SI)b~ay6Qh05HnK4MYG1TuL7SURTmiiW+pC0M@2B%GBu8AG5 z7xbS0VNUdLXy&!~L{= zZT@BfX^0)!f!lin9Er=FK^E5*-VJ@=ohtTAw6$NDlaY-8?D2_8e?0n7FqLul=x2b8 zx43+PHm?+=e5Lt{V7lVeQTxCL$8(>z5<`Z1Qz1>%G>S!ntoKQUV(CxH!|5yqN7Kn` zB0sZ)v#%bBvXSB%mA}vHWRcHJyWRy>9ErrN865D=(Sj0i&jB^dX1OH6 zcPr9Gde4xb-B;Gr#sYLd2wgCkN>@zWO8i-I#_FzUUkO5}lvCpeyEI?Zn`$!E>|B=v~=|c{-r5SG^!5gmJZ1~ifgFAubJ?i z=g(Xe_{du18K?8TkTpE<%S1}5{e5|zT9AlmsgXidg+TOLn@kgvtpQ0058u8J%y7eL zC5lk?@BN*u1!XN>NmRllARFr~4T;F&6MI;@PWwJ-LSw9K%)Js{m}C4@T-QRJl9^q7 z6khgBH!cnh{R575E+|^QlmCa3wOS?XAy;(rJ4AhEK3|qQL%7fv*f*fca8CV>Xe*hN zfY@EJ03miyjkR{ZW5pq#j};bNom6hR@NWZL_^?E5{AV7rW>nBUMyQGWq+0V@iNB@*JoS8T%RIb&;`Nb*r6FQ%- zME4zX4xje>U7so@v)N;XJ}U3)nq(j7-gE&KucfreV>O*(OtfRgL*lYXB?&4d`Lh*k zAm0o26v~gss3FMX+Cq{#;o`=MgU>JCgRgKOIwu>uDj5sbacp3A*x}je+85M#Wjgx6 zGKEWCD8Ef5Cy*saPWlO#m4DcuQ8jwd*%c9q-U0IqgWYMr>YRYw59L39Lad*bt#QV= zrR+i+H1ul}Bkh%Lr}Gs{aO<+T&l|A9lYCXnH-r#AmC4%zNz7|yLm*lC(h~;vufm=@TqswukFO~G2K1_cdf_EAyqbb8Q_~Zhb&H^ z=u#=-zBd(d-5G1%CkQ9k^wDB8_BY-XVj`c#dP7qud~WfznGT4MKmHkY{kzawN8}f>DhD_ z75U#%43G`tK8)`CI$8rCGa!dyTHBZhwp9<^%R1Q=7E`~C zhQ98JCO%AW&y>=~sHREkbgLJXvz7oEPyI=;KFT_c7DGP1)KE?>|1Q((-Unz%J z)LE-LhWrEfB)QFK`GX{P@p_rNFl(+O+V6qlFP?dqRDiPp+Ivb7+{^Y-`7!*A6#AJ)>d4EA(wM~9Sv%?q0q7l=GU47US8p5^C7kMg!`o}|<@Q*f zU2WAYuuBfN`;vn98_cs+++8MJ6v+Kw|Ay=MAc z!7%!>;QP*>c4{U)B}e=x8x9=-4j#Wcj*s>nzy+@fvm;~aVj(tdRb^Gj5ZkW#ily4P zv4&wlaBvettL44Qe=h8m`UW;RFw@?p_IW_4PngO^BWY3&kQp+Ulx_^TwOhMfJyVsS z`V#d!rS9HHad?ey;Alt_;@*O1SHD?@V}F(mUbw5|$DAi!r*sbdILqq_C>sohwN4Xsv3oPqF`G*TSW{69bF3 zn%rQiB}FLsJWSJoQ7`ws5*)g21E?_%R-BX;{iQS?F!Q&~`(DAjH3LmhSbXQKIYqNf z5z%GLe#-8}G#sAA!4N$4=A8U|hE%A^{v1fowkyurmTnj}&gBUFvOe!)VCf)9Vce<)6 zUz#VWYHbdp_xyVHR4*p_RPA-v0Q|#Z<%vZsx62`5mM789E`N6pODRteIzF=ZrkdxUcUgDeRFa_))Pu7H$dxUE2OJ7ldP{|_vhqbuf2$~#yGVz zj1~({DYnPPZ_1m=2b7^|+T5|sl%V8c>by@ep7-mBwr9K*#{s9_iZ1!nweN*#!p~!f zWV;GP)y?F0AVrk<2$H8imxor@Ew)gE4_!t++yVLhg^0o5?oVVBTlQCQvr>Jp_M*k} zBt@0OVLX9_^5%cj!u2r^O#XBW9!#qb`c2=(@vddf<3}tXT~7S-WJ#a>-|n+gbaLeF z_a|rRxN^c+yEEP?n-4h(%3&>M`cbP=%>{Gk#hmBVP!H*yz{OFeU57ju+~p5~5}@lo ze<%1nX%ETUR+=MhE@(iDz+Wx-ZmUsZPc&aB422r5=ACDsh0GzvtbwX!1zo;|nx=tw zv8m|yd|#yS-o1-2oAjheM*(%sh&f`!j1u6WO@wrH2BsjpRF}gPunycEx#*jtiB>1S zh=vp_wMr52jK>fZBH-a0yMx(=`1~(GitUYLm>vJA?{xby92=Bw=bah)5?b9@m3F#C zczAHSvy6rKlOVw}$kW3dA7?GgL13k8pjzV^p{5UKwXrc_I{9Wt`llfmtA;RuU4tz~ zDtIejpjvhJLgsax(y2?Nkw=?cPlVI@XOaOAeJjF@xb5{hPJxIaGQr{wHS1LyPex1( z)RHGM+W6OMuzX;`P>jKskX;smLLb`_bIi`3dk@&lJiIHf0W-XBPKZ;6H4X#t<6Z$EGUv>((J5+ z+mzm6i6unUxSzjJ)f;SS*?6CO8#oUgS?$pCR^*iD%XiX#7O#t=zR4P2Z?cJt@0IQw zS~8G5>*R5K$o8QH5`iDR599*<9>8QoPzs1J2gcE(p*3NmknUyikdozprC7a2<=ln@ z4;gmR?Ppov>88XFrZ^MnJmOrKgw|KtY;n>14*XLVpl9501|BiQ#}O8eadzRzD8Zy` z)Hx}0w2o?to+$ml?(rM)e2#qjJW3{1E!ByH&lf#3l7`;6AU?@`M6{fF!e6K^e`IAi zw@x$y7ZO3+W1jw?7B@hGV+41HMO|}2)GIESc2vUh6h6(n@Dp>!GG@Ks6vHkT`^0hV z!auHM-J^3qw4NAJ%F+zpg*tIqW7=fD$QE(Q+g=q)lZ`&yeDt6w(ZK>mMTUrs>fg(j zc#qKdu5BvNS@TQ14&H71Rn`DdJ>#NdZ~9@P3sQlIV)vkbmDiq{g+$_n7pp;us(| z^4bOF0KWqVej-H=m5y@8ttQAJ#O}zZ-Sq_yCC$N}oyb41%L~i9F8?WlVtj+<#5Mjw zHYc&f_L!8yRo+~f_-W2iPT5VvQuw3Ni|frNI(JCFSrjh|{zkuun!BE3DS6>eVieX( zl32wxWlFJt@-?N&Hr^}+I2cahP+W4QqrVuoqAsN zh-m0LoH<30r`~w@o+3Atylvr&6jifGhX_j&)F&|l6{5RRgEAp@G*QB(P2hNR*#=P;aq-8A0H5DC= z4E)MAJXV%q?0B8c<2ZVvk>DO}nYOl@F0DTdNXwT5=A`q>ISMW0r#rf#E#O+1mM^&O zNHjO0yFvEGbq+bl-h+XF7c9{!Xsvb4{}46lv2J?Am;!(Vb>M#TEOLar_yf6;NVvLs zeZ^Oc#qGHJ(Ge|5S@9*;i_vtR8(+||YnmOs>*knX*W-3o8r#>PYB|Ej8t77{lT05+ z_;SX^tDc*1GkeD3pc3wejSdZbQI3G@J!bxFYyz>a*>MsJkd)S)xb}s6Qub*o?XkTA zzjFbz;Ug@1HapNWvvNGD!A_z0KTe7x8)xgp@_W?GYy~DgDnb3 z3rbco#pBj5LrT>O%Ia0QlxNu^FjAD69}^BF-v}O%v#Zt;LSAs1;z(&e6Tl#VY^*k4 z93@%21+vm@rVHVmM>ZEH70Z%&Lc?oxoV~RHmpyu>Ek@!|tM}j4Cp1kmD=Am2E;OhK zXn1!9Fn8D$Q%wRKFpdkJGX}2O@9H50H+}xR?oQZkc9}L>vVJXD!xu6zR0IA{QycL=#et!8>@is$qf7qwgYUgk(s^R(3ZAz`||HIv6%} znAx%9$2WU|2)Wi6<)MdeBWO<7i;vxa%we}{I0AP2C=Zi^Sb;I6Ytu@8WKy_3gTuNM;x9ej`X+opM*hy z`NI0s-UNF$0g=Kv?7P__QrUpkm1+asxuRogL&%Yv=OT`h!Dr4D2VzH1 z5LhK8>Akji(g!>(yyY8y0QbYI;R! zJlE3q=7SlwrLt9U+_6hx?~+-QS&%P`v+FL1aOf(*lBC?71k$dvQ8Dq=`p60uFBJOX^mGD8#c3BPlpZM? z$plho2AjOPIR=eGc4SXQSzfv?{IfR;xsT?&jm^iuXaQuYoT=0%iS^xhLn_)4;3(WX zn+z8xB-gvVO(P*mH*KxI$}PPfYbpuVRjNv!@ypt)leGr_3|N*XB6K5&9be6VMmrXs zrtO{GBg$0}0%RdcUT8|I@I+kRqc8{E=_aIdOXN$+Vvt%jF^cIaMQyajL@h>3YkUxx zA&l1Q^pA3$M}KSO6Eoi_D#@e}^vfuwx9h~rC{`gVAn$4qRjBD+O`qTNtA}28bsoTj z3Z2)5cBg?GVy!Wlpjj{QEPE}P@(G)HWo*qW&~fl=iz6o}(fG#8!HDJV+KuGPBLrN? z8sC{@PwhizK9%L9>1wXl=OJjy$9zN$T#7O*2^wKN!R>A+fomTM%&FNyKPD+I6B?|b z`ug1iwMm-6-%0jeL4nB}o=mtPPtfnU+Y4BokGVe1afQW1nIBb@)+3S!()Gk*k{Ml)K^?LypuyZ{t-^8 zI=PF|L;Bk*XPWGNn)sb{O*L#{$W{YO_fD1opOS6A6qb`}cVqlXZs_)z_%F-60zLU{ zxnpzK>XpAK0#-ZdfHh6}sh6;K(dTQN`Cvx=fZX)T#1N|;f1rAltxeq;aN9Dm;ldQ< zsl|J_Q6wRn9mL%2=KF(wg`^i~8`**T0QR(N(SL9z(tXC&t79S>3(7ri?Um~8a zf0*KB1LoFTgKvvBZZ@U1O3kNFqY8Eu2?V`2HDa}}_fACS zp|$G5lm#a3J_op*!?+aNQ0DVwH{Q#BVUdkelS~^iO%TJdPmG+E686OSVrbO0WG-(l zSZs&H4%TE=EPO)OWD&&HM7?ejGZ{GF?Y+(XaW$G!&r2>|F>tYv<~nQKy09axRwk!gFI`PnA9wR1pS>vkbE=$L&96DPGP9%ejwR zehmR!}S z*LZ$VCcPcuqf5ZvDkWTMOFRB{)=|DYCi{UWx!y#?JUXD{o;(MnAmZ_9$0Amg%SKu8QP*C|VWTXv zh!;E*}Vn{k;n$73~m~>SX6`#!ocE(NW1>!Ii+TK`b|DV!H3w*%1$1T-1*; z-JC#*vXRbCn?HOeAx^LIQXRxiL)3PHjSO{rr&Z%2=DVK@7$&!7ly5Lt@UqM#xT_*d zl!j&lai+r7mFWK_{f->JeA7L?_8Zxj6CwQ)j|%S1Ve(*n7?L!M3yNKSy6;^el~Ks0 zP5bu8k!}xibMvWvwf-fp3+iMRIi4$N_UmX_hr8=&1v@4VN-;IIn6?NKPw*Q|e!s)M z%@n0R!mdAmFgST{Pt!B62_J7PF!t<K{_}%QTz|;(4o7K5Wa%*hO=sv8 z97RK=mltsddHz!BqBss+4H)Pa!<>6rcafRnE26La1r>1D4ekIR9RIZPin0H045U{# zuE8#53M1>_O3&PhPKWEahs(CN)Nz_Y|N4tI_=#7~)~L>4%!*H&dz~ncmD!1>8^JwB z#A2t;0#)w?f1OWLrNzXTT(v#2dvSK-dtZ#ncX%w2krxpkiu&T}bD*;FB_~tey+(Dp z+#pkX#bvfxV$o)eyl)lQkocTvUcfwD-=JzqDaLBmV5{Krrs%R6nmv@MjLGyGB{c#5 zEyjE&W6)ORB&2kGYjwFj2=v1O5xBv5rrJ~Zit_Bz&{kY^Q;Xcv%BwxiNA1&4w_FPh z13~a0Gk`f1Hj8MG(o6{j#aj|8akB^@Y#ee{JV3!s`oP7^VGrKA<^`H3&rDDKtyz)m z%I>e)4Ujv5I59!uWUYeY3T>{z&_4IEb7vHNn{y*eeinhf68M66$1$ZXIq6zb)l@QY z6sy9;@>TZS)1)_xeoBEkh+LTp{;EU9;Yo8p-<_mH2e2UjxypZW(iM=Z!`YGlHDSyG zGUw;w#48Fe%HL4V?{x#sRgSwvJ1@02Z=Z>a@I8!vx?P#1k2@s(U6xhDjs2^Gt&PBI4h=;G<8WL^WQ>;7v-r*vDku|siHfZeb z^5aWEPSD?$`01GamFL)0$m~>6REa?&el#qnW2h<-2t%XY7aOMv?1|e}$l4IdZ=#PU z2#5Mk=_b2gSNGob@FT|qb7>1$g&ngFBW^LGjmzMhGKl$HO+2RzXfZ*vVdqTyU;$ga zceRKAY>X5@t}dg4h67#jkSDIfM~=hnzSnDTIvK$&vob>NK=;hx?v1AXNpLuDH^WY# zq-WXSZY#pkvirFs`NL#cNOvf%tb;mqGSL-2l8i?)_jb1OJ%ZdLgjWGuz&NxKEp7Ro ziiO$ZmHTT990J#kz`XpbXS)H)5A_z^Ro;onJv>jO?9UdP*Vcw*!w~ zTnviiWJ6{wBWv;ua(!(2ldOyE1{pj!)5%$79vt5%OqQU%{7qIfHRe5b|BfA6q2!C^ zBv{62vX z9KIUzfbv6k?oU~3q192|V(7Sy8Z1X5l+t?+U>P_cXWJ@O-+g`$?LqXvJu-Kzu6Jc$ zpl53z>%|h-_22k}KY6oTV2?0u7Sf|)(KmXZFrP*Ai}|Z^qP?G-5%H>v*s&8R))ux0 zu@wZX?yLMM^2Y~Qb7i%uM|#i&^zgv7zL--R{A;hWUE3aL+mg}@b2?`CbPAxloZsI5 z{T)Y~2uU}Ev(4@P=O0g+5=}Ue`Z=m>nBjcm82;80 zs$603#^wql3oW`s75RL#qu*|M(1Jk>KBj!&w2@$R9N5T~e*E?!+^Su23}3y}zs+}t z8i|)x?#UapKd)iSIL4Ai^R9PzdY7VQTTOu^T9=%0bMba^u?G7d{rRk@Jxyy!DAP|m z_lgS`VagT1#fl!1b)C3PIAVMt$NuZ~M8lR4cx4-WX{YH!SMZH+b@8DGa(%=ngYBzp z>Y+A5H@=zm?ms)ZGx|=UKv-p8COYOi`_tc}m%f)h9}CJGs&#ic=il?mGI6*<_V6l0 zlymh}4d$_+Ykw%Jx4MKxpSyhg9r7H~B3t<_uZ|)wKXkz#ICkvB)yGTkJ2DVwDGL0Q z9ew-J)Ad=v2)RK(P z#K$jox(J_NN#+{)e5L7+#crrHQS}Rw)gb$c*iDVkZg->k<`c|kr9z&KYUc=QWfjC; zyIhStziwV#uo+<3f1d#?S+bSk;CuTW@Z87+%~6pf<)+a;qPkB6B{$2`ub_q8QFYv^L!kAs5UghNkRHp+ zjgYJ1<36Rb5Q$R0q|+T@&Uu+=dgCiW#Qy80L2q%8nq>G_T=cJmrzYB5H#Q-TEfeN0oV=jLu`UQ*AfU2pi@SIOh zs;tsY#=$(6Y|2163ScWB8}Njsu>%%6_aZe+B8h$U0TVop$Wna9nmFPwYagp_&&3)w zS0iipVe@vBAcv&aLo(x76+Y}gUch$fwk+g)7_edCE1a2M?5C<-j@*p=6R{eD`2(HF z`~=0Z(5;XZgf&<1B3XS7D3|O8x8kCQ{d@{eg=dbVRutFx2jx!c2A5gru{W){Zcn*B zJ~3PS8qSM zOk|ohN4DKz32_gPg$tb`Eq2O|xgSs*|qHNHf>_F;_?rZ$ibWE=a8%) z`wd%SW=uqdlg#__0EMd}y14&H&(GU8iiGRH_YIH#%dmUT<;g1K_>1Mcvu$jlX7jQ* zTq`Ftmv9ak`1=uKP0Wv(T=Wu|Tv0gUY{zCdJB2&sS@7pO>~3mdbJ_$+2-2e#t9B3{ z#vWP!yu&-+Zn|PW4Y5ix!Y)-aq&nVV!8&ddIwMM;mkl;)GX@uHL8-^`_LXgjp2_>H zAF|iB8b^7}r8BZFsQ*+&3W5fXCiTIL@stsN)n!OHyk+G$SAwEr^exVf(wAvh3-u-@ zWAs8ebV0546FRt1e=t9&L{nq`_)m}E_iPF?|Lg9+V?DIoGM$E7C?Ej!q-LT)rETSo{YY;R1emm3w^ z;K14OAoU?p9aH&D1r!J+jb|3sh$Qlp!Y)PwhJqo;o}3KjGIY>)Wq`2JS${7e zO_(Xn-|yBC?nfJ?^vk3aleo^JJOg!moE$Aow5{YR~zjeUArTdQVa z)r&VTyJMruk)1Pjm+>DLAE;O(qy)S+MNV@gG9wD(10kLfPKTKt%{mg)y>J6sO@P6+ z8w5I%=rt$iKYGFcV>-I2#vPH1k2>Hdu#_OI66Axy1}ml$eFr<^kGLpAOB%C;>Gb-y z$_0>*lSSDIRFlFtq8o7XdAU~(pHop^7JC~8rM2WDj|osysFm>_!W~>plR*<^F zW}sV3xC%m&`3+)zfs<}!DeLXVBcDK0a)Ydei;$A_(Fa7y!X)*T_Ak)C-9=b(D!-f& zIF*{WA=*#EpH^Mkco|r?dfdRZqa&K#$@yJ#KT4XB>XKiANtvoL!9-q3y+lFu4hp_y zcqO1?ICapW-Q8k40~vhaztqLR{}w8{AcU|~d4EBvDsLS*VUnyipwYqhT?oy!FBMaH zBS7Zx2xEA?bXqyllLr|SeusZRFVV%otHH26`H?;N)#23e2VO$oKdpWrHU(Ec1%F&f z%I*nT$k{Z?oVTdL>%~{_X-Q_%!av>USoji{KDHOd@XW@glsO z7qvAkZy=>;oQb?8W5iZK(xjKnkhM1X%jJ#Mq=@jgc}1QAg|Oh5UcaA~)rFN?lR<2u zZOoZ%!ubkqokfJfA2|9g%*8zwU6P!!J2+X2A|swoLm=pHtmOX)k$L-j(}C#LeI|VI zD~U+k%isti-a52_Ktlw$Yx`OAR52mcxK~qEB9keN}8DD+sL@^6*9TLYB|5N?NrzG6fz*5uu^+rLFCZ zwxyIDLrCEMTq`$ybF%94>N&MzC5f;kM869$RNRY*-h0wrpt!Q9{13+wovZ$5gn7Yp zCS{Sexy_O<+9K9DB*o>-#Xi!n4159f4_UIFv>bsvK*kQ#a@bOo&t!ANF%wAd2~hrW zYu|xm`xzQ{LU`J#rLz9q0%Ood+gPjLA~$-;Xl{^t#Os`Hb*z67euzgcr5GSK18tXg zb8dg*5h7`SEru_}%sMW>l+X5}as9%o(e+gCVvyB@=rH*cQ2$e4)S|g3Bt9sGcI3Ze ztl?|pu7S+Jq$XP_CHFkbUiFR>!0&N}aY|)DzJYy^+0TKm;NF7*;)LptK`&MgpE?Ts z<85^?IveE~J!J2tVCyJv!y_Xp7I+D+N-pLqX!`}xdSGQIfiiAISG)G4x1uhfDA_7j ze?MGf+;T5~mtJ#**mrg(epXUIp}qu)Ru3AL)P68U&G`$2UCsQUBj*LGU>L9Jm6qN7 zBw+ISdT(e+ok)_yA z!f6-w8s@*`EBpDI_6RCR!?_7YX8v;Y@EvJ5)nt@w&iQdh7UwbOTLk*dk+)oK#^ZUi z!ZG;YR-X`W$Z;#VzmJy+Iau9DBfJ(rEgh=I8@wRrSl7 zJTV&8mO)@UpUrQJm9V3T>Oi{t>dj@ZzhSgPl_d8>=WTDRDKS2)15VUDLj;OrKE@1% zdkmw$$eE#J7MD4S0uO%Uzh`dWb`y9`{d>A86c1l2RwJ`(!})16vP~%FopupuP{92a z?Z7taew^I<5SZ;bd!F&^ZKvgN_MRl^%Gz)4bvdcQ4r|>X$$+(r!KnJ`L&cq1(>5un zsc>^R$Gs0&F*?S%Q`^>lHIAgd7Jol~(eChD>Y5UNyvEsMkoaz?-8+Y=N=x47wz5O( ztM*pG6+s<;Tg@q!lzR7edUIf8*kBYd{ZtSj3i+eev{ena7q?ODghG2xO2k}6p}P|v zou`4zv~9$GgQCNu;cMde?$*u1M%Mu;mRW}cm5%5|98T4T{sYNP`uCA}#|h^KM`Px~ z-~ad~xO}P>>ruIX&%c}XH7>G(Cfqk=l(icW>3CCWpyDRoLlK-0ZL)l5i(lT$7&AQR40Hq`KML@e}yWHFrofNZJrz>wb7D)x@e_VzK z$VwpXl1%jcSwA6dZ1oyDWl9z}%2&4I@MU92AR5O(Pp83lsf2N#o+1-tke((?bpq`OIG;coyl=t&Q=%N}O0}5|cJ;}zDf(#DC%12c0?*?K2NsR(UIb(AHK^DI%(&xU zU3Z%I{mz;#{(R+!yIKMC8g!$`s5L$imyL*1RU z)y8G_+G?EZs^#TMNg*wMrzIWAKCUnR)@iGuuktVMS8YKJo5v{+9{(JC{uLENcs{ZJ z?(VJdgC*LRpnaQi(0bI7tNQp-re&<=*#=sD;nN#I`$Y&pwZrZ7m?dq|$Ai3|&J|DD zB(Cp2CbTAOvsPtin)+AgG0pE{frERRZ=K0MT%pcQ2kxRKl1&X<`s~)k*S*kd6z+U@ z;aBWYsBT^U$y>cw>K?wz?sVFGz^kJ7eXa){Bk{|7tT6SV9={7;Xl zX(Mz;rUEWK0&vz9*ihF0GZZuS;%}58^cG(#6BZVX$N*9>rPnJ7+hqBdno%>}UBTLM zA9{Q2Y1Xl@KAi&Z=iPm*OEX8{2S%2^6#I@|OYOI8ESOWgj3*`l{Hp**{JVDyqeHmk znFqotE44v<>3<-+o7}`*(#osU{Jin+WUktd9isd&ahlVt*~TD$p9}-?1%sAKkS;6P zN{0AsU{LHM;->BOJKx-XY&0m`$|Zt@H$WE{mTp^jDb|!CMWTMHCD|k?FO)RGPbR2W z^FOxF9HKtRdL*&Fc?r23PCBHy@NVZeDVAVe5CrTzvt8h zsEIb`l9A)w-u)D9N}4xZlesJ6#v-h-hwF2wBjl&Ch-=~E)NiAyDb$r+Bu#&1_n~*h zbeMRfW|b;T(_)ILvb-Dba2k_Lih1a+zn{`H{jtdI-lTE}P<>=^ZCWIVykp0i~qk%S`F z-!#oO7d{G>i@hDd2X<-9R;j8w>`iqf-j$OZeb%xVa1dlmXrR&TYDS4{-OXb)g}q*l z(35oVv(zzAhX}pqN$K9NOl??VuNa&XfBDCfC>ayyJUDSj1@k#I54lv~i`bl1*%|FxGnE-+4!Nq}gHXi^gn_sO939#V>$%gG7>@9HzKEf3j}B{H}#0S!KpMm8CPg zu0=}UFHDv`SHIWw`-z?%!ck?>#pwY(;D%hf{0VKFVwXRq7u2%%hLlT=zwIkj6BsH$ z_KF9f1e(5JFLKD?E37>q495@kcu#7JyG=$^#5$I8@?zZ_SpHxC;s1ZzG_A>x*uL!_ zxX-tpPqiP3@XaT$O9?}INW{qvrY~Va?s;h2 z`jTV?La*>E4Ti{g$3>88w%BPB)^`G33dMV_7Opck3Lq1g9u6XZZNn}qSm|fHokL;&ZJe+K$mCh3q`ACzTdtV64@d98ATT%*%hHaK{F zWUJ_`Nr#&lTkbd;XzCZLBt2q^sI+}O7*r+{EM}AQ*eKo@H+whNm|~b2Yg=9t9dvve zqq_C9;9Xz4L)29-xpULPly}!ZWQ@9zZhU^F5IEPoV6&sC6i^;?RB73ojua_2C_v9h z!ETBbCn#Hw2so6iIGm&+3Pjv=P+ok>>CJ~1j5frVe}LcHo_A#5yvint-LFt~mIr2# zzwU&r7>{ZyT=~iEP8F%)CHMZPWvppuYhYNIpdq^R?3<_c@K`GamJ^w!x~cY zEy9;7J;<#Wb3$0{yxcK5MvmRRokeskx30Beoby&3a<(e;O{Pv{E+?g?hu^?Eq!^8i z@*n3u-N2a$KR=F44ScbS`cb5Ggwpn_d-QzZ`Q2lTV-v195++&fa|M7A34-aBOIJ+` zZ@U0um5jf2`|4M8Nk)xtK6#XV_sdD2q{=$6V_|VrL63m zX(P2tZyvaIg)1e5&@?_eytempO`@uiOqQ;##QbM^ulV}H*UrSsPUf4|9x9x-@S(Zm z{?*jlgIMdE$7=I5-d{Rw=$aYMsom4IQ>xP1{~)8p6^ps!go?YTkgM^YVVwm6s4pVs zUyxK=AXoZEN(8=lPe$ve<(P0rGxO35lmwSrr!whB#sBlzSKG}w8Qi{xN8%UV_>$dL)25J zT%FpM2br`&9^fK0kdBLIw`J=gbEVVlqhfCSw=UpIBq_&86~mT{muOK*%jc`&JtenI zliGGpR72qGbS&@Gymvj%V_RMkzDG_F89rvHyWQ*GI#GMp?O4ThVsdg8ZWrzu@kUwg z^ugOq?uqTky2np`-YwFLd1!sVaI}>7hUvC%!X@K%w)B*IJU_cn zBsO9S_=TnOEIuuj&3c*s)({IpzL*a-ZgczIF?TNXIHxKmrxNV~ibGX4?m#~7}#zUUa$vkjrsw7*SJZIW5z4+Jo^4he3 zP4ts)&Y6qfZ2CIQ6m!mPNd)$L&VsNh3jWmrPw9JOV;o;->#SS%;*dTa5;!G^(O`DecK)#~VLed2H5D^qBkMBRq=fz%=X zccJ%_J^QB?w1^3nC^VFlH|dc`QjaCBNQ0X|JIcIxEa+NwJB5`{p}l%7{e7FrR>Q7V63FE;txD z^awk&MeQL@x?@CNs_H#Q71;o4?{)qEV`fHP_URy2Nx`^ zXHQMnKzuH*K)ZR1A;bE~AItt}SxzxMB_^lIT<=5j{w{4N#D++QtYBhJwmQ0mHrIRY zM&!qh4)uRjvj<>cQf%a0?DCk)L1y{&YDMR~>Qt(DC49D%``B%he?PUm3@Gu#k(;xh zHtB`@4qa2NKUn!znN@cLga7KoTqdP^8!(w0>(|`lgLO}Jh01=&Lu;69_>G3d%AK(4 z{+ivZ$vrv`JkEPE-*HB!7ap1^#HO&b5e)1Bd@n&t`w4RK^ z`+l4HfyiuxRN0cJl0-+?aHo1CAy0L@=|1oFa!XOvlxx4=Qy z?w->VOtb-IQ`s#Ry{p698u*8iS1toYKws7OryzaPuBso~PnHJH-I7aLOCcq~TJ4}S zQXLiy0bzreX|crJ6FXK3IGPXQ%5^PfZTF}gfA(`fIEksY)BwkR1T4^WA3crC&(&Rg z4GDuTbm;=88`tTZuO%$IiAmXR{d-y%uX$_I(>nBzRy7`A)%yBBI!DLeuadDmq<+6M zbD2jn0jrwAQ=$U5K<6OK+O8uRK9$ZSW^0mdlL2A|Zcip>1ol%b;MTl#pV+ROZT(|;uu?DJ#3J6I|93r1m-7Kl)S@0>@6Te| zlqC+I<`c4A+u4;0xMnOw*5@YGF#41=LigUZ6an;K0FG;aLC-=Mkxu>tR4m(I#ZD2= zeAB^xnSj*X`mAb1k^h1$a}L3zDBisCC_d5QUfKI79-1{YA;HJuXN9h@l--&2@88|n zzsKd?TPc`2y^|Br4fD{&l`ybemZney-<6f}cRO!~UiN}DwUF_c z3py3fFxhw&rqyDBn^?SFd60wd&9t)hz{^T}@fuE~BKCtxfWWPiv!P{p@fRZbN=7g0 z6Aog{+D?Z08JBx2eM(n;lcxOJR!vE#u$o}BDzS*ld|9JCVN475OnPbfyYdIDmXaW& zXnI*gtgga4sjm`%(&j^+#ZNT=(&%a{{)XZTqSEMJOIMZg^2#{O*Lcca?sk^Eey`Cnu1OLNEq==S z#_gRAoNh|;V!oSjqIA3{AU1~hEYLm2JU5PXWPtcsSW7EP^+*$+_ueF%-p zoWyZ4$Q4Eg+jl8D{I;wY_>uW%$)ln6DJXT}##<&Ad{X-bUfhL$w7(}8xfGgZ8H=~~ z;=^{83J)nHYdX@e4QUc0@S$auM!cdB;#t?G&v-y+=go2X)B%d2o7Q+}igE!nRI8FG zIO7VEWO}L0?R`aZJM?o7I*g3iS-tZn5E|N=IwW}B|=Js z=unw1#5^bOcZD+W?A9|^>AK3qfz{LoiMss|*R!q^-kem>X!*u%$o&?qqkPEddkx8qV*Tu0wD2`icdiyKddgv zAeVriW%VDFcHbEkkJzN`0ec$dUhT%EpsB_9u9Da=fQ>qIBm}t0lyfj%R_a< z^TYXJuyQ4dwOD%3g4_aH&Y7K%J17#iOiWPfj;6Ttunn_)ko>HT2x3 z;^g8NG*1c#8F^zlT&zFaGyIBHu_uM*=n*g4dj}=Vq0ws>Ot0L{fkyr1oekfSL(+75 z{`C{koq6!m=kQZtstOI)oO@p&a1BO2kS{O_C{MG(!eAZ)D$2kDvi(zS%+Xyf!f#Dz zM-Melxk6eolz4xBX7WalG5K@#$Sy2~zT3`T+Qas#y!CDh{3V4g&bo}3II*q9t=$Y* z(?#O!8*R)Z=pEv?O}=GnOOs?u&DQ?oNzTdH`Q~t4=t|le=kvbJ7mbBD?8U05HT|=j zHs(v>vkfowXDSxIgtulHdn8;iS125N)V;&srVQ5tF4X+~BFtdzu+K%Lx@Fv=H%21n zHGiyyBuxI7s6Fkw$AT$05!MR(221?SM@Q)GZX7jE1Av<`MC@qI<#EqBaU;KAr9s&- zs$Kg9cu#@lMH|y|IZZ^v`h--d!H$-}TSPmVf#!OhL2!~IIi(3TY!vZ$xz4|4r!g`I z+20e0lo;kEqzD8e^SnO`dp-9P-b8ftc8LJCdOlfsmt`m)uOiu7%;iRoa15FpgaoNd zd}0Yt9a)rn@xsz0>{zl7RXs-SpN;($f%pew6Tyj?ceO65xQ`BkyH$nXeU&*u3c#0< z7Vsz5mOBB%HC@=J#u|aCX*Yt^81KjGr$)qN73USwTv{~-o-sa=J*`6vBwJ+C_a4LL$ zV%U(52s!XU#9v!8b(+us!uB_vo=glKs+O~f({CP;HT;roUuB0+>}dqO*Y>QkIpXd#!c8{;Dq*vzzO(jt3~(A3A>tB&jZP zxTNS-8pj2duqUm4S^aQH6p1cvQ2^)M&M|Ry(ceC+G@;dJ}2s3cN{)i(gLX<6-=Ne{d)%)H4h>f z8-aHMTM|6m=Fz8fWctz61mU5Ze|f0y{R2S@)xodBE{hAG08>acGxeWrtB@DUh_ai@ zALL_VchmXnvY1;taVKkSI%g~oD+r%ct9BDo_h7)amZuk#-8UqRtDTmsG__(x9wZ@i z2}W50>X2~@Xq{QzM0PiXnB{)vnQuPoJbaj0%>C*gFrXa^ZJ%bCAb#&GV>vP(mD@tB ztgiR*52;u^JlS`$^Ei0r7pV@l>&*~raP`@V$fD*J)wqHXPbs8r%dyqJip|7S`ej{TuY3GyCWwBPCfdzDY=inXL= z7c)(|hVb=1Zj^LmPAe&*8h1wcAwi)VhKG%Vb}&ykR%rPXyp!h!9Zv$PV#xe_8hu4? zqD?Ic+ou6*CIYl>hA4Lja~{L@JcPU`I!wKFq4Uuoq`vS}AFyt8SqytyJT_?Vs-@lE zntDUA_;yu`vczAPRjO*wWhrfj)C)J=!C$rB5{A}U)0B-fMFuF2n~Ub~^2`tHl7xFf?Jmb^3}x_ol@mS6WkNB) z3f~NoPbm)lVUGEv(c97W=Pu>&8EYg*ST}kUU$Sd`QYNO%DZA-~+0Kf6w??ilO`gmY z^a2}aA#Ihjvx0^8SV^YoWph&}jpm;OA+7F*a36Qb3Cqbc+>Hy3NG^7(H4D+G`&6#K zG%{gU zbhQ*zB~T&bof_+`_+*lMw8W&QKY< zOxB9G)ZIE;=m}eMqrlCdfb)PEnIP9vAtj6gx2`qvY|Xz%p$U~-z5U1BB<{!e_{)>B zw&AKqb)w?d0O4inqK_vobbd&B#2pW`w-FHQfYqYx?wrIgi0vU3DgdWEk-DJC$i22v zv)DGJ>L93Z!6Ugp-^p<{8YUMU(=q?p<%2g9GD6=tYxkHvOA2&N7Q``RDo#gOlpsi% zuMASZ=@q-XgaRCN!0p1FEWzE0Lcp6*iFW8uWJ|{bD6x?yNfVy~yyG{fHkL(RO>JAf zk~Xz`wrw#cP((LEcTedeRnXJ+TO*OLzEEQ!lPuf49ROWk{UCI7u&I`NR-|pkH0t%8 zI*_F#RpKdkocX@Ur~GE+Q9T$s>&i5eKM=dqaf$C-<6hd!6ZQf!5VwfhbdOcCDQJeV z3>RYnwmm~QL|F1|ajhGFt(d8A_E&UyDviFe)wfvYfz27;MctyG#wCJcYl#}0Q13hWo^uk2Enenhl4%%+h4FWL{943&1LyL_Jx_KS$#PWc@J!@OdJWDGF`qj6-P4E3JULB%hcb(4x@Hf}Xy!FP<&DBM zU##W7lP{uDJ_tC?uhE;%;Y($9|{yh-41k4;iqa0DMa{*?Tl~3 z4KrDqNf?`ogPheqF0VHx^!S*v{lY9>%VSjVwd?t|_JD_NqQk)N(VO%EjS}_cr{hAI(W^04CK|=?RQveoH91IW_fXYkZNp{+JN?coHe1QSPtp-<{nf9ZA>KkzdX3 z`6~yTRcxF92-5 zfY>AkJ`J?7g2fZ_L_HTqNYYEz$=D@eOoiSRYSf4#Wv&}sJQdq(1ATEgfeN|6c`v!P zE&?9zk1c}^Tv|1*JxBrDq~0Oj`WjWBK5b_EyDyno`lZk|E>8{E{wDdrm$+`Gs{lRM ztZMla2)01D46!_9E(3pm(^CHF)1J!Z!No6>8kDlK z%h?(SOy6FdUf*4N;o-W|4qg=LT_)HwSHc6^?=vH1^1(aE)9*Weh&#F+E6h6b za{5L@&2^Ra4IGyxy;6Vs-05> z6TY#@PqD{sLi+x6WRQ@tKGGBusdnp|<{F(7TyxU#ODmW~AKs@E2vs^#25uyICs>G?i}bDi+#Uu2d5&S{w9(zLQe0z2gNGDPw;gHQew80kjJBiBJb=_IT=nScBVBg`q;iwqxM}^_iHB~H4xp^w( zZPStM(op&F7a!vWXYjP+Bv?%J{Xh})~;rfjB3 zf2kwwqk7>KwrbmH$V|a?!($G|?dUvNDqShybn`@*K%kjNeu(uzs_Kb3ZI>}*Q1dIU z_*>%(DMmOmE0c4t(L?74qdGRh?6Yse^tsG}=*VwT>QeRy8A zAZ)>``T0k&KKld1Mu-_;OYJzfA1G7ClSww1`MFsWc>OteNAuF=RawRHuQxsJt(l#P z&G**xXRe`@r-bGF0cUchU21v6aU``DY7i4Vfse^I6v}KaRzDN!lt9;GA zPqL|dl=gjS5Y`*3q{mk*D0>ieWANSCoPAHF*94teeKRdty~Fxg#nI}Msm~H>Bq6~d zrrsk?FtVjtq_1EQQ^OGf@B)v-x#<{O{|mIM?Z(^$|7<>TLC4En`~ic->J8Le#-9>}Twa*SBd;5|zE`D@ni|6wi&WWEcNDR3>V+F?be*Su=I<>~dJnp~Xx{RMP;J{NyrZUMAp4o2Es(;hN$)?tMhg5E z3R=5*RW1u|rouiyXjiK?uyJi;zH?zXIE8*z?#mW==-IG;^fQBKf#;uS;xY1e`SS4* zWldET=v#>NZT`I?eD=(Z{%w|UqQe*G-mlGUJstnlH}sOZ0eGlwGdRhTeJIo>lZZ*L zfQ)!9;d;b|`*p13)5_$oVxMaDz3{M&>u$bzF#0~(af*vAk$nDaJ35fd>pwW52ge_zVVZ zp!ruKj}0dB_r73Ow17fPO~u9AUTfHtEUmIXf-Gf95eDhm^EuwlS$fe4b&VEspo;?h zMPDA9dP<_dRUkQWBtbhRn}ye~%T&9BSN+=Cw%&PEf zedhPHztulr z%Vs^El5lDDHLHuEbUG|~gKM?bV}dwZb02cdQXTMSEgm~+XdLRQXhAcM5U)wXEuQ(g zTezdT)A`BjKbkBFYI8{}rcq&8{1&%sKIe93f`uD;LQgz%^YfAtz2I)giUN>iDwo!qx10uvO6-lnrgrY}&@nH> zfrQ`;r-6&>{kJMz5FRxScH}x+KElqXvN9;?BsA%u{1NWs%`7 zbL5*>mpSBs{Yu&u=&6OyJ?E}7O4Bfv2LhZ^v#h%t34_kv&CtfU{tvJ#n@5eE&mCIW{!RK;z z!IK@!b(AP=l_Gua&De{m#I@;=kw6H8>mNpcU2>)iD6+Y{J_qrPe&Bm~yKw_M(H&p- zj1X0VT(IApJ@M%$p62%K0gl88+Kuzvo8V4<4GIpK0L(UUEvE-zXp*-7+MYA&%jo}= zOX@YuHX!%nHZS>)?36z7YMVic?`^)4@TYEl-g(8@o``YY>~_BsVJ|^!6_`wGZoc2- zDf1VZIJAf!DNQuXE#=Oysl3jLEo*SY#aHeJn=ge#gP@&Fz}i_uxftq_8GM=b;G_Tj^J;R=-kd&%#W3u5htziXiJHgsqq-1#UXd*ST0a zT-b9`tozcLepcE@wFLqZXEzs%6ty1cSvj9lQ?0nQV)t(dBX_D##LDMv%N?^d2R7r9 zxLv|jjY9QW!2v(x){S^ejkIyyp^eg}s<%%suYfb1CW5(|D#j_J-_92PTfJz_=xpry zd-}gO%q64MrBcl!q@Iirw#?*(iI#^=g4{AcNfj{PUjME!!%8hOej*w8;EDpyL)pDi z-LjeMGygS$`{vXaw6Lq=qXYeTBcY#R1B?9xLBeg1eX6}OkmG!v$+UYcXGPaiSQ2`EYyKt5iJ zz1`dZ`5@;-_fApN#tjWEKXIHM^8bhmL=54s$y&Z`@=3kyHa(=SqB%-#+S%@hwqZJ{ zzEORwmWonoJQaS28k$|TI#iA-Wh-xmUzo`H>m`Zm_(oR@5$VzSE=>duPJUFG?^q4@ zOUQtOqyT?;{IZ*_{_MhR;{QHxCu^SOyC<^<8sTt){`n3{4_R++c~V%-HAhH)y?2pr z`vM=*@gO-3s7XXr*2k}7_LD|kivp7vO~u}6#9C|Hz0>`BnXVXUif5CQo4d^Or)C%| znHm7xSDVR2Paf&W&UQ?_Qd3!Fn}tI7Mm%fE2ziba2!#LTwOpCcugP0}0C!w!pfp{* zoir9Nv1PKd<}*b5b*!4kpFbhhv_IH6IrBSmqH$#Y=*QhvQRHFpAd`RFTU!Dw{pXxt zwShx~m&$AsCvtdpgr+v@b9M&xl?z(5;C7rT8|9?{9Z6tR>kn*qo$D@ImcFprKma%N zxVCW(o5HASE55@deZ)z)*V+-?XT4%Dm8Ir_T}Yj4-PYG3)P0oONLxm84tr}v^F$3y z^uc0gcAgpR&vnL|?{rbb*mu?dU-E*`T~3JT-uwQ%#-W@M))OrD-VzRI=Z+oj|+X&WH*=3+*H74 zfH3(E5>!eO|H3yFm4yV%tuR>4_+P{%jQ(o!V1tWpkVIUoec`UX2L zJXH%4dscBzT~27v(A~O2QnD`-Lp#R@_J=LjaC)BPY{-H-nQ@$Mqk7~^EKbGzwer$| zi2RUxsO!JYD>#X!*LB!Sa#c7+z{0L5LxRQW0o->tRcB)T*P|%BX~g4aKt){hILdG9C%0PYXD6p#Avh|3bIDJJqTq z;`Wvt5KfwT#R(pIsm^)H1K73%E;n7Gx| znm)PiJd3U#l?%z#bXMD|31yEb2;K`dCm|MhPEkg2@jJ17?F38fe;ysgl#`0ie^Qph#%evjC4r`-Sa_u>YFI)huGW90O{7@Hu0!PugJ0bmn; z^u@hxBM*yAMyv93W9VCk?!~2vNE^s_uZ>;;?g)9#%#dX{%96f zG}rZ2YA;Y6R`y}>s926<;AH_f$DBB1n#$?y8(lxwP4WnzY~vKHo(c{6naBpz>Ei1v7o2MHqNVzf8mfYa+T2##Bpn^@ zWBRi%$*05#8h8GB7oF>FqolC@`J@iCJ3X`ovoTC7DFK!P?rW35Mfy(@$_hy7?#Mfz zd%|O)n(hRk)o095C-PLmUYw|e`>uZ2>$(9eiR~`-Qi%n1P#gb{omMm0jVzuE{_${h zKx7y7?Ox%3WQT+*iM~Z)!q++|q73_tb2Jq1VVVeqza!abp#v3`CIq`RHIObfxj~MO zqvAHv<9o{G4L1`6!ItTvY(m$R3a`kur3L}&p@lcQWeXDFAjYH0n!FwJMzuansA-Ff z<>9^X4MI2L%R0jLL*{jPhi#kxuvUlVI*B&xZoh~Uyi+R31Z+|C{goGqmtXAff`>fY z0sR<-M#U7=3l66VrvW+e>?tK7=9!uPZA{^(`)X6ki<_2P?EbOC+I5O7;Q(hs z89QloaTC$)yv&wxZqNpyk2b5Z=$AqK-V49AKoZufY!^W$QR!sZGII30Nx*TPzhR@Y zrYXw!`fVWJdRS-XgZmG@O2eVW$4df09uTilqR|4hTT_@xc6MoHt>b%$kx4z8mGDmH znIq=a67bYN5BB-}NrC~`X>2#g5cR=ubuI7!Ok#)Cph6L0o8kN@*O?)>_@2>1Kc-_y zQ0ih|TDCn#JYdOSv`yFYS4vo$$GwB8D(i%eWa>!7iwHVubZ4`*sT)ts2zN2s{{(Ut z%mT32{AXuy5B}VRO%Kv|pKEXXcQ0ig8`@n+2;GDw=uW8qS&Uj0=vcfjUCmn~r1%f@ z5+ar|8?lR8Cm~-#u7HOklzbeM&LLGSy>j?Na<$=|K>TK<+PB>;4<5OZE7OhLiLK1f zdYfDe8GiREQghE$vi!JsWvr-cm)6`K!%*h0Pe`I>Pxq@4_Ld-*Pl#WQYx z_FY(xw#-}Xtt4XG+tW^MXWAFH5t!#=uOx4Y@%U6W^fT{D8MRJgqPH67^#?|eAA2i| zQ3VO*Bk1uOK%WUUWalYCZ%OCj2&dUkb9PkOV4sj&6}fzLx|Iz(ra zq`>GMiM3^opA4hR>kk0}L(N^l0O+O! zWKDc|_{Lf+Exq|4NCI&0-nX`OcG{}z1Hk{G>8#(H{@=I%HW5%J4HB~{VIV5aR8*7{ zQ9@!g1~rfvu^~t!Ge{8zqJ+c<=@>o9sYo{iCJY$c7;LO=KKF5akLRziKj3;@*ZFvy zlB6iP%DFFBkr5IeWD7lI$+3_V1o2~P*=IWh-Xx+`<}T~BACsCe7kMxn%C{k?ctn4X zOuMjf+OSIBIzFjIzc;Xd9gZ%}@%nTXq}ldVTJRh&MQJIcqcCvt73ZDddkuU03tQa?y6$mrT^XZ6rk~o_K38u znbV7fWTFI`VY)rBG2r$UlpwSxg)7@o*YQ|2^=xZM9u}*pGiWk9xra}k zo$m@clFlZ%NZ08wUvLg(glqHe(i#VkPY)83-E$|cS_UN6Kaj(h*(*`b2H}DPV)S!% zGLbCMQkzu33;7!JEv=>gPebEN!cVN<>JBw7LHssX%FpB7DEn3SN+$ztPl_Nfr(DP7 zEdGoZ>C|8BX{J+uh*Nf3nK(atYyGfX^S9tq2QY-`CHr}0^qcM|I8)0bJj2j22~F`> zo|FM$mO|_O%8aX0l-K9@@M2wx%;p@zr%7VS{iWzN{q#6TIpv{D6&FVAG~eEU1n_*@ zVPSk2HEZ+te6qr3eR}+>-~aqJxB!81@kNSC=4S+Cg6l|Vo#5)iYv=Wiq}$Z>2bllR z$Kcm}XYoTnn@N4d;Pw6|;Z{6jfg{}Hhq35UZ@S6LD~G#r?pL0kS24B^vEU;^C6Vbz z<9xl&Y;DnbUG2qOOk~H^?ROfK(>0-)!&P?W$cDSvLE!Ntmsw%4YeHN6GuD~sQ`Gl5WRFKVuc<#z?G;wTf1W5O`4c0VYe3VPm2Mj+AhnK&vVUECwM8XzdM^@4|6 z6DSyLdGAPaqW$-invBMG@cR!gm~CA8AagH9IJ_@LRL>|7Pcwq%c|9CN-+(_%z7hkT zKGpAY8hLcw@DkW=JpLd)Ic*~OAO11RbKXQenVLW~U{+LpM%{!JX&!r90dyQ7f8;9C z0hE5MUa#@780D+qFpnI-YvC`JK*{0|tUdcu$oMO0+ zhVkV;h$>iE_wz&dQx@MBGtBsUMXzV_EXxF1dX(Pqd1C)8y*>?WT*j&y_=zaK`K{%4 z4A@xtuk15%%N?PW-d-a=1Hax>>pN=wBJFo<*E--DF{zrsA)X>tvDA&HQbu}1)MmwS z!EFuv5vI;>sEDN)@eZvxa&iBK;X>MR&`T%TW0!6w5j~*hqGK1z9(VWO`ts|NHxZD; z^ZoDb@N3C&2aNCcFs2fwQMLk~|Kxtp_^rp?YCXzU_d8Lz zfW$OESqi}dV-YAJS~#P9w>QgaAk0_e?TBHtri#G0|3gIP`LX=|JNg1I*2m4BPzHMv z1~ITp;NTYd(8SwcyAyICEKSO%8FJxi+mgLFvj1Bsh_9$XHa_ZwvaKwY@mid?NbFh= zwftgm*BD~{F4$HuRa@&WNC+-Glb4HRc=CPzvT?_|8}PdNW0~hl8y~}?4AL3k`17Uh zdDs~fR3vkvo~xxZ*zRDCZW!6>fsw7_0cVhRGe@hJ(N%)p_7Am?X-OgJ~Ma#r$j)ba<$n%Ow zS|m8CDW2&xZYwvi|1fHhAlwYuOSB5&$_4(~eocIpVE5$4Mn5JEGW;X(SuG8K&0g$4 zo95aDqDY|#Q7BGL?&SP`S7qaA>|r$l3R}BpbXGfql#0+Ty1Bn zb0z+;bqgH8xuuilRL?u2Y(JS27_G43n^`8H*h~E=g-Vht!zw%d3+^vDWGw9}o9ADS z3>f$rU!?nVflauySD6E%JUTUwc5d;c2U2OemWAYxR8L-d**xXtIOIbe=%8t(RM$HI zF*v=q1@6Mh?lqE|rXa5Tf*}7jeSdv8ZBpdAWQ-ZRxJk^ysTl=c%5`Ts6?xk+!jZ#`(69 zeumdkfBne|OXIbNK?_Q7vuT|}IsoLq!|<*kJ^jpJ$1QGI4|UR5q3M3Q?yBG_J(QKY za^J043Otv$AHz18uSPgKTP)QpJmBXAK^1aZ#`xlP2-+e3QY-Hr@R|*T%y505lEQS4tSgUd_?Yw?i*(Q zqk^7@2df&hT;>V=1Ot3dmYnr&=AV#HNJIk6HKm7anca)^GRN6p_6o%K{O8}2?l}(b#EHYdSOtfWhjhIs!^3A4#8nOg*ge{T zt{in8(h1EK^I}4JZx#GaVC}ZhpxqmUHSH*ZGqI|4$Zxf1R+~>|WwwDDM|gXiPML0! zy<%_?nSF}XOcpHBn;q~Q*#AiFt_>{mdg<8MsSW!tl*f7=e$vke!+N`F%DTmBR zCh3Xm&Rbb&ccHz|Z{;sW(G~0f3NvV8`n3Q-gv^vvh22h=R!e~%Os9Y>*U60_8%kow zkAYbGUNgEQ6-%rOwZ1D^0Xk(UY>v=tAO8-^V9bvkbS_?JIy@(Eb`WX5;GQ?Ys}u3^ z3fwd+j}{cgc43T8h<7My>W9zMFcE@H5t*!#>EigSG^X##j1XZFCI}x|gXs*Jm-5}9 zzOTG;yaxWb6NZEDn~Wc9D<@YB7Hb;9jHOX4ziVL3i=dg69Kc9h8o@d#^EM&S6WkU! zJHRC2BV^_`<=B+BwhXN-ZCb(YQGT^kviF(Ry7`&Zi#_iQDd%#!wf2&hV&~_Fk$e}v z+$Ud4fhQU1t0MR~=Tg*>FV``mX4A<4;GZDz zR4ssO_Og3CI(`2_Bb+C!VqFP4;>FGXmA~uqO|HtYJWUpv7NHB;2)W#K;gIcQxsCMw z8GReIBh+r@xvqk_XL_p{Das&zMBORqDXL0bk8_xA?9#NG-0leeP>{}Jasllgk5Z$| zw?73MR%b$sqwGhC0Nq_gWwljN=;Kgb2_DSn#X(<9jLLY}kNiQJgb@O1Sb$$3jz`NP zolvW2jo#oMec9j%2;3vE3?Tb>AYWH&12)*gJ3dam>uA7y*OMVXB=b~!0+Q?*CQdUP z*ti5cK?UOViuKyQ^e4i)Q2rbrytR9o>jdE)?CjPV;5yRZBxHN}5GS2}7tyu9M9%Y< z78pOWWQiTu0)A2)9pz*6k$uVh4Uu};W#iuL@W1)?*7^GKg+G0r#L0Gdn|oVMV_rSU z)A_qaZP8iP8u2uiqn0o=WjXC@#M3Av?Wr&u@R;FSc)-`+ePWvAdVx?>|r~Y)=BCmmJiWo z@DA+Ec7K5MRKPD*>6*+m*B2#iq}hgcW$W ze5Gw`4D)PXw$Ri&!vhtNza^8wOnln^rhGV=df#5rY>bU>b6)1@%eeXvarbYvP)DX~%Fm0!&3-lxbGVJ(jT9}IH*2pqEdtvh# ztU`>;(E{otDhY?K4I#+(<|w5K{R8SrbYHrraQChwW(U^Id#~qJ+{=7)oBR&l=NtCc zUyMU>L{c%L`4MQ>e&0+9Rfle4we@le*uHh+zNHzOKhm&rWKt=|deQusKGPqkvH8Ep z<0JK({G^CNHN!?1vsy;#5%*rP+{)rA!r|e20n#-Du=|M1^0BQS0 z`&MRHoV^6=V|uq{Ei}wU!_Uo(hCbYXX~Qia%SMNSI(wfK|Boq|O?Z}4_~`h>D9n)* zw!p!;&?fQsC?~`D^IRQ5bEHtNSBI#9U)R8A*bI6$oX)CAXa8n-vSVGj2j513W1fY) zM9`Vfl+RMw;6N@&;`pPth>ULfX?%FF*TJ%GJ}K-UB5-cD{c3vz*J1AI9!U+=;Ve1- z#8Du#o*C%;8`JJv9>&yef@UsAI74sa9O!O#cboD`0S3L$z3o8RpQ$lRUam?PQhlFo zRLQIj1M$}!5x#AfE#K#38Rzl~MV(xjT_cByJj@)IPqID@YnRw!a1Y#heE!Rp*SZCq zEjLh?YC0e=e&bvorE{X=xIEZ(();jk<>SFOEr$W%9K8^cUoh=hU}AL_=^NRb_MXDyNsf09P|#Q^A?PH{%SrV;sjjXBz2`0 zAC?lpYP^??t~a*_NFY<06dmoqCz_9lmyxxp^t> zc}@~6fp&hy)W7BYff23||D=?AgG1zGZN}gOT1fGE)7oLqzL;D}f*Km^5>7QRhzxx;&$s8vD!(ypIDH{qKlmJtwE6MMb-(}IZ#%ku8+)q z2ky42#|_1~w{LtNuw{cX8N!3tN9`7@x5RslwGgMETcT>^aM2GLh081F|75gQ9;_5f1hWJBckE&m8sb?5wL z_%+`c4?7=>FzEnL{AGsK=MD`$r3n?uPB!_b=#ZydWV1+8)dV*vYm4()abuQl9I-17 zd+-R$L;bh{^pPZGqT&NBaOiK+wt6T1?T|pj>J!X0aQ3W)sd@*`DR>z{+M`qiuJBJ!4 z6mXCQAu5M=dA`^z+iGv?FxT%sr(=?Fl zlX75mL~xFGW}-GxgLbmt(d$~EF^>b!<>Evx4Ls7XTg0KCT8;$mN6=1vp4ejE5*%Et z5o1e|<@%)_mig0pIoWBL1t~%~#Oqd-+qB0xlF;S5JFU^bVxUp2yUdiu2If(bdS%h1+#4#~C6(fP*2%j$V;97T7F2|M~NPRr~K=p#KL zu#~Q=aaZK=*U4$6;b3uu7JB=z>0$5p;L?`$;S?=kpYb90E>y4GVyIqyv}LZ=^SD1i z`H=-p;k(88+pc`i;*zaX-z|qcyhkaQ2fgAby$%73(87z-pYq|*0FokE*g;FCr zp#)eiD6%4<|6!=f6LK@Z7a{Mr8~Up6kwilPS<^ zmLGkf7yc%+9d0uI#IdAL;px3^8dXyA*Uy{x=n}u!mv|RY%8l4cmFg6?h0(`qVR+PU zU!%AN%n)d*>h$X3sCISAd~}P@Qc8VZg}iT#%uMJXmQwf?l#p|kq}tHOfivSGzr&OI2j16P z8zq)DILsj-A@Fw#YW*Sl;T7w%R8{H@0YBuZUD6J ztL_oC?bfM+OUANc`^=@cU%hPWAycv{$C=zeFf<*L*9ur?`hPHt|g{%@hd3^BSl zPMpm7EP{k<>$Q!VA*e6szhD^1?_tBt5ftkUhlS&OhEwpsk$KjJ?>|p4TYMK^qkDVF#Hb zG$z%|$wl~xA*z&^$Iu}tvGIT=*>^ZYF=9-Wmpxj}iWuAAc*{hN-n!1+3MgwZ`w7mF z@n&@FA&Q#)o?hY86AKyR%9d+T0-j{mKmRSvWRN1=_g)$A*Id*yUP9hf#MM`4>UR;X zcEdB^eh-^40eOJPo$U0=)>f3)`;c1Twk4YsGDoC_);dJoaol6-^v+guuYjyzJG5u+ zVU}Fs79vx;1s+r%RFc+p9`4RBoQzMG0ny~dZP@IUTtrWhue3gC%rk6j16`T2+{l`x zIC6x@%FJHgxohpPr~jc3xBcyZF8=)6?qUz4-+(N&STtDOpL7Myxa8SUFS~z51$epP z>)Q!b&XyPRKpbOyFcfh&WAsSg28XuyM6vtn!*>Qvzf`JW-y_yE!irJdrVUdM&qika zxc#!LkNA8830)e0k0vn7q_rI9DRr>rI$fVLX0LNxH#0XBfU z+C4;f{*HbZTpZ~R|AhZDj41`!qaE~4K_&2#gwZ9MFvpR6)cmiK%i{JmKd+Wkl~iLm zMt|yz-?smKDegDvs=ZHj${75U@R<+jw<2^+!7k`Vf-y6bMKAq?ESK}w|98OWvb8u`BHdqdN z3;jXJ_hu)4fJ`=|2c^IY2f;8u&)QjmwNMwVF|Y4iCNEgqOV8L1>c6zQvaQp7Zpyti zXG(4h_GsQ{9V~^EFUx9;Iy}=CVpg-zNw*?5pG${A6Og9i_b%bxL$JKOCNOPwP|!2= zwroQEQ}gD)zX`x$_J`%t6S-)M$F#CM)VmaT#arqyji>>*4M zyIhU}y9dSAV$%)5LAwpEXcps7Q|t0ObylF4d)O_^n+M%%D+UuL)BKcw*_+{+QLtV4chNa5 z5V1ItZrq?5KDHZZ2XguH%RCAG$tDIQD5il~+oVfuojUe& z6Mhr927rrERroW@OR7IGaue{No{Etyx+>s&U7VqkiFj3yd-qr7w1HvQ25`yu)qizx zN`qX(#y*$&;Ya%ezk7)+*}#2%hG1S`oF93CWPUPE ze*BDiOO?F*WbUl2=PA6*{9lw>2R+|O67dWi?keZB>6>dPwXOJ?{cWDA(mFlH@I+{n zHR{iDjni0o*y}I07ykIt?d&sAP4rrZFNKH;zTKKavw&|6vBO2NG4(G`1)*6Vc)K0y z8%oarL)eATrKKA#;^S`6yi6RBdUP&SA@SUBHVVaX7#=ATxIM?~;}6&O<+1-Yrv}ow z1*Q)7W~IR5>)iVGfUoZ0YM)`UiOX(F3lgFJ>}@gdQY+mMl`O zex&%@pMJm^@+NwJc3i5A1`-8o{#bS|p^=Hi|H}dp%)W~hhUX>T@-lql=X%BHaw77M zR!O$YgPs;uUA|(4-UP_w_YYVHEgX6|fHlph8qAVTp1)LQ@IvinphqX?+sTu#Kbpv= zQa}~Ml}rH%YQ;I+`FqTN8sX7b0ZR6L41h(af*Ty4t~qRn&rk_L><|`Lg$Wus4fq9{ z)ZY6`78t1R;W@1JI7}W;a~>HU>fP_vv!Zp**G1+Lvk&)Zfc~Az(<`NT%nr;T8|$r5 zdFMX?Z4MzjVe<61>~;2e_{)BE8zi>H_MF8qE^)$N&tmL7txSG;jkc3L`*l1RYHM@u zWC(%$j#x17ob#PlYoV2xjZ&--_4VvtHQYY%me~Um9jsYX*{v*VfCD!Bs{P~RFRW5( z^=s-|9KeX_>n!z189)R*n2XNTji0;i+Xt|x6zI=7tHC5rT} zgugKV61RK^->dt*1sGzrev|0$(EUj6C$+Hm$g;yjfwL<^JeZwxd=`KY8F%ieecv81 zy9)LTvUuU2QW>V7llir2RnFq3ric;=A}hj>_O2&F&$1^jEWh5i zl#}vRlkM0ijCl*+xZ5STdc+c+!Cinwt!UMy6Pi-GLvyx2G;_Z-Y*F_z$gpF)R*w9* zga!Dyx^B2QN7O#=+evX|^RFTV$83=mq!-qfXMWAwJ~9SrrMX2Kn_Lh1v~d<@?xWx1 zjF%kdPoeG_$G~?07*hl@7<0E(i2ctHT*%#oaL_v-P8|3Pm&s+hhA__v7SYSV9T|2_lc)uXSwr~Q z8PJl1PcU-cx!!w2oC)UFXEG01fj9I)&~G$JR3mtaVo4Z9a`&-2z8o~T5J3iWKsy-j z0OBr~^BY+VyaQ&N3+!__i}nA=$~t*(4u#FgC8LB7y!&H;S9ii35dv7+TWqCcpWmrW zr-(m&<<>LWC-+P0BRBQ05lQ(F6zCQPdqB%62^adw@Zp6_^8S+#R<3S*b{XPeZBx1| zbJ9$!r=|7G$JtkNJs-1jeTMWEVn+wsfIgQmUnxNNvSR)2s|rf2-{xu@)NRP)>BriG z@w@t*$FT6;3mPE`9lNFjC&A^%0!i9d(IagFzNdcB!vHiy%;L>p z4O8)L7ipAJ(tJZp3aR_LBS|Z~$6L#>Cr~V|dy5(ZzM0S^jqiH+MW}Ma2O26m9Ve;f zh|N$=L@(-==i>YPZpZiuzt1s;ot71YJpXBmo|3;DvFV6i~hMwhF zePy%fVZP}CH=`W)6^KSjgT#!eE<;>@O_x4l9Y;q>fK9~_qxc(`jYXC&N+HFCeII97 zqF2q)^_^QlAKv}ze}oDxAF4l`i>|N3{mHcq&l6f{Kgu*brpexpb7F-y39#w;7IE8C zQrBt>bA8Dq;)sC3{WyEMSUpNJ(v_*|zgvJ^pSUy+Vt)u9xib$2V#EWjLMd0TS)j*(vbddM=ih`dS*yN8GCItuInv(_s=CW zL!4Zv#v9H9R#*bzEBvYBy}1Bt5?Ms%*&$8x@+K z^0olFBMEzP8C920!jbS}OnP8;OcDwmejuZdf~bgKTylR&f-Zo%8zcd5`FF64mEBum z!%(k=qhS4#=-81wMtRX0()JJ(xE~->{0?HR zZ_6d~#BNre1aw~QJ;xJ^pU*=UfEum0m*^Pz}#7fobg0+_Z78&b-_aKm+5a z@hgfrtNKrrxN&hmU2Lmm+Ok6`KmqJ(k;9czIp@y*?Qdm&a+? zx6Zc?(%_we{U)*>#$EIMwZ|Qp{|ey@2N&yVM=<0dzd>vf*m)QyB5J<(%KQ9^UE~-- zwzhXNqKEIo$Mt=psf0&OaWqsfYjkvFQ(4+m69f4`!!&E1X=ry57SDfa60RNsWX9bO zGx6=Z>qleX%6CS#wuTl(7+aIK*WFD@Ir`4%_FZ^`d|}3te+{Ddpf;plEoXP6oDJxobN7<=0gRhaU++^$vy!41Hs>z)tJK8&zufn0Uj&2TshXQr?%bE6#a zgDNclO%wBYSEuD`Wg@?YQ+GoYu~zHkf8v&AABl$U*A{G_Jbq|@P2gJF9WsCF1%An` zvLC98*GS7<(5(8_lTNh0)Kh%&rK5pRE_!7Jiv(lN+1UGlv1Re?RZ-$!UwP#S^>!=le(_)#cPmx(q}{TU%< zK~(=X!cz8eSOLOL?#YtB?mE6}G_1W?J}%LH9q@3+AC_nYJ|y1#U8EcT;s)p`1@VyY z)3&Y;f#;#>h4T;ZNZF)~zXCrLSl>N>B&LcfKnO66AyOJ8GKLnE<58;U)3)t3=-7(B ze5a2;H9S!oG+z3#QL#rdY$_+MqdF~RrB%%)cjc<47l!M5m^%DtUu(so|9;K zeB;jb?c#1jn$3FZK(i_&0Yo4PiEJNjW*zF$UyQIA8=(uvXrY%wA}QaJk|cutWy%o6 zkv4ood?tfMr&4GC`nK~!9lhm^c+)d-dUe%EcN&!ynYDc>7Qg?fmR$X88Lzz^^2q&W z6!lb0{~4CeCg+#J>|NPfngQuc-e@#1!tq+GV*vbOiMm-rA3E;wExxf8kNJBs)Z8C! z(tm*f|9&KLYm;#^S258JPmU5QaH3t1S4gBRUXZufzgb1mG+DE7JRY7GMzjzEI1BY|&q`pOfbZh}Oi$g`l2w5((Ub`f#=HCg|Aa32ug({Hok(I4BA zd-R*ZAM!1k3zq*68Wi9~tqgU=Su705-=+Mei>C!CgIEo-O{v(p{;-R9D=)^^ z#LWKS4@C>X9gh#Ph;tBYtD}$a8ppr12ju&oQzT_S%6ImM-HCiaExS@hzVTiUpcdox zvupeF;3@zntOn=33mXi{KM%SJ2(v%I=o~vR)EZv02S1->d-dgJGiZent^{h&$+xEH zEz_5aHv%(oy#ojYnZ$|W{6leZTPt7%VaeQj0WOHYMF+35AjE0wX~0Z8*l6usUU!cz zm{m8~-V?Em-E7RwJaDDRkkZY6rPu%1k=_yXlQlS0kKGQ|Ms-`FFs|4$b#LLXw|E=XO7Q?C#R+`ja8vnAz}`@IHn3Ap@e_bvT8U9=TLdr z*$B2Le7Ok!;As`V?ortyDz!UqM!oQw%5c|}L5)e$wcG+b5xZP%=_w^xWr}MEy;YNC zBs|DyQ2Z0cv~o7HoHh`j<=>oonomuFX6z+Tyl^$5K3&P}lUWsVRY% zrd$P){xl`>*WDz1qlKZHj9?{wz_E^oPuYd|)*Q)kD1u(vqIPOKlA+g0?lN}UW~-ix zZxSDmu-sen+~j~B7s&tU7tx@eC)=gRj+-AX|1^;{6)rPwlyfuero9ib(%zPGCiU&` z`V-(`FP4CqYo*3CuJw*AbRpj)fy4)ZF_&2Df8cY&xhmE$hgq7q@^+g-JDa=FIz$RI z4wBt@ycs4l5+pPY^^)c}WP08%-KAdjYE<5TM#r7I<0U`jfTN%u_f?(WbVo`SoV<14 zMleD3aDHCiiOT1Mrs=8`y3!^(h*`j#({Gm8c=}TB<>+@dbeO@On{VBljjo>~SThS= zM2zbP=8mxECUMR-NzOVzr(`U*dX&m^D*b8A>>{BI~7X5rv^{huRYf5ZGc zzt%>_NlE*I(Xy+`+Vi^L67i^Ip4nt`$g1(5;1|$$J4w-rco6R{h zf%Vv~8&%A_I9Q9WYVzy_DcsK=O_RtKQb6e1c=P@pmP_cqb8`kdpr)TagX2!A?xrY> z%Q**JBSsP!)6BK{HqQxf$=KP$} zoll1L9TU8t69+>AqH%&HqGiU$r-}w@eog_tC)2K zwmc&~+Y`S7|BRI`g1Lf7esdI@IS@&S_$XKqg8|#R{BwhWs zOa1N;EhUHJne5qnQr-1VKv3bJ;gIUv;6R(q+0#3ZWzyUGl3gCQs*eaxUzd(`N$HXr zYq0ayFRyBgFS&e`r`jRoPnO~p0Y29UNB*Wf$xT~M96nueGUKXaB3R+_>hTXepVY+V zyNs?gqdwMrhIDxx^|SGJG@h-MT!GF#h-OSiQNeE-FR2Q9 zpVWCRXehD%satTse0NXi#&cCm+ju19^BZV?W>W)=Y`qkbY`(Y}Ko@k* zByPD@;)vYZz}l>u(vu&D!waZ|LfiG^3HFbO8Jf=guiViwo^g6;S%T`lLtWZ>34O35M(~DD z5p4ONaEUq%BXKFm4&qty&&@=3&i0HvM+TV=(CDk#!-7tKvz~9&+|^ zzY9gZ{t0(Y!npph4akY(ko~(i_xGj z7;9aP?B8#Fz?#{3{ttL{gG;()lp&l+1^dsf!o&u-X#X83Z8oa{o5t^6`&9alv{so<9 zN3gL$d6}#-*SZTBv(xuXK9H;)TFmSQUL5a}w5%CJ?!(W9VTshn314L#o0IQeLq@nBQQ|7z#_ zCnWTheF9{+V9}X}yj!e5p&nh|iGEJ5bJ1fD&KBy~qWxwKh~f#{?LfmrBF5OgK#JE2rI7ylDZ`xsV4oD6SI1^536lfQgC`b zXIMl&`>v9+e!~>F{-otWh3bg!ol=xq;P>iX?acYcXhHwPF~B)$t>`@Gb`OfvUdJG1Al6Fh z|HR?%dzMn-QK6|9?6>|Q)dd;XHm#-ifzPLTy?Fm336m3yQTl5#Yi{(wkC% z%e2|Trho5el-AY*z0><$+F##r8qBgrjRjmc>I0|7z+Ev!i_UbOW7hSSVA&c^INi~8299=mVjKjG|T_nxwU$%9uO#X7V~(vHtz7=Rl&0@ z>IlcL$rFkDg5vFJr+$Vgwy+Nc&=};kIG{h~xXaP^Bk~p35BNtKq0d6j!%ud4$@_8k zc-LtOfg|6KMro~PM5#As+OKpeG#;`Lt#UYuq3z+Z1j;V_4p?gXlO7s;Q@nAZL4C?O zYZ$kjo}lCo_uKP~xQEde;P;?Wnb$v%bOECOaGH17!@>sNLCxG5piP^VIH@~d2x~wQ z!Q655rSHzr>#;qW>C=$z5xSwKtJ~B-#`X8p`g2KAjSd@U{8#t7SW)vu^nUhpeUSOCM;!Fcohh>Y7bxUr`V8==4W z0PH~rt&EVLL8>1#5expX6_Te|S`Y6=q^$f(ea)ZqbIW9=c3Ni@6WjIxIqXL}du9^w zi1D-Vo)kh>1Me}Wtp}QVqSpG}A+)f;k?@^>+2`ErXy*__q93I>foczbI5TQ@mQ~quNO5Tbp@w`1KS+J zs~Zz@ad6$Y1;hIEP@vqetF&Vg@54tOtt=zG9(|DdS$KQyv*s|4P$KP{yOAYaz;<7Q z?*IvVxBlzWhJG4fVmHbz5tt2I>!rn@UzZnNkgx7Puv%?;URDz>xKRkfWdpD02&Lt$ z=iHDSf12uKTv;;fnnKbhy!n&lKs>I_*NP6JYNg2pE zr2R^YEhYs#_0s-CTzhIOH$IU07`jY9xGcR_)32Fnh5UH;YbkW-hMZ3ke_^;NRlH>? zmVEA~ayRqS&r7thMr2L4n#zfS0{cAhxH8VN@Mh!S&a-kOr}oJ(1A_z^>503^N);zp z8M#t(@ELh3i;IbZ~xpz3?~3ilQloEIfrtYcyC$KluE@e<`C-wViUU! z;%)=-=gzbdtic`rNl?``a6fYe?#c8FL4Vek%BTtYc_et{EkBI6y-ap!J*3s4-QZS+ z{@&3o!@JQ^Z&Z99wLA#qN?r6*KJ4Lovwh4w$+p8gJBEaKFjFYe%q~x=E%_knd${By zwVBogN7lm`VeNl`(^UuarC3FCJ-Cj{DrJW$Z>@to_ot0_@OSs|;lsQ0VY8DQ!Vohd z6xOaXJUyGWYk_J1$PO9<*eB>n8)6%3#rJmcOYu-O6`ZPU*Jc9-moki6X}#iG=f`f; zZOO*Y&Z~83wsTm4l+QL`Iso!PZ1?R8M%Y2h#~@9b8mDsVhi$&A9M1%6m92Bq(kYqE z$WTF_*(Xi_(tW#E+%Pj}%*-k|;QB@-y}oqI_zzM?_3Yvo!7=O}Ku$*e(5JSwO#~*AjB~ee>v6 zFDqZV{ILvMkM*FsU%vI5|JT{(lRrs-;H}go!;n`a6a8D$N0@`{RdP*ya-3% z8aAuwJy~pWwtr@1v&^biqoecZCB~~Kg=3=MP83Xv?bXx61IXcl*}!-O!>rB2HTLSg zSDjrB6MZ#N_-9DVZ(RE`YC2klauQbetUUNOY$OH-1r#S&&h#F+lCwA@K@fA%PNn+J z`DCdXZ<-q@= z>0SJpaQ{DU5-OrnnH;8sikw!?w)zw*qBC-sSq_!clEav-gPd6+hr}vHQbuCVlhYi= zlALljCgwCVv-9nH-}mo-xE}B8x?b<+>-BmaoHA?}fuvc&+hvk%4*14gPhNjN2zN8t z-*T@|x;v<(LI?iYKXt(ouPD_ZRjC2LasX3i{z^$132xq1TWjjl6ZGQzt?Ud~Fi%V$sTirsENK*uRr)^JZJob zMxFsR!CeZn&#bO?w(5b^m#|C8jr70qHG@lMT3ju)kbhPb7vsqBevujM%Vd?MvO#&+ z%@YB4t0JG}Y<6X0-P{*)wyJ6!7RS7kFv)cj?^qcDf2rnq%cbTPKdP%Pjs4WZzE}z) z4;=$`UO4yEu)g%5?T=K&nw`mwa_5@r9iH@Rl+cmUbQ1|K-yiM839YalpIrg?%M2*| zDE)_sWcKHl+WD*c0Tu}dL=ofD0*7o>#X-^)wA)o;sRtctyDoJCv&FqJ(hO=w{6~F6CVb!RGT5*4z@59=Azc&pm4RKWEp;~c@T=c@ zP3mesjusM+ic-~0PCPvezbmq9fZkJmht^y-8hc|28tEj8-UymPI=fz4+TY6H(K|DnQ(4Gf2Q$lt7}``k-9*lFtnGj@}z;=zBj z9YR)c{G){qqH_Vh8|JIDtnQ89IqelV;=lMXi;Yn`QNXYQBokD+WrcVF63e^)Ec zeEQq7FMnA|b0XNU;XTJZN+l>zniq~mxjiGy8%h+&U-*B;k}sN>_?8X zuxO7mUIKdD^ckPYwFxpA4&9sV-oo7ImMVx2cDt&AQ6~Bu=}6Wqk@9dIdF88=WeJPT zm}~Ms&^T(c0)5RipS+`mb-@ENJp^kkcxqGUuoUuYa=q5ipS?VY&g9vv-en&Mz)gclZXW6JuQWSk zf$apBsC^xd9Z|Bg3aGxuMvmfP6q5&OG7)X$ZV`o8kpd3IXQ&>)j8MClz>c<=+DsJO zX%?XY2wMW)9rGTNmMPesuY&V!BmPDh>NW;JsNYofWAd@`5dJ>VAXG$PsA6c8DH&pT zOSoWVxm;PTwNeM?+sYm+Z>(0sd!y3PW?mjglhW!RXAm|ar07P;{Bqs{i!+oTa|fkn zz4s9hwtU3o*0Yptmh>M-JFX;SxqCH!UNUg-T$*|$m!;w6Fui`D8X~QW8Ja{9XELWE zlSdl_@6Z4T9*MKW%39(Zuj=FfV&k~b#m3eD^gN6KhLH3d0+0(%d6KdQ;YqZ`prcbo7Z!V!yXa` z(YiRF=Av&Pq3X4sv~#EdeUWyK8=TLJ3won56~I^Pj$S+%e3%36`$U zRjlAiD$%NyX3u(hHpL70S0jGxZSjx`j9_KYN_q?F%=ldWfAh`NP(B2Xj<CQjp;h3i5Z9&|6Nw6s;*XP_By)NAreWa| zFx209NBC2gtJ|d+u|%MBq{`>%T-Iv<#2LRUJKO*5xK=W}J!9xh>SeV9v_o;BW6zwH z!^3u0=|t46LRyNG1J3@lEg#VM>I=`_r={ezFNAz4*CTNl?rB=Qz$py!MqS~{dBz?p zmtM6^MPAzY@utxyZ0~o)@`BT{!TA@n@Y4a7AAzvPGEBs#y6lM$5buvq4U^u}F9>dw z5g=-q{(4%D&>8qaA4o_(X~#O0t)7C=#yWL~enH6*sYkn@bl+57*NzKZZF{-80H%+FKO> zZBm6caf_yftUu{hT{Yc28Wao!vNoOr_RMR{>E7@WaaACyJvVt1$ z1FkmNv*_oYtPtz)9pde#DMhJWV-BViUsnjY;C|Ri+NfapN$B`dvO@;MVgipr3GQ>! zX4)T*kK&}y0Z30Lu(>qDk&DN3@gKl;@2l;c`FFGV09UI3>fgl^UN8S_`Gscxac@4J z_!9r4SF@hgLb|XFEq3p~-tl_8!MfD?Ao##0;_zoD=UwfqCt4q)#K9e8@TgosklFiovS%g7uV-$#EYcqqAv00_ipSS1i zSnp3W1VDN zUXN;Ob~_OY4t&P*X>rvkZ2p_${houPCK?#fSn(9h{)+rIy^nfd-5c$E{SFgfnJUmD zJntPp6noWGKaQRMk{y90^4^9teb;-opinq;Fa*`lcKF4c8=i+M>z|q2Hg5976RBMA zC69}emW`qW=cNJekmS}NJ6cR^Tghv*&lSC)QEf+!ejXr%ERBYJumZh_APa@b1uC%e zA@QvK;eamvhP?uAjI7Bk&knqtNeMnueBYWxK)vWm8o_&=?ew5KuR88^baIqoe&Xom zG4ScFZ^_h~iN0ZL{&i7f@~HyZmDPEKKZLV=Wo*bmrNsCP>|2+nor1p&%)h8U%zGH0 zB#|DRGxdu4@U#QIyY-ou+DyGSKpZbuQ2C?+T|I9R6-+(Ga|pg%Hhh(npf7yff0?cP zu#b>ry^A;eHZ|Qs6FDXKmT;!tB;Na@g1tlV-qB;8t?T?~)Q#yF@gu^zw(a*I?uT26 zRe>naGl%y=7=@3z$wZ$EDjLg|V+Oo#gpgij+MZfp z^Z5|R$5{>-H|uRui|p+FU5W!wUWiJYHi@dy%_c73^?Ot$_88XwLp{Unr{Rhlmq<^x zmu?Cn_B$;h-;0tWPoH=2eyPw_{0m}f@RMn3l$E|vem=bIH3(1__N0?9Rkz3GoDT-sB37}@teVq&ft$DWp3R*u{hHf{Lp zJN9}qW#OKW$d`^eDFv22*Tu;OHpw~zSX#MF;=>f(SMGtp!raM$DeV>Rph<(d#TWS4 zEo3;P+aQrr6!>iLFXdEHJ<4rP{-{}?YqqJ9t+lI=k&f!+vJeF?=Vmv5Ci`!`|61YO z&@_|La-LXk)(YqTa@vmPPM@2557r>Aq^i!8Dn9Qzog#nFWCP~^qk>aEJ=@Ai%n2vs zs}#0;^=?x-BYhnm^UF^)e(jk#K!X0_AD3G*_qHZG){dZ?Ud)u`LQ(0Fy)!!&K;04}e7_Kt zW|}yz`aiR0G047wzN7k&1yde^I_rK_P2CR~v%R2pJ2h8|heS zz20mT%Iewzr@4ttO+n{!%Ih$U%~pD2-}`pH18s5U)?tEaP?xMR=dxYsc>bich1w)r zZo$V{(Lc1LEu>0$q!nP>80W2a%IV7}i4QFBKCqgOYG>vUwg1liIlOf*qFvUH%A#i+ zS$<{{@fP!lJX8S{yZrVDU+WLt!nB9jY2N4Q=1A=bkh1(!GjVl{Z|%j9ZJfsOo8q=7 zj@}k`RU|I#gm6jkMRV8@KaE}+Q6~1`-Rf8<&9o#To^tS(HW#AfEYqamtPfGN?VVcQ zG^^^z2CMX$jsIcZ)g6ZrEyG9bX$KhA49> zD~e^2{#9~0)NU#yk!pcd|3-+741^?EK2nM6Tk}w|4I8BVQVz(541LL!UG`p!wZ0!^ znva9m^fnyqH@06qW0QEjcgEdrX{gR$IRxNi5R(INBtokskp+SeB1EULJLn{u$3(wKP6RVs2u*p?Q``(OKQ|Snq z5Ebf-J0h&QkveV)>6SsNB`p8K1bO7rI(3jHjG>6eZnl%bA;pt=+QHyJ%j7okuam)^ z{A2Z%$>%YcLU*5t4Z2D$@YvS3^q`ZwZMLNJ{3~`$zh=Eg#V;(~0z`UQ83!bh$Qxpi z#`x)3eS{qVCI+`ur#)$=vNw%qgjczx`=aBy&ibwknk~qoeRyG2+f~v3G(^lC40$jiCtv+wD;Fz6nhioGtap z&z4}g@eu?;A#GJ1>nR=BNd9_wNw;O!`bxyO!{ac$>RQBaTJm>niqZ*gIhGo#o$8AY zw8yBqjvX!R$fsPT&}{4P4`v_LI9&JIF`&=&I}kSUlCzf_sf)8Gf{7P$p8of?fZkvg zftiwbQ3`RJHoblle?^5MTNn*K+kqRcywAU*yIDkjahvA^yWH6xBNp2(!;&bZ-rt$B$Z`nHP@uHH8&7+B)FXph?dxjQlI5OMN-d3N1-ZTBJA1 zWz;sb-dU7>sq**Ad|&;g(q|eiAd>bat8>X|FR1-TX2`W4bRUaCSqyXsFcbJ-GTQj*&qSqD4p`~)97nMiDOeAHhb+8e&dEJGbo+CR0_TnR{v}O>xr;2fjk&1 zw|u%1=SN6Qfw|Lxo}N)mY_V@4^PRv z5Z-meLL*!$k0C40#{EY9<37F_zPp0BN%ZM8v+0-ezMKcGVq?+Uo{UYl{bBaM~uo5Fg zD!Z~re(Ox2S)dChhfTcTrd2)mJUtSgXh?EVF?;TdxHuyR2}(s21K*3Hc_V(n$|Q2{ z2i1v(^S=N8kY0ocG{yE}xDAKs(#-1baRKf#h$Y-~36&7}8J;9^sXBQzW8QWDF-3Q!-bV4w`awlUCs(sa zy1q8#HnxPu)Pr4rq7PwgTuT=y8&HxLK1TD>bqJB1{g>^#m-re{e=ICaZ1;zdx-urP zvL%{dbAN8Q%`k9cziF7%OH#Cj&lXCVImldxCENKjbcwDy3h`TMo8%B|JM*J)WktzW zjJukgC)#pSjxzd*@107qf7h+;1%}8!8)-#;JLG!)q+> zfuDn85fyWayVyH!)Qu#`xCoAGzZ(A-_VypBa=s>KNtNAJ^G{!URe#+nic;qOj(feH zV^<)bvgM0=6ong=xJy5H^s$%hV@1tYM&KKq7UF0LoJ}))eEiL1x8o_cyJc=u2jJBy za6|1US#Mrg^dt8hi2<4WYJSmr`|xC08;g-74Ft0Qo9;WaA>6-oQW;k#(CuxT@1&`8PsCaHD0wG8dGjTPSV>RO(jLiNdrT$+al zx$-y-pSmO;^N`Vo>hu??M!A+4@=WDscujmB0wWQycydDj6@!KzHkn)bqfoK?W7t3A zBoomAv*e(pHx`J1H_%RO@372XQ>+X~G6D5wCS-Fr23`FP`=FvWcNxBylIx(KP={3U zit3WE^&(a9=;yd;_J1gl{SkmTiirZzTq!(tMwI%T#@ud{QbGUWonW`1^uvy?n=fR+ z!Re;fyYJPG!y;)m1yDm5PDsEEgHk%G3JGB1V$dFOfCx& zEqwHL*)dq+>x%l@0X)UD)DeV-f|hV)qS+B1qk*DOqHFYP*1Lcds68#W-D&FJ`OD6D zxhN}8jFt2|k&YK1ox$-;0w{>wDPzy^TR$Uy%MVSyzU!%PvP)xX7#g_`fIy1Di-0&m6pBjQ z8vrKh)r*Zak;n9O^4kOjmiw7k$QH?FL1(w>ZA@tm_dR403;%f>_TMyoxW9LlK*2oldAGHzh;viZRaGLl)I}ZG?>C&NpoiYk zEg%A7!MNy@A^nWksq0HrRH&@_Cd#z#3{Ar4uf&~O`&xDxqBDXUrqgot`hgvk z(~Y*VXWkIU^bG`NHgbSLUJ|5^eGpt|P{6FV@rke|@m zn*8@YfQk>%F3L0S4SLHO@cyKa`p#;%br}T_5Z}Tq%Y4OGLyb@~%yEmC5tG_iB3Hud zj#MPHRi55%MDZUUx}3VLR=0HMpTL}wh@W*An$6A3yTae@83vJ%G}V4>5Q()lg3}=f zdxOrwGj;v}+JAi_KLR`Ky3=k{iBBuH&rZ`#&1@|-&ENA}x!$9#bVjS_$m~r;%13Q- zL>s&c2!F8SSV+1-_3by5_TV9bQ?|}|*+=v*_T~?QX5wDV7Hpm;KhcE@U^_`0l?#}o z5bFYF)YKzczi+T&8N;wd;hXM~t|{B(r$+fVE_n#R`7np2Gs9OI z+S^NcdDqpYzw?C3c`M(d@jykd;YW|&RjV+IS|o)?>)AUjPCn;XHt$YN{tV{yp=BRg zpKXvvpULV1CnFpMz6_XnJ3i5bNG1_v z5k1D@h%wcib@Q7yn|BUh0-CF3cD>$%WtkI%Rro^; z)3WS|OX1hH^RI@2QIj%|63=MBi*03xD&sN&OmOJS(|ntgv3%`@p%zj1%k zUilh_EGHMHFk`>Tn6AF_&z0;G+%f=3E}HuvD*SKIcCWBaIA||CJL9asOJO?#x;ZzO z7co5ujlTJ2(E0`7koVh_Xz$7`Jv6pScDS5EEJGJ3Ic1g%1dtEP^bvY*8utw>`?kWu zx>nEI*e8#9g`b)DvUzg)RlBzH%)%3E$V8BOq@q2P`wDf6o@LEHJV889S3i?D+D7eA zzfVX0+S*%(@sOV^jXTJW_WOxsQG}%xt67&XP zuKG-fK83Cw4XkanY&J*;lWhx*AfA?oStQbNu*s^Vtzz5y>54KXq2d4gxH6&ZE4Uf) z{|l%a`x~W$jZW&E^cC;`iPLL3mDR(d`#*!cRd9z;1A!i2RRuIK(PDl`7`sVr_Z^FSVf#D-KK650R&I!{uR|GtK7wv` zPf8b}aSNPsjC_A`N6pt1!J`_pT*p)rVRAVCVRCvCviXx@%XEX@%78{H%w8aMW z%?t9EUSS(zYT?|uu5dQTD8hc@NIQr}o4H>XTK(B$-0sj|2*BYXx8#+U zwVvW7c@#v^S8j3R?k2VQnsmss{uo=d({oi;&#|SfFbg&4kA5{;Uqh0yca$FdMFprohtM<%AS&KAD*0bH4R_$%NkSP20+%1J6 zyUkC>%ZE+}`fut%N*0#mOy)+OFYXK{Lh2e%8tJJOVTo@WW#|F%ala?sM8U#a`eB0W z{89xoUbSuHqZhP&FQcdYu9{q3WQNfdWT79bn7MyHbw#PJ1`Tf%u?tMkGLsgU-Wc~u z){!^rIw9>>iD)cq%u?>^5~=Oy%R63R#~Iq1W@tG2#%j!?Q|&2#gzhS-%z~&op`rfj z0x9}L^Lp2e0~s=H7N@WpHpkR9%te5`q_dt6D)G>h{~ook*ZYL^o7is0?Tdf1kewb@ zh)q)^FJ&)>sjk1Km`LoWJG{-{cOgzBVuBh@AujK^fGi!Fyw+20&NdkN#(!$Uyka%a zyxrmG*Yz>($#N-^+cS$BkoJcftGxx_>L405r#t<5BV)8Oa*!n7Ts#v?`J;^HYppg! z`hQhiXEppSO71T{tDNO~BF3xlIjR``gN|+n_?~DC%C(fZ0}!YWwjsy6=CSl}X3|wk zqsOn>lLMoTxV6V|3l_-Oo!0iT#gjXI&Q(^ZK(G{NL5T1;U1=4L=OTN5J-A(|FLkr9piw z9{ZrIs6=HTniM`Y$&nXX9X!Zy!=q$J+D;zR*NYekHj?np0o{^S9VZ(W3ZXk z$?^V;Pb&QdgqCLHF{2@|%uvQcFaCmP1NAD)$nd2~I8QZDKW0=iS)8D~tyooQiYz9s zh<;@{Eno!mYCXJwe{=Qj4R76TiROzyMg?DZqEw~r)A7=))QOIJ1&F~sT8TShs7y<%JNxP6Qx>=ez2a3fJB0?B!1iFZJ{vc3GAf=Pt@`VzbKo{rf-4E zm-%_+BA>>o8034BHRY>q@@X%}gIPZr-|d{04t{44nm!i}XB+LArmdMmav-G2#WD`i z%?&D{Mg>Q{4F~=u&=x~#L`TZ}rCtWL(?fscI6(NXkprK%6<<7|!(WSf?qt`KzPp5* z4bXKI#{xcFC=IVpWS&e#Bq*itYX`O8{4Vie$bk47(2#x7rd?lF$x!%gsdkf*5@rSa zvyU7$wtU0@GPV>RI=u8C`MI#g8!rzG@9F(@LcXM-Y3|TVTrKA(w+DX@4PbZzqrq(( zt5h3?tTx$HV?Es7*-HfDONN0{LOf6GwSRq>V1p4)ff^q@(Gph?6tda_O|vBa3vn8d zUML|oR3p#HRXo2I{-sglg6}^^Kw-vFcmWp?`I|XS`o?fjGN?jPm(}jED@OxfM(vI~ z*`8d4c(51kAe(r5&Y#tOML-_@M1|(&iu@Eym?6OJ1>bhZO>a80Ausb&gjCfh0DN!{ z+t#)9v4cmgH`=q62v<%K?&|#Q0m!(c9u9AOA;Uh9 zq`X^i*OjQ<1V5(eq;=v?QD-;brk%W${ld6GFA~$rzk-f$j&#UoogwO9Xmkmhkk2_v zHbj{>yt2HAd8h%IN6~V=XB$?$L>&Q%vqR$zKD<^VB&Fi0@xDv`&-ul@w`G|MZ?LJ- zSl6}1CmHl_ZkqixQo9hLD6eyEz}WT zK@cBo4E+3`_vNt5^bs%nD8Kfy>Wz8*nV}yO?w!3GoD+*d2UhFd!)TlvPqoNVTBIAv zBiHC*3^;6QXM^oAGWy16ObIp~=}iBWi^1D+G6XS#;aMN%5ckta?WIY~HPwW`IJqp9 zZqzw$U#kuMoX5OQs^FQXUL0Hc8RBs?D7re-ul4l5g0^bw(@z{5uU+4>#+npKjQKHf zUn5;mHkgrFL18^5V`{LYSC0QFJ>viY=)A4P0fD(M#-YXgPNkbHAq1x-RNGYIev1 zK~2X(+D#gTaibz)8Osj!m=p1grC1D5+zQ=2HFWxg!GD-HB6A1sC-6)3HOyXt9KnXJ zj9qSgX7>!SrvkhM_PgWwv@8m*EYf426oS=wk$BWX5<~+Ve0gt-uYh5?DE-gIR!}}5 zoU#wEw4fjvVWb7Aq5vduEZCSBK1|Wq_|BxTy7%io_$M!2Z1SeRf_Ib2Ke$O8Ks4MU zs$e(ksK4|sh{n;3!vYT>>Vs%IL{eq6Kxq5d?_4?u`e5YAhx2(&{@~#2Yh@VYo&sU{ z>nxf5K*IVvJ2dBUgIc8pH(lY7W2$O{>g9>X8suKw!?`cHSDr7>xJUI?)yA-uz zW@}rWIumB>m`1uqlu&2^K$ku}@M-_C&eNu43351P{?Jq1oX*Gvyj|vAEj2=(bQY%wucGywQwBNS?&6`-s)#)tUf8DFR;3Y zOu~gv0WIiHEY*F{pib97nk`ykC8Ld0z(_kc&mcpIinep;3o5ua&H}1^>*cpNrck4y zXVh~`!D^Ok!EW>{74?-?RM$Ejsx_qd02XbCN0#*y6d#0ls#dBeC}fXZPG+mgfYj&T zpZ(!tRS)xd_4ZE^3>77OC8OKvlrT|&z`lZ1u}^5qXdA*_FIXNY=2a%0nS27k@5Z9D z2|1Q-7Q@I`UuHVn*obd&%++0 zHwav#mO~a5_G2*b%{PHlC{>F_`TIEykn(`NpEpw!8Rg6GpHn2bH>|n)%UFtZ?D9w8 zbmWgGm6OjX@pL&U*e?OwaEzs!9fhc$lqDU@;ij<&_eH3}YGy^!dm>$VzY{(v zF8b{sPA$)=-y*rDDf^K9$~9!JNN)3BS@Z=dxo}o(Q2VT@uUirtJ(2&Po1DFU@HfM~ z|KW%*Rjk4N_hKo4cO8)Arx$}kM7U4lL|64-<;nE8hu&jP8C~=Q+MzN-)8MyXXxe+Z zu(NMH$lw=dJvja3vw0_N<|v4rkAXjX;Aei|=P zEWEptIOCv@g*aO>>K92`4adCDOm0U+AVuf3^o zz2rQw-sSw<{4&MR>cN=KIM{x|^|V}DIi=Z9Wb7GO*bEP8Q*%_*Ex3bq0g91(?{rI4z5@+kYB)`HIY`<-c`Yh% zZtavZ^(lPN>gH^(*St?^kT`@jTDbj6ne@$jWbx^W{JIKDA6+`^a?H2H^%-vW&3E9* znX?i3Zqna2Q|K?UpSA{On03HRA%`H3>>od*lx!<_jP{GZ80cKMtY*GUixTN)wRotGkB)=$eobh#)G-r_%!tde&G(B{^yu2{95Wo z+!t$_;43p2R>1W_uo4V(67rISZdOg3;{60elRYgXi`ltSKBRon>-QN{l5oK+IVLFp_ZUjju4?KthkkyWtdb`En9*ajE~B{<#S$1{*L#Jl3C!= zkVoo2PBVVgxaLR!_X-)V*$VQOyxh~;>0Q*)e4H5 z?P0ozJ$LYu(Fs5-UA-JrDqoZTl5s1g`x~gNCel88(_Fz*b;H_nXHOE9Os; zfs}DZic7V!Y>LUBPA~mZuteqrepOzb{Ono6?y}vWsV~V-WH_H1DF4v$1D&{>9WKJu z8W*}%KW>BoBLp7y>Cuv1iw1$_Zz={kwT+(9oOA(uYk`~AW4@Xg$#!Oa2Z!31z2gs* zZB!p4zmP$_Eq(&m)15+I8LK^6`mTDX=lgoO$5C!kNuF6hCKT&8OeK39+vc6zrH8pV zdi9ONM?_Z;>HIMF*N9<4J_99#<%63ix)bW#h#G^TYFyLMap|&2P^PeF>K^*fjzvf7 z!8*ALT%WnUpmzfMulB_KP|M^3V0yzCcr2D@i`j~cR*SN=gvVM>m^E%IjEC=!{yto4 z?dOZZq#RbTC%Kf%yq#LVIHouAxZd$0)bklW1@yGx9%Qlc4v)M+BzC02&+2rd1luoZ3Ew0YEYlLWn#GN1JW zW3fwK@Ci#ABuE6BNlhO>SPfxSzpheuE0zFsG*e$I;lmK(X-U&p(5D!QW?)D%qn?;q^ymKy5^D z@?xw-v2lW@F zo01>takOg733zpun6&OA==Rad?bQ|WLYM2+g8KSVX%3bz*+th96VW58Ov= zuWre8@bBNen163g&#`VESyS$zc?$6S`eD&bG>E`{)U$k|Ux8C3w~a?rapU^M5`EWV zQ7iP?a)&pwST9yAAvfA`c)yv$hQvh1M`}nek!0RadXY`^2eo`vE6GuPGZH}*BTYYx zJzJdhg#g^5>nzx=H}g3jMBZi#S`Clg{BLuc^da6$A7VM6^;X&=dxCR1OJb_^kROu~ zsmmvcL?G5OXs&1Ap5nS&e@pyns88ft=#K||hOb8jH;6}qFa44nr5%mif8s#yb)QAf zcTVaGe$H-4XTM)} z=-u$Y@tgFY<#MIZRhd4Q5~w=GCIoFIM;d|3w?{WF_AZuxRZrf7<4mjPpMPp7FBL+k zPU^YULI1+YPqZcrLU;dr%Bg)}a=C_I>|@Y=!AXAOYPa!e-ftx4I%@vI3H^! zmj!E=j#VYmX<79{qYYp)DGRAixNWqwHh-G9uDZ3Uc$iNW34qhsMn;Oj(9QHiC9W+; z;42IkDbO6>5+2oCEiozZ0!%u|u2@i!`bqd<{Q*JL7L0;y_=!|a+oRn@$rwoO%2gKR zcBvB&7o)wMkj(!l&L78YQf9P8>K;991ftqX zx-KjoyZr8ucYd2uZ#;H%(_ke@vK{-s`7uo}N~Ih@SB*p0wnvBRsIH#6r&*A8Z*_G> zE54|$jgD|JaG++Jtn^sCtFIbgJt0p+R>aV4Eh+u6Ne^AO>OAk>ItU0%MyS=9f`0B+ zm@>3kQ*O_HdOx)-b%bIW7%8I7*;ePbW=|RYOUOQWj(XLgIf4c-bC}bv&OMrosE}uT zWs&7i-c$hbGQ8~Nje4KVY5Eo7wyvT@%m=Y;!q+3>0#2&SdzO5gwCYs}M;;ulCCCanH9e+EUEZ-Em3TE2DLLH%7D*{~ZfT?^Iecag`oC;!F~?y3Nm zn{>2tOZ3)^YAHSnO?h{o6sk(*5LNHTm@&9_5z zQ`LOyFq@DhrF7Mw`N}z}P9jvCg@jCEr+ZGuTVT2nP{$deN%{sIG)aWt>ZCZuYPJ_! zMajN_-j3cMlHlpJ=P~pAVUkzFPEQT3v6*`>(uvzFF6VoF0`k)G(;U-1v%aNb74+dN zKi|)5hc(!!tS1?z`_+GlC}!vq3OdB*)|KZ{1g1@q_DfnuA_MBB7lx_(`d<&4JW=1#w2*d zJ;gb&4Z8|gAFU77_Q`e+eP_=|l@=l&Y+E98iDXBmmoX#v13u@Sx89%vCTCHk`h(ec z^A;Cx9?a%ixuQQ30keN3<}dsTDFS+<6xwT=mIwz59NuESjTVZx<-IQg(FOqXUy_NZ zo~!XLXtta6Q_U9-tNbwXjSk;DXbp&4CV2VFzn@VI3`=39~-*Sru_XWw1a zB3fqyuHw@YF7a&ulY!3)guikQ?gnXmh9A2y=`R)$eQfI}{7cDf%NPvwg4Tc9NU2{# ziWi*x{)832i<3zjXC09?u8+qY+@$7qle926r$5*`bvXErCH7e@zY^#}N?j!=gTw7B zgI^3s1SWOc5%j$`k2P3e=nT}tkp7DqU{yqISE1K%$UP=@+lcLWU8<(kvUi4h2=wzC zq4$?oXith#!v)o@M;@;1=PM=;CCnwXjyvdE;r^&eXM!giSBfh?jKBFt1=uyH12W~a z?~fTDHO_cJC0fZp0<%cWzNJPzhu}Py1f(LW5jI%y^|?CwBx2VM+ejaeP7)VvLq8b> zgv$>0tiR!&v>^r%T1#_w|C)KzstS{G`!dpihji#isaDpagG8lZFRif~ZlW_G@jgCCOlRjrlHe2!20CVAyo9XcVt{q&<(tnafXQTpvH>CWppyS#4}PX}1l zzwsE495Dbb2dsPg%f8{GAKt4@<-7x(Y5DtmXm@U7bM4PO3X_S5kcr}>{zbfC^b?1Y zS0V2hqZ4tLWJGE(>2A&icl%X4b$TBxFXu-Oyg{E;(Qsmr{kkLN6ffOTOl(;~ z?3m+PBrMi05fP&aF_rC7u&73UWj&i2o^%`4%63(m`ue(-GyvrydtH_UXdct&S~6U; z<<{~)r>jNJ5AefnziCDAQcrF@O0m1)7`RbjM<|NL>{zXO#2rD$ecKu(!OCAwn~Wx! zPM!sis=2dEX$+gc?sat!hI|FHazwkk*Jp44PkvK;D1PKzD_izjYCNQeq#H!6QsIYu zb!KjveH&w)nlRD9%t+csCFVtqiRmmlj?>gQdCGFGS&-&KPPEaf9C{&>F`POtc_f9& zpYG`ba3wcB3#rwIsDEx{pHH5^%t*90TB)sOEMPo${LO98yO9U9bs|%9pgVmK-eKWF znVZQSe7Vx_z}}=UA519_p>X>e*c>K>43!r(d6Gzyw)K;+Da%jt{jQ$*e(GI@KhfY9 zwQND;isI0Y5H!au3Sv$Z2CGQ>Ji=|gxLGk=PO2Cs_&i{+>oV~n#Bu&fxX;6lrhbM= zfb!P-NxneldzRjFXjF@`!94vdWQfPnAP{6W&v8@M`#HaN7odD{hdROPFdJi7l+R@_ zG2`Bm-4lPb=?1sw6V`Q)_)oMmK^EDS4igGd>!w`bh*HWB7E*_%Q97%qhS*Qnx~Bbn z7kDZA6hEj%M|Q9pAtFGWO~dR{_0WA;co)aU9@6We|C9u$+O50g%OSI(clyN_&aMPc z-R4L|FD>c>4tBfhm$gC)R}`525%-29C6~<~@q_)g167_k9=kB6+v~X0-IlkY>#H!a zG`uex+u7^Y%9(gRkB@&;B0^&=KW-wk3p34L(eCdAg0S z`L6Get<1SwJ>ix%gsb5H@=G6H;gjDEG*P&mG#8}cSHy8@e%WLnPCG2h+`|{$6 zbmsq1{!#x|L}eL;kY$psB4gRQB*3+kZjqP7~4!ymMl|QhO7~ioiMVDUDlz+ z&R{SY+Zbcae!IWl$NdLf*Du#~-sil}Ij`qS%-PA+CX_Mm6N^89x#Xt28c_D@4qJFT zuO7NgNRJ!__}^F7T$}UFwe!#@P-Q1=!DjDlc68&1b>lrlb-^XzM(p|}DDZr(L4(ALb!)M_7^%L~1a@{0? zSnQwcEt)@H9L}I0srXD9e{?oiRashMITS+!fxTB3CVj<3Ydj1^OA=@Jy3lV;)n?7U zYlsOTMIf;J><^E|c+~FOK*Fny4fu-B<_p%$a-cI?>J1 z>wmFF1ASI|i)?mgtwxRi)IpECJh#GG@6x@c|JANq&iVF#-mMQ(XocKLOq&XdL;&aqNJ)_ z9m*pWPa>HNKx6N$hHAV!Ols?vn86D>#+UpmfY+a7&y+(+vQ=lx6O{|bcU*5zZtvhs z`N>giaT(|H9GldGdo_+crP9AFnjO?zSA*TdmZ??IQej?0I{G6t)C%+W(zz)aYEeVS z2%gVZ7!t&~`b#!JL$=Odc5~|itZjt;MQzkSyPAI4+C?bxbHuw+Ooqbu^#}WXp_h~M zG8H>Qow_>h&TXCOK-k-5ug^^Js_mWBTydCvj`XZ`?dQXqS0^JixDjIl zQNWkkrKR&F{vM8hH^SDM2U43#4#)c+_2u>Jsn>j%b!mAc^N(5|ab7XFW2ok)8z1h_ zwp9v{g@4}m-rr=|E7*n4*3QIA(rWdWJ|woMDu3^KjLb+#q+REwk|DT=r`v&Tmf8<( z=BpUZK1q4=@}g;KIr!}%7mD#R3#gk?=yoA#DDuAYahI6u*PLPBr^ub7qM4L}mN;sk z+K_)X|4`&m>ZPMA4a9Zi^g%_U%LVp=)-Y~^YRmkPNq01VeZf`Y$(#o@SFGnDqWkZ} zNkUi?RPVC$lT^c3%sq{~0l{JzgvYq|dbZ`OOKNdGue{v&WVP?kIvWq1Wjv2Fo8~sY zR_}RJ489BtV9!*XNhgHQex!c^hnyn468pyMTm0Pk%IhtwjhdoqQtIUod$%hh=dLdY zqUc#nKV}E{NoRS{flFw#_}JCtuvO!(vOnlGiRegG70xc(=8DPRWs^Kq;_u92$#i;s z(y@_#&d)nMU_SE4wU9bX=h3BC!W!+`lFR(xcYf%PGaSw9KiP=JJMT%_AjCLpRj;Vb zf!;m{yqtSC#5sl+1{VSg4pEz)w`QW%x}dUrJ^8FQuNTGbR-@y#NcwjehH9Fj`>*Oo zy*%OW@tl?V*V?Rxv}JEp&pq8GlO@39()m7LjF(s9)SWyxzCsb#z8IH3f*S5&F%}hU zE4~ykbiT%7<(1g+Z5ZcA#BydvNCRDE=vNFs34+`a+c@1)(z)>1zf*qjot2E<`tN(2VHa8&B%q|c?qj`nv8K%We&6*2P;fm8^}M1= z=;EL4C}~kzn7={Q%)@y-i&3q#rp5`xGo(8U@T#m!1V#M$W+KJk-iaI?4n9NvPEGCQ zw6#2$&X6IEL8n%ZjY1u9xK~zJ(|u>@Z;$LulpordD0E3^9DhtkT(tmwr92`ixgPw3 zPAB$HX!LzzDb0X-YV^VM&;XztH`)Gvu;$@FT7H65`>lawZ+IrNC z={}~vleW7R%*pX%OVLo79Yv6ms38D1PHE{9c$~R5H_nWP#$*u`BQMXh99LSL){%rC z4tIC&19IG{fzxp=IU0YhJv{gNoRk9UFlsL^UxLtCu05|;KXb#p z^eMV8GJ9mOatRu~fu8~Du}=2QfSnFq-)7~O=e@5$JJoO)MJJ!$u{1L%=hnu>2&#tl4RQQ4+>ewC}6>K#yZWJ zAPj;7G8VjEmrB`pHZelvmPC)u*gmu_k8pXKoO;(+9~~4CeSBHspSmCd{VUH zUAR95PZZonkZ?2F!2LSRdq5{wQh)-&UHSq1x(*58`sB%}GD!=korz7G+`DmLzHQkZ zplil#6R@M)*hFG!{DE%Hw6}Dq#h{-5j6VYxvDXu@Su#kaiP>}>YBusx8$Zu|`EQO- z2KMx25kWID&MC<$o>8gDIJ>Z|4`?t8z!OhU?FnC*Y+aXdpdhVn2RI>3VFhpDW<7^8ynJ_$T(GU0(aWP9LhAvN4 zr!?#7bsy%eWX+CTRTlHjMhy&^@kSwN^B}G5-Q2mon}JkzS-0%J0Jf9T_(CuWLtV^@F8Zz zYn>ai*7{>oN&+`9ZPz}J``>{3au@f_`ZAHcpS@3~vsm(Ha^U|_>vRho<_7&c)AmAJ zfEXKv7m#y9BW~!GmJdA!grixtnr}BX6n!M5x?#4oFzah`;jk%E+j@WSKzJ*mra$5M zpWC@YktgVWd!|d}nm5NxKG>MI=#Y8a$;nl`&-M2o5}YGKa9>Lhn2%8Xa8PP@7q@TU z8aSksXh+$Lfm()rBtP4qIi3_AJzE!Hcc)6}u6E&Vd14aZ-!Bd)Mfu1lJS=}F@kSe> zF`%)^h}rbKykvqtjK!j7YNsrGy|?jPsEWy`Oa!Uq+v}V4Y68)aiT4b>6vtX=3*0kT z-~)rZlArI$6EGMA`Hk0mIZi&ps+eY-sy3g-`Sbk`k{8`(JeJFn%{KjB;kfr`9M`Ix zn6;joj}JJd0~ut0rNg;hhHT%#=af3x)$w@I#f;v$<8yLlq#Dq%L)KH+E`e#Mf30oI zi-!qp#cD@XWgR}U3n}*JL&a!sURwx(??aArFlI3<44rm3D_Iwpf*TBLgR(v5C%2H! z=^@YAo{LCuu8h5QvN79&A!o5*Pk}!p+KTtNiS*%$=nx|6ESc4dWo2=^2GZ7S4cQ{Z zEq+HR%YYe>xW#;Q>WPY0XE?x*U>b=?uduGTubo6cL%hF`FniW(2aczdPEVCx4lS*7 z`uYU2#Eg!z{O0L z5;%ii$OOH@dNNH&`k)C?Sor1UshYuKbjLhe&qwSK!)JG9Z6XsZn+-RyyNmlB=|3`u zPQ;M>|La|;^VeHF#ictB>Qe(r;cK|VLc5-<#%qKIfdFokjl`Xis?n_%JjkcuTi{w& z%Lxt)-=x~6{51+f%ymcHCrYhvOrRdv3gkh}h%6RsRf&E$6e*a~(K$u4lH9wj>wG(! z+98>hQzkVA_3FH)-)C;UnYCW~)UYjc(ELV`V2T3}J@1=t(g_v*Gx+fN!$nep4%XQz zU6or*0-**nl`OyB=8C<5z}K~n8^Gr*ZrKU8MD=WuK0BM=`j#i*{gbeFc4$b+v)vUr zBxm(kUor*(sYlqtlTG_v67M)z%28I)+8DlPL$loGtMZAjXf0=aF>{;8JA8)Nzjicq zZV)8<-*itGqK_exP3ks znEm*jT%(m6ll9J>vc~^)T!TAFmNHsWr!cF?^^&qb>t|l(DNU6|WGs;iyS23;xLnm9 zbHY10fhyVHz2}jt(hP&8*J|FWj_h26%DkP;sm*&^m1?f7Ax+=xlKg-FlmGkkXIRdp zfisx=B-1=~J!a$a0k6yW%--E1SiGkp%l$!R`A#b;b>)wsW}F8fhgyw4EMtY!)trb6 z2~$KX=~e!u4fhz(nrFd7;6a`T;4D;Ev7ISq1;!6$f_Fu2w_ZoUVTs>K&YWB#B^w9NYSMr$3 zh@}`Qo7G6KazEerVDP~HRG+Rq2`fe!=hlRjawe+v?Y&m-UvEiV=W}fR2g^}4&IDF= zAWU4(10gtSvP|M0BBgFs@nhLa*)QlXe=aLcZsV$pD;_8aKZeAIIBMx;#`=UYO<^a7 zm97B9NO|gnfA44755+P7X{DyimWkjE6MM^l+h}`qyV#tr?3EzcK~g}SH8gzmM%fi1 ziEowlT6f8@OBXP!czk9=H0v8{k=}&=2r2L)oXn?tmqywf9QX?KElKGHcKckKH>@5nsuizA^yPr4PnJekMbB&A zW}R(x#GGll{w%NHW$Q&+>Q6(llDTl%ib8K)!5F8$md2@x;twL12FHDbE;jG@^KA0m zcWu9NApeT1{-d^EPkLYC;@^z(M_O&y_OuPZ(Vt_~tiNM>`XkP--e=Jn*hv!;RUE>p#YEMX6O~&!qtmlY6;P?K8tm`?VWdTanl}+YDs5 zucE*4rUMD^?|~eoBCs9e^Q090`)5xtXXm|TQxpm&k*Dw0?&{X!5jc6xi0btuL@Q=U ztfT=pOg|fPDl6hK2$+UspB+6{ax*wVa6)gsC5Lu|&#PXR>ajrUS0)jzKrA1Mnhe3L zXM5>}?5#aO#tde!#-orx6~5j~&e*&ufBGM2RtmE;xlMM)|J1t&jvIub$3rrHxcqil z^ZmkBo?jAJp!llxj;8lx>&{_b7Sgb<*d-$WPkZO@1^LPD-^vB|p{hkSmq!^=q#ds2 z8``W^8tXjGa}z-Su-qDxs07ot>MNXVlZ+b$cdZ|~;nIGl(GbfTJvZO;Xdj13ic$Xj zTaP?4jsdZxD&i-v<71x?%fp+gGRa#H*!}6?>}s?`13;}4NB_~ePIX>wY=?TsMQ$dGPO(e zCwocipJ{ZS#SCdKCoy?u#|Hcx;K7n)#l9MQ-2$5?csKr8&g1s39{`%0M)>?mX)V`e zQQIx_bBJgOQY*%1;ImGQl2sF#JQ+up8XSSH)G~>5LhQ^vRfxW|FE6v@6gg3T#%xK~ zA(xalszADH8As!fWd{~a=rHEz+|D_QOb=qc3NoJpn^)3KTF-iOw~j{JR?pZs^N}QG zQhb^{kz9)fJ{2Jgtgo!v64$|ExkIg zD6rWbuB6D*1c$<^>+E)LP@cZ*5l~*AvWnCBiO*_Lmt9S3<**~m1u&iXaH)y2B_hjd z>iDM@#4^_}Ij(W4YFDn-5i!Z3?tPUvq_x2FWY?*8e-o}w5VzH9_+LIM2e*{OoQ_Rnq)I~%J z`;wp@TMm)=M{R&lE~I;#hUBpPVL_f3)}NMj=%Hgx%~=QPI&Dj+enxQs8JAmcS5>P! z(ys_Hp%TXw)TFEGz;#Gnpb3*Fd`>(MJDb%Q>ImK)R7N{g* zT>lu5->id{63^(vSTG)Y(jN}>>SKmVFiC@i#6iO$WV6c z#2mP??ohwU;or<$aG*H23y{oEru>mb>%SMOLw)ManL`Cz{{KLokDu|FrHIKOo;dWA zERG1Cp_|(+mr#wfh*7`|1nDP+a?k(Nq*(oRJ)Kpt`)&Nxy9q_YIN_MC`-!i61jsuj ze%fQV)Fh4DYrtKsruSyu`5Pm2Cr!IQB$;&7>(_>MB?PPD{E=$zaZM+u^iSw2r5S%m zzstE}ygf%V68sFxv%Rxracg?wi2Jrmo6>(p+l#PLqQ^|w&zbcHC1WlWkoONO>XiLjwGCQMmT}4wjf~D!-DgRp@O<*W6Ja0URGhk$ z@$`CA-BVGfxL*CtuOA8r=k}Mju8&|h-0LMiR?Wg)T#??ANA8fYt3wzu7&PS<6!te| z%3chVDD%x7t(F~%7H%@`)vOcB+S%B=vSHGjxmx(rT;}Ex-H`WSB|~zKkss>n+Nj2U z<45C1u@+bj=hwr^f4vtT<`TVO+C{t_;^Nd%~t_N3%J6hXG z{-~z$3=3PYtf_kSXg}S5hn`sx+^Y}zmd}edNCrOh$0Ya{3VWH~eBTOu{-KN#9{Z?j z=J4GqV;s28mYQkM4|ryxt`I6`?hRv!h;wF6pV8ozry7dAG~Wq=LyLt;;m$vubgUy4 z_C#IJB@nD^&d8c_O5z^wxOthw5^d(}tUdoza7@;jOM3av&F>7i*2h43?uPOE=X%T3 zIlo7qOc91y?!8vcKl=x3dT>Min~J{W$!OaO>B0}XdIv*hyaXw3!0QEEGs4AGO+%$2YI8KJNl~~JXpU3a{ zAoUtYbZEA{VsZ3b2TO;t+YA--Z71^d3REAdWUkKLZlo&m==^S!$I zRy$6t>e7f`n<%qj5^CT2qX;)_G5#<25AIiR$M?yJo_kyOm}tQL70wh(S#AH>ar%iE zO$eCwN}Ui`h8QJTAm}+${E3`_idaZrmH)HqeyPuovydsOuiy9|y9Qb%@KkWhLp0ntEaJfhG`hSs-3IpL)A` zGNH%vTD~dllh%(r`kVg#rS4m<{Lm=0GZ7S@CQesvHzf}=1b33eD^R1%=l2+7l@&Cc~cqvxU5aR7q;L+d1t z9GXt9{8v%pQoH?1507ls>;=SV6%!I=^-aZ6b-0ewIw|EP*u2S*L3O`$X93 zWc3cpkKx5n&tbpM=xhEiJ7kFu6DMiIqi(7ePChfsJ9RAax%4vSizvF=rR4wlpbCriZx#`|xIZJ~*X}agD^#oY@z133t)uvH_dpcbZHXvVpg;+?2K?z#%w){WEc@_Zp7c}j2MxJe-r;FC~29V^-;J|L`#W54q{HFf=&YhO8 z;0xjiAl(1p3^{PW1xM^dw&W%_ts)hBAy2RYR56M->2eFuSXTmG0B`8K@hi4>#xcv0 zcF2hBR!Gm-v5LC<*=@D)Xvr^Ic|IejkLQD-A~7hWeKCyQ)U7fxcQ_N3<0!4^4!y?w zQF(21>qWO(B<2Wfp??S4$YnPWPwb=*S0|fpBwb7Xc!b5O3xPe}uGZTtzT66tTIt#= zdjN|RO6j-R{Ik5X)okUUUY_+gJHlBnb>*WRI7PP+#GAo7#II!Q7YU?O75w7~oao#m zxdHIK>@S%O?XH<2=)B6!H1%b({o!j}T0+fxBvr~XpvooX{o12Uc!5+Cwdc@|1RV;K zM^|2Bj~`i?#2JR_b;0r>l^k~<`V18H14@3rWDld7Oqf)wbcLm7SNZ=QCmY(`!TFI7 zgqLb7=uvYC5fM)UXdj7(C@-2MCkB>H*;96w+{k(}*D15sMaN|9c&1ujfv=N8pX|Ut zz`46lkM@{ILET2*2O9+IY0jPj1a}6=gMajym{aT*36k*|1;tzw8-&`R6xDVA7hG3o zYEAZy2lIPzU2&St@l@p$P3KBDcVpJH!TzAi>kpX6NrpG1lg->mRO~Ziy*f_^>y~+L z0fYr@s_tvH4fz5#c_lR&P;*#H#1&FbPpd(0eAsVRdG6lE%DPtIDcr}r|I+hFbE|G( zQQxqPe{=Vn5t9=hFgu*?Ouk}6;jmA)+Ok@Vk+q6n_5k>LJ}P&5J|!dXhLzBFk{0$S zmz8@2|7c$P~lDN8bVjAG7aK&>c_026=KFA!xO zKPZwIOAhNplL{dmqq~7%I$$>)F|}z?JS?pE92VWz<^Va5*;H5>Q>m~JmxtA-;VN(w z+OXYN_(?bpOta;9&0aZbbR8av>JQ2gi_980;}?Q024j@K&T{Nx*&%QM5Kh2_=oUt_eP@ZHUVN)v+S3LY5#lX zgnh*i25TG{?a<_ze3sp6BdYTW)^%D3%cvXiI1{FusxWZni?Z*jwVI!mu(+fXYJoi2y{6@)rfjHHVDBu3B58jGg3L?&zaY`2JOtJLJf` z0wsNH!VlbA2(0vJE6ai_I3&0K-!1?-(VdpW945Xa7VbjTMzFaMZ{4ejifd+@V;Q1{ zSvx1z5&<0dqq8?%n%1W@EL`~F4i=NtpbzFO6CHZFIP@$tk13dksY~PWC7Ty#E%$5p z4ilU|pDF%uR&{x&bNAg+=CN=)D;+L+N(8i;TNe@TM?2cIn@Ur?TH1CSdmZ}#D3MM) zGL>0q^!x?%{D7X)o{g<8jC47WWxoD4r6F-koWO|iU_HRP&%?$tFT6c-&tIZ%hk?Sq z?Z@mZ(W>m>oufo{9i{kKh;R>Uw+b(3c`NwrNx`$nop|5M-xNG4c;dw)1D7*XAB{k9 zaVMkVF6E=%Kp#A4xe)N6DL?MQ6@#0%gHH+`L}L4IjVlgj!ocP>A&*{BvIBE^o+_CP5-g2#?X8b5SMhvgJ?3~oD5EUEK7ZImrqi+Ft-l! zfV<=ut8~aGJNcY)T|920K#(D=?(;5mLi|=J_kA_M3-;UD-b|M17sOpQciTr4Z$kiy zZyt{6GsoVqe!UAeMbL-TE`+9K?mwT63r*g#k=ajAwH`nfY$^>x<<*sH5{r!Yk&E}4 zv0ob}6fbcx;~`MjtoJ)nz(RB=7Mm}w+ImDwi ze{f`sV%}v$TJunXbZi~&8YXr9WA8e(U{Qm4xL3Qsx1phRT)!*5{T?*;*Zqyd?~7ZI z>b~1WSAonW^jdUVN6*>)GU>@Vt#kdlWNTJFx3oQK8;)D$K7* zA>3l?U1Gk2cNcL?oyB<+KTiJh5qgP!$6QB zn9Qlmsn{;&S0leug8x(C!^{yo?%_^C{Bf6*)%fLE z)7JaX(8*<+X+}lY%h-{wp%MdBMj(B4%UZ6l1Hp+j^96U3`lTO+$sGDSSVFaxS{+E4 z+D})Xfzellr24O}uY6KhiBfNPNB$3Dk~&{iZ2smLg&<=c$2yUHSpJ}2rt@dae*C_r zBzz2Y_%o4erf>LkAot=6Xi;Lot?fH1Mr?Q3=@OnMwp_H#J3Cdj?V&aI#X2>YA4+!Y zE6tM+Nm8^^d*OWR$F?-fV5)YMuJ5q)D|NOWS;|5V*~glGq@EI7IOwXv#K9bcmcwGU zc>UoVoge`erp5EWIn!(3B%RvgFvo%aiAcqdEbT> zWuAn@3^fZ}3SCoXEoCNFm}$s;x0DjNU&5}a=jiJ2-y@ybxA?{b460yq9$_nKCIQDg zk4;(rqU&u>?D#|z^;*mU`NIp61^l}ISUmJvsfB>A!VxB*A~IK_)KWxmxZ+`z{QykR z@5Jz4hmDlpX}WZ%;DO%Wg+$4E({Cjk5@d9!rOXXwcolJlo=w)A8vXMP!iE9!{XD=V!VHihQHzeB|bj7f4Bpv%j3EWFj6zzw0&f-C1yC z$#A{x69LcgVN2OPVORsX-QzJjEIbgz?SH|_#V!TOh=W^EpThNT&ol<4g2){wp2 zUc&xEiz;#y>*0OE)(E++LBd1F+Bf8WbjsWxs6=qBx7haJ6nR{A<@SY}9D=Bis0U#R zu^}8xS48KEjL4#lR{lsbuR94kV!3fIj!!!Ksg|VptTA-Fl$%4sS3m1y6-`KhGqJf! z@V*xMJ;G6*eIM=)>vD@-THxa6Jyy|)qrFLf*ZCjX`a=#Y8(FNwtG_6CK7@QChyfXZ zbyASIT>>KF4t?-aqTeq9{oY!f{#j8*!I5=ceex znz=C1vo>-H!2hL}6R5i+0MN_p&NE6nlnaEOC)iJ@q%=Q_1Y?)r=VqS^>pjsUhT6zz zzVW@sCA0r#!S;-#XfNPDpJ7WMu~SsOpbv~50h2FlC%g8|J`4)}{ThAttK_>b2_X`1 z`2waLvlxibZas72ji5PqmKsg8Mf%T#IF05gdm!HYM7o04_|8z{15yRmbWMK368Lp( z|I!JP;GEfA_WR@y5&1_pw9AzrP}P8<&j0urLtC8Lt-C=Y2DJ)YGpBB-w-z$qZ+ys# zEard7cv!^``>5z=;bN>8SfWg(8<2r zv}%z2-uL!y#>wqVvbw)cAOmmwxT@6hV^hQD7-))Zys#~p71bCA91wil8_j^K7Mz@} zStO;r=ykm1ck;x$)Wn@1a~$+dIkv8B{}lrW6iBTcPqag{6!BF)La@#3~j#5ylCaex4M?I$J?>?b?fPx zrFhhbgas2nG5rZ8rrb6#%?@CEe_H(X757^I0I*rTxSctu5%PW0VMu%JHPma3*KhFCA!ZF=Trk_jup6g1aQKix@%w3o4a%}Ww~Tj&~Ce; z5Fp7m+_mS+WWSjNe5BtNB$-w_y}kk!Zj9V)`Mt{VEc)RJmTYKG8E`P`sG^~n= z`$fqmw%EoV5VV-9dvyb@x@wS)Uz?E^WPc`LP$?Xnca1;ZB1MoGUza&WkJe<1s8h?Q7xKRH` zv0f`SWa(ORgJ08B*}N)wzh5s`?72c6^jV{B7 zL7~12-7ijNRc@uzH8~@-!=0Ne_O#}x|FN|s%F3@xmU`yRwZ$VL5rY^wR5sD=&X}*t zUVza2e)LvoQ+rVh`3EC0P@rc!+$T+v5@LatoL~|U%)^hFtg;VwS2R@YEe6Ua)<1pK zS(h0WW!$^%QevkByQMc@Gu=1w$D`(x*FT_ z+4ogbpZMIxY=My9M?cYMEHY=Q0*~DPFrJc@`_$#12oVWq362n4>|Usbui92*smzLOtb_$(bDenU`avqf9M;+d%P#50jD6FvhQblZ~__fxAFLT`2m9+x!X=D0zo zER4)ZKM1os$a6!HV#mE&P86Eh!*N5pdxFp z8wc#mY?8nAsQt`LZU5SRu}OuL!GfDpUdGIoOwdLBMUwL7i9b59`Di;2Ch)Lroobbg z-muj8O?lZ?A6b`(Nw{+7b~18ZB*r>2S=8u_!eVme@Z$(2n&|%7N%k_}C0&>SR1CRj zeJ0Dd3a>q~9|!pPg%!gJ*9WaIkcoNk3%>LY?pWUJ$-Ctn*j#Ys($q&CX=Y}A0 z+I6c-WaNA|8Hs(3%#G|plsI)p_~|@l?yL@z?j7-G&hwwsom94Wy{s>Q2dOswC^8yz zM19ZOt23;&C$|_oiAj|mM6d_W8Wncz-;6gN+g*{wGplxE-~xRTUW_-D!yDQz`G(z^ z4g%*d4HjK8=M+|67`*KK@JAbxaG`Af&a)~Hc+}L4>*$XDx660{bzvmx#G3$Z=Y#4W zs{F-BvxHlVk3#2Jp-tDUc6Kkc7Vr43jPwS}4(W^)cCe~;CmoRI+{JKYMeYumoP)P^DLrK)e3Y4wg~-`E_0Yj&}^`L zcw+q_tRUM)i1;NxFv>CzW>c>w#P|fAyLpgT;Uvve+H-(Q7SnT5QSEpJtW+BzzCNb|Fd;00y zZ(Gsn2cbJ{tB>tM58&^pMl$NhXbbJ`tq?8PM=gNQGW4q`;Q-(`7ntSLHg@yR^|GPh z1!ho$^Z74W0pPWOOBT&6#eRQGo}Q@NPK4JF{Z9nk-y2cH`m1BANf&t1$+5U=03UGr zz@p!8Tj8&G>WxPD$V7@crklxsFf{{r&X~2W;^`WMbRZP6cbb@}!&EF^Q58?Iv>)V#BT2os(>D!1IM+5kUn2){j z{kQn;j`QnE^CI4W9bt>9P#!N`4~_Dlw3lMKUYEE9&c}&7esav7N$WVi=1?t2!jE)q zX4dU47a|rv4_0+%Pu0zw@8Vf}sb_f`Ly<0#_RzgoxrzaG z7GHEMe@DAP^yt4TAV+j)Ab#PWD5Rqd)b_Ry7{S#z2MxT}yrU7c*V5vLhK$NcABYQs zjfk?}#y}MtAW%!x{S|25K;V6>Xq{2k%6t9K7CRHDnIbdaQF}5&5G$7|oK9Q$wb0-^3zhsX`9S+Dc`69C zC+YKY_hm$ys%rLdRvh2((wbz59{JWsjk_rv)r2qfqgIllgf^ROSdlQ}~D zu;n)3;eaf%TX3(N{hLnNno-o*HtwpXlS#MeWXv$OQJJ|N=8g!&BXDV9eG}gZV zaWQ)1Xmd>8iHvF>$ng-_B91GM7axRn-ZeZO`yDuahSAHOAoEqZ{bNgV!{QEqRA$!; zX|J5e@5%Umqd!|TN2}Rwl%_ho2)pz}@hacXpEL#Pw=xJMj_-;&kZ68yK4WtTZ$c~^||V=#Fcd;tB>oDj@_3IgSBU0--qlW>PwGG zq*wfNoNUqWd8gelpo(0~wd8L(8JJf6QmcD!St24ia?*!FMg@BEeSUdH{G^AbZ9jgt zY*clxuw@Pb96L4!G-Yd_sMa^1>TW6;nGCL5X|b+MG7PxPI*#jJGTsmh784&ck{*fU z(}Cq)k-X3R9U4P2>3xBef?v|lfMN!4TwtIlXNTZi9A$C{(*ri(Cu@XzI2!RL#gW^+> zLo4a)(ZblrVNp{}zaP^Kc3V#$xMVuZ<-KgHm{#Hvc(AR3mf!ri0|d9cD*%1Xy*op{ zH6>bE61P3%AnZS?)2dtm3I34Dnq9mII{KVc34^NW%@d8t&UulG))%>?86TtevkINR z(4_o|O+t68ak3+dZ&>nWmRg-m`SrplmNr|d7W1a?leRUd2%=Gv0~kJ?>=Siq=9W;n zDbI{a@a>L^TvriKUU3P4*Eu4Vg=XGt2F;NtnU+f{cn7k<@$7khREg>%g_2vt(I141MyAn(iqaSzo6>-v2c$;N9+YjSB_S@{0(V4X;6_sTR4ej7#fJ~)m{p0tI3YUP38n@J&U#sd@ z0dfA=~o)|Gp*~je9mgIv2rX$)Wk9R zbG9s?V;VORm^bLfrfKQF6T;~7*SU>!R>0XjutRd4YRE`-@-)Y5nXZ_{J|y#^EsUK* zS-|77C;Xdf1yr%R@h1__*{y2+WmiWpoiDJc^IFmrwR=0``}5zw#+TB6v|jH1H%q#H zPuF+-xBTI_zalQl1`@)4GGcTxm7l@Mfn}`&YUuOfKQExiC%$?=O&DKF6LskP>si@H zwqc&a=n;edvclsvY5P5g_Y@t6Q2#mc?f$LC z@D>Ro6FRdq>-=}nJKaN#uCJ>+QIg#BI{{Do>n`(+W1+I;H3y$j)SnX7FZ!QDR8-Iw zpD6gTD$7FdYKJcu3(=(T_JMLM98>0ie zmyFDA>;3&E6P#ZMj?!VBe>R$0{r)sq5a`A6Us-Q{nBk0W~bkg-)#LQZMez+efUs$4f$sf<0JL zBb218U6J59-Iz!Z6V36q;^yIn>u+R9XbR?0`170&MuDRV=Uj_a!@0=A<#x;TXMZo@ zCv{q<46_01e7A>@vBO;P6G@sXK;0J{3SPAey%id#3yl%&FUzXle(Bkzm z{=o48<#|vDsJBI%mq_!}*xgXtSy&OpcJ6y%p7-hm3SKXnB>l}wT-PkuAbUO6Xf|qc z$DDctKlA6etM;1m2#8g7Pw8piY8@^B6w#8cg#Y^}ZP4)THD-D}qcx_6(rqH8)XFaN z3;m<#btR;;F{j_?gVLOjsmjV~z48&X+}(O7DA9BBd&|9631rEBnB`XI9&TbfLFZUK z4zblBAx6jQVeaS|9EKljm9|ESFy%UaQDy%ll?n{nrsJ0j1YD`tDMzXi=Jpqdl+3Y> zr+3}Heh|MU_7G?5f9Pljow_TAUYtA^Y@-L-fbfwbhUqD*oSE7^S6VbODB&|DkONgY zYtX5wMX#Q`Q#2^-1vD!xY4DMh-V%!#di_6A#uvr#z&5?G@zJ{^kE`H%?VYEWG_Sr-YvaqMu995%7Ur`4LJ;xb$SFSgFgwa~Ho{+WUf(qG zktSvqb}s2SlTP7}-Ox*&_+ht3_9!9zTqp^gO_%@KEIP3vx{lu?mQXdKl>gYBbZ{c@ z7=)C%ed;iM8lENASdcQbP`qq9s!`|v^sf4e00HQT^kHzs& zx(ey^;05H6v>80r(BME#f7e9*2v|_t>+H!_fA`jSABv#`eeC(<0j4R zX?X7Lp9|w9;_QnZn(KU7Mh)m!LGIV=6pRkwDLhIZVLDHUA4l93)>CV3YH}a$rDt0X z1ig6IN)cDRItVQpATZylS5_UqBiWQgo?$`?9W!eNtcESu4?nmG5{Bmc%`MfuGGZ{_ z6!4|oQEmZBqd}STzr~AEbBAa<(<%e6q5;LS;HyqYbEuoDthkB0vkv*E9bD+o?@S79 zp}+2b@lIDwx}j~>ZxX3DtjVtcuRl;Os>)JMads-;OJ8!~ulbURKM!(H4AH0aHh%Ec=Fn23Xpzo`l8!hj*!~^&~>8^@LE@o#s^bbU~=$Fl4(g~WyHj?&M2RB`>@QwaV%y$&z zYt(gizlHT*BS)#ZKU3fdgMA9S8k4l)1DJP9&+Y8p3hOcsl>R!BqSF+^4>=PqRSh~|Z0bWdJJz-E z%No18Oh7{B{w|+X~pZ``QyL|KU>J27iXK^wT;9dA%0u!Mt3}J4suhsDdY4kb{J#+T& zKGnAnCzRX~6-pw;n!#g9ZB(iK2v>s42~PXaiH98R|IjR{%EOhSN%}5l%|r+N3;VT5 z0c<|>5896iP-)K&!KQWG<_msB?n(~diqX?2T`s>7_V917lcIFAZ58?qnz;^l+RoPQ z4~D_EeAS*Wn;zoH*8Yo6@k_Em>CV$taoj5ysSfs@><$U6t35nnDPf5?T?%2DglRZG-Ru5Z{P|k;JCaTs_YPyR52YY zS$FVcll`u%hT35JWk6S=GBj{?DF$qp0EQfZpZuMZ!us+ZJtq!fvcx%FTLb79=b^5FBeX0k#)nn7mEUQrC z#zR8=uQiHVG7H&}i%-FwQf!n6a`w<M_=Ao3{frOeuHb!9diI8BbdA3=;p*%{h{Pt9C0X`8i- z9$xSn%kJiF=g9J|Q3;;*4m8iLRU4OVG_9!ny%EOzCyv68DDEQAbFt07a7i=d^$ty5 z*>9^dPXRedUde$>SLmh2Mff9NaqP;Sw2AU1C2x_#_Zx19(SHX@CN`!lG&gYCdP#}%W)w{HTxAw5OLTSf zV1K&5XzP1aWv5V+=*+vXv@7Ct)^YpR-*k~@rU9Xit>Ir6ySd>pr@lIINB700a`by( zr6)tTP64?G58a_xPc6|8##t-_N>y00fR$H)^A=K?_40M z=?lo&qUGT};+(9|uI_#ybKl-?-3|0LE5FPo%=6;D z3)4}r1@J*#@%xutUE)FyPLzygFo`tpqdV^{*`Q&h)Kv2kkOm5PaQ9rssb5X&%F$Q* zc}|w7`KWgx{Ffw>i_B6)RA|0}PuIPvs8p%`a5YYImtnc5G)^s={@JC#z2l$MGYwx% zKq~MDO^Sp};t#(EFmL@`2VXBT=Mu(xTkPO%)=v?+>zBHO92U1?7rLM5yZ_u>3|`1I z2W(DuH6tdR0KoT7$H42WPOK}<>lw^X{7eTS=ZGbr(eWWsaHH@kiDKC&-h~_H^5{Dn z7(@!A$)=kt;>SGS(;%BRH(K6B$*Fp|6JL@N*k8;NH@8^sl&%FR6>R>EJlP4 z9UJMx*G0zye_!j|Yw1U~h3YeFFExDdKmkE9axQGDGsFKeSKpV!}B<3 zT7_s|JaE>hM`9rbw^EJiq2xZP1$&daVegmSe@VTe{_AIZih-p21j#P{S`PUgUdtpM z{G9U?d`*xXt_-mSaeyRjlP$>SR3%tW49^j-Zp0|Vt9_|80jBMTF&U=}QF+4t6e6TS zhWg4hrMDWko;fOVZwXeoONjHV6x$JgmpIlzsl@(ux7&ud!j~U?I{%U&yn+11cVMQJ z^^tzcmQToYz|^9a4f4)eu+?=vLS7g{N8kiL!P9HX?v-OZeIBqh%o~@RN6ia(-|F|Y zVSb30pis|%EsM$Do~Bq1v%Rx)%*nw+%Z1sajiFCssr;rWnTTOsy9bPmhkUk=tV5S~ z)0R>+KG#VroIku1dUstwS3yrY$Ra-6zCtk#Y<48$;=eG#VT-x=X8v}U#n1#Js~&q7 zI0Q7{vQ>TY)*FpZ$$(6d7Fq>-@jCHRbPz4Z^EFy}g2s}qWDPGm-gcqQ8gjjg=$uo$ zqbwiJQ-p) zQ8I6BSlCA!5e8$dkrCZcY*8)Aci@}s64H_~h3R&bo#5E9Hf zA40hKAG~ObQ~($zUzT2d?$c7^)wPBSr=vjpw1|rLm7Js0UB^RfccS0gz~}cG_JKTQ zNmgR?3%Xl4(v}pw6HQQ`W+)Z)Q%G0v%OZ!s6$+8T%ejG%)`pB$<02Wtg@P)Xjg`er~ABKp*h_$r#e+FMDrgmw1Eg z?Gk)^K5|gw&z>t>EFzk_>*JqHD|PYF!I%c{7Hyg+dSOVQ{>Da#J}nX>m24HHiFr?R*DcWB@{#?UgSSR+SH(c^8T zW0Rf#A#I8eI$7UfC=HzR#a-V^+s#!Br>!30`*3Q8&m^GlA@L zmxFNDZwcon-4SnpP5@_K3^j3i4yM&D)@4wcA*h=(bPm)!(WW}xWp@cS;uIWfEK@#s z=R^C=xj&{wzH4gqycvk%Y^*(-6=(nvshy<=Xk z!P#|2&uCMU znXgc;-Ucz3z9qU_Q42dw3Iwc_g~h^8JAYdMw3H`2^Ts{LYoUE*3R zJC0ROH{IaxA$Z&jgLsYTOb=pwchK(1xqnG-XC*eHs#CY`@2!JxdyOZ_t5Rsx6Vq_J zSg*b_=lY=^Hc|b2?c2_at22W40_AR}+**gLEdhow+t?&b_Eo*RiPo_Z<4><+hNsj3jW2}${B6Fel`Fgf zBYFvx^gaKow)XgCjtS*<-NYkb!r@2Cau$cyJl@w!sPYc4eif%W)1Z&}X4-FJ4_ZFH zgOJhdQgt;P8zOUO6O!ukE8&PG!ghl@xgG4*!N+5mu^Bk*ExjdG=lUS&d5Lr%{;6Dx zaJbg{{FqXK?tay*tfY)mPo%7znnMTB{AJs1XTzneouU>cleBZaY%Fd0_g5;yE*5ct%y%065fd2wB=vW(IQRY(H$fdWn09LB7Aq+-W!!Re#M1Jw!zee^^hw0#zOHvB&DS|!}j~Y ztHJz<%)D0`YL^!G8o$gVm}d{N>=iS`P4}yU#djf!<^{Hvk(8_Z-Mr;$=W%a#Fa>uA ziykGkPe6ruPGr+}%HId39sQzV0Hz_C7`{`n{E z%I1Dk=V&LV2{t>$hpRVTCd9nZ?q&-McEI0H# znxi0GpBlKCLU&V*A@@b0}iwO&f`C6A;qY}NRHbCb|W!qrPbwTy!6y2P7UiJwo64?7O+*S>sU2E37pGN)dH47Rs|ZBvoKfz+%YS{W zpq__<9|odidpZLmddq||gubtvy!d@yd(4%_n}Zk`FmU@0-gMLM*|v-g7+livW1nftel-ftr^Q zS*VBPhJPru`1AX%j3ectdRr<0Bu>~ZM9ebBz)rXrLjU_j6bgS{SAE-2!S0u!GOK*j z^K&!XjTeseHhF)vt_tS(QT_L743Y^eU-bN;i#S<_$p@V(giX`}Mq@T|O&rcjpX?8} z$K)VHhyM2Vg0wF{T8^|~1PWqd-e&ifkK2XNKb2{Q3k%q!4!@?ISjGs}7wCwiKj>CU z#MimSvLA^MJE9b~u&N-}>R>lNs+rG!o@h3ZrN`3yt-8luiAVY{{~ z#nIru=Y^f>&Wk0fy(vET+`n)S7BaJSgN4?*ShB2Z^7$7}=(SW#=dsC!6~qbI5lGx^ zkj?AjN7eHsKMZC!PJfCpu=grHHwin=t;9TzdGRML8heOG2GWbH=+DQ>6FTrs zCqbb172P0O1pPToEYj!OcdVX7x?UT)T!JmJfyAZ4JXuMH8|ExDF&h?I&E4Z;1`T0> zqok?1I}No7>in+%)II)EwioX*t-(Sx;f}cY=>wugfUEKvjlEf5D6O+gJ3*=-qwc9_ zNArFXL4y6`{H)t72=7i#<0cYkCfxtcvpI_PGw*(?XgTA4qTW7{+jHN& z!CkUcZ=CsR%NIJfYijZLU(onD?_r5H*7;B=asy!1l-eojncePApLzA{Opp6%*W&hR zLBJOw3to0ZCr`0%!067XmY0+t-R+*rv(<4vjnOc!#=zO{`^sjb#|LNMM~p;Z^+ErZ zet?`xj-sBMTo-} zn6+RZgqV5T;U8%7dCB(>rQyV-hcma|j}fH4aFvyA{5>1F379p-4=I$aH6XRJ#kP087)J zk*}+17ZD!xlHOb38UX9X;352h*oliwh+oR$y#eF%*IJ`XbzYFz>Cdnv)v2VeO_iAg zsK^fwY(AvqbNjT?9_`1G>*b_40-!)h+@P}Lu|2GMK1r?PVmv0`C0|%okf_nQJoC!~ z%r3tckLs(hq?&7hSQfV5SiM+zETtTn0rU6xu5FUT#G%zvr6NNFM zQQ$O?Q3Azv?H~p1{vpq-U^hUSb4&?{{qPm7`unjbE1*IW$ix*dZ%pKdm2i6O2~oMZ zw67VP37~qpG)znDzH>JYAtK+Q7s-$A9M$l}bSJe&Etzrl?dgYvv|gfE95vfUtBDsq z)co<;|7BeSf{j79z6^g9E3xSVGHiX*zC6a#o{l5K>Z2amJWXT5_+oR~C-8LL#Hln}osFn9|PL?1k zD6xKtSGYzQp4x&Xh7{)7UNZO zsXo<#LTqU?-Z1RvD*=+VV~-deegHi}vJS~4qb%R-s8gAP?(irB- zdjMm9)y$}|Oq*!TvRIe0iOU@}vxY|vWVTD>zvEkj=Lucd;;q^QP|%0KgBWs<{Wj?7 z+4>ozrx4~AmZjW${e&RI@P2r#$nwDyU`xo^~bDD z`nbD~Jl*oS-mvc|woL^8R&r}B4*$zpf?h99dVuE3LtYCOUA_7P?nVneOgNw|l7T5L z-@+Li>G@P(Q4#~~aLgs~x{koXZBg5a44I39gd zoqZ$0&N1+*h6%y=ZG2M>mnz z_DYQdMc{Da(TX_P$dcY)cORYd&A@8%O#kUx@4?TaJ2$PpR?ZPuN5JhXblR9+cJMeu z7!zD2piCIxk#sZh(1#&3ZstwgdzrVJ!FRUppc}g?l$}$r{dREM-~^S&=jCHO_!$-7 z0$7^q4&kSBsLTv+P8PTCG|a5yp~7^8ngtRr=D?Qs>bG}kZFEA|VI(~2m3npnxqX9P zd-ygJ2YgzoTxOscqg^;}vS~0oq?IJ>u9R49h5>5s{;k3%?B*n_AwtsAY;^Vjptw zRS7$2364==rFl)qeIbvyjqR;hv+W(>AS9Tc7Ujbx?Np*;OQ?$B`D`#w>>o*_?c&Va zv@P^q9oBYG)+8QWMg2M58CF* zfzDnHSg#2F>AZIyPW$2ia)EA#=FFh`R{8DdQ;M}#(x2bW#=w@HI(S=FH;l(0<1Ml} zm=(ivu-Z$aE*)F0T&T^A{a=~j_bzGxzB|;btJDetw~Bhn96$9?U19wQ;&P$g%-aXB z17IF{;!8dIn*Rh~EXNl6u;W*28XVy`FuypAp`V#PmnQmGf6SEmm5hP!gW|MpE(-r z2irCaFu|+U?JI6#xKQ9SPN$aZkZciPqdeI@jbm;*7A;ZZKHtD>|8~63fRPeySeEG6 z!e&5|&I@lLhb(a^+v54hP}GN{ynU-Obn_XKS7tVi(4E{a>|XKlE#zpRf!IY)l}m!_ zecU$7ZQ;mb8VzvQ;lK3zz*-L#b0{73)8zbyNwR+O#LIf8{el(a&ZqF{v<%G)0JLrg z(X=yGNzFHL*|zhkOr$SG7bClBr=RuhPDe!2EVGSi9JdQ5tPIy|<*?5w) z+{O0ro^INQC{=Nt5B6$lgbh#+VJuDp_Z_d{4 zmg=9d0$LvYIFUoLZElaV9Y68w9(%Z1Xc53!yP6QB#}ku~m}On@Oee=|jMBGvGgTn1 zAVM{7rAs7SI%Q-nYwgO`^G|yfhLA{Zc~O4~LH~?G;e$ilnNe7@i7C&g@%EP&?iPmi zHobW!4>tpm6^8USN&^XeGE8AN0x*L8f1q&OZSgbGt-8=(QQ87^6<$&e;eV8g>6 ztt#5afI*sgxvE1HI@AL9EkNv_eb?3TR+Y+h7fug@p(jUQO_OWvchkSBp1s*lS@I+A z_ekK6Qr0mH7tS2#9v+clppn1g=xQ5Y%7y(l)y^vg3o}QDjTxoVV$+B43nbtVl$*V9 ziV$L1_i?al#b zumW`b=fiSD`XTc5q2hC$YW>l9QWPmW)jX}C)?)r_M(vKW=;b^}`OZ^<81HV>h=z1k8Rk9j#w`bvh23Wld1 zpOM$JHgaKULwPKE-wD_1{Dr%)TtQ}}?VKc*+n`(Rd1qhG$`C8XI+1JLsqrOTLTgW4Zc_Rb3O|Pez$)&M?bjhPt#|9 z5yTr$VM?^ZuB4hrl@0%NL zmt`JKv0B<6W`D)lY*&ggEI?=nu3L?}d4d`+yEi}QEb5_~pt~(RG9K~z2CTu>2Yx`W zN5dK*?UR{IkiUl?khWKfdQsIY@xSSFy19(gzmYX)ur_j5LY3n@9{Ff*pnhWM@z zZTt!?G(%eH9{VpsTwCg?xFcn_v_{SK+BlZBhUYH2Ypl9qxW#2;gYZv?R9?3XiF{;L&PNKA#l4?JjOse+~CmkSo~h`Nael9!bz zswNLc>i~r9)94xdZlzW{q@K72H%^?O91*zuP-)vM)Z`r-7MH2{?`QP z56N_jeh(oaMsnk#((z%vUKXQbj8sR}@Zp&{m3j3aXRgzi=L*Sfi`PY*lu7A`E($db zU9w(U-nuVGGm1PVJwm(sR>xNe10Eejlt)=CRDfb2sPUnRXWy_d;y~}zG8nB-m95uu}>{)Ldz|F36Y480CU;-Es>uWw6F+Zce6o-DQnfmx&HnROJG1fsYXS04Fy4QTX4xAXPrf)Xm1vBT z%TBoXK{AI%3K@z$p}lW=oLAQiS3ZaTGybwa=qqWs5G53zxA#9@n4qsXZ~x(t-QFebaI^KA`O^xFTQK`Crzpmj-z=ZmUS>1{ zopF_%7rE1cRm$&CmlKLo!2mhE$d5R+iiroM_n7#yq@D$LctpQSZv!;vm4X15uQZe< zLavxd{T0x+&qGh@x*RtCoq2-!eQ<%8g&X|TdQ6oX;n&A_0U4c7XLbmVKU#-g6%;m- z2nRTg4~_{gB}-Wh9O2k&SoOs)1T)-NPn<3FPJ6iZP>6C;cK4Sh0M*LY-EQF3O}%+^ z+N1qKd`2(z2``YPDXq9vqk8`S!ujaakX4j4=8X0yQ{*SpifgkhF`Xydp<}!J^i3a- zOY4$IT3o9_RZ*CTlFLgUQrIa6jC|Wn2NhSxwOKdjGY{+fsK&T@(`$c zR2kt6sG9j&>zVj?P%oGP@F10xG4*MO5UvsZ0e&O9~s`Rkf?`^d0&gT93gfgC}m0mOZHw;gIF?v#h0aT$u)^wn)=3T z%i(UwFh>507YHM;^6*%KbI48%qJa-8~>b#F>DTm&cJ(oxs>`cgJ1(!D8c)xw-drL{jk| zvKoXL;K_Giy?q(8>xVa@7&RYm-Ub=>8J}_cn z0L*#;UN-kK-yPewga47BGp{rMF|n0h+Sm|Qo&5a^8$n%c;96l<@lipn*XOUWw3Tyo zNS#2vd7Nhbrp`SjHM`=ZtR5ZjcTFb{^VWZ9djiweSN05HTEWC%poMyJ%-p6&BWtLC zzH!cJLMVpcQaH(Kci@mBuKq%k?o?y?JSel%xB?bbe{nZa1f^Iz?HK%Ddpd%;UW?&m zsQemyPQ3X*oCZe#c|z8up7AbX0l-B10K9f3WIliZR(3nx^y#pHIjdGQk*|S0Oav$B zeW(3QQga6#0ks)Rb7^+Igu(?>sxo9~Hl4C5_Vsj3v31K5pRh=AnqiDQ5J+kNBXCc1 zh@7lDFJ66fSWDV!a@;4Sy%+FsiN7c#-&gO8>ulap#QFM|AdL9>1qdlpBg9!o`0{0? zwQR54g@M4e?16W5FB~NEWnZs+8o}-0pMMYb8DZgf%;^TtqlESdwgp)f>9TruwTIE| zgU$Zkg#tJ9LNQ(7Tc(7HXz;n1AKZTV)A$!KO_5u;n#u6Jc$kWl(F<%GR@o!>U!?xm zCcnu*wZx_%J}Nfl0KY0y)#2D^ZA*|DQu8A|ycJz%nRLeElECA|_p%MK-{mtZ-vv^# z@0It_;6&5~MftfIY`-YD?qh`M%IlM!1{GF%c(TPPN2OSG%tgYxK;(i6B<93FTj=&~ z-7l`uit1lFH7i$Vr#;u7`dHy?H*2>Q>Sp>XTu-!X-2Q#DeJ3Muo(-E2+D}>xET0$b zU2^b^^$e;C$CkVh30vENg!up&vQf4D#@cE4Z2!x#Uy3vg_cJhwitD@jg^ za{*g@U!JiW>NZyINrJ}i?%4R5z0jBd~e!H(4 zyKo;M68fF&fPbbA$a#ebgW;P+c~cDO`nEJ61Gk#Zn&Tc)(b)&5s; zlw0OCshqwAejvyy(j?F&t>87`<^h|RhV47G)q&6b;pVnoE#YGkE|0KT`I8~)WExM2 z8N1Sy=55yEms7ro7AIdP>??JSQrOKhlTeplQty_|$Vel%d1A%;=(uV5{+Au%a`3PX zxWv=6s*E4Bt{Y5Z<+HMfB*aWr$5SB&A+f%fsV;u*?7*+YkFLXgJs(mNqO*59Zs=le zz8!PPd_g_`99m+gHKe9fqWYqkY~qFppHC6$uNN=t!fj{u`?7tH*+5!Z=!broXRU_Y zWh`FLZBQFPJ=^D>Iem^f$amX_`JgwF^^fyVVp5{8tN0gZ6yZ7;6A0}Ke|Sx=V||NQ zy%1up+Nh>Y90=u@#a%VZxHk5>msrwKHW2#ohKa$(xp3)jY1*in)ahu?WZ?rETPs#b zRKCF{3oPW3C91!;k(li!`ZzG|%eg$u^ee*eSwC~=4uH>Xo}O?Y)$1RnpNtHuhRFPz zIak=_>g_%IBDY}NtH{#jCC0%r79K9z^Ulce_RIS-eS^+0Uwm1EZtpvX$>diugx3qZ zk+i4EdiJVKy(FcH!4pbVpP7aUJs3Pgg{OKSDX%R{k|c>O7kyp}HHP+$8_%Qip%SCa zMH=BWN&Ann=DRom@q^ z@OEv%$tCkrHS7`Z6jYYP+5#Fcm0zYGzji!RpBKW*VkpYjmV0}P?9tS<&oOI zqMm%ONJVQg(>@3J_{~vq2-`_jhG9Nd9oQSo*$@iPTRn3y&TO)>kc1t#DaE?t=R$8z z45H-jPPoucxV-3lxa;Q6H|@4q+MpHs&mUvFG%BcryL-nVd)!Px0yZ1=it9gT$Rrfe zQn~p1{m82uUyi<;)gMt^Qdv!Q5%=gLL4f1HwEK&m$^tqP)zcE??dZIwV;vL}CI7AQLXxeSkpv0T3<*`gHR2KV1`sx?a;o zM;sW#_eALS0M2Pl+&lO@&kxkz8QYk$JAv0RS6he4bvuS43)Kf`eH1?l`;Tryvg*#+ z3a9Odv?>IkC3pRGR^HV&W$LZ13ANj&2G;cM&xh}w#D(O6HHN84JIT|#_wz7y9Xw35 zpA=bIh^0Nd=sv6^3BR&&Ak)95@e{B#fq8BJWG$+T$Q|mC;J0~3aIfe1ewtK7w(#s# z0eRUG5IRHZJ=aU)G=p@T*du5Rf%s-!;5YF)-So>VU{B&tL{zd)H{^wEx!A%7b2CX3 zr}%M^s_?N5mutxYF?C*vIp}#H_1oLZjaB!;e>eTgpLy=lLU_QMMB8Ta=uF~9dzaMO z-Somj@BWQTeJ3Fo!%caIP(@PRcXcYi&+p51tEOS0O326Q<$b|H;4kC|4W)j=iSdK_$(ZhivAXF4Wd$NC*Y{2y8*@_YNPH)04e$MYo)$?v^9{9h_bg!gMGPCC$ui(!8OJ9fKhB@)9Nwa< zEuX<|vN!XpAqxa2=;QQwQ<&}}Wmh9H@TX=BtB8l{S@PgRCBO)I|CF=#73Y%dcJ60? zfD68=HskXy(@yxgh7f@V^Y@YNdY5*jsKtTf34UVSW&P#{!o(q^4#&rM6T2d|U-j?L zGJ7A`-SHmW61YO_-4}s?C4h2)OYN5)2M(iD?CTex>fma2SADriA6T3^zjZGUwy-0h z>n{Ia2K|X5(e$jn2_Y`v?j8R}h97Ui3mrcy$-@*HpwhDh93X=#HbLF#x}|X@@WAi= z)cXgp>z&Lrajzxn1?dtMcF%ym+SCXBao7dd8)(bGAS#`sH1^ z7+Rb^J3f7GZCjHwy2-%eB}SxK9ALo+lJ+}%8FK{u%4>Lp;Y$P)9X0kIv!A{Ck4!Y! zR;`u|KM|u`HF*Am%?cm)kqaVF54;rUMcF$v;OHZ>VC_r3J#lRCc0=c{jOp`uycj(`>+)$+8Kz3HAlJ*$gEQ{Xt^s9GgoEd@Z$2 z$sO`5i7Wr{PMBJ3d!9V>+BoV5EwX8?J@ej*?GW2X}-C5JCQPhpa)y`rsGJ^=sO2Cy&6#WL=zbo~Dw&W{- zk-czw@9v^kIpl%yKL4R%yC;$@b6$b-bH?Q#^mGQfVp6dP=8?p~*xSXTq`F(bj78zjxlOVG4 zM3CTT*qdVsr=Wo626pfEOgG-u*gE09Vy~)*|rSXOtht6)Dcfa8?ZXqmabR2IyFj zH2BX>>#SvC$2H6F+`n^geRBd?OQ92D*l*o=p4J*&t_X0n?e1jcHgeclNX0PwJs0{oh#ef zWk!f$JEzFExn6WKq}+4jVIAZ6i-G!|f?D5~T7o?q9uD+}hB1c#=5tkSMi}ph z^nUr1NuQ7Zr+DTD2i|!qzN$oq_=Nn~MCv%|w!*0XBakR6LnsP-;#x8`8q)NG4%*rZ zXskW*T9Z5XF>?g8`~Xk{Ev4}%k-QOs+F>@&FyD^?w2tKu51e&4L>;z&E!xz1F!iTw z|GtN9zwUL=b-J-;m7A?U)YR2UNx`x9cr(Zmzp&nrtx$n~)sG2%^8q5JaYwU5rdFZu zm#DfOg6DkV6w?q+ZFVrv^+|U3;%Wa37bT|S)Q`{}C?Gy#fyX!{H^*X=h-O=;wXAyb z1g$L_GVmZ7aV{={nQk!?SOB0<{D{+mV=tm>Yx3!C;P(|DOOczRpqAwTZZT~J^25&$ zCYKjmLvN927guUs8enA6D79MvSwu_oekFWHB0uJ`=GyL1?#1^a?HVbCE zRHU4S3ntlJnPIwOPTx_UF<)GVLTd|I84(%+{_yjHaP#W%+#;MCsi z;j3hu79rfoWWPr{uL_Tvqd)=&>_j zzwb_O*e^`mJg=$qY=5DUiDKsxQ0(eB;ri*RzmC3rOpZEe`sv&|`T#87!U4n->yp#; z{iEOpd`xEj*T0yUxwR&f+is774-DE|cDPm}*J9TG>83ddrORc`x{fYxcPa7U^OK$oOAbx`2IO+Bf_q>BVP-eQqUi0M2+Kwh zS^fFZ316$JoSxXN`g(A!Wuv!vGn=7?8|`Vz`F;kzNK&=nT zJs%$9%-YySE}c7LSwyrxxIWTZ&h<}Zwbt32cS=LCls71)Px3H_L%(WXQBG@(n)!j8 zzu|Zb@h%c+d1A@Y#Jr92guQ^3uzxYfF`2tuK1)bA1NWldd<|S&jX43opz@@mRm<53 z#v(tU(}x-Q29H86Y+BZH123ZjW@X$kQ%Lxq=11@a$(r5Mjd7jd-NTf^pd{0RoPV+n zds!f%A)>;Wsz(i@!Cfv3-t}mF_o47q_Gk@QyRqRn)D&63$?BmD-Bxu@ci9M!RcP}x zJCXmb{sMrGt<@!?Nn7b2U_IRW$B|!DAGo# zD810XJhPyW>wu}jduNn90CI8`I}$hKGE)wPndY=im6&-!49^}p5@-F_k$| z4Qln#@8oE9tk2m$s<+QLTsykaRV;3{*a73z?fK>(ItzWXvgqmT$@6fP#wpBzB2tr8 z4swM8wpT3i=z8s}-U1)n#UEr+I=lu4eA5BiU5vLN|2Pb0AO4J!^Vvu&lxN8*Q$x3@LZor&W5o zfC7R2Sb%l?^TbU?qs8B7@wiUE$qQ>ap2FkLgRgJ>q?J1P&?S1=47r`}pG4kce`naj1i~-~j zF{s|f$;`&Y5dB^-Y%km6jQjPS))RF&ZBE1TT^)T&jiFwgd4|BfRnM;9t!;XK+IGGK zK!>$G8ZJ=dHE?5W?mh6mv(}f9SFPVYe_0hy2qz$E%T0pHukul}1uQ7p3x`Z?02`#9 zz)x;F<4qNj$>v8^#1@X=3dt$R9_sf=O1H$x>H zk^hEt*T*F(mP4dRaLLd3o0J_EY)WUl#Yj$N>y9a8h)aFnax3>n* z{p@?pqE}2vkAAmOPWbY0NiV`0Q`n_g?PTfg_w8X%m!1DzeSXo}U;T}uN#gvmD`pvm z6}C%En`>4)Z&~X?uktAtJlKjm0e}E`c$@VM73r3-m5fQ7h z<;jsbIryni!`<-_75CA8zcGk=jVO^XNXU}X83+X8elGjI5A)h zWM`=~VYv+ppEs#73o;r-g_D)%`omVs*v&?=Pk5vODeZ{GM|!ie>v`42x=#0QVu~O3 z(6^}FzSS#{vwLGXM){XSNG=PKtytWxU&5S15HhocLs>6DEz_5=F)i{SN4sPiim@f7vibzVO4uxEBy)#CRlQaKBQ(#&Ap!;n>Y7L!& z;yb-^7+sbPR!2)ia;Vx>h@l19e1M0~cI<-1OUUzJUgaZyU)E~AITpV5H=@6|UkD4o z(?laI)7Qu9%+ienhHM1GS)eK4p3H8L!1n1PV54(U56ycIeO+)_=p-C|X**q1mNqL& zhEfBsM6kFgpBOJF7n*6ISR37FgA6-(8#cxuAb-l)DKSvkUR}Cu8Chd7*{h{*Md?fx zXHbnG7^?RcbFFtNWrhucaNmCTO3B&tGL%_aqjU)l$Q5;KXXGJnn%}jEINeNE#1bwC+3#qbz z`QquxX)op@K%~0I3aQoSd6$nuaaunxf zA`1h{Vp^UD{?xeu9G+wXdeFqzMGw5vbv|e(T9bC21y*1TCuR=5#RKbCs$AzQ+V25f z^s{H@^^d`0YRn?^iy3kY!ub^}r+VJ0Uoe-{xT;%=9CCPez|pZtJC;we2*^$u*BaZY z7tr}pr*&J|NG@4YFf#Pr1iCd?JPCw#uX(3F<$W-{pMa(b0c$a`?zkV-eSL2aWm$eb=tufpG}9_&1#}-I532 zw9=*ht!f#!-@GhQL7JP_ld|AIWagdEKne#vnLzdV6g6AtVCp?&lyh%gC}!4dXJtVQ9SHb4?3!fgjQdk6?e z8@Oo;hrZzaD659NUXPH`{6#T+NsxzhT)dsM&M`*&*^<2XE)MzMcbWV zm)f;s+78xT(yn%5mlJj!iteJGE+=P2$|*P>dmPf)DKh0I+@HoJB!(eAtH%|cs{y%k zcK8dMz!Q9<%N=e-`*QvS-`Kz>>G@WCA?U0Q+Xb%26XV%S=nd=V(U@>Z0!{DC)oeDa z@zJ&mrq@0nS% z%G1_b&peCAhMe0(he6J2^kqDrm2LL0{?{XN(DCCIwfXBpd-eLuhW_C;_sRQ(%}=`s zd%P2co!;R2)<8e9(4RVgQhMgGLC$lL8VH7}vIq+#at3V|4DHr2KVIO*5sWWDF@%1G zq2C!_GfbS-jQvi>MHcCVT^H#DT=`@i0TpDEC9g-OH9L@UDBtmPuQIx(a*6+PU!)sG zC)}n37a54r38OoKZve1}nxtMb*B?eK(t)C5A@ZM)GrkZpcb#l<0>X9b+`~m?5-`V? z?7G=LV$LdLJlQycA5T8Q3H@E9Yq5Nu&&16lHhyCLh5C~6jBCL^iIz z9KFy;9q^Au`Y_U;O2?bkhtMBK*L*)ir{51U2zGw>hshNUx7^ z6PvZuOs9gii=zCNa$0 zy)@D>b-L3cbPN4Q>4)bVi1v@c&ZhMz;{@5ze@wp8$aJJ1eER!L&WnUEn3I;^9BnD* zxX8rN+pmA+!Q`@mUR6W+RF0`&PBeOdLv&IwMowapnPfeZ6&I0=!^95P3ku(^!ih~NiWys631U~Rs`dFuHO}2 z-g4MJ0-z{2nEnABFL2||-fz~2OkT6&hpBU;AU6n~>wLw}D-?bnpx>|CF}xErR6T;1 zk2zz?{QEde_{&9|+a$1KbTy|(8W~Mt^exH}f(>%j=cD`wndRDbmrf55}~2K4$$9uXEF19oa1A{cO2&J z6FTn%k@K7rFpoJqSafn$ZS^kUTOv0=_*jrLJZ5&aLd#WYyD8@CL+v5>xcbVJpTzMH z*AJ{B!U~aP2|aQ3BDvqBs&GNp=!T0_2UUv<2(So+vLHWIgFNN!Mv<dxQXdXPjhOqic{LC4JQX4m!Q7wvfvEQuJJJko~;v>^Jsi0z#R>~lfx=$O5b zV99`Kg-#;}-hS>CG*BlP>l7LmhQyR4iz`|=dw1+cU2Uyn8ImLk4su5MV28W)>~34; z`ajp$Udzev2GUEUJbNk3Qd&pnllNMho3ZwfmDca<-pz0MzkT~-8*1_QC%gW(%B|*( zPWJ;qZlN}AO%07We&`78|C}DIkG@;_IY-4}-dI=l?=({{EU(*PfA_ans;oGP zO5G7Gt6R<)mf}x?wv~h0t331Xdy01g0OC`U=yAaR6ZI-Tb{q#w_5lET>(TuB4<7;m z`0KYng}V>$E5*@+hY;uuM9+bJ5gHy&$7JpDKTYLV94o`As%ij0*Mj^u)i*sm6W-t; z0Km?oZGPJ0oOoVa^Vb-VPVn1L*t(hTm;+Smt_ttuSZ*^7|P)W_GxUB?k;6=KIGE z>$%M3;`$84@*o+WceYoIb`FnK&R&uw2{P?;aK^U{#+hJ9jM)5QZt}-B$_85*Wg-@z zqu6v^E76D}qC#=k1DC@ol^?Id%*&oa@sJMAyM!5jw+c*H~Vo8t>44dT^Q*L&0?sVj&WQz&GIbwre5OACIh6+jFP7?Kw zv);1Sb70EaA)Sui^{2=A-yz(m+GB~w0Pk$Z=nFQO-3LQCB33OhBb2sbiDAMYDl9O! znCwJ1LU{}e{Ab`B6NYFEssc991;vsrh7p_Et2)w>9?FZ+1-H3Gjb9=1i342{zL5C( z8RQE`FWUPcmQNxl==GtIZA|r3O_;AQ`1;fFVqZjS^&^`avH_Fl$RelVeUfCTzig-v zj}`DvK?EC*!-Y^!j*n@(^!^a)1FpV`0b9v&i8)#DoA{(1#&!wM^X$aST}FrCB8U`$E%G=Pnq8gk(dHrr`CwZ@V+>HIA z(8zxVI@Dh%MAYM*BP`{BEY?SG=3~wRR&vt+{KqZ$lGUpC1ZCfDh>aeBar z8GW$!u!xLC`ZCB{+x9VfvKR1V{CIt@)kBC(0j_<#T_l+!|CuA9smytS8)tTmoH*{f zq7wK^h_t7${NSXZ zPgulNbtfGa+~O)}pAlO)g(@1qv1)1jCiI)(xLjH0ot3oyCItw`z8dLPkmLM($U35> zUoTapF5VD-cOxyN&kvY zRz`bA(dm$nFit))lKKATBFj-e))_#MqY6jA7WNfX&3QF0EgoS};Wm0y62^rupQlq9 z270ovQ=k&Pr_&EuzY+UhaGoWYt{XrqML=<%;~TopQ=E>8Zk>-zjrQJFj@@&?RqW=@Q(-Mq6A(OX2p@yF!S zc9Zob@*arv$m@NADW3^^EmJ;b{Y>;BdmrUD5IdXD1t83KiTno)_JvS&ndKhI$Z+HIb720ZJEpVZ0^n|e9q!Kew#KO7}booz*kHf0rI{!Rqd4Tx4h$V{I3#_nE0TXU}uBHEe@vT2j z#n90sEPiF>tY2a8j7QSwi^tOc`RRyL?c)*?EX(rwe0cfMr)}RA6&qucKmO!V-=Tvb zyX;S(0f6F7TWqg+gM$3L0oYxq?sj`T!gv|SO$E?7uPZXU zYEhhv)^5;0oY1aIL^c38SynF8&zO_vjP0-+$I~5ezxwGc()vGE0RXxc_TZf3PAB=9 zu$!)*E50@%Mu%W?eJ_~8qMxq$&G`N^OIz1%#Iyf+2(9{2 z+F!l*kzQZTsq~s#=eD^le|%ww|IXY>ZkXy}qu-6#Xb))W(qM;DJDOqXG_Y#I-1-b8 z0YkTwgnH?1V)i?4-!Y6fVzQ3}x^7DP3*0)o{$3qJc+OZ3_Z}V_e&^>OVmbUenm8XJ z@0aQ11BS_y1%@q_Z7|mfdUsi2S?Cllq7vVU5Ie^X1AJrAMr@tWy73}JMu);KHPkm@ z?lsTi(7_7RAgHE6Lc8GdvTDq^dNHah?Z58u1h}!_uE`=XQsNP@n8OhRR@f*MtT3}z z{vIr~5ljDLSUcKB$FiUMjn_~2{xiJZ9_6~+E_ebwZF|1G+tChKk|em@uG6F=JN)<1 zc|d?0vMl@IGiT*%&F_3J%7xgt2*f8w!Yb)v>2H;D=JK`N<8i_5Had#{RBrpj20dWJ zZPOb@JtOKNcYqtROQsl-LC+cW9!PGVob{};-Zo?QI}E>@F~BcQny^YR956cuR~rw4 z$@oZy>pVuAF^pK(cW}alpvmvxuWS87Hs5%x(&zQ@HI&C2ow7@YC%{dyXcRhOgPaj^ z_Fm@n0E(Rw+pN^@Cz~3)zULz?IMOA!dhl|DcW%%yxsOFq(e9@aROof~g*a4|ZgSCa z=P=)X==>%`8U+6Zl4QoJaq(Q-3=i;lOq*i-aa;`Zk*H({o1GlyH}}Nx*83m)CN!%* zjpQfXFTo|t@OlHp`blDaWwJ>ti{m64^s6!Qo)E#2doMUSA?BR3rKNo4FxTInK({a+ zl%5RrwT(PS{-?E`$Z_78%*bZ|b6R7I{{%z7Zln{V(djDN8`0H&EaY2>r+P z511SkBZgS%O@+~)o`c6K!^Q|=Cb=bR^u+2T_BR=KOXn#E0nhA6U7qOmHXf%)=~!ql zf6Nzjx-vMi$TH`I1(SXt`iQTuYS34_{tzM$w?S|5<0#NI@0`Iq)ySN_q4J&8M{vS3 ztOsah!!`|F9)12~(g&Qr6viETdwDZP;+(f(;A>Ub3^ZfjX`YkQynRUYrj7#w+@gIb zNg&6wQM;Pb72Et5?G=NwmY^RQpS#^I&c5Lzs2KU4x9|A-Rm1%%)z7ql+%9+m+@hT3 zkDHfCe;fOsI7u5KAqjef-^>>65hu3O$y4h;E>ak^FX(2r=wth z^b7XDz(4@tBB$x~28-}0w!^4D!P+axL*4EK&&T?PlHE9|s%Ec3rB0Z{uH)sk?3O_z zyGh)py+HS(lcOoppTr_`QM$E~w>sSi1bPIg0!fwxItP_wZ2YJ*Ag?b8-5TVOC%~ik z3)eqHztVY#?wtxibT;6eKfvUY!f)1tIdwoK&NS0=s_GFs_1}g5A@z}=_;|ggVg7zG(j?$=yW#fe>s5LLP;K~(d~a?CczvW8`-#&Z z)Nax0k=4Wc_+RL9K$46R7dib8fH;}(b`FhDW{`(`d0Ia*dZKn0$A_9YFsvs~y9wNV zDw8wDoX{0VC#s@AcDo?U5@(-C`tiLo)msGfk!fAjNfWvg9v7*V!;%EJT(VAAd?Y=Y zMi4bdgk*F?h!?P$zkAzDk|(+%C8S+@^ULA88igB8C$_ zlv!j|fY{X>-%<=6o8`7Jk05%0*v;I$LV|>VKym)dB9chr_)@tp>5(FB^e(Y)i4&^C zoD>yq-Y3X0o*spHsd3)N>O=UJIoomLL2_z8iCCf6xrls%GdEW+K8Jm~HU1!x;y5Q* z9W!TKE>azFdgJOL3H`>~Jt7Y9dbIwgb|+^iP^{3L$$wqGN@N}aUEdM?#hk;*{GYc6 zV6qQD>0Z>gVtr)3UV2VHQWEr&HI{ff0x0I4`&FSH;<_n|#7El8Ula^=QZDBuNs6kG;Teo%l52@=N{R^Tm7ieN}a`()JojZnv|8p;iB! zZLYQhCTaBK@#xgGv+ebu;2_Mr^Ja>F{#!*`=Itd62 zXt#3h(sxTggQ}_s4D#ZdTW|1tzuWCbr>^$4FH4f2%f{=H^L2mN;XOMxZ$-&A&7pPj zbyp!GI?8XkT}T8-i|Yz!CwYuxWVoL=+69M(VEU~$K+@||MUP)*_pkotcK|@);Qr`0 zOp6q_W78Jls9?VQ|NQ$)s@tx=YXLj~0kG@$yxs*Edf`X_z`Ea8;>dwg%ll3@l^r=| z`t79$pP-QsM@}4vbH<$CF7NvPBKe_G089@9;(Qwbr?1Z(<20~rf%Rz;w;sUANS_`E z*qp1*nAj&^b_yJ@r9A_Ng?Xs#2OI1q7=9=Ah`3&Fjfq|97B=VSpH zIpDVBu*9NqCt!mSNN`ee3?@!J2Ttjcd)~+1lB3*n%!n5!duNMTy(CG(kwYhN?`?14 z`8VfL;y0##gSw(uzb;t6W+&LD0_fz>3U}z3fW+h|X7q7u>f4JB;8bmc_`J>(Az?un zFgO=|2IQbqW)jlU;}9Aa1aD9Pd_Dz@4b3=ytP=b89>?bOC0Mb1D-P^EMxQ^Myl3w$ zcmmy)M(d{ad(hm}YM8W1CL9>#L64rDD82KWai#mqarDRu?m01!PmIFw(LK?P}Ny#w?4GV(HrEd$VD(dT+PipeW}aTO!l{}DoT#Wq(WZUaynYW>|5=Rc%36-z2;JD>GtWD8j7P72osrig z1L+wFNKA@GR5Ys}0Myqtq4rb*s;ld;XV+nDE!vObqW##ic^?#?V$bKs+;Ba$LmZM* zV+6j>FUYW!dl^~D=r^bm=B&tL8d^q_k|CuDU=MRu1Ibm*9X(9j@+gasia#0y!LP+!-C`uaxHRMp|Y zzT?=p_b7H0AH=4$yV2@Xps(f##+iUEbDlxXSQ4rX=s!3Y-Fs#sC$|HVQ)3X9 z5QTsMHvnx>zOkVRK)j)b{85On+?XEUQOQUG#HcM>-{^$x6Cy&a_o$~d_tI$7{?O>m;(^(sH!26sQqM(+V#(XD$1I%Xy# zF*ycN(P32906=47GftFM;pmYQ*t6>}wr$>nzg85ZqM{m*h_jPLJ&gLE@GZrl^$ZFQ zME9QAmb7qi|1lgta)QYTqG$AW#KuQq#07njSCEO$x#@_Dk3zIh>*lI~E>x6N!MHg}HaA#gD9{{|#3D9vzmtzCdi3dnzyJ?pQ;u7U*6%`7Q;VU$ z$$W;*YXZFi$nTkL$?u2uAIH%n6?9y9epgf#af#75Z**UD>zRelx#@_Fj}mC$luiS? zw(ZB}^*gbC|3SDGv3)qt1yW3m~6;- zCp0_yvx=ZbDH=#j*Wn<&!2p)J~Y%f;`reT965LlJB#*W z)a~ZojZz3P+!}?>|$tgyKqo< zoHuq5@_OeWHZdB3-T<^Tx8g+EN&Ne@ukr1NKY~W4={FzVuA=r^noIX$uv5giV<$Bo8@CLBLhj;HT<89R$gz>dFEO}Xx6!Q_Qb zS1wsbcK1x=_w9+E^6&Hqhn4wLc)U~%QEU}>Tr1P5!`*n188Y#;g03h=)vRlpli=A zR9^7{;>dwxC_h}zIeoLudUt>a{YUmg!Qgyk>yEe`EDj6K`- z;;*09qIlhQQC=A$dx0J^N^%ul3cH~1h(5^bo`tlmRD^_w!0mBswn23*s!vp7*QVXr zwq_ePEZ>N_nmXNXWXJbe^}n4x$?OP`PK8L2RNm`!Lvt1xSmxxsqN<2XibYaJG9yz? z;FfGIL1SG5b5hfv&mWMFfnx_Fr&kW56QU8|4M1y4D=Lql#Mf_pg`Yn92@*j0ihYE^ z%_FGY4Ty3NNX$q=xBhv^%+Eq{r)0z?$09r`94?QWRkc40OKQgF{eF#zLc4l1#eie zAg2n>v$vf)7j{K;hbq&20RVh0t=PG4yJ7tR0GKm5bxx%@krxJiLUS^2;^hl*Qc@9} z7>&e?6wA8qD=I-#eWSsS(OMcF8->1O1|qvK2OV-V5EdPQKz;44rKtrqC#rCGcPaL5 z-i>uXtU}p=V+Ollw`*iWBvw6l2e^?xv=F(2dLX@9C&Z;Cz#9?-*(IZ;sReaar%-kL zBzCXgj-6|^Vf*SW@U^sZa>FWDZE~Upb$M%wASiyM5TBleF8#V8Juedpol+2;6pPU4 zaJV?fh?d4?RF_wx^5_X1DJjK)EhX5ovIwV6R2x3VfWgTdY(AnI-cR(V!t^I^ZxK75 z%(DVRy~rQd8=d>*A-Qu0#H1#`tJ}P)qN1^`0VfZa;rQOeC|OsG;y*T_`q)W9PVn<{ zyF})b%;}Hg7b*v}_I1q9L_lZ|WatMgaQ1s?@m@66)C=;K>IL$#3*T<~J83_2(sFq` z$n2d1t92Ho)rY<7w?pOaM2lSGH~V%2`ukz1hhZK^oX!<4r_SV(7@^SZaiL>j4l;V> zBBg5vVmc%uA|V<9!Cry3>MCn+vh+C0_aDK&_1m%cuWhJ1StHVcUN6$0idIqTY^vxV zp(rMO#n0CP63LO}DuN@!keZ)~gzR)AWThc0B@O|>UIc~&Auu=yiqD7U`bIR>H=^!j zHOluNLB-y~DBV(mV>|aV`7ZRk!3mE4NW+jz0$u4&df=V)02RD5p)nWNM+CbRT2AN< zR&*w;ml3B)&S{wH34MH_)syCWJ!M*7aef3tZaEJHH_vmzFwUcOcF=RXb!a%|l7OiJ zLonetYS`G|)5>9kfz5wyFzHicN=QyJJ;$&2Z~ALJnwy#s5Ey7z1fpYOxTpQ+VKccV zo%dVx1r_`Ijo1BQKks?%(P!|;zg|Fed@LS(?HN3A>%BO7@Q|sTu!wMszH|a6&bS;e z-2XWK{O_-(@<>ET0O;Pg7nXmg9hYr2Mh8pwqWtJF#3dzS;Q8kQ0R9r9@3HqE+_Mkm z$BrW|F#$dL_tx%RCLR^c*Z1hbL-_c$cQIx5HOS8EiU(eO8t*QC1-puh={?@yAdH+i z7L#UP1pqizRgE{Fc-~*HomFaXYQ__{-h;>Adj(+;;h6Q0`8d4)AU%rt|8Gg6>o?54 zM?2?M>-02-hygLzi(&Txf*L3DMEX&H2}>p{tfNTISg#n!!nv@%tnzk7TQO(f+1NFz zxq)r4ceLEij7G9qq)$4C5oy8(Bhw1YtTE&|X_Rp@)O3Fvt{WGv`1{S9S7HPRfKfc>HS1I?TCxG1jcyZhHQ_kv(z! zO=FSW#d={70D^)85fmJV_{2zb%}d9WE6zn@V++3gWCfPI`6G@VK6ys*fZ&io1czvK%jTeDU!LRFu~U5vf!`?&(;# zuHxcLhv1s)t@++6U47k1Tz%b0+w0TDJ&c{jrKc^|;u0b;aq2l3H)$XelcF8#5D?%- zbW9kcW5UqApc5{-xIX}(cC`4oruS?K}NU%cL6dW6?2!$ub1`kTf% z(o|re2Z>41NKA@Gzd^aU>bj9AKVF58mi&qj-ufBMEvaYuXh>eq$-Ut8ONRex;5XCE%d znw7=Y_Js+vFPzzRQ2T*J`~cl-8aliHlct@E0YkbueoZLL5`sg#NY6+_dPX7!59^NU zv&W#Wwh{mNW(}6U`xEx;I>I7karO_x3f-h6m_~AuSR?_bBuM}{7KlHF>Z+NeG2zl7 zj?Ws?^w}3;`s@pBuYdaCGCX$Qd(2)V$5V0k9Upm&V$yC?uCPd8Jm%Uj^auHppL;*) z&+xDijGH_dldm`rY3bI-S^%0JkByH+Y-R3* z(lO_ji5M`nyB}LA0x_`>=#-s;3nmPLs;cmOcz(&9mSw#2;+J@R@n^z3L6#+C<)q>ITQ5cbp*Jr?z~4fy$+RrvVr|KPyhBPM%~&?j~FQ+YirV%f63 z8jgw$!;JZpF>1nKTe^~E387)Z$j(hic5XUGj2j45Rk8Bdjd<_HFR*>f9)T{zg&cvtPb4*H^%* zLH?^Myxu^Jy!aekIORNa&hO+{7nj?G(C`q1hKC@tYdVIE(&lz$M=J2cr_1ogyZ^zd z>Nf3KvA?80k)3Oe?Onm@b{wk*RkBN)I-Ot~Q?tODj_j&@{ zNKB1KTBl?--jdAfqpG<4hKU$@p?Q5m@hKS9c`8?b896<&aL)_3AU-+PbiX$!5b4>e zh)Ian+s}?IG#!g89IGiKAtesut{jbF;|3!-&U&4IBw3ab5*Cb*uwW#l#Gy;~ER30^ zebK#p>t1~K!H@Xm%iq!3(#rauvoBZ#F%ip>jN2E@LsEL8gTaAtpHv-G>%p{7qA^?T=0P?CB41Xy*Y)E*HS{FRy=Pw;ShQb0N;3J{BPn z)~5-10zHUKh(=^WG&*+A!uiw3qN4OTzJK*|tom}9V3#O9A7qz{!wO`VCf{AM44)Az zigx^|flmXxffzJ-1O`qVhNMjY{NeEiAT})lv1tj&?wbo>6jVjU-c38P;@>}E{SPZ4 zsnFgp^Y)>_$q`OY(RnAqr2iBXeMH1YVf5UI=ri^l1lZWTk}M%8EEp*{9g&jL5rt#4 zxx=>Q8}QQ`U*cfVZejhzDQTO33MUtJCp;ayBu0;y+?(Huh04?cC6Y0mp(7m z>>WLFonc;LUZ)e+FT9~anDzQSmhWk9Xu{q7t`?lWM0ro@t-aSVsl-W(MI0o~U*hq_ z{5l4k2SCS3ojaial#%E@dH@1LoY(I>-T*`<#Ue5(7M=QbL%&PUhvHMPYsE&a{^-9b zS+#|Yo67Z*LH-i#k~ldBEg~S5w*;#SJOKg78{Hq>M-M_~zdXo(B7yNKYhVxhf6eie z*s*La)_nM1lpi=u^#fUla!D?;+~VXvuMgPq+dfD-wrrn1o{B2ky}T1Yn{RUZ5tUhn(x&!>4>YngM}Pj-{lV3jUx(#4)7!FtK-Obn@vV#z zo4m0=ZfFZuIV{=Zv&hN5TvKDC;}Nzoam;y-|EP#L0Rr1L6rtyUKDK2@l7z8SC*hlq zKDE7vPd)qFm-YQ8UjhKutypF20NA~E&o;e>r1)g#In%yfCAQZj0L#Ds+3!1ulVSUe zo`0vm_D(;3`ZaP2@^Q)Z%P@TW1=zZFBPuISqHE85WahH{ki+{A;-v?lG(BH^vJ#)Z zxdhYZUW=p-saX8U+xX#=uW+zrFHTj}AR;;v!^VzA_kO)_uh@ zM90Ts%+yN|78wDT%Y~eRe9W701$;hVyS#@_pSgRPZp^rS9y)dHjD+Mw z1O^A;q1m^Z?*Hqz6}a%SNwjXOeqL_dj#WP{!?>%a(0fKqHTzZi$#zAy%8K50suH!R)NQExeHY%PMXHeuK(y>GLp@?|@0ai`QX$UBp_yJp06_7(S{H!6DX@;yweqAT2!}r3cEz=P8q@ z0=# zV`^i0_4Q4*MFfX~0{$E=tdb;27&^QM&N;6;URnGN{`K~E z5+qE!<~+=rchMR151--_A~ENd@t84p44!-9b9}VqSCP-bYBEhub<5w>9PFr6c#zD{=L#ky!M|$5^p^Ya8W0uU9PehOSqlV#4w8Q!_B| zoNi}Szqo`*%)9LpOqe_v_ul>%iZMH9g`955Z%yU>QR`59xvaBJ-ZI${@dQb_H745IY%ci7Ly2S4@1)roP8`w5++?X z40CUti13Jzv+8?UmQc{UGYWcl#+(I{@bp6;;E!K7vGJC~@s}hcJ&3rW!JqzR5j$jA z!n}o3aoM$_&e&EuXG9@}4DX58p8X7Oz4WzSU!#*U=wi3O2nT19=y=jddNx>9(IF!d zw>@|@`VG!I>-WmCgu%mmV({>uC|bW8_uujgjvlT6b*kVtK_E%Y8IjPL0xqJFcHPNt zWc)>(oK@Q2JY6?9*vmm7L7W~@;dZ+)XWSzvWtf@r8Hd66!;+ZTrmfT~&cm zmkhfa6jeCsa94=(i=LFVWK zT)U_ppyQCRV9dDva$GQFxT8aFTT^ykC(M6rHm8qMHH>x2L3o*t^EMo1l7j{ z{_U|dO~j597FmtXe>(jZ49v%z2WBEI)Bb~{wx-lh$+%|W6__%2B0hZK6MX&7w~$nc zI=z_Xf+hfXJw*6RmLxRRHzC4;%mRY~K_cWyvW!V{F2NNGrunU(BG3iXhmG|Ow$FG& zywo03RTW9;iMZ>vJCW7~=P^_NWTxWoH}1sOZ+(OJo>-!N++~oXVNqeY<>~n-7}CRk zfrb*%XtzNo#+YfJN6t*zGsGCCo;?W zNfPqT?S*dV6ylGi|HapfmS{Yx%Rl?X=?#v)*!qHwNrX1rQnY!7Vw`ty^M77W^Ya!O z@tco)>8Gu%Ull6fNu)a&pmN_&K_Ke@{5+0#{?l$F&ST8UA5akQq^QJL{Cq$#~lu_t2c_g;|v>HDwT7pyMl~7f{1$Hd1!i^^{4?*=WnRm0CIG^%* zokeaH<+l}Ikui`tyHEv*1jxqC#UJks&2H7^&)i2G+LPcn(@6wbPz>0{x=2269w5!3m<1@`9vrJW?ib-xXuC@-B&bk`mkr9|U<8tA8eQh0HedK9G zL`P!U+-uvV-aelX|9bLy-2Bgb5D*xEA)|)_(DYVv$264g+21bjF{U@4cn-xIi!g1@ zbi^ekV)%qHrh8gjTk*@6-{Z@7KBSIZ{;cD+bG;cG%W3QVWO>ba-{#fpaM$!3am7uu zP&lX`{WGTBTeoBB%Wt80!)96@|DMXS3M_s3EnG40Is|$HF>2B{06;@sJv@N{XlZH| z?*D5Ua{%1Ebvvq0oV0Dn>R*4S4uE^M??R=)SYNJ*Eo(^tH+PQffmOS?wu30b_;Zc!T`1B*V_u8A#+S+;srTyOHB6HiJi*yJ*3=8(X;X1>*KG*M;4cIDIPXn|41nqaXszUrS->|r2wP;6;Hpw2Jc@BMXmwAL5 zF)?oK%CTFd_jjG-BQEHLuyFfDfgitHheO=)Z|$G+5mbyhyRZ6W9X|i)cU*C;^||Ac zBw@m(gYn{`FWdCqrp6ZAHiw14i@jrVW0jShJ_DKylJ#joFhNA6yNuRdFOTJ%ZzOUHp;51xAQdKw|I zJyT$y2Mh0;g2CtJVd1>j&`{rOdcWP~WqT%O9GD~L8ISSH$V$SKFI?ZI2#BYXI&@6H z^KZ_>znA=mXCD0oEiJ8QERTuaNK22$t4kIjsU3JDEj=EuEx8r{nEyJK{k$1aRruW~ zw9t2UdfA^ISM|s|D!*9Et67D6jS>G%dT;vm7vRR*F0qXebv7x#pcDSJ^bS1#qC+yX)8AG!8RwxQHHs-X6*2k}+}GPz*dL4-0R41=}|7^IJc=`Z^^e9gqky zf7fz_72o@{e+&`6#E5GmUALt_i2dXZ3cw@JT#s|lKfNzeol<7!6uk4&+*Qy z-&xnss=i>60SJzW3B?o7-+*5IbNs*fG*e1i9G-b&KEC|-ay)VWyYRItEJC3WA%frZ zAe(lYlU<{O14(d*7sLr6AkdA)|GHI=M0j>dk|fMtFbOFg67bN%H^lmJ5%0*RSbHat zUu+^!DhdV-E5M^K-sm^t-+WPz_~5!kq8ODTFC{g<8DX_4h;tMdEmV--qI%LKLCOp=Et+S$$s%OD8!4F z<`zW9MdF?pZt?H@r@e@?Kg9?+yxvOgn1n~)z8gs$&VF;(m|~M+@c5GZ@bPP3;+;iH z1-s=;?Xj~-{JhE>0Z0a9VAfsNVEi@qQJT**1%(FT+J9Vy!58$$qjR1@Wkr=AzA^H# z?lfjYlfj`uP!t6&mxLaJy5sU&tTz>nNzxZLsr=E`6`Pyw*Ll1lUR{4ENbQu0N0&Z? z@aQxDI0OL3%@~W;=2m?0%u-q(MNtu-5{HN0e*p2R@n=HQWB7+iaNU#hAfJWp=Lz&+<`eVKc&Z*-f8QwZN7n)QnES$Q@SNqi z_P$^sydgn&dF~T}zUOn71s|I1B1J`bYy@UJbu&8mZGZlVNsY(#FD}H&Pk+HDkG_r8 z<`$g~wCRLa4=`s$&I#47T^HRn6(eU|eAac7WC`b7J{pOgQt+>v7onxGS+@^upAZ(p zoaLfZDSG?;tlPLOeAHsRyf-^CA~e1+lT zFF?mG*^ne)Pw_6S{An4=x$&_}KY#u$R{Zb_dJP$XoE~|IijBtM{ReSq?*VLDy$+|U zsu7zQkKJ2$0Dz^f_3S?$g4^Rk)rq!6KU=?IH5N>nfpf;3k4`z+*s*yl*8K7Z%8wnV z{reu0M1nU{5_8%ARqu$Co3KATlwQ}So8C8YZ-)x)cv^q;@|XYm^$*y#ZWB}hzSdS}dj9$cY+bt% z5&)kb(a-9>qX$ayfN78TQesVuK>Y{$MGyYbN6+aUossYhUJOMk8Z zeI@3OyBxg-_eWNKE)tTHP+wb%(mngJan)KJ+`Y%~`PSwZ`rfi5$NawU={p{P%k4px zeq^=XegFOVOYGlS0+-7L6+lpM5KeIpx8%P6(@9kYHD^8{O*N=NqPLB>{r8Wpa;tr} zw(pNi7AH5f;ksn~KFjy}&Fvy7iYJX&*}X^ zE%v#;)RuCZn;kYP^vzti%Y{WR&uJIuKR%70G!Q$s9mL1)FJtv0d}mj0!gqGb^gNIG zFABxq`why$Gq27?V4$^;-yX^BapA6qE<<8c6kdAfD^t6i+YPihjFmS?ivSpx5Q*28 zENB_eH%R z>W#>l&~|hFBPlS@gD0P#jagSbjZGU$;PW{w@=@zi)TgFJ5g2kCJx+}_K*W3w8w^6% z&bexXY0fR)%$YwPv*wLGyAJh}+-?^Z+%*N>pg_Fw(zklI@%1t0gdQ07c-(mSscY%+ z+kZz&N{PW6?=QrHIWJ=Mifx8PKRpMJVOmskN-8zdRTe2oZhtudh(+;Pi0qv9Il_QXZjTG|7EVTRh!?Ls^EsRJaP1T@_(eYr zCnQJX^`-wna%vkkM$aNmm@*V89pW&5#@V3uE~u*G96u=73kfpLzvLX`^=_L3 zI&Vi9b7yD6Ak>_!!HnB4LwHnJ+Zr$Ee_0qe;#8BKorcHXyBE>%QEe;p43Z>Cm@@lf zG&M9~>5HFR+8t*5qy3X{y@>Loj$IxZ_q}=tdJS#Aj~%R1mx9iC`t!$e=d^oq{7|{Z zza-+BkQ4e)gnG8v=#!ujFJxJQB)f3^{WIIF+ezdrD1=@BNI;9j2d>^AF8~$ckzu&^ z&AZOp`H!SaW?ziWE4N_l>a9=}6=9KKxbN+I&f58pr1;c$%zos0JTd1)q^ zi+h(mfVg(*SGzQ9@^HkbC1BC4$I;T<3~=*p(Ow3(S;Eh^K>Kn0oQart)1`lFeIw!` zamTy&O3GH*DRWkH*b8>&xc}UUrU8VBDisp z?S=F{NpViIwW9MdL4g?=3> z23O{Y?X~20yvGPzcmb~}|UVZ#oQ#t>e65kfSSv8ea_~M{!0lcvT!L!xwLTVEUcpySMFV6I%m7 z$=2) z{nF1)t9{1&V98#5_~PC+J%2j=vYos4bn4So-+(`V{0)Ep_?xLt{+?G=^|ao*tEkxV zKAtXjZr*B3$5uHE<@|3?Z2cFyeaK^#V)nZvG??y#ar_wGJ06{*L1G2h>H87`NE{)6l{t@iCXh8^1u()zK*FFVW) z8rmNpJ9-l9*X%@}0oDs@9XiINLwW)Z9yso|Jg3Ez;1DlTQe$!NqgPs9KYpwdo7eBb zk;5lYT~!BPs}JE3A;`!~MDPAx5E1G8RajceEVnHaS`gR zUw5L>prAm=vV_N;or%OG^9In7LnpBM&+RBbUWJPCYPel4gob&M(jg9cJu;BhIoYx8 zZjTFhJTMJ2raWb$L%IkDk#5N%iJ~aDaxfw(lV6L`4mL|FsC4)|KFJX*nt@>ku3qh{&iA zq;`lyzkyxRdq56C9m*MeZayAaGy@Cgy=JQy{N;}?p5@p1uc5vPfBd!ue=gsOV@FTo zL`4k(1Ko&<4o6~gGzJXGMZbYv5gNvhGjV%dc=E;TF?q~`jvMj(;)YSro{8S3zZ)Y6 zny^AQ=_%Ifo?!`eGg83dzKDSSp)QZL_YE1=y-m)4r3cDTyyXB&50s&*vKEbv%}^Bu zLBU?cB}5{+(r#_D2EeA}CaK;F2 zY*CTvOS>fT_mfBgs)`fkHM$%SQ(SzcEuWT`RiUod`lvqus3@=I$W2_VGuli1W)NRL z$4GjnFMP;xP%0}QGCTopJoEa^ex3hXTYcEPUXwK^E2>d@ssUjUA&5_mME9OqNJ(?v zK)LpYv8bzUzsLT5d$i<`2&&92?7Ykpe8zbu(af_1!V6gU)$n0At zq5lwbgxga!^;osM2!~6{QBhU}U#kxxp+SgEh(vDpPRPyA@M|m0y>$|P`fd#>Pt=IV z4N^J*jvlTsIhCiTC)h@MJAAMVt*zE}Mct_eQ=~sJSCNsOjQgIt-mmlDiSlZ!TeS`Q z_Z-FXBPY<<*o@YeR)mFzAUrYzUGqDkuwM>hjpW+ zN5X@jd6Gn*XYKLXVP4-zY%zyyvIm8E@zlF_A)~XieralK#_C@;V9WZQ*uS$Bl_#s< zcDWE98HTu|7hG*|80LqI%po-YOg;>wAL&k$($Y~8o5eURRnkgaPLdE`gQ(m zsB1*gU)!*6$3YxBcpUZhjc9IcMsP?FLc)WQk&}*Yy>gJA?R@;}RkuvVp?ycO;`>$B z_9OR2@R5VZb^D%QE(Vg*6Kx$)jvhP)U#kxy9kb^fYwNl9G5#cSzj)ZKM{|vy!|rVe({}||bUiu&FB#kjw9 zRu<_F!t=7@5v<-2FQ5XG=e1qF00ud)D3Dw-R3EcP!lJ`*&wKa#b^dFtYs8Lq#n`%X zD~{|tjM}PN)Ss$DXhax7B0`X!(-FD-a?zz%S9lz3=WhM;aN{#KWAVI2oPFe@dPkC6 z6vKdtTo`rbm^L~89ocgT#jCer*ShVfsjNZGi7L3<9)v`MAUZJ`U3zsz*Zz5kcQ|I& z?Q!GA=N90JYaYh#jXMqf=z}D?C@w!-hT!lJVO1xggT0I@FFkI{R}HldVniv9PDsBH zIiNy;3=JFYBw3Q7fJC@r$HQ9L1FcR`K7+jQ1P0*R$LGNvAdYW!MHRLz+ko;zWvHvH zK|qif;V}{DRFI8M1)bsc*xRJR5g{0L?O1&A%u;x~9?V=c9|49a;H}LqC|+5F!#fY4 zT91?(5)q1|tW`Son^WRWkQwvFU0jd_!)92H2YDel=p?0MYVNv0@ z>8*SHI{!7*H=<QB`oI3f%o;UP%P>4>a8U6EPn%paZm??=UNh?0@*vJrPMm&N(AF7yv<{7!eh7Tt(NM z^BUGQ>!P9vVgv&y!GMx;&N-)HW=O*jCg^-QP<+51cu-yDOel zRi{qj%10w#^?qbe8aB*Yfup-qk(J@e&B_%}v1(;BXx9{NF6xZBEmfPhP4CVaF#Kv{ zWtwfCN)zg@gcMAC}1J*-6lA@!t@2ggW@zu`#&epHE=yP0u6iaE6?p2Sft&<1$jOJ03EU zubk(2a$9^)?S6G#FUE2st8#ts;$GxXFma79jW{XnYLI!A_dLdmK^f6+Hj(2_C~gU- zN5QuS&|F-3(%IGCJ@ z505~<;P+X|Fa-@G;6@jlKW!%ZUv+6{egIJJoO5vRGmm2U(~lr^ z{{j3yVKQb;`3;9s(jtEQ-J%$c#y=l}=+7xZd_v=rBCpAsd<;Spg;h;vm!LOdRO_F7b^Ae~pr z${b9cxC9f&&&PrN>BetkVr&e$yc-^Q`kK(FYy&Rsii-zy!lHSb+|~&ub8E;5({Vg3 z33?;%`*8P7l$MkQZr~;+#-T&!Cb;w7OO)UD?mmo9-v1fP7jN?x_JhS}jao^#`~FKY z?3O;pdRC~Ah-+@_gRe$UH`dROhqgS5?`1z$u2LTFj=UF%iRML}LutqH;X6~XVD=_& zb4_h0PxEJO0Km|sc^y3Q{B`Kj+kD-`qt9G}&FlAJ>!#$$?e?A*7l+VjVjONCehE5v zSDoyZ%-@VLA5Fu~?P$_(PZD%4^IU+1K6N8&f7M#-3e=q+B9= z&*i}PU(dz&U(Z$K!doBSgF%-EUt{*k`#)pa)D?;x0xU!;w<=aD7t#4I{pcxt^zKy5 zo3RchrDaB=<)`l!qI`vNxb-gsaO+1=GK4e%= zoEIWr8DseN%Q^UL^tSk2*Z&I6#G(u@*wQK@=?Vcj)w>jDqW*Dlv3T~Cn~m*ubv1H*EloT^!bU~#9MvXHIw+|nTD~9$k))fGL zo4gEPj{Y5~DH%{!hb;?WhXDX{XRL)~S?JuY1s;Ct8g%N`A~3InggAUK=3!iS>C4E; z&iC{kH>UlIV-B~IBB%$GljEl&rsC5PQ$-(f{rcXh2he|Tuq^oK?Maya>k2_Hw2!$Q z7v!cGxghOvabd(OMzEwyM(ERsdne|3zJPl|{khj}L&q*==j-$%C-KFoX_z;24YE$< z5k7!Y&bb|$;+{vY!bN>M8uNPKiEFW9$yV&yk>;;Iog9!!$Lk;5k9rNw8#MWOh4|v* z-!S?6`N+>NG}dqFf{plQ^zTTjRtb0Ae>rZreW20tt(` zt(1@$j}Bd1;P(5}>jwAjI*3v4O~Ud;n_U{^_2$<>d3cSQRq&Swufnyr^fUHD`HJN* z^u~+v^{3MmI+1(}y!gnc3f+MLGggg6)#{Z4^L+5ucW~s;ad3NDkTIWqGLXoBUL*v* z*dAl!<@fFlb+$}RK8jD?nTW-6)@$a6l)D^{jg3X$!QJu53pb%gt>8_;3Kh@6gU{ZG z=N|ZoMDXDe)rh?$_>Ih?!ji{7a=f|TU_SZ{qk(pvo8z{>st(=zcOAs&_a(ewr@u;mf~#jlWK?30i090~E%#oD!9#n6 zcEcZ&7vtN}zas6xQLzce6Q{TN(^mliI-S=V_da<8I&^Cpm{)v49NrxLIBvi6S!8GB zK|~bM_)Dc@7skcL;hDFFhaU7u-G2n{JpVbitlLH8rCZ+v`&_$k@vPMt^X^16YuNw~ zzIZFT^_0%Jw?8x#-;Ms&n7M9fGS)eSFgpOpUZh?F;=-p1=VWx5)T1 z)hPv9zebuTDRgdYPA)o57ipm44j5kg;E_=0zv7}2eEabfO#5LDa7y<`+?AdhnJRx&7b1YV?a05uCJC^Qe2Al z%Qj=z=6%RKk%buBb{&-J)xg2z!=k?!xcqUOS5_Jxt$pyYzP@Xsax!zVdf|E;*nJSW z*||teOhmbgiD=NIF0xK$t9Fkg<*;R0xa08~(4bj8W10Cm`56DZ#XOqZM(L@Jw+;3^JCb@7 z8y0WEk(47SE-FUiIfC+j#E|;5}g?WYe z{(}ja`_nuW6%+|MNX1HG`9xfO_f;77&~=d2i%$RmAOJ~3K~%~dgr?^0o1=A?R;X6H z8ZI2Ht^q9iWf8s~F##uzo>1pU#7^wkvjgsZZ8&O$IC))q+ojmCc89d1ef{tCw@+?* z@)Fv-03h=@8QF)+RUu#Ld=5_NA-4GVICSgZ4K*~GR$5Yu&C51p-{!r@K9!9a+eW$a zKX4;IX|rS(7O+ZI=~g?6=LeB$(S=?Hi`?1h}>nKJUI-@0yZvO zhu=Q?85OHk#E`!Z#h_a+H9Ed^9nc+v@3e?DNJrjJ8 znx`GBw_)PD-{Zixed0G)&bs+?!++UtO8~~uzE>yQ^1_{{($dOS^$#3Blp*YFp19sooJJx02g2FJav^Lu z0$1tZ-E^VzLl-(;|4OJEf=s* zrFK=^@Z6o~93oG9-!Krrjr09>O0*`GuG28sRAy!?;7;Jd9aZ`+V|^@i*LOQ3&#H;<&b7KavY2wnr4WL zkH-zK4i7!7bS(KWeth+F>|4KsS5P>Kayhnn?rQx0;RMue*$`JfbsJh;C>_5B-ggb= zeesJie<9x#`-$4Ej*zDYk?-ZA+xd=|r&4+9z{Lf>K=zX@?8K0Q07dT5dUF1NWr+xZ zVJXIv5#y+y(CfwdvQekjxcbGrL-S18xEr%aPC)Xy?e6-FM_4XjyK-U2qP18!b}FjY zuZezty$+qOxX7qaFMaY>9Nw`XDI0c4eMCdi5ZvjgABkSALRtzl+OxzXK+2_z)2QZA&=+ak=bx;{gWg{Kq3uS~dU&HmN^EdwAqY z<~?_We751W%ahMG0Di*&7?TkJq4Jweexs{(O`aPc!!a93UcbsIIngU>&S z4&A$;PJ?=gi48x<$P;4>`MG&WKYRo`H*Lf0U#4Nv%(>BgX=tzt$A2;wkNy1xBR>Ja zCD&Z(IRKhFqEU;c#`@3yW0rWI0hsdj_ZWIx@WI6fO&Uk)X54sBWQXE$ACK`#GgAwn zzVm)8KClH9Dn%D<-m)w-YSsh~{_SZz__wD~R#t}06DM(C&wgxKw*gCM&&T@ZtE0;6 zKaup+4io+wVz9Tpj z`*mhKI3Hpscr}$c0MPRNI5XgUKgx)M+x6#Xan^-boma{fIKYUf6PZQBJK^E|CtzEkVO|9iN4 z`L579w{71Tj4dSJkwpT&=*Sr80=S+o=-CEMTgX$`nJ07c7Hz`^pFMz1U7H!pdGy(9uz0~1aWYU0j3Z{vDjt@&!5cKtT7G4YgIM@C zG|wrYfEQl7McITK{oyoxHF`Q6hKK}}k)DaS|2`21_8-O5FPX1SzU->*_-f3I=m|Aq zyQ3%kCtnyA>X^A~(KbB$$XM68Da!gK?>&Nt@A(9OedG!Zf9UeSd}Cv6{O$EyaP!q~ z!YOm2%GYts3w%wQ)keQTGU7pgUJ+h+{0l5uxHYPJ@7SJ(2k-m{uYYhi23^+GSk9$G z&UYOE!{}1O?|t|RV}6dq@a)55F@MfRvM5IR$j?`YQjg=+=f-2p#$>$kTCf9^Wm&lY zv8(XJ{i78=m(AAY0Ktv5b<@8hubzG7rcioYICm3XefAsVWEVu0UtVq@#(Xplb7!o_ zM_)W-jMR7EBUfVntPMy#kRCOg#l^+ofyal0j@`>oe}(D4u2Ph-slLTUC73vFK9()q zici0J40Y?fr`)a{)+;n(3cT_2>sc*gnjw-Z=nI{w-MU#>U0qm3QvOZA1R9@TsjV`pWhVxxNoB?Cp=3 z$J4=-V|e5*|3q5KG0E2?!dnnLbvzp%yfG20mu|;9pFU)CP`m$$YcX%eS{yxmBA~qi z^>rMxt#nQzo_p;M#a0?KVk*8FGYw^>G_sZ@A_R#@K@P+5W0`p8rExg0?+BiJ*?ir| z;2}Nm^{3N(^$qH05$VR5o1|a4nP%=+9bVYGJ=%5%UKj;{4XbzJ$$LLQeold)YbcvF zDl9E6#oRwuVcm*t7&-nqG&e-7>@%nbgDLpt(_a9lMqCT(&y1rd@ct{`;K05^c>Lwtja3~qqzA^2{!MKD z%6e=4N;XBCwrq&uk6&lZGdCw6Z$2{?OXjST5gC+7i2nL+T(uJq-SiLK|ICfJX?U=m zTfJ5lJoLh?c>B4rTD~OGrm~4^fjPjCTl%4XBlE`Xj?Me=_-$_xxur&Gq~izXVE^tk zJaPLQ82-c!xa~n#m&M1&;ih}cwkg*q4ym7v{HNlP+oiuB5aBN>sLK67I(5P~I~hE6 zA=r_QtG<~|?POifFkEqCf1Kaf9BD0O?;-sC;SZ6z=U_l2Lw^Ji#%#=-yb#Ogti`LN z9!KXM!MfqRKAkXVXm8B%sS)^yz};5w^bGOVxi+k$D!Fy@W%IC1P$ z=s45Jf82hKbRFn`t9uLFDRVIXqe+8q`1z}88hx17%T#&} zPnNLz!`I`~=M9?I!!03E9E%Ey@XjM2VD-ZFo-;|1JXZOlq^JbbC(OqBrJL~jxR-rV z@_0J0e>e2Jv=vt3?NRAlUfxWoip ze61>9mjAvCEPUTkyz7ParNK*ePq+V>e>;%#}EqenQbhLH)w}EhfgsJ+I#%dj0IL zpH9J)kw5z9&9)`z51O9ZvScG(x$b#9^x@;i{-{>BI<9^627LGWR{{2-L+IsnBIz;_ ziGW5ZP}iAYoX-VCwhy1@0WNtk7wYyw$ogQtX7$nK66t`uZq`bC_53F&D=HOstty}0 z{4>)}V$7o>aPNpm(RFa}f!oCL<#5S8LvYbBd2r>_(G&RKuGerpE!}UAlX(wgz~YH> zkdb;64}JE6kv6(qasd`knC;4kGR_Z-;I90zV=Opj?%|{hukM4kz08Nzj;E&M(+A#0 zdh%fz5j1H2K=3Y!#I5ftmpK+ zegJATtZl4s>ehW2bMG6-&(4!{K_Y1D_@P}X7<=EFxa7fM7#!TCl(nc4aX@?Z2RUx_lJ_M99Yx`+1S=@WgTn820-8 z#)y54G0Yz`1q;SbbtBYydWP$t7{s6qWr#rx98dq8JeZEDe;bWmi`V12*B?Y|0BzZ^ zF&O&leHbs)5J$s_SGRwmXLg7kS9PReCF7VqE$1=ow+(}~F z#?2@#Ek$gA6J$bS0&3Q&g=0t3MJx0)U`TkhzEj6fAS2`|+Msk*m)`hg%>q=aV%~c) zrI?r)RI6D7)oRv2*PiF&&PVcnX8ehqC}YI9xNlMOzhz3w^kkUjT0XX`C$fM*-V}H~^NF zIoPr>*<}XOuE;o)-z9!#U|PyC96x5h#;I-lMj`d^nA>s}RZNLG{}tpH;eoq8!p3#` zJSo9MJ}M3c&(6xjqxXD*lw@mvufX|DO$9y z=Viz6`itM;i&4|z@Y{U6?<2v9;}>GtqHV^iHfdf5wQE=Le{Y%c4QqEa-@EYK4oz^` zkn@dMZCJAxPd+dP1^LCNl`nrj=A+;7)Ax&v`L}3YA6E>$(43D388e%gmkl{zIQwL0 z<>S8FKERR%TLbc82ArYHVR-$;Z*k(dISOBoi`vTXqM*}r+c!en_GY>A^_Ur$KYL>c zADQ`T%7n$3{KH~n-hBpiM2%WW3ZDa%?{zs~iR{DDpOsggx;Dq9SD8;z&zZ3fPd_*s zIoSoIKGDh9y?YMfFE_k{%#*o+dB?}c;?ZZXiL3{R-njDG9;jX;IC2!=j@|TMR~z+~ z2~SHohWqdQ5ZPJz0NhjHL5^yq!;N~@te3;}x*swF&Y+$oP>n$ahaXq!tKt0zn;mUG zes3yPuh;MKSCeA zdI!-lg)*WB3xt-^4ytz;1Ng}y+p_WOtG62KzINqKJbC}eit`^LK*ks*d@~ngKKR*~ zZ|id#p;zCIzQszh8R(6FxpNZm&{Jloq?0GI@xX2GA??61m9M=B5-Ma0WQ<|iqRn{q zsn3o1CnUz>{wJ>S(Z7s9m?3ufFEVTDU$F5m1g%)c=6#; z$j>PN1iy3nXbW7G2QEByG6&B+@DcJ2*K1prg`qd~3zXx5asuvTGBhzaf}SGpVJr3J zznf{GIhuP{%`(RD&I@1S>ra01Y&W_|?3WWR?v^uo+#IY}xXD=6#?9-YW}T`cf`L5w zr}7t-H;?@77NZO=DlEq1cf5nebJn_aq|hZaxyY172FG#m>Dv?V?U>(;^}X!6-e}gQ zp;3-0`oaNX<80g*;+U6pG6&BMe;@gIg{s`LgXJhlxu1`igrC2iE%I`&ehp5v|8zMH ztsj`!)bX56weS3TK+0*Kz2MQ0j@up``{WX2`?%9s*hI-QpCP)3@OhxU;)8B^V7-hBLHoXp5nouEj5)M)@-@f?Ol13RxzClUFsu%HMp-}^3p z7&XOn{-fjR@&g|qo8pettCwBhAMqhkSl@f{6Qu4v==QgM7{$bgjKL`@!-vn0#=h+b zjCo&v%fJYFNL#c6nGb{}YR|$4L2w9Y;hq=nFkV^dI1b)^cm!52SSRR3_9umW998{t zG&LPB-}w*Z<>VV{edjZ`y4TggVA;+!FPhxmWETO-;uuxBnd(hch&JN1Ql?esJZ|tRH4!*M{B3J{fq! z01^3)F^2aa9f39T*2sEtyPZ2-QaW-NPNtv4ckg{~%s-)A0$OxzLGlZhV}J!WvJa7C zUZfvauG&P->UtC~Z36H}RV-u;v0_pMbm}b~rKfx{2_HZC5wbEeefF!c!>Ih?hao*D zxcG#4-1L9^HVMoHuBD=q87Z5GdbQ)tp{ln0`C&ie5r4Zd$Mo zpFi~>N`>X)N4nBYAUF2uy z1GJt(uLjeN!yL@~FiYf>aE{sL1!QD08lfx*h9xOqw0bFW)79JJa06rnK8M%C zo8N>bF)ssg@@71Oo5Uln>{{SXwI1aCVEJK>TQ5Vk5NFNb-}wgf$4n8CD8VDj!IhJa zr-uae>gRxA>+F^I?(z4HeO$e9U34Ed05QCNLJnzi-NW1V3tn?(jNzweK0?-!6Uuio zA|Ii9s5rm?hhhGwKVk9MslokeS?F|CAK$z|&i|#Jpb_+JQ2jw710r}{1&_YCV6-fG zI9!Pc959Z&J?h~zDz*Xz5vk^!BEr_YCDA8#MM-k(5FY_ z@}Ut2{P2x2A7IXoHRyiPh3;6Kp=4QB6#22J;jEvhh2~T#sWO5(iaYgPeTzAG$<}q7 zAa0f-Fznjgt8TtNv_7rcn{N&SfEDwXggy^SnI})-;%1$&Z|9!V{x&FCmW9eylF;wU z!T53MOiWoh7mb>m4+HZQ{iubLu~L`+5Y)>%o6e@Q>Axra`}p6J>)LvdZyyoiI6NHs za{}RmOhPL%&Vq0{e7cl=9{jcKsx{PigQpkU| zsqsVylj6|uphEqX3>z#Ma5Q75ab1lNeoK2YV`Ph$t z56!=Q=cZ7*Qwk}WKOzs8@q0`C-U9CWYjET#0LaZP#EVaSh0>C;)2f&L{KK~lHsue=%lG1Iz zOU%(h3JZ!c{>!-GCpyoFy>*NG4u=e!Q`aiwAT<%hnCT^#IWuL$5bS4m*@~9G`tmB2jq_ z_T#~N?O@6=y#M;du%4Dl)2mYNp1$5zf2aE2mPQd%>DceY@5jZpIrO;>dA@MV@6b6o z8f;->%5+O#@V~nIw%(JBSW|ZWpx11wGpu zhvw~9zQ@*$$)tUe;sgnzanR7BDxr053lF8Oll- z#GnofEXnExM*Q0;O4o}AfrCaa5M03uhn-wlT|6-|?K(9Ro6hRVGQa*5c8?)hmKQOLFS1*<+#Cf&Kgvm(TiF z@r#sx{(c^Q{c!;q?;t%uoMJ`)y3fBH8N8{&Q@c(rRJpI@4`uv1b#I04Jv5dPOzQ(6l11Iygh3>uDqh_7pNX7v0 z*?SXl>O{6uUoJNx^tgXM&NqWTdGkA@>^mG`1QDX|Rler((jl9#@Z`1fzAjh5^*IxK zBAkRwxS&gQ+=Vlm#{Z^7<&dvEfGSrMI<*BITz~gv#w%Qli%Rg)eg8yuW{x7iDc#6O zIBNM^E|wIR;;kn~xz`2U(7`;UGKjx25=YlNowb$q!!;avkMoF?R^V$Vy=hl;kZ--Y$d zHY<^b;C(N=-H5%z`H(xQmz0#^`;n83`PXY)2Q}(c6LNs^gJS>i-@UGhERCr>EA3ML zrLdbIsR-EYA13&Jd&D@FW($zETdL~nz&$4 zcVU;y$VPz6Hy(`GexRR6Cnd!tc~{`V2enF5aY`AHRl7DZo}!Hr%}(3 z#))I6#5_Wr1oy5hbR%}sd4@agzkL0m4`}2zDh~kFUIGYRa=FdvR>z663bFG+_z{-NpQhB4IAATEwgi(unI!px zj~AE6Wpo}z-(cp)k?~WE?z3<{^^410%Zn#@oV*8OyPJnt8MN z!pnM~N{uSSxs{J6cW#k*Yl)QBxei|`>*jGgV2>Ot6T+)X}*Oraq!XkY3 z%qJ)-4UU9o$JiKj`(^6Ctea@+ll#jLz#>yb(&3t~$ z<)9q{x}F|nk2oPt=RD%oJh-% z`rUD0SuyUo2|Y^mv*R9Cn;jxUV&h^_r&)bMx4J&_B4>L2+Vi^ps1U**6JGrsCk`DG zhnw{IIgcDl%X1@lNx89p=5nn4eW|gGE|>N|r5aU*{l>3(=IbAF-owzgS7#&{BAf!i z?;|H7=lCfi_dPxap9e~3H;yyLov47{-v18i`wvAPJV5d-my;al&ePydVCE$0TXhQN zcDaar0*HEYTtqha%8xwV;_^X@3HU`>u;OC9agxrvB4WAM-m3L#qJrTH z^VAKykiIL$-TxtaP>I;gBVl@SKsfP=bwV2pMo&h@zF>!^78iAve9GyDskj?C&r%}g z@p+VpXzt+_ZJ%%6pw9!m>xG3qB-agPL{(Ql@%CASucRDw1buPmNO(@2wzqy_QQUK$ z1knS0eK^J<&W3=n59xl4v=a^UMs3~GeE_fhks%9$@Z%&=;IDB~Rx>bxUuz&9n#Rv`hUU|;z3-!7C2gq}3oDyR) z;&Ykr-reTSoY+_!)oLV>_dPR0$0JM*0buuzgP1yTiCi>S@*~V(8E{zGux76tJXD?o-&rvw0RwVA!s=FV7;c{4UdJuX4d=g$f^s++Z_8%mdyZaZ~tZp>%-;%&&y zE--u_!bhaTPo2oY;`v*QWt`umbvWM#;bDzeixEgK%Lq8S14NKf>sMz++==)UFe(Z_`qap&+l2 zybqPGAoFo?|7HNUHzxn!F4{B3a6BUmn>XyotUuP^>(6E&v0OY<=LV2`T>O?h`v=w$ z0M@MBiC-r#^VA>jSNL$%KJsu!;*P`c-apLFpnV2(LiOsE)o}&_a9c4(1CtOVetLjK z#?#UG+LRst%+bc*0U7rII)ojnzfRY`p6T^ZaFvHZlbY)Wh5R`IpX{ zwr|=ON&}5s)b;A+5c@V*AKvg6^OZ8|S8m7jsY}#I35wHzUyke0wk(vEmSN-@=EF@r z`*%V0npLEo;4-Q=Zfaz*0ax`j=5sv#6lP3a9MEsliB;9xvR}D;EG;R+=kNa*aU3mQ zzXsE(ZSzf6`JT>yx_!d=KsZluzJL}XLf|BF4A-BK{ZAtp3Hv)FseIz6A1Md?bPLNy zOstJ7Z!t#z{bBSkNZWr{&DSQ^SizA_Y#x6~Ckm;5jvYRZAIF-bOI~qPe_?Ovb{@UH z8X*zT0HAVJchlu$MkYRf`AgCdka~wWS-SFFqX#k`VNSx8J;#LA3pZl=#CfuRL9Q#+ zBcYLTr<>Ao@o{K=t~v6;!fDG$|Eu;xkiEn6b)9YiFksdEb;vwnzGb0H&yJD~g`U*v z4VOM0h-qAeMCd}~=T%79yYf9ShAVG3%ema_JWTv#GT|3$mvjDs$H&C3<8(mya^94M zIFxK&a~gcpAPDDO!N>6G?Ln^}!kqHBgVzrq{)m)aDPDg#(g~FC73kp&VW&~K8hOKS z>F>+1b@evV@3x>vH#SO`9KE0$bbl-p!&!g zmrcvek&){*sY~RQSlA$kMEv=gq8@p5~og4F3~fCZZTi^y?xa-tfLVYgaVGJ>Km<(p~|n9B1lbDv7)GJ-qKAmi=P38+#7vjrhcP zX@?lDP2&6$DyfJ)4_%%p`qk17SE+PIo$G!2mPeXWoKm&^5OOQ>{N$GpC!ioZ-*ZCI zB3Ie!`0>LpZZ|P(TN-(@egDSYSo6nnK~D}DFRmADTRO=I`2+_UsYh_)komB5t)}$= zN96~_9wPnN=VtTa?merwWApqqiXISrA|eSwp$FF)(w30{JtxNBNB#&Wgg?$7+DAAA z#n4FNo>LW9G@UQ)VXSY~u~S$*bpc%GK2k5p2-mK?A;*(DN%8AOON&b|ePk2|Zi$`D z=h4Dh&5K~iBeaF*!|iY~uM~K|wP)P9OU&OT|HMQfYwny*Z%$C=16+>7eDyfM?Pv8+ zio@jmhesytHsoR?Ry-##kAkdR{PFI1F)q>`qI@kQ$?4B!WJd?{M^8dQR`8836{;tp zZQpK0UXn<0WSyo;gPO+n9^G|-tfM$uz2r5`eEmX1MzpxI9^8Ir4%SVb4*+mV%aD0E z0|(Y@$EInEux0vEH*#m7-lX#adR@Gwly4K9F$Xcki5=kWb%cE==YtS(joT9OrwC zrs6K0i{_Yd?>im8ux}rHJ!6U&HShm2iIs2y-QT72o{Q&Z=iqpH_?5YRFTaF*=YGkZ z0b88gI=BXmVfs%~0p)%7ub;+fN3 zy9=;k#hTEvPB&SWg+W8Fz_PUMXx^q}KSS`Kn8WP&tzi- zFv7^!%D(HJNZp-pLqF_uyi{kLZNY7zep^jw$ zP^(Uo_8s%%24jh)@n6oC?emOpOXLTQ9>{Y>p94RBx6oKdTzo7#cW*%^GZ6VnX5qLw zsmvwcSD8)1u=K;Y`6wzZF@GN=zHXKInwYxvsu89&n-H7V@ftPplCU*W*TwIDt z-!BgQE>gZh&u9O!7MZ7lC-MN$Z&0VG`!Q1b=CfI$-vniq2Y2|bK^f0xz6?wtp5RlgT-LAwVq@Pa3C=k;&lxko<3$L~!=+JR$Qzd$aQ z!9w-wm2pu&vy=3wfBr1L6XVKpi2NA+-B!!vn>$-KCu7t4U_OqCu`&3{^MdK#mblVz zL-Nz1Y%c_zI!ZZ_^ONK4k0eR#9}NdH^Eox9`AK-4o^`tB^x!atT6L@9d_&~M($X?a z`GH5MlMz$oW)xkNl#>rpf6qB!$-E87$_$Rq1OVstXiNG-rT@0=o1j6XTE=g``*Ic- z^hl2Zk%Yu}x;Two;dsvvrDb6GBD0ft-$7kf`9j)ldxUI9Ne@4MGY19v#bn$_ztV^m zmW)^dLN~nJb*pw7%cxziI*}`G{XpoD=SL$hnvzn!qir&Z$N+vhbmG`4eE7x>_;&1c z{4r$-RxH|rUE5NSk$wufIr%uS_XyFu9$gYW94^n@_xJ5Sh`f+%#~L-Qqt#cHX`H0ElS~C&w#$eB?az>9lXk**@J=nTF_==gB7#jnwyg)FjWm!rj zD;go^yo)**>pS!3r6?^e6LOmO7nJ^`<8RANJi=#7=B&lhL&rnMQ5_c<0ZWer=i?Jc z)jK=In1v zxen%De>(Qfarj}>R7{^Z7c1wj#mv=bwv;NtMKW5}BD_J$Y0arj%0U2%y(an3(d=R>r$mIS2x;~YvyhjQ z2iulTI@}KD5z&Oxg=Y^4Crt1CCg(Om?XatKznVt$lxM%#wqg%SIYso45*g4PpW}xz zuxarIV_Dq?o-gfgB1c4IL9hG^FYjf{BYEq7>{+*qT$@Pjcwc|G2gZa`7N=Jp$;q}b z_xqW~vJ%UeLz@fRD||uoq2&m zeho>5c?DQCZ7Io5I@JWt_rRo6rwt4HHk#LOd0P6@TsL1xxxwX-L&k~oxf1cyVMyL& zE~85AYRY)2*EmD)e}guSQ6uDf)_Gq~BToGu-`bo{EYB@!c|aHBKE4w|T+G ziP}GW9^|RBsO%eJztV_rzC(+ey%59Y9lYk+ks|;)%+cf-jRZ*T=x`@^st=UwZUvvZ z_3D1D`QYNx@qeHoJ6G6+l#jXo0CDOy^^5#t-G&b@dl=(~{{uh2^eGmN znS#=iQjbpoAVA-MA;#0UG;$svS4METu3?|$)6&s-81~$gr9}~8xRIuPyg=c|J#YK zv$(jN?@1hI7?<-;I?tnao_`)_bSl*B1=X2Q#ig0yO71XRf8X+6hzKn|uC8Cv^5{I; zQp*XzmHa6VY1hND5X%QuUqs`R(_kDv2*dBekcC48;qX8I(-47p<*F4zSs>$Bmf8*k zLop~moEqZ>2m>?{uM3YKGoMZefU4Chk$j-#1I_3QPPw^-m^)*GXg}z97FSAZu~ zso;CxCr2z@u7sr1kW2J=-vQ+&~wHvv)1ySgwfajXNn=^eKUVQCVqb!8pzKD8PHJ*ZS|L>Zy z!ExOv5zrE=DgH$wmA*`LS5|6XjI7t7dbmD>G`e)0iwTJ{98Ap!q?6Eog&TP=!LK>%#*n;Je7-*k}@OT_a4y6*sqx< zbFggTCY3%I9T!W|xsWsR1iXyYB<-M?(^lf5{=sFoZr2!9t5?R!<5@m_a5NZ}iVBM{ zZSoRBeJR~ZXAb2($nOs9GcU*q{VU{XP8`HPVG& zE;7cDy5HQtm8(?r+UaQHO(MD27JBsSY?R@9ccx;?y4~bjQ3$(CtyggUg5b0t7ouI4 z;7G?8T-*siel=5zs79b=p;_yOND4WKwsh`V$ju!;ROwB{_3@oIZ8>iF>s7|G_$E2u zR8=Dqk?XKXBp^kvLGp{dl{46$p!KCEJ9T@-G4c(B-i{=v6lZRw-kMw@^veY<$9oR! zX3T%?uS-#wSE$gTh?GJ4Q$)_7&OgvMUm5ap3b16>DhwRjJFvX-FYbiB+maP$JwcZ= zvR#A-FeSyMnD+f#@M~bjIP3il9nwf8d>liI!rpDk*uHMJN-v_mwpwokgk>QoD+j-R zJ4?!Ip2+-4$SqwyT9z+zmEx33_<#TQ13155*TA+^u3AaZg`?RK>ioi6W#KS1NqY$d zDte97yIr?7s8HFwhBa^MB7bBtk`Lc})uCT*xE;Ir_ocY|h2Ur%F|je|+_M9g{IN`% zCs4Z>;qy!{&7CwKMFqvAKU8~B=zAEhx8vgWrm~tGqro zJ?$78wK7MIU-Zi&L;v$@jcr1Yj&KU+a^ToO^ZHT6q>2!cWC{O>d>N%{GheCCX+$=9 zU8?P)A8E&%Q)x*l=1iDPoE!8AN80$2exebxx(zg6`JHnr8=Dqy6wWY;Trt^W0C9d$ zA!7_H|5%PL{etCv<8zy!O3fr>W@HL}Q6fb#hJ?g;wCfqN-uCNaEy9$%?p$$3vi(p* zWaIkEVMyJXf}_cYQM*aq!16j@+y$E!tdse8dc)`R#J%(k$?aEScX4|mKyDyNj)d61 zs`HYhF9m{@>y;Cd*3VjrqJkn{J%XJLRVOOO928{dp&%z8NzY6H`dB^@{{>VL*gYC;U3g=#U&WUg&9asc93Y1Q% zjNt2MEJypk!TeFbZ4)HbsDjMnr?kTrj(gp! zSfypcIZ--KdQNR+=BT_?8q`+oK{-xpBtVHveeclmf`y{OLKGJky8Q@uUB>ndP_a(w z{vqZ)bet4=6Nd}9)1fCX2!!`L7!#2-W5_&7k+%x&UnkaK4AEb79>SrxF#|!+8$TW) zP=Q{*(RnF!dX?+MFhG4h7;d+E{j8YFfe?QZj{S9_8@)A(l%1J{SyO+*x1&BsT5?M0 zyA7H&!ezsTqWeWX(5!U}RIXYjqAexpNI))*SRnl7qcIqI+l|KDDpab7N=a3aop}nh z;n&=5{$}UqtqAL|!zpQK*sMw5x7XZ$6Q+tNf%3cNt$NhE~eDe$Qev}_ZyFbC#J6!vbEHe5Rf#{|f2o7K6;cHa1koj@w zm5t_k1RSJ2;Df2+iHY%{ERdgHB#cw04<^A8iu1`RU(|>Whk`@HtDeOt8496g#)48Ov(gylzzagh}1t_YF)Gk~Xg0D^AWw zV#?q}REZxunrSSjeECE{uc7&Q{9tT1;A49AdmuhO7VSDVHs-Np;g-Pf!t?bD8~S|Z zGIKPS#!YJ>DXBtK{TP-O&D|8$xJaneg`}5^bn{r9b53H^^m!(rU(Tw07Z!yoscHp{ z?*rtB?|Dc%4Y|beR&DE}My=rMZ;Fdcv2@`UAtw=)9QDgxT`sR#ZjK<;{eo5oJs|Z& z+jeIDSifes)JwKT{|XGzTV6d2@{6%?ojJM)K=o}HJ@EJFwHhn{5774rHeLiB8lJ00 zQ7{e#X9Ca9D@1W|@Cm`BDizRgP#5sXw2D6T#+f^XfsQ*gI^f15=s3WDL*Vf6i~5>3 zZ5GVg5Ml&FICVgw188*PDENw{TaD#)>ed1=v9{ZxWZVIWa%ufdNz_+~gyYtqZhDCb zF(HX=;^@%VUhw*Q^aCDwk4AQJ%lB`>seHuiuWlyUO8Gr{w=>pz)zWQp5#P(-4uJAR zHPrqk?RxRC&v0Rzd(pe&1GxO6r*PZVf5(G&{1bnBVk~C=zKZBg)#<6*1#OJ=TfTS; zzyRuW6U_IJ^|Rz6vR`j7tX{TV(n~<<*rhobW5lV)t+yHhA)FpItl5p+oWcNoE9Hi5 zSt9bEzS$@F(h1kUa2G1Er!&ffa z6grl4F+xPH^VQpubmK5}9084Qn8r8qeOhmT%L~cZJbzm&-xIK6yj`&nE~ni5Z9iS| zNPj#si9;ilSi(8SvVrsZm>sy6%v|l`8=K%lE;&^Hsu4Pr$gzxJ!}4v$@;Y5`u6ms? zgN)Rp;Nf0SJ2vh?PG*kK0}i1}#my9%!UQV!!eJ%gHv&;Yw91di9s~9N*NG_KrG#jkFoB*gJ}x zuw}&-kM30c?6Zdyx>WVgsSLBeDlI9+ z&UHIgxk>a1xL)BAxVTfdE#=0kW9Ix5%O?uDmJu}J(X}4Q*dcU9>}(Kz0OflUflWjr zQt3yIk2&qxxEpz=aztb$nV+=xX+&2Y=k{ou?q*uwykrx~%F4vJ$Ov`Z-qP};IHXfM z4;X3L7B(;27}~aG9a{>yqF$5i>Xl}lS|T9XJdf3U0 z*eIQaq*IVQ>>wg=DRgK~+m~-q?Gs(DdG#0a*74fM7%VFW%wfnrZl0HDbMA@T>uZ7-rJ00=vP;$Ho@vm$qPCXxSq^vU%muP@gN2!_hOC09=b z3j6X5!cTts2Js2ud&B^cla-CfZyAn0O*`PtC;x_n$tjAQ;MJyh+Wr)befLA$bLI8u z*P=7-zHC^;a+LfKG@npACT;%#WM_s4in;o>>!CNi&!vNm@6Y>fhVOj}*DhUY{PyC@ z20?a&E9dGps*i+niN8vU9u9zn`Y8t7_|WI1LzdSR-h?kM@hr7lnc$BRJ%s`Eb=J3&yP|aZ*_y zacDR*0p^3M2+v6Q4UBvBpKz1 zSx$B6+|-!ew#^4nQd0WogUGJLow64cBh#;I9aOT6kVFaIRJqx`TV7eBW+!gr{d; zxd{y$)%2QA=*ox7e1)?=A6FgM##yxo;KRe{cKzzzZo7#+LUEa&YOJZaddT9~(JZ7M z2#x@dkPwGPO=?TNB1W-N4`YqJaS-)&eD&ogwN>1lEJDZbcP=g?=K+4F0FRCc?sq0o zf2ZY0`%abTVzS}=-u3*}#(MAAnxY!_Bwt6Vzsk3=JcxSbeXw z+YZ4eV*y~(+C5%<+|77f%Qv_jzy6T?Y9sSx4i2Z8uRm(rp(&_Sieg6)ehEukH|ztA z0HW7}_pfRnQ2hphhdErt$Hz(ihyCT!lueDZwX&Qz3^l)=(A2dQ%am>Stg^sC||;NrVB??+jgc{~+)7)ieQ`U&va zt%C01t3T~WDK|7aR<0$LbO}ypbdxwjQX}1}bPgWz&qo(-y&E>Ghl-VhuNEsSEyIpY zdr5tnp}uN+gudgr)}QN^njJctwr-?sLdtQomGQ76IPb1swndd=wmN=L##6sWk@?0u zEUjC**)LZuzZ_7Ul7t@Boqj3*L$PzEoL231ecnp@TVC5N<$7L-K02p-IWJ%7j2>j~ zZ~^cBAKpJo`}pRwkQX+ouiK7Ror9yq0>JilJB2)CqJNm?G)m8=}zS%c~YCo8HtCb{oA~C9&$aW(ANe%z?~$x z9a4~Io`5M4(O1)OrLj4ZG(aixMW&KWtpJF`yOyvVW$3^1u`d!{$FLE4X*tg1T`_^jF zfY`Uhe)H%ceNU?E02h#A6XI~)TMr}toCK*S8Mo8jadNJZFy(r zxV<6ma)|b75mdPoD%a<55JTmGQg7`rD_wsgIJfUX?MZHC2mCG;f=&}}-nJEb4(w~B zL7vtwTZ!IH+GF92*&-jt^CQ{nIBfDW%Z$Jgm^9DY}5{kCu1BJET_d`EErr-7?U$H%-i0#|qKgKpKD;Hwd% zB05k8rK@g^-kI~iH3jt(3&8b$jfTx4z5kzp_#egpTk7%u3jXher%U%?{Dctv-;+E! z&FxXpb$UO}BQOhl$HT=uo8$bwC?E^NEr~M&4)}3ePyU?R+hI7L=jpguHz2sO&O_D( z<$kMHj{w}a{^S}$3eMkyuYSSMrpA&PTEyrI##uz1MTyhJDc-o?lktK{EV5W*!Vw8) zuw%?uFaqv%Z}fe&yvVp9A_b>|3Ik3jMF_wxl$C@}Xk%Q{3^ZR_9raC3eA@56^jvuLz^ON#IP)5GOS*TTYLV>vM~`lc-x z9$I7qZ!#9vZds4u@0+%$WBfipuLuYBryI(RRIi}tZa*AL&kBuH*SKjNW4|ej?jdRG z<^z9foPy}g6rBtJY>%J9zCWWl!uhVG#9UXNg7`iRH)mwZk6bgP$St5%Tl1#&{=G+( z_CafJs9e?MFv0iiJY+1hN%PtUIS# zSzd^q4b;>$W@H{_Z)Wn1H`C>9Cs*k*efp-cj;#L4)b;1O`6v6(-81sMp)UhwJ^LY+eW8ie0AJ`9X4k=jLRW?upYOs+Ej+B=0^*_=q|ssP>h_Y2*dExxtk7><#0y=Jk~NT3Y?z#?Hf7&ol|G@- zr_UY&6lVqvFDxv^!DRDQ+cIJUwFihi0=ahF$2anOGaqq!)#y}2uv8;hh;eYJ{`5Pa zMckYP$oo=XbGt>Cr!+#SWm#y}#(d4^;nZ{#7ZwY85Y9Nh`pSOe^_BXK3Ax1k>tx0$ zWStC-W>UFoMO3a<34HTTI?*^n9|?OYB<yT)J=8%y`m4@>I$_b><3%xUz^Y;(v)BU4E|!yJajEgKs1+Ph`H+kQtI*MJC~@SH@|btcNM(#}ra zVLsGXziC~wz9e>3SU>FFegLJ#WrPp(NOaQ9)a^2H{VyQsEG(t$PV?+D5_yj?HxQ6V z57h8<%^Dv0#>GpD%sV6{T624Oy0I1T<=lmAK@fJ<(d{z zjyutTNjbn_T1b5@+d}Qeb-@^1XAUCog;N>VLm~Vr=**FaepKf!G9R=wzP04xFh?1O z5PL%CcSn@(iv({-r6r{}y8n<{t|RH#&DS9~k6b0|3ql8_C1#uS(C$>FzC!KRgv#&ul#fjIxs2$$6w)G~g1EcuA*v}8eZR`Mdg5$8mJX@`l3{R0;- zEiOSuiuvHFekeui8BX8in%S_VtWP+8cv<4IKc=!B)QlB%6jp7a`{8oHNuj- z#+}GXrJf9RL!33!_ohKclH>YJ)o;AM+-?MylQ!uWruNIxU1r*<*|feQC#2Jw+wS7L z0;H@DxqYa6Tio;Q>!{zprLTMl`AZ|&ae1rCXP-U`*uRi;3s3$*h*vrP0l2*-ocY8& zOyb;m&hqqd3~{Q|?Q+?ljOY_}9ui~m2#0d~n4l+O4}e6X_4I=q8B<2~ zB%IpqQ0;gqxZ55D{}Yhgfx^D_9`Tf&MFF7TmyiFgjm_B${<4d-Gno*V^1F^%;yoC zjwd9BM=vZYDp8Dm{+(RpahW149Y{`>OwNh-PvLI6(vO(0Gl`3jMa|k(#QP>Fq*Oo( z`_7k7NIG#mC+d8Hp=`HfIEluS{=SOS$z)i&^+ilSY|_lU$#^g|Bf|G#^$Kn`#}6Mo zVJx>{lNu4emr5Zh6&4husIcT;7$+5$m4lxF4&i1#AvTdAjs-q!Uxhm1nYfJ=ze#+if&#wY)<7=;C9 zy_;0E0$zXrZcLr|8g96?FRIr_BKlW@d@(@B5eW~5O`4h)rHhJ6aQt{Sgs~~?5jpNq z7T>&lgK$nS9-fwJK1tuKMP2{+65P!toPVWJuGYthha|oJ{Mr&dqMx5mRa}41ANdbn zq%@U`RK4Ru2h5F>Xh@F3kdb~0KK^7h(w-f_2WEXLpA)Af4|nLMqh$jQL*wRkL;Lk$ z$}y=QAj{XGsUAV^fiWCTGcQ~zICUZi^g25ZgY>yFSqXq;5ggx? zYuzj>Fv5)(zp&)1Pqg-&99MZ=l#FyhaZ^M-CEt+wlDQiq*=5|hDe~q#V@z(I5vPYB z7?w_+$Wq(GwDxG@tlN=P50{mhZ7?t>krHJ6AYd4u6&ID@HA^oUb=P9n+(>BwT2$bG)E>BJC4Uy)v{&9n7kWQDn6GEd9x;m1UVW3MrUmkgs z>Yea;#o<)*p}B@l>k9oNBY=64hKLhlSUT!Oq6*S4)R{;^ebZB=M^ieHehU8|d+!}+ zM^)_s|7Om)y`*>2d!>XD0s&tHq$z?tKoJ$i2R!u&R(y6Jiu&La!HS3q2+|Zp5EN;l z_nH7nAidlqH_7egoY~(Wv&-6hcA0x~A^Lr9-#^w}XJ&R?d#$ziUh|ut?jDja0P4p` z|0eY(VE6|#s9gC`reERm7kGar`n(gL!(01^&u48PYcN>oj0**Hw=WB z`ayLVJbaKduTAT=ka?@mC(wK&?r4FJFI`_+{O+wD-BH(Ahe5*zIqkbRr1$m1*7aNE zcqV+j1!Wqg>z$O>J_AhFyROvI+@g)U40=hFUjUnb0CnAkGz0*xyIOUqXPfX6(PPD*_SBgC5tpyWQe1Knp_8BCgW=V4X&G0GK z=uWA96jaO~aC@oz%itFsAmrTi^V0ehDvB8D3D#`hv`x)lQZIqbOXsiHFOvI4jOb^B z`o?R-lyRDVvCXf?uKkO*-J_;Yz&Y3d4?cF)*D?F3Lr_^$4W+;7OJCF=YLM=d;Xi4ZjyIU zGfkfEMsO8LO=VLLxf}lK^Dm;S(~V$TTVID^BZtfTk2v-yr|x;F^xr zk$IA>R(l}UR*HWj`hV&F(!X+YX_382`=b}cfAVDqvTg>W!Ek#@q4^JrSNk^-?8W5C zK48RuvGc-y5S~!xp|DZQK9H6#Gbz(}-+l-TO0R>`v_n##z`-*MEqW_K-t@|Ko${j7 zG#-8A$tI29`5Ps2F_x$|Ug#StVsPaf!t^?IdddAg-F_6U>gr16{EEdD+4RyEEl^H* zX0V}zreA!PRs}v^+(i%xoWkZYyUjaVd2p3)^UkE?&9)2=Y-?)q)_M4dMr_%%L+elT z(80F`*Prv(*OyvbI&=Boc!}a>MLh?x*pK^*vdCF)?%LtBQRmoNDLp*h{i#+sH?9*eM2=0g#wDj z0t$r~#R^V~idNTDx;qB8P0#+%2SgzH*q4v$pHZWSIJ4{O>_JU!m3DttZ(8)K)Vt6& zi>Y4Tv8@HeM`jPkDk_WEXTnIVeQS%d1H>*2AK^Ys+u7WPQprUAQFh5{x4b;CWs^Su zP;Pd}{BzpjKFhsJZf*KJOU-^1&I=sZw{PE#OTKjjzWsxbdD}mE+8BKKn;*oNzHtU# zd2tP%cyu|Qe*AU3_}ohL_LjLoD#QmaIK)oSP3&wx%KORnkz(jEk1GTl)s{Puy z8*&f}M1h$2D};{~;zg1qXxicqjGr`hjB<`C!UdRoJ{SxPLryfR#$O;i_>y$OS^B1< zc(Y%A?uZ6uqs1%g7X{M8W_og`9wTNm0|Iu#OwhFD^ok0y1`*Dafyo#hT6IfzdqGB4z~r25?F zZvjBd?l!4^O?@0&>!scVH~S4fJ0$IZKJHFA@m1P9EGG3$2=Bh0rPNO?7GjJXGZf<| zkHMIIhGX!Mdeqg|pkYuQs;VkbkaoS8(n0}oAx5znqfjhhf+v_2u=lU4zrBg8NqHzt zKNBSNnAr4pr?!@M&0iCU8LIybWFZ3qMcAEG?nCt@>p&Vb#`zJW-8#H=cROloDoKAi z=94+^0(vQqA~bDw+w)1&#*%TN^-c2|Hp1;IZQr&_J5;ypB~wqh`aSKpCXY^_Ky*9^D2BXB0#+z~}5ZhG0@ZgcqHZrkyLceD0K{-+7#SvIJ zrNc+L?Mp{nC#q^H?d@SiPkH?-(3Fu6iHIcoN6ibttIv~(`l?MZTiU#St3|I8f01qi3jc=iVtZ2i9InLcG;I#kzGX?i%-zbP#g)3w)-5$-iJfK=Qvh@Qkh ziKM zEzPZJ{u^>45M*3w%lfSnX-2oRZa+~&#FBn2_rpA?v!g5R4x-PY>3`F|;BqeHE1+&V z+d7q8FkKGlJddLoWunIfUx@Mr@yBtDaygNRcX1r0daRHJX%*rK9D_ zka|`=&$4DJdt|zOl)=539|4x{jj$0r#Vaj&i@8*V*2QuC@+Yugv;}k$Fk& zl%Z#B_P%+GTh<4U7!3J&Lrq@N@f+fqpUs=M%W_43nCn7)9a%t+`p#s~a6H z?Wk?Ef2w})$f0Sz1u^agB;|63_wIucJzFTmD3{Azj#w&hM{9>V zWYOeI`Aa%dSazNT`i%DXYFq&&_wx1bl)WbP>3Qm zZvqQ=-5%-oCyrt?4j=6G>q=ccsIINn=5ImTuZZ*m?-%}AVizwyTinv*maP#}#_9H- zuQP;P6rvcfJ@_nM{PRQD_kDhZvB`(b!{kHeq14@jHBT?c>L*{unx|gHme<~L`bDZd z)4w78$LqtuNc+gXU0Gch>!K(UNX${HcOr$Pm(KfwDlfz9dMu;Y=x4`0_E3(WsJ|w+ zM6MGuFHOBE=WQzI(%&%lLFM?{`cv#Hg2{dr{)0pU>SvUL@Doj5(3-kxGz@ZoMF0Sr zw{6F>k3OM2N0uoW9;VxC{61yo3~zY=>^ecqR}@TmFud~Y3pnubL!J4aeAWkX^<`He zLWK96@?PhC5C8S=kb1{MpI!C@CQhAV&Ex2kPsFmn-4B3a-opJ+tneT^zrG568rVIe zBDwkRf8lGm{n{Jxn|uF=%Bm`7y#b(MP$L=|8`0X*VhOHE(>u{B+8P`GuasQ<7vkSB zN~Yi2a(!==ySJ|o5Dm$NdELeTtED`8AgeFbFa0}L&eAkH`a86BQpDdH(O(`T*Q1Eb zOQfx{^z(K7oR{+YeuLuGOOR=W%yYdxJ6|{UJB)o0>|wu}aTBgM3h?!55i}FgnR1W~ zUS5Zusg36{(;q$+LeeRe7eWYqGgy05ndg|w7`-m?fpIyXZ-#%Hca9-a|Cr9+4L9n|*rZeC7Ap4WL_YTnUG>H#SqvL6L+$c=tK<$>mA ze*j>0O{I3f%8wD={k-``xQ-|kY1e(Jt+j*91O55_@V-RvhxK_X_l|bwcz)P$x4i7y z*%l}_R4;cww6=6O%N;zlA^5zn-R-SinQ?WEgIg}BT>0ffmrL_`o80RBkf9A&e8e0a zbm&YRbm&ZPklY?8>%6t=0l~W)Zr;x8Cq_nVAQe(D7D^px=O^Kas*Ctkyo zk1WS4FTTZ5R(U@Vd$0TpruCzAZkf=}>GhGL+?&!PMh?QGFI^d|??9)a!yC16l;ej8 z=^MsjdnWZLbsjZ@rF7UrpYa2fylFvmpom7goF4nWMX0DK3`k$BeRB(5eqnXS8BYTK zQ0wk?NTexEj?I46e-ABw5@zH>kYpfuE@#6$2Ohh2wkkbplxv@lD(F-z-?YmC``gf1 zi(;{Wa;eOV)%`7m-9&V>b(3<{{E7`;=_zFHX9>|0o+J+kCDIkmC)UTumytQ17Cq@& zoXK{9@(lYPI1>jho`ZuAn~OOMrl7*}x#vAg?)qls#NVHCJQ!0*G9o{!9|C@n^uy%% z8g|O0WU;O62L1e$ufEyw2BE$Hz+oc?CW=fmegk?=z%dU2qMiN8D5if z5)7r!gWw0+h@3#zo1|Z9u%QSdV*$B(*9nX8t`io8%OBt*~3z&>Q!lAc)oz{-$2+80FqY-fLS?Z7(aC^4qURV42n$v z03ZNKL_t(9_FFt3^ADblx`x^T=xIkHezCp|(ANhlm@)Kng+2}`Ad~KAiTN^QxIZ|t zbyurZKSJ7Bf{?^i{bX>Z@S}zP;Cy>aySLmF0dPI9f1=G(VZVLr?3U&hvVNgdM?^zL zUZLBw2>g3{?$pWmdG!7v&1pS<~{-(P*Pjy}dL5B32(xbwzLh>?5 zh|K5oaGf8jXZ)zFy%6gQiP%Ph>ZCk5Qb%hCM^EGZm@d#j_gmDuOrOV8UK@SB{mxcZ zZxN(K^_8a21)u2lG9jOrC3>!Hx7$wEHrDX~MUJ2*=TC$v2I>blxa-W3Cv|^2(&uAD z=cm}jPn7(}_WRpC0f6;G8d9W19^5665n=M4`6>EG9g^t5*IKX)L41o4X~9&Y-(!g= zsr%JYMDycv4omQ+XMS|Gcd6%u-GW{YMYc$((uZJto;XVYx;wh`@so#GroTk?1wrO% z&`*zK(49p75<%|#`G%pMm&wAvDhD0dR!!`*CH~W z=1cFQJQMkk@-lc@*gZ{8nd>&CXX)X(L}KQ84d6lgj3PS{WNwyTPoX%8en$fF*+jIhVLI$ zgW|ugd4uGB?Yco*S~}3x+2cgh8qNa(gmW(rFU)-CCb^F{Y3RC-4jt~^U;==q&CR5m zStmcVnBBdr-PLUtny=oy-JZ{T0jR01Qj^t@k5AiblRZ$MQ0nV=xM`@I{Qc)_caG;9 zJpoQFy9X9Yk5YdCLd_nvJ7H`<>l<>9`nEyfRTdpG;6fn z4No)7+uo|GinlQUqW`J3z6zS#M>?pX(Y+zr-qywc%Ws!Lw6~O?tE(5)j!(Jkb~J|_ z-fPr`M3$xc=2e6reCH2n-qDKlFL+-reJoOCWf2PxnSq6e%)t4deIMFdJMq-xui?IX zp2I``co7}#UHz&bOBm|v-7>JZQe9m&G-MgZ)SK#bv)+^J$baO`af5mjgL<<*j&&;f znY>B=mG68Q^$i2lueaU!5MFwIwYA<2jqZc~_VzB&&Cu){Jv5zVT$Aq?#u1Q|MnQ=Y zN(hR8NHe+{DJc;Y0i`=OkZvhyCY^#vNlv6ey1Q$mN3H+-Ui@F&FSmW}eV*q&_c_=1 zoa;F;Yd_06eL*;fH*F8S8LGwoYa8gtGqzU1$Kb7Kl9}IhfnLbysdY8A*g`Vz%bAZ0 zBf}?AGHDDRnUyn3ksPJeu6!k1>smaD>P?Te zdNMSY^Z~#nXd$)%(vIcm?~VM(cTp&ha_8NNI`G(6#ii0(=||l^K*WsMCjJHa=k2|9 zcP&{&Jt_lR?UCb%LYkVW;{c@FlSF+I(c%J2L_(blkn4S=ewpOxH%)tFgb_cXCT;c; zOg_rGU^nB%=kf%+Ph#Q=^vP^U2=A$cS7UiEsGQ)=zgK6EXpex9t7F(93G*+X{YA{o49^_2ZDL)Ol^C?|$2l`~@HK?Z+|UrlY7c#ageX z*6W%=K1C}#wl}^__I>p=brWr+pIsgG`ZHjToXkE^ZLhfi4K2KPi9&yX>sv!}OoSi?0vYW=|TJ-~KIJAgcTa{iz zLe%CiS6v%%To4S=;PP|nfmcI=#x_)uQmh%?&=cK!M!(4$A2o-Y|YMHskz_RRlgo+$?p}c zm&)!{TiyauMxKIF$EzM#8#1GvkZ7Gi|4cmB@y}y{?CD=8%u;h8-qD4lor@JH|4<(t z#l%9clMEvb?-*G`*It2}zMP+6kd+b3f9G$+tIsPDmmxa}z|XFrKKSs`hyc zBnjTXbhDxv59Ya9Gt>aWD@kl*)ckE=utS6EZraj&V~1%69 z5zmPQ)vYJ=e~pl)?zXW%FB3vu^8z@ia@|W-^9@gtoz2oafq^Ruko5H4JC4o9u9AJS z#n-tze}pn#y)iZlrGy4i=t|mYoFeFm@~RQj1*FAt9wlNJBy_Z#-Ovw>YRGIJAOvc=L zoQ*VFYUzz!;DPQXm6%z;Kiq$79|r5?Q_1%Sl8P0-3Q=84}tTf?$R}_z6by>MpMovKPmo8=fhvPj1oSKXkZ|lSTfibH~ z7Acnumn7*Hic|huUp-fK58~sxSu|SS-H-Ok{S{s?S;P?78rULL07^a3i`Xl8WWxhb zg)#e9R#y_6waMu*1gFkrT%2FxQnBU81lEPuUal@Qc@q6S)V?W;^bRvr`c1Y#fWTGjhTkp zbBBct?1q}IM(2J!SN&MR{T<|5y4dRE05mm51D_bd_~FC4@{7+}{8I)^Ps)hX`feZi zRT0ti+7VHk)5sW%!#t6cwR`cfRy9a3+4C1%t%J1#RM2hCalcK1>ouw?QdW2={Y5SK zce7!FpX49Wq+qfaw})i_I5y)Ry%gkrN~+ap{AW5n^B_X4%FmrD=D@k_bbQfv;6FKT z_YIoS?#{SfYWbwAXXFA!ulO|r9~(y-+R8dLjhR8{@hx26_cS|7{vF%+uq(3cP3!SQ z=N4LNb3S|00a`IIFxH@rz9+5Kzn0vpLh#)(U*%CFiY2=cVA`ZxnkU% z9)zJ@ZLXfXYUiQHv*BcQUH-o*hvJDcrdR*Q8ajtoLkGfWC6Xbc8j@cqB4^6$o`;_T z(lNY`o}f~Nsb7|T+{MM`Q9@s)k%(9L@P@+%LMRMEN40DUJ-F!}-Q{BT@_%{xjg?dn zJdCkZQ2F}iDQPq}BN0N0i`e-!@x;TlC3NOkD&ZdGGEjdYkWN#!W3`k26j`v4!KW&G z>fz?9=lWEfJOgl^Vt&sW%%RmQ!DBw8*(q-jf@)B%* zif?X1@?k?kgQWn&aP6Iwy@k04@WsqAz0d(3+B*UQE9j?zqQXFLl!Fgb^9SLLyIB$nPp;T;X{1N zaB7ghK|IqQr$2jIb^*+v463EA){1i*A5gS4EN>iO& zt<={lkPB=Uy0*8odR{d{S&fZM<2Ja}-p&uu@i88C4p;8f-l4_S6uONkIM0~_i7W*l z2s?LKwqH#O$S{xRH+`CC{Aggq=W)7QYq0C0nUizdaZ&6xzRMon3)B($!M;Q4;f1?j zA%GP~H~|`opg+4Noyd=O{v%}`LmKX*@*h|39iRl2r}`NaJ~2|?p5=4sNeJCo)CtQ& z$$`sX>k9t;pm2rDV2AFcxVJYu`=ak3SRaZHdVgB9YT@H}u^<#QW+@h1cmi zC(a}+BS$OnkEj^)D10)m$n__I2ZLZY3xtg;tQ~ap%7I|L;h~*qoDTyhcbxjUkKxQJ z|5TqD{2qaHQ}g$#`V2wqqbTCeH1=$oPuRdVEQ^!zOghSwhn{+E+v7>FhlgB&OtcI# z)gKH+jG~SY>o6tA}e@7Ld^W2H%r{8k<&X!UzbE?xsSB@|<9k&E}td3ii!z z2<2OH2hL8KSALzo*K^aY6#wNjH)161qVRff9^HVe=P`)P5Q_=h$d-qEW7{~LT54`?-O-wB;@HDnEKl!XQfbojK8*6th z>aEN_WyH;{i26|BwqCdK^~-7|jhqFPedT^sbcH2btr3QxZ(xOAB7 z328ylAM}(mmq>CzkcJxJi~bz0JPF#~2HYi*^XpbAC#5P3@e!XU)n2nG0yON@KSgWq zcx3GxZ-ULg3Tbb1TuL95J=OrCVH!inaKt~^B&2Oc6l|&Pp54c}%yCxG&&`ft$;^?<(X8AX$XpNy4i1C;U7Gv66HU+`W zXjfh6?7Lb~TyohLjh1^HOV*)>4oczmgJnW*%PpVs1GBg$(jOKo`s8(|T=+U>(DTrZ z7qe}u$Cl{-bbcSP3ob{Y!aRpdlpE(Z_l%_ZW-WNmAyniq65qMz#iif(0NEeaKk_{} zdoqL0UZzFcr3>`YF%4@{Q{IXgzZByO=OWm-2K-C0bPyx&5;DS#>hHsKb*uMrs+_U< zJOd$w1w$E=Z`Eh;g#5RBQc3e9jt8C}BGhMi_oIT->!7;7&s+~M9B2T2R`7a_LpL&?97)M;Nlm{^qb}|a^Dhu%%T5m}xX5FL;AL!+P3+0U1{I3^#T167M z4^H?c%iwtZpZsYZ3;}D(*^jXhuB#Z_D{RDim%=Rjo5ef_Y<@cBljbvE!5SsOeKF&2 z-nx}E6hZ71XeKq`{NFg2_MkZtLEL;`9Uh%g=%dcOGm;LAr2ebm&`wIJCrcrOK`O|2 z%s_rRPrZ5$AgADn&ccW`8;*s%N5@Ac`&yTgxoH%K~G$J4(6Q-H1w;WtJ@dXI=4_cxpzVc+!iGPD#1m8 zx`OB>Y_QXO{V;no2?61sv$DJ|$#!a<{y)n|aUOa_CD;c^?AeV-eda?NAkTl4zasi<3))*<#l$# zLDOVC5rnUPEQO4~XYEEyl(;$T&im-VUCFpD%o9e} zw`dQ#opii{U#HN=zMOrTOCiC%4?M0*{f}{?D?pZcPq#eS-owuG!#%PZP>GFGOp!VV zL2TFkyh6m^%u<n$IeZ?#hU%s8oICSGO>_s>O(6~@@laP?rqB(Hz|{I86xpA>dF$t zZX);mCa;uUtuS>=)ZVZ5M0xuO#c2U!fE*COr`85>lo7pg3tkyR%k41ro@TIAX6&`n zOq5uDK%#`_k3*Sx!5WW5B2=F9uZ`=6*;u0&mbiruhjm|mUWjO#)%EhZ z_+DSdvSawg(Dh-7;lqv+`k@Z;fWTqqm&Mdj6gJ`*<%C$yy*BTl%{I&K44Z_!k~OCI z(o#c#j*!tRAf0hB1OZ*RDkdh9( z`K@&DK1?VB%d!~AJu_``H}~Ws!NOGpL*3G%gUyf_+F`oMGneO(8$^nQXuy_w%3c4q zhq~Oqf7e`(7OCt1yf);*MpT`O$2C4m8JF5Vp%()cD3nOS-cLn^x^GjI`|`EO<-X6%su?u^O=>)Dd&(+*Ii3DSG?HB-D($c==p?pSDn}XbxXp711oA*DN6} z$)_LM5gA1u4*Ff6k+#9dE&_NKe=Bje5SKzMlR}zIw;AADj00+1tSUjm&vwMV$rJ?@ zQ@0hjpTUp6cmcI+E=sZIdKKL}w)gV@$S(rD!P(IFf;ff>=)ng9NYd*u&rY)oII*$N zOfB6*$f3-KEPp{rYQUod38Pg5pl3(+$0OsM$BH&P2WkXPscHoJj}U!}wK919@$FUh z88PElfGS#nWa_FnKkQokQy6Oo91Zt@d?Re$Q7%5h~R&06Q z7kaV?*t5K_b?A&g6ZdQh{uXW01N}GnwMdS2qB(+EW zx286hYJnZ~LDcSF3@w+%*B2FQ%1X=y2fs-J6c(RsmQhLrs=h1!ROXsC1ez?Sj%x+W zkeR^&^Ea<)N~5kfeK*Bh&q=dP3CR;~`k$kp?TY@LGMz%SD};;hRz0lFDMwN|X6Rtq zS;ih-alH5@^6^d|kJvv|$)hQw6iZbb}6<;4$q35F|vVKhJ6 z$ybf!F66@wX0o427v4<`4zD|lm-Pet_mZ}aPh~$R2?*0qE6kBjp1iYPm4c6=;{>)3 z0|dW8Ha`5lb=i1clcS)u-enRUf0lCom8~p%iNPU|b62f!`}vm*y%1s>{y<*3R$Bi6 zqRA6_^{@zuO$EUUJY&C?EdQ4jNBo|dO)^MBKszj$`$Q|y5m>hJ7}FD;1Zg?GB{8_Y z$#}wi?-sJth*0pC6~ER!D&o_trX&oI{2?)8#5IsJAUiGdUeU(wu{=tR+J70Xb%2{N;Mr$0M(; zED}~24W1JM0Cd_kozSwIE2|&BPtIA+{^+^XmnA-pwMN?Lc~Kg{P(q91$zPQ%Mze35TrK+C{(dNK#-qSv(NYzZ#8nYHvo zI;~^5)4}gfTd@Rnlrg1i4AwSTLWbBMU*-$$-Y0s;UV(mp3Ta9zNrLn#?L|QHOyY)% zm-{2?YT>(d5snT~=&4<<1a@(g>oINcGhSKLHp5SR0a8v+)_(ryCt-tcrVFBjYp5K! z`vl(l@PdX~8f9hOFb2fqKJY;~wDEpN2@*SYSHZuCGFW4Uw^sJ%2u|Asz>eE2jzRhB zK!O%Odp{{{AJE4{`BGCzV6qIqp&x`xudu@%)(?yy3qo+ggK5@00dm&`o*r=rrq9vO zcb_i{`N}fe`5g>8oDHlU_pjl-cwg06K`MB4`XN3da$Y!I5hP}v(x(5%oF&uYvmx;M zq3DmJP87J7jD+C19L(zGYy{e9z0}|$tGnnLbM2o!iu%_gaZo)a3bk?TTkyMZrP&_Z ziUVS(uVn2Du9k#4eIJ5?57HNMsIDEyzjjG%wI^+L08qa1kM>9+2`8~QHJlHy1~YqI zx0_%*8^|-3nP{_&jIzAEDnYCUlH>RF!up09wB*9SKLVu z^UmN?qCEr-ZZqoq8%8p>WHdA}hT9J41ipN&n+;aX7u|W8j&Q}F7Im#dy`8h&ro3r3 z^Q9qu$5w2?`Fh-{ROfTeNk}AvaEjzP{OAnu*Cz0Ct!^{jJMv6Kyz#~Q#pMp1s;J^H z^28`hk!e!LFvseADfIA?g67v9hN!ol;`diy!D_FU)I#_2Eo1f|Z^-)$iC4Q5bEBT% zn+PbWGwGFO>q;2(BpK^>b^d^ZZbS}O7iu`OGq?7ql3C_l`}Llqr|U5uJ7wcuuHevib3Y`wgpvwuK#yQ%Fm19pmr$QuOJwoy*gc z5QBhn#(s<_H1W0I121LTuK}4do*A{=?!_-JB!A@}TSx17#J*NeRteI$Nj7PDd^uh| z-bQkg&Qu1-V}?=!Cp@G1AB2)j7DN)K(dOGg+SV9@a5={zv>a5gko_xq8{go^-LV;~ zn^8Rrs6nK{Zre>q(T`z^NE^99TVswJql2V*X1w5eDiGo|kv&m${!@68R{j$rgKH5< zasGp5i%&I)^nK72>hsM`iMRk>XM5|E)Q+r&LQ54I+9SSrN}5PiwB0|kOpm}8tABblKT zmjyMntL4}b!J}HuU9Iu-j!ze}{+xcCE9MY!`?zF4=&oZ3|IY%v30(pQis$>^s9bpY z?A31-FFPW50ujDHhz>oT_I2lQGXaDRzP}Qn!cHu$Y>n_gXZ%C&z3*%RoQG9RRd-O_ zB^dI52a`VNCnlr_m(->`^*r?H+3x6IgGZL$Hkeu z-9zPMLAYPb7L&Wl^7;aM@`J@7(=IW5IaK-3zH7=AH`aCa zkj_eH4?I}wsxhUM!JBcGwCxOXk%szj>g>vdkhN~&h?yzAFyZorsjaJc`s?B)o|^Tm zaKab*CC3R?f$x>wIhB%clAlsW5Sl}tZYyQ5>!gHpzl%kteb^-fTxL zN+4s1whQKAdEq$gZ1h3fl;WRy9vj>IL_-3G-eoSCd&YOBjq!7(eL$ZP<2tJf4%?idY;7y z1(A#i2Hu;qykd~Sp5`YE;~|FNM$7ENf4FzwTImj^J595ba3x`KH!+geDxcBCSGR0Y zz}SLfkS^3`ESc!@o~jR2P!H4_4VeASFP>63@hYG4EN}P{t)XO~00cP|gjHnVPoIIJUM15RS!xMwX6Gf1@jESwCJc>@Z5e=84>TCXu5-Dd8L0 zkhQR?nszOv_lJleiW$?kwOUfeuB0Y(a6?svH&!X#IHz=gR8CxnR z8EQ*IB^e!J?E2|1EIsG6tqLF6igIH08_;GAxM3BP3ZmxGe&l!}p_Tp0junJkqC87B z5>gN%?+e*r^=@b*at#R3{CzE?=Rm9@iMzK;BrV!~i<4({;mw3y&qC zeY0i;ejeHDSOr3bo_Wr zTZh)D5DcFoBh-E8pscacKbP4q3Y@J(_~f02#`O)CsGYJp7y4S8sDaWe>X793rnOm2nPUlXnM=rGrCwepzys3In8b7*cBrC~U znQ+DP*9PZlAK_xQV_X9FJ&FLcaT$Cw!)c4C^f{pokMb$A$p%@=&ce)QwFCdk{z+tx zJjdI~GwsW#wY+J$EW>XSTA4PpIn0rzIu@P@*AYzM`$>SSuR~=t7qYxiCMKIpJ4Onm zjZ^K9s_w(Ph|@)_+VEvX=ho{cY4>B5t6EiKf5OGpfE8s5yO#YvG58DjH&>#6X2itG zD}6GqZ?H6}mj!hovT;fDvp;v&kWcdi_6`0c+={VHkff_5YmZC4TyLg>Uf%rV9TB03 zurbpm>;tywI)-J1VRzQ8;(FcwMj%e5X7(-{N?vO7cai)21U?cFeDrSF%lLt<>k6#- z6y_=P?)VPc2`VIULlB=MAs5E6uQ|0h+Lva2o7MsA58XdmL%YzQtn7#qWpE!)WO?(v zQS77yuc+C>Ppl9Oxq-{4LOYdAgevfFu$%HSF1eX9txJ+M{Cwd5T!mPmN!6#NBFmxE3RUq z%H!wnY5LbmFphrWVj=p~VtbzasX75bRt#)yQ9>T+wAGzACS=Mv=Xw*edCY&h_wY?p z-pE4to^Kk3L$q)^S{7{qryyWETJgIYG+xBQ{3k&p9RsBoU)7&+g-a{06&E9`;0vf& zxN-gnKfyliFkR7Qx9kUn6~E#xs3pd6EBN7#|LVFCQoJT`IP`dVF@LO%{E5f8EVQa+ z1i8J=>Ae85^M~(1S0(0y=YQ9?JTt`=_*{GES_XY*)NfvX;;(=!<;wXz}fV! z9aqc!%UFb5gLe=F@&1q*$^y*!!IM`0W@iVMNGjKcT@zCq#H)Rc%Tk4JV_fE$4I^P3 zCUg2luN+v%_Ui1WkHx4d0DsF+_aawD7H>WD)Tje%8>3wNG?e)#lD^5xj7SP2Lm$mC z-(u^2HvplH0<^U}qYATF0N*#kyd>^#&;Q&OOi7bRKinp`dSIMAWAwPBN69*@sq=y#$xe%_)-%od)H8ubI02r=?|o ziG6hII3MHVGW@U#KYs@c&5SRTi2ua8EqAkjz+X`GLyPkrc}q#OO{fyhLQ-*l;VMkW z$J6p?AkbR6heAIw7X#ocWa zA*{SMpi1^>VB=u-F&ii?rB&hb8%iG#f`$IpVNXQ>k;9X`h%R(YvjuGq7IZByCYy8!Z=1!CJ z=7#W;1D4kXo14p+R@hc5@?*KF3~I}Y7&>G-%H4v_ifz@4BG=RFh#ETMxhZ)th+pG?@ON3CYF4s$A6O790^EAm9S1s zaLv7~ga&I|cM~fzf>5a0 zglJ}F(nM{(3q`JLmLcWZa(|LA@#{Qod|^q&Lm}L)@(4y_qqms39kl=a<7Rr-*GAnl z-(rLfCV#u(sAf0AEwr9M3ei3K(Lzm&ma;slM6p~lAV$k=Ok81bpqsS*>~4Z$pK-w8 zc1sY#o%SlS23}%KJYhE;Hff}6{O;uL-(s@SJ+uXROU+3jo&T=TU5yS3vDLpq^dK3_ z2_x+HTTXEQ(}}!J9ykfKc$g)DuHV~g2I5-Y=xu+;BjNxX;Zu6#Ln+UKF`kgW8FD_I zs4hz%|8KUY*i#Qoe})hl`iE-3MdyWR>4uR}a0l|=ul^sm(q+5%@2l7g1oXqHP5vFs zte#Ns^nz&J>8Z+XHL~7)0SsojUiMI{*)zr#X(R95v+F+z6L9Hx(5)8LMG689mdG%j z$+XL`27L_CL%B)(J|uF0@DmY)@$ZjE85NlIFm>XeX=lE=i$|BZ+SlNbs2q_yA@Z?= zewTRbB(Ywr5Ik?$EyGgdiVjk3Itkz=(1<{n4y7C_qAcaMYs+7g7s+G!QT+7| z>j;1pDd)=ryjEcTuf1<{(^^DQzX130S5H=aoAVs@WY(HcGo7Bt^QnM}3|h-n@oVHc z_vrzsZ8VNbl7(gu^e#FZovhNz&r3TS`a}dmx*slq7;`tB8tVt=QuO{9<_4CY}2kAXB_* z>#Htl2$VcDd!ysXtCsW-cH+vP(Np6DP4%b$xn&+Wc7YMaK0oL6;0Dj}H-N6r0f8Og2RQOVNB6MBG~U2g|m9&J3TDxat-WaV(GVcl{+gz$y;~q;f`!fOTT+P7}A6< zNt(aWFgJTAqNpmOZnU-D06HIPE|y*A2#;kc5>EKNxaYn9mf+)Rr-duvUdvr2MtQDr z*xmh0g5mhGM-G2-0iE44R<&@H)dFeZiLB3vSj|i3aIU8!B&=v$?pK>?%z;HRdr6RDzt(n@c3{gF3iAzAmIZ?Jzi5wrCB;+>=5FA|pp1U)SS<#=I z6QhIFPUB7IeEqlS2qNQ|<42sEm_72Ea!BwcPB}N9-&vcZ6Kqf2fLx<4)YFkm%8j`!6X0yj$UUL%E+K}X5YSXZ8}M~AE1#z1fz+*ta?+H z>d*R>323A>^(@VJm>K8n-8vVjEhUK_+IFtV zY81l~`LY}rQP9jew4?AwX5S##BsW=!1mQU@{3xxz3?1R}FUo6N?+kEA*t(`eNu1O;E+CU}-eEzVQ4=(g zVDjj7=+Cu+!5_O(CGf1oKXD|DVX%pIm$;^5rXv%0Xm|r;jIw)dVz1;WkgAh$z{Bx0 z%Zw&En2D|-QX5$bDF?itRr^P}gmPM9f&aWr@b=p_1^?JQiAG#Qsc5Mnc1>9&7*TLOjI4G@&qzJs9YCG9w|5zQ-$!irA|;W4Z&s%eN-q!d zhWYaL`SHuD9=S-l7J4D&Z79MY>BUHBe8CgclqoEZ?L{b)5L3N+tpM;dJ;hH+ZAWZMlNCz}5})T0`{x z7m&8_z4@x!?D>V$a;99m{~lE{!KqN~l#AUXFxp!@0I27z#rFBgtT8C!pK!{PJ(k~n zU19I7l_IHnd!ZQNPLBg$_uJ2`ALqFCpYPc`zE%p%m-Hiv)B<6RSTAu59zV9M{jct} zpUzKV9P6mRWJHBsQ@MQ(W=oVrb}Y_#RD?ILnngHzei&Tq(`8P&O9}|d$PajK=DU== zs|RhPyIu{5x?1{ftMmdi^jJGzmHoX?E)rx^y0)0B!(f?_} zG}t}f7axMIo?P8_3%-29#HTJ$!^6mcq~>lei{axI8Yuo? zX3@3r(mV5|-BTXzyf1f2*+Oow5BB-en9P#j`@c>$Qz)UEY#E*LQ}~ik^q`n|sOMRh z#EXNBKc_|#V*ahb&CM7vMOYbHD{XFw4$VKQX9CxN_3xu!@ZV1)S%J_|rOAxU4JpZ$ zp-EAlk5ln{Kth8i=xd+|Z!Lt9P-TuY+u8FCBagUFCNi=b+v1z}C@0IL@&Q5$G8u@& zyd0IgmL1h_+g&*68<~@h_Q*Oax#GuN)UWP%D;~FQIkxOm?VKPUs3o%B+y18(S1wjd z>957C8vp1}FiAL4hsCG($XjKcff7fa|0@~}k=jNsGi{HHYME9c+%o&p}xzK5!y zcgqY(mNyUd1b>tpJ%4Ct=5@omhgezZxar;THQ!SK@6Nc5SrQ%wjH-oU1rbHb*a!BH zbmwYUA|KWtMQfy%Ca%U^DT;S~adAB-rk*w}YORbuKAsGjv0IkIDfxvguBb1)G(m)Y z`fGpqd#%RdFixw6|8l5=>dryl!!foci&MlfK7Ps7pK%QfmP^Dw(5=66P*@BARx71_9yYiD3(6eDaGWs4|f>q2dVxqH;^(|=h^D~eJjQG6?(`02Etg_OPib{(R}VJ! z$#QPH+M=#7#K4ZkZn!j0-&H-Y|D4MImLXvhiPoM>B0sRQ0KUoJ@?es}L~D2O_xG;Ky}|1^H%TD! zUU+}{5XArGO>Wk;m&`vb#gZS4jP;G%vpr+l)*4f#7~%3O52YV{2EPHXIx=UZLsPan zlVhsCHX_~=9x%6Iv#E_j$)ZgC*q z_utNR&P)8n2R(0a*Ru>oT<}#-hpx%i{v0Coj#%HCyoBhHscLP%0)Dum_RY_P8G!q> zT|Qc;xc57l40i3=@&FZk184%#LR~FRuJS1FtK0*K6E4k)J6>u@C|bpO71Mq_#2J42 zd{Rzcl)K}yH2#=w{u9V|3iV@@;7>Ax>YbjopNq1O1nD0Jh)jX`+2nkPWACdtKl|7Y#y+Rz@$S*=u<`libz;F7t8$M`oPO!n>~a8F0Zf;3 zLxQCUVL#7~k#%-T3|vY!Uzs4per@bs1B{ocH7N{`^Ah z8@0D*KQ-)ro~6$$q6TB;kIb6q_mYH@?mccHCZ~wLZz!1AO1%YYO?qeLyS)^(*yO>6 zyO@EjBY~?)sZhMlM`p)s8Z~W}q!>yEyW7qQ$0ITH6O|^dL)5%w^ZU`ym%$u+D0Qx{ zikS~5;3^F;(M`Ob&Lhy2ZKQ7ekVkK zrp-m&mQt_n;H4w;4L(F>DFdG&EQGe^U}DBhP97&jL(O&u(r( z&@XF{;J=W9#*!!;$~V_%RLx^;ZDl4x2UA}id^CGss6F1$br7DKd4TMPz|=Gz-*j)% zOp8r##e0tRJ>1Ku>SJECSo#?ZH>tL0BoVLA=Zp=8?{0VnVty}*Sktqy* z#iN@Odw_7TyD3j@*(M+e2n=w$nmFOT^e8!%3*=+F4cej*mrnd>;OSomy<3*(=2jxs z0LaOj61=rnXj%E~!}cE)v?m(zjt-0Z${VZ?Q>@Sz+$5dD5dvfmnZ10xc1L-+UN4LLZ2INmXN1g#TaIIG zHp}n`rCU(_+X-x^)^_H6&JBJ`C-y>H2~Xq>{Ek{a=j6QbIOO}Z@^eBUT}Tec*xHK0 z!i(zOQ-5A-r5ck(DevOmU*nX{PoxnqExi_c%gd{$^JS4mfF8SR+32X_R$q>3ytFFb zJ7pd!f+g`>dmNa5TzVBO+Etm2l`0#;b_+T6Tn zUpTB~s~eM+6o<+?)t^%f`0_2g=~z|IcW{8_1$RV8%);@WhvIhj*&1S-h|ixX&hZHz z)~d0C8Av`H^OQ4$qG9kMH5vTw(8y@NTEfFG*^8QRPk-3^4(jiP|3#fhiD9b|I3vu| zrf_Sp!9A`4SNJwXDxV4`7*xg3V)vFUx+i+W7L7%~QbB8`j zwkW~ty~qAZ7P-$wfXdRAJ~ARVwBCbaCodMEJSU~dekM9L@!snQm(|}y5Y$b&+1Zek zL+x{^pv3pNlhjs-*#~Kw@AvABT9y^%91Pn;Ne(U+{B&(CGp>}*3iGGgcNDnXXHpHH zx+(;}O{;}z)lwo~zaA)oWqbnmnN-f{-CD@uso+foZA)06#h$^U0?q?-RB~sitJ2j3 zDjBiMvdBXZ`7ResMvz*TlzQ7Ksg5m^;AV2+wYkjsdP1JM67|xxW;tz@^s>E=0X&*s zNakTsbigkKWN;&<`0M#dg^v8ec?S1Uvn`Dd@zht$q{n;NcNQInQXiJxre{s15pDS6 zG8sgiQqb27HRZ2m#mXQU%vpE_QDIBKPxzLxBr4s2_|jhu;noXD2=XWFyLo^3ejY#F zpG+cde#pt#xIkqSTpt~Xq0PMBLWx-zTpgTdZLJ_kH(!{rwF)RX&5HV*3Lk>hibw3Y z#^>9+upXnoV~W1t1v~oO%?hdrp17djtEznZ!e#O!-YwRBn=ys7+4Z%}(qUX4>ML$~ zqYT;RxHoem0#&|@YPQNcQ{@3?3#o%^%^SP2PRfH-zc+P~5Du!-cr;F%=FoUZzRO~I z5KO9c!dXIiW()yKVgmfhs2s~@P}JwXDiJ|h1DYV|JEvFke061iN4218slJhe^~DLr6wxjdTjrH0MbA$zlm<7oQLh^d2Ap56oQo@QF0>J z1w`)~>!4Ad`U1elmFr0GJ*8>;<>7=UQ9CB(L{bq9DS`@la7{uG)+7=jp)3t1N%>e& zE^pxS3o?!xf|b9*TmY3^rPGh-H9DV3@Odt~CiMjM9{`0Qo(&pKkiOKHP(Ra^J<|N* zNKv!>n^wAI4FE<>83T#L#vL|+C`c(I2!K|a5WN^h80nF>t#p3SbrzSK=1uOP#bNu5 zQbBSVO2>-^Q%8K$=LtP5_92?ybo;(zrcac1Q6VBHyq$^G-gKOh z=@;d1rqqgf3VnT*2KfnOK#mj&fgGp!G@K=YLhw!Nk*ObueKM#%-(swNb_H%Y@4M)3 zb$>Nx%ApH1|IpO$aRmN3V{knHeqH#1IyLKUVE?z@X~7kQ=gp0%N@>*5io>9`?>1 znxZJe=y79lz~Y0k@bE=A@bE=gc=#ghx9Ff;!GC|f^>&=J@Q7TWwEV>9KApQ>X8O&K ze%hz|*RFgM0QKklMmMhY`)+2s;pe}GS}5hAk;W%wfB!{Mg!;w?OqxCoM;vz)&b{bD zT>Psm@Y}!Nj;EW~;I^k8z&C$(DL#1qC$RYFcVWcnQDGlIl+GBwao&ZZq`=+z^7N(m zpOrr1c1*Z-`|~{b!w#dG7XW|9}@Be+o^T^p`y69I&tagmp;H zLuuT^@%Z8ozTM}1%ublm#c;GK7O5(~_ zSKx~u{tujf=uvo?2Z!dB+Wyl1tI4N=K;JwVg+XiwfN))PTzDE&X_37pg^_|Cxw$*H(yC3g>!B?hw8d6 zL>kR?JmTw?h4U|XFNO@MchpDv!~x#C-^OR_56*w)^CzLE*8PdY+irRUrIOe@ zGM-NdOyfh2&CI#up*B&Kuqj@D!%Xs#ou4|xy?MN0-8LzIx_s)qFDbdq$9pm|?%y*S zO5W#Lj(jrh%Rc}53il#Djw8&SKharks9r8QYwmayi|)53SG}<*-2NQN{k(ZSA8$*{ zxZftHWA||*Myz;soqoSIZzB7=;U#D1U4%mqpY5(JAn(_LJ3M+Jlze*0X4IE^g0QAc zAB&2LLLc;|;In$$O110x?85rB?n8JWMJ*J*fzvJ#qpfZ3WoU%C3nqnn+_0k#yDWH^ z*Ogt*+Ix{dvOMo_E%+m%a%9StaHinkVdss0OxqU|K966KrJe0v_|f-ob@uA)c@y~n zK^-dcekzv}ta{yzBsXXNB-I~O&LSZPcH}2f9;7{tNPD5#8#-?3XD0*WES*nxQuWg} znf1eaQ_uJ_q=!}@^~zGF^M*W|ynZMj8TE@*{SEmoCkftKW}L&+!l z;kyVf5%uQ?27=c=gF?pN;CJn8Mbj4dr<&&NH$@%NYV*tJUwJsC>j6Q<;RP5_C`6b! z#~m=S`t=Q&eg*VRv->DWqiieaJSBk{v zYqY~JgXi^Q%#WCs>WmH2UekFc>X)%s5kGttd@MG%BOI`3mJ`)UCK*i04}ap=$e-v@ zPojD_A$nOJ{tG^YBp4g*l!x%p7O6td$Kbys9nTn~zr6BB%Msrx6^ATo|CM@5SpA0E z*PStMQYJV^AsKN+iCCjF_=9D5(N76<|08SXz}s)?y-gm3qlr_;s`&vBdEs_C9WaYs zf|4`BPoFmlg+c(yBv#{2^H-nwQ{!sa*U;_qSmL@tComX>PxTjCIutiQFOkc>SF z^antoc}e-2eTK9*w4XI3U9pY`B$I^~zh^{waCXkQ|9U zr}p30{`x_6n6beBl9leyQ9l&gJO`l4@nvVcd=MLYNAnLD!&@udFRjkpZyK3bvDQw0 zs%*$1@gqp^oSI+hI8UGN?vu5zuO;J3f5y4`Oq?^>S?0PGYm|J5&lw86631A(VvY0J39~1mP!#(gVm=EyA;v#N0ED3jK>Rz= z!@_<|Szt%L2Y{CCyU@O~O+)xJ=UL3Wo3_I5v<9BIb($|m-6ZJ zh0;llMgPF`9!&>uD6;t`CqhtJJZGg^<~_5`(HR`@3Dd6 zCrq7;`3D`KU1{Cjg2(QA5N$0jc>EuFJVQ-gZRT^qW(urktnx(Zy|>4eO4Ygewu>o2}yt(QoZRaH3l z-}T@@#u)DULwa6x_f5BD@;&wAAI|(Hlc&QeaoMJ3BUO6rS5te{5Q|P5MMg$T%5V&I6QRs-@?<_OyOxi;Q2lK$m6)|E8m8h z#!ejX{2X#$i_^?WPTr)h{N4^4YR{f0u2;?je~0OjZ4mwgR2R<$-~9dMMvb| zA%#>9J{XoPosSbwKEzqST|3)x<8K~N`4C~t@>FQuY5xPK%loA;Ydj-7-E4t2E`ndM zzh^^jO_MPxpz%`ZJ&A6%o15>M&3J!C#{Gs8$<3$zW=gyFfTRys#y z=`s6jPNT9DhJB~=jr2dAKeXN&4=7%2qS10JPHWISIO?b6Ym9F=KjHkwnfhs;8{zde zd42w=e$YP`W2rAFulL96%iTx_2QHdP_jz1$#ku&83*K+(-;_T0)EmzF9dg(lg{-C3FD7zE^3KnapTRKS3k;1PUO445#Wf{z$b!2l>h5kx>y0Rah;m8^hdB&S{2 zWq0p%3gv!}N1%yjouS65Yc*HkmP1^97|M2=ws)oYhWTzrhS z&Mh1ED*8<33*hJnAF!Wy7MEEDJ-cJXv4WNbmYKnzK|3t-9@TT&dYJj7<6RwZ>g3NG z?EAgiW6420PW$}q`lSo$$i9}Xj4w-RkLXwWrIuTdSm@rbwJ*O_KZ%y(I_4*!B$ah5 zPejH>FS+xknVoZE$a>G&#M$Q)$WyvPal|FWdfH>Ry;hEmvOx5heZ66en!bGrz+)U( za?m6hw_bdV9NXj2t3Rk7;qtB8Yl+{%8M)}VFhKk^K+${L(dYD)^s6|qNB>bvtImxz zcGYb^AiW>+bBLUPX)mHjNbsMh9+d1N8GnBK0K|LJj9?B8PukI6Anym84wNg)_HQWv zh5yOs?|Qq(#(@sbo<1*q)M1+)svkY%)^3>S52sz7dbjltd}e%ukUzQKMgLLSzdvMs z5{4z~3uV0T_;r`ItSWUYAvP|?xnIpxXt53;{H60y*?z(4=TNlin-y`##@(b}9X$`x z&la5E?*LHcP4pv#pI|EdP=grRd-};G8+cMby8TM#=TiBX7Kwez7jgh$MrW>C^EW=`_=N`A%`R=&98kB^1$Ye?r{e|okx&NUluIv-mp_! zM~&vSB>kz?vzE*ckl;0Em^d;27An@Qh}d}j$JTo{?IC;(JM9-4nfLm%H!3;`RU6e% z^pMDOXIU)hk7jnJ{0d8Q$oipb;~GBg-R<~wYGy>)|1=`_*dxs4#v;d@Izc`>C{WmS ze(bL#mvW{rmKjjcBEuj%{!v_aj`a+OeeDj@H>`gVR{UgRS;&^sxm^Yw>Z|11#JY>~ zJE8r6&Py%Q9-bi+2L9T#Tl;+3`qdDhk_h(k0c#wl%I*aAR~(G3GIOcTE^`9;f@g#5|k{o%+fEP33M z_Sm!?>>IYiwtnPtVUJ68{5vd53Z8~D&y)NYLu5LY*RjO-5qd>rR<-*hEcAoJw-8xx z0>a|sO~zOIt3E#87w^OTZj-+cYcKow4;Gw{YAw)=z|4OO3&YI&6Kvk#RO_Ze<`sfA zJZK1aSfXJ8J?ghc#^uYtW9WLpvMhWu>0LZA>_P6PK05dw~w(>x~{_kf4KVrDi5Dtei;jw41 z@bkHlISqLC4E{0QYses1I~LEL$1_NNH+R14diUG=`IhR(HT7+D^ylw<=$XZ`dB?V{ za^`!3aeMQM((b{OWhKDy%D@1I)G0;Sc;4*nL?PL2%93j0_&kw+ZvwtON%F*R;tvt{+-~7RSzZdP_UIadGSr&d>qu&^9*QriK?Z^w=rb8`leVf+* z5iuWqxfjsSd8PdzNG{ozD_)S3lj}Jyg0Xwt4ZYv}6^I9CG~?Z^7b5Wzq+kHU)=Mx9 zt=_tH@hWz0*Kcfh?9xD^7lUY99@I@dX%owX_O%O+o$@<6wsG^So_6SZaI5yU5EmC6 z$Z?(B^|Z?pE!%bP-=U?!_|9qawn z^XE&9v)_+xcJYi~2G*|DZ`@R_RuZMk78Y`8c*!OAX*tdHGk3<+3>|vC#1_+drX`o2 zj8tzi{>sl&Ovkeyuw$E;JV?ijL2OPKN`K3G)9uf}p!`vfGxaWyW`z6o7yTwkrK+V+ ztYn%buMFGARVlyo%>|VPYSbCkOD*3;H^_Vh&@-2bwz{RWE*hSUzW z=>xXN9VVNXrp#AAF&4#Bxsx-BNg0`Zfa!Zz{iecaXVc3sV_HsjuHC;NdP$OFS$?HV z2uko{`N}2GqJ3T8wgVQ|i+oen8P}$&7XwOCH{+#a)hs0wLnP%G2KeTwMaDUuq!ath zHic~e!9sAx6)InJd_eT4WnshW?b;G5Rx6FtWs5odS2F(@^5%oRsTFqXZBJU&vk=Er zD*bIJ^xhr%Y(b`Jph2_h+&)QmhHc6)ei~vEP8a0ZrgLN8z69LkfNjQmjT3mtxjHW0 zng6^qFmkyusocn!f~0Zd!FS*HBG|H2niy;;Ly365O{;%*7wSOk&W(jVS28q8$B}xx zpXBGljd`XFHckvf9HYtXi|W^Ny&k|1_I@t=LiCi&p1J*zaNsiY;is6_^*EaVS3Uh zh)Z7Of(v`YryTItsj+jINi8X^#<&IuHCE_;(UVPcdXw@yLLIx5wzgw%qmkR4Y#o&;?N>oJDenUT1>@%J}SzkNe(eHTw(!0~-Y9K9&~ zTayPlnK*gy1kN1O&q3?o)JU2)dCQ;3a7O(V$$xfmT)uczXrE$SCr;%~S zH)VYElr1L!auvUe`1Onra^^IPTsJ_E|K*wO96IFimHMM$7Ch$o{p|Q$Kf7Z9;#f*& zporsiWxp*JhuW9fk_lLr=zrJgYeteXzQyEDG zlEE^au}q9SvlpigC&gVW5>7hbr-=0M*D#{=2rEqg+4mCX(w{Cy@Qg> zs1T}mYz#Ydo4bCRDOOq+vh4YCnE3JBzLNPOEOmZLkMCvkP1yQZs*eP(MMuGm3W;%U zF#m-4fg&>?_3Om=67}0bcs70c_-RLQ+z{=2tP&>e5i+`(=pssd7w%U;&XCP@1<%?`84!@(iXB z@+itBdROUBzHa2`P1?Usg%UO-%!a>A6Stq!knR^W3?TjDCA2P66TffRtQkSq2j|Y5 z#h^A_@y-hq>>rpIS-`t5PVjujvMel`^%eRwY0sL+u@xb00a&?&1poLzZcZ)^?9qP^ zuLD}|?rqy6I@TBfCXRj4dCqGcgU15^03ZNKL_t*k>rJ!!S&e&s-+>`F^~CW*`j5eH zB<$My8*Z-L0AEg9eO!dybVxt(v=jTQuuSPub|&ua~AJaAoiV>}-3BX1lZZsnC6JKi6+G z=@B!BFG>yIT>wliDbW3hTPm-JTc?}WMyVazvoLz#LFMQljrgPpk8zL+TFR+ zXRvkMhU+RXuP}7^Wa{b<@O^b-j_!yvzGx$rf!9AS5Fmlwdm8`DogeCF!XTP_X5z{QLD#i8Vs1n=?T8b@BVsHa||N@&+9C`bU}xd6;F zaQK#FNJb9iIi21z3TfutlByW&%bt;;=F*S=ovy~o?f=*Tm!P&xC~1Hi^LJ8;oQhxZ-YNz(g{phMP+2Bb|ENpGtdcxdb(Oq%{U z2HevXWhxYt?F%_>&2;38mb-WR0c50K)wZ|I&5e|HQ8tK8MJflT(mum5P_=e>l&@0C zpT7|DPmfPk^|qe>RDQw=9gIE8mx59ZxPr5ZUGse}g1Oq_VnqzLrhO|G>oX@5ELs4q zyEKvb4HZLqKd;3b$avDMXYthA52JJMwn#0Q4C#0`2)#r7d5Zm}pBCWGh^Jio{`&Vcrj`6$I+<5K8L=Y=5f z=ASkp`Clfr-Pk2k!Si-G1DL3 zdW2%bFwn5Oetc|Ox{ma}L5|-R88VOUK7h>hE7~&ZbZYAMm&10yg$YfAAbCbeG9N5m zs}hPl6=r;Q^ed6`^t0Y4O8`K@vc-7N6bpu7Z$4D)CBjg%42IqQi@aXsbX3T680UuUD_MX=GxN!qwwviQ5!5V>!?m#! zL7rSdH7tGB-+=F_HxLk7v@ z2ywixswc%sBlM#^aIDUIxPQp?FMKh)Fa$lcb1%F&@^M#CRbJuJg^PIgv1f2o<@)$z z$1b@Z2Tz~;0oE@2S$gj3l`9zDeE`NiI9l7xNb%Bn3$cIq-oUngGU@sPl`I8Ea;)*0 zlc%t2>o(W?*NqK{_UgD@TeqP{y;d02e+YJN*@oPlT--_9>ww*tnot1@U3*ASQvn+r?wW|DmpKbEciiuw_SEA3lL(G8G<06Dbsbo)&r-^@BEnwlsN7$1 zeh2qU#kwBygbnUrd+l38!SZ^sf?exSDi-SqeBDQyhgvY%rVMKwB$_#=kZ9)PN{a`& z#<~`BRIDyCdegobUNpoc2<$b~>ta!m3KJlh|I*W6ATv{+<{bdWzAzB?KGa1jk9W2g zZK&6UkjO&DCiym9zG6{)GItD0m2oS6207Wec<%A($jMQ^jL`^@BWx1w19#rtF`&Oy zd9XZjpMfp4b?pD+IF24ZP5K$i^N{M%0plN?%xY=hg1nxG(!L03?go2T6mfZh?Irhv z(xTrstl8sx4Dhh~J6->KD)f-y`mZJ~p3lJQ6+0p>&lgBTAcz*@Dmf(qFTFX$|7a=)Vx7fh{=EHVWZP(>~muZX8UYM=1 z_|l-XMXrm*?WbP49bbMo4llkj1cQdON4-WBkuNb0t=rc|TEP_GepPA1MT^&GbcvD$ zJ$ZWm++}24%?97Z;QoLG!!(eSlZz$a=)b7Ct$$0Dt;jM+*^7wWpCIk!{B`WT&L1gU zG!=aYwn5|ORq?MU`{1kZp2MP*FXPGQ2cc2(Y9iB40QLtDe}}hMWIAB9Z*eXslkcGO_1E9se7zEmG{+@oJhB&R089rOrJt3jbEUB7V^lrCGu zn>Ch&RX_daY9AXXS28m3^N;!od;hySAT~al=vgT3GhD$hPSlojYN^0od$vHKBB|QC z7Jt3Q8UNmTfcl~Spy)f+@2<^26^A{3_p3$lQ3RQ(L1%*YgShxu&v^mYCp=@a8Z_S4v+_Mm$@qjAgN<5^>3qNR)!>gEJskMl60b->yy z9ZL*%2Df0po9PWlF4YWfir(fy!Or`OOh>^Pk5oP4#&$NDr3>_%@7?>iMd|Xz0JZ2)TTK=zXStfg;7z&|^@0G-zG}4?Z~%GnT%Fxhp4P^ov7LuW2=96P1-Of<(h% z3!B&IeUG+X8Y87ZlA>2gW(6e^B*#i{*N{i_Ush9)GWRI@oA)cZPwiKlL4uAmx!;8! z!{=KjDW9~T`vq!-GE=TE$PkLfA7>nLd+1!Ja^TP+ldXmQyN_VM&vDK7JvIPQ(IGAu zWFbQK`&l~z{Mwc`H%6^SRnewvQ;d0SIOeQ-5AT079>X5(k5c7}6MZZD8&uEusc!>t zXR2kH|A-zC*NMG{8wY|R-jlW0y_hBX7fi1hqMtG@={MlYR4zp_nkoFSL?%i0gPal1 zrmNJgh}$3T6Zk4Dct;LyRpeb+UkW-VS!_znlrDm1o%Gp5m(O1%>j6MJ-wXoT?}FqXaDR%QFDze|{ZjS@x_SVB z@`KrHNaKhH$MD}xNiX=k3Ga8IS5$jVG9`hHJK>iH0p~Z%03G(`AC!z)Y@Bob!BbMn z^GnwM-t&Kd{<0Rz!HlvVp;*?F#GinPE0;5{`KOKAT5i6xE8^ng?DwU1kk=dw`3vgey)uB3PP zbNsz5{_zs`8=V+UGbH_P_E@wsB0Fg}Ml-^E7?G=rJ&FM>0uLG|mo z9$~?ZLcUo>E-rWBfcZ9i6AR$S+B*C<;pjcsL0Z2r(~pnVw|7BYLY&B?7U3&~ z=t}mdLXy0ghJn*~f2yXpiit z3j1X`_B5?jQ7um{96QO!J6-RP$Gu{Ioc9ko%{VQi&68lE|5t=cT+i9K| zFibLN0v4g!IzY&IUixT}o;YDRnM0-ibNol%e#dXM$oc~HZ=7IVnyJ!uUqp~Noc7x9 zA!h=FK3!Z<2 zT6e(*uS}GlQSFphyS?z3!S~?(mtM!(A6MY=#Y@uP#q$@iY4utR>(tYy21;8Vv^QS! z#Y~H{nV~-UB!-%fgwHr?7s-Dol9n88j(W0e7~)6>EN60V*hl zgH`&UV2fjLy2s%Dt~M^2^EJQD;5&~aZMzTdr_95ySAz6#+^|Y*K;L;l|ZR(oW2n-FK7i=oen;!MZ`zeAJ z{*4!H_|hhXKd0`aNc#cY-vp<5eT0VQx>)~#3&RT={=Yi~kdH4IZAi4_w>sElF9ky^ zP72!cq%A(DXSBR%Lxn@ohH~9-UzZkx?C$fX;ZK81;y6t@8!{8*#V9ry?CVDlpT_CH0z8r(%CDVM{GU3I!_;b(k zfc~?T>y-9~cAe^=LF0;&yhxieL9}6@eWyC8RJDY*j_((3=KU(d4IvBR5ySI9Z@zva5V~+{=1F7pMmSg^Q#RJtgy-^Fh^PFmUe7W#3?` zn{RD^*6nNS^`=AvR8OmPqh^)SZ?ODTa&&YQ8aJ~ni-6f0rtV{f}8mrH3F2BM-v82h4oBBBGy&U)HmLC3|%V)WQP82xl#JUX@yMnBb; z)1$}sLF*26Jmdfn&Wkr@PK=udak?mNDA(;fe*V$>h?>~gXpDNIw{W&te-PSI`rS=C z^5O8KX{+i;)nv?N$5PYHF=}@W!@%bs|A?F%@39pX6~Z&)??7B^wDTMdMp5Nf_%STF zhAz^XAp_69HWYF3F;X4HN~EFBpf;#ox11d02EE+*flDr7dCILl4ov?g0c!U=?3as{ z?74z%#`w}aBLQolbe-Ba8_$`(T-)?~iSc-D!rhYIlkJTqXOL0aV}?wOerBMy{4Kx! zfn!Hb$?r+{56i;LkMzfH6i7?P{Udu4J?mp{Nc%wBZ5l{QPQZxK`e2_ezwX7J9fzfM z8ALCD|Bhe-4AZlJ`D8@N$m2R@P{~B21IkCa-886_Z9Z7Irpbl@0ELR=m&Sp+T(&rF zxBJMUQ^?8Ie?{E9ZEYbhkmc8uRqy#E$XcN631{ z57_-8lL*;7BXSVz7g@YL!f#RgOVsDm6QsRje&pF;S-!%Ge1(NP&Hdzi?U&!7s1Tlb zWf)?jqe%O_~KHh(XUbbkx_O)Z< zVlZrEZ{9AN`9;nMB#y6B->2OzP0_S%9siDWAguNqe_wW-6qOs(-LLAnlJqpQv!YyS z{vwS-#XebbIcj|Y;tt90<21QS%X8T+o1b&1F4mS_v{Yf-GiHDjgae^JU3y!2KM((= zK*3Zz{M-$}(XAvl1+DPoFNuHOnd~BOCPm%m}A8^`Z z`;Tttr^hxZ@TR?(-zbN(r3skSm$I;RG8Z6-#EuX&<7ksi34IeiQhQaiY2yT&%cX!D> zoc4>v!o$Z6<$O`$O8Qm&IA@tWZUEOh6YW<$IDDeP$6$J$^ea2vm+{w>$ZadOgxlx?i40vdn+P{!09@3CGbiKa0ey<+0{{-gNqXZ;61x8C0i1&XHeATD)0LK>$e z0}{fLyxctWRunF+4@UX!_l_nd#0i{|5=l~3;rhvnmp zCB7N3*y+SHgR%)ZBeCfO*ZGZauyJdT4}n28Pgw9DNHB_`AANv+i~-By(=kJ$34fsW zuM=$K2jo~&J24i~*Gxeh29|xiP+LS&S_(Qp*cUW7j{9%i4|RfY!p?EvEYlaiZXq!> z5nUf~2Ou!mzi}6i>^LCwDwW%?bDXu)|63RTqAe#nE(SM^=tIgOelI(wn3oHZ8PLhV zXPT&eQ&UuHT|a=iLcfdPNXI_67j@ zzx)tll(~c#2t7fA>1_V8V`ABS&JRQ;H(|G>%~zX4*!%NloIR*t8}0b;K%|x|1fEe2 zPCw9$)%4g|h;e5J@3CU!57j$0MXg&}k>iEke%GGm?(9Tc*|u!m_6k4L^%4G@db{4V6>xuEp`lFSo$c!4$iaNYqIw8 z_Os*HS^J6pCN$3w2|M#2=Z}=RP|$pV4B#}=yDAO(J9DR<-y^08e*3vsa8s2AXi=gH z9_c>>Z$I}67Jfb#Q{R}3_s749_s749S08%@x3{?k4b#e^L&dsyu+JUXzG15n5bjsG zt6`VleCh>suHFda9(Yvx-3wn2{J3B-zIcC{=ieVYc1g1pulD5ye?0LWjO;%I?aS4Y z{yJ8ugAqLj;>3|-fK($C+VHs^+>wiB&%?c4d!tRM>S$D;4Ei^}2~P|ef%ji}o%b32 zdwbl=7<>0axV?2(G)^mvF4Y?2;of)R%V{$_I3fIeFPi!JDu#yk#x@izQW(uT%KKcF zWnsyjdD8s=uxa&LSD1|L<7wKi3E8=PPW^BG(+BV$JI-h;lkN-uHU=#yUqPu zU-xgPPn;A1b{ap*Fbwp#!|l^Ap8d7_g9V&Fa~2zZmi-dLFwkvKKY#J`0_Xa(!$;xL z1$mAhii*O7&!!+QAwKYV!0x5o9QS%eC@SQ-??%!$1R~^9By>)W_uM3!28Lc|KmG3p zbsXo7R@X5YWjX@kId7*JZrak&l6|KBFP}FG+LYD_6#ralm%NTDuj7LIvog)rF%0^i z@S+_(On$c%8hjart}}@FnrOFf{X=;l$$z8#v^0Ek&oAxfk@+Tn3AVT+HAYWcN@JzY zl^P>AUZU0OkYCbnt8i9TA&Hn~oy?3dUyLA&aoihpu0ve^e(`2ZpXxqqT!ofxYv9Wz zPh;4K&WKNlA>JnO?ij;`w0L9Zdc8&!F?H6z@YIU~kuP7IZ~e1A`x#%)ULy++5hm6S z`MOH`gJBqW;f=dcxNwRh>{di{p~A_&K`+;???y%x@_Ty+ofJlE0m1!6ZKi@E?&5T zIWtxU^GzDpdWc9bMSR`AuAt|)ZaScSwqW5Dbno3vj1wzh9Fgk<(h~8(%tuhaQAJ+f z^pAeTfj{*NG8HNn^(o)22Qtz#wPn?+SDrCI(qo1~d+E8U?|!e%sA?DnUVdvB>NF@1 zx8C&DqpnOK&^CF%ol+Ccivi*_oCONhn4p3rA-`T56f z!XE%ze3I#tCoW+AoYmSg>Nco=mnPlqWF)Yp#ZyoxjQKkt8Mz$V44HUg!ca7BUe&XX z^z^Iv>Wfv19Lai@wKvGln2KDoMK`CpO8IQjR|C`uCH@@$g4(4Q>EffZ7|ESgQ{tc8 zUXO9A@Ph_PZr^$UYgg(s3$$uq8}~fiO|-{ppQ8ZyxL_o8(w`oGCn{Cdr<UYv9v3KmY$)-iSRLNUJi zF<_AV2+?kBF#rJmg}cK?`PeSeE?%xjW*?zneCmN3Y{B$Um_KhAECYn$d!%aZ`W0n4 zl>8ct`+uyy?96QJ-tPYLkio!Voq5Jx(JqG%d@@P>8~cq6!$9}`ZSdlMMj$HMULd=C z=?W&j^o6#ra+Qip{A9-$NqSpZJk$XVp5z(7_;HaIK=|WZ4)rUj|7LoRTOCdHl6O7e zddAKGqR5vif_nZwb`}fg=s$L;)u1xQ{pSG~2I&`atQu`U*SmyRciwBg2_ zuk3e6Z!KTA27m26qOGCN-Cfb=?k>chfQ(m2_Kq(~DZ#u23Z~$-=})3SA@2;XJ2vgb z&MkY1eekk35dOH98`QoEJ!)z*0fN^j?2&jsUvT-Y?s=ptFWg@t_I~?@-P$TzbZUgs z6-p4hWiZ1b<|WR!6UPMF@>Qu;c}$o!4rxV1W}`PAdrwsa{7 z5@e)b0S&SOly-ymvmeNQCDki%-dnaG!vAM}DAO~B=?p*}zp4Ht<4Agc;TDQbcItQe zmCyT~z89fE+W$cS03ZNKL_t*9_y^MiVjRnrzfCv8bC}QI5fFIdT5oGZl zBZm$)O<1`??%%qJ0G~^FtOqqX)wX@r+>>@xLTJ7`AY9*I(GNLO(v*-FYi&cW6X1e%bsqiQL-V zV>+1&O(NG8LQx?Me(ez?7t)W9@8A7W1aC_HNrFI~{@=B7E7E;@-e&!8LXEZ!xWC8r zZ^#J-743%;ti=2U6UD2RMc)_i_h&5QuPK6*q~LRMEHcyEhkyISS0*}-9Ej-XXeap1 z;OiNTra^#APZ@>*giH*WFcM|zRpDh_yPDzCHDCu< zE_>IVt%*Ut0?D{*%2P-vkO+3%amWc)Hc5NJWW9j%!C;x-Of>A@32jE`*YAFrwgB=v zm^gvMG!5)rs$akD^7t^6tY1Z{FYNF!%2b`gK0pM$v-VTJ0uDc@ zT_e*U+5KmRfzQt(qA9kkN9NT`todODK6`r#o`2{uOnLbYOnu{ROndWfd^vSG_HN$+ zzN{tOI*F$KCByK*Hxi8Nz&bYk&wyH^Ukf9n5A68^%NBlzk0-u^Pu`r2kKcS79}_%d z@(1{F{v!ObV<&j#C;;4#S7_DGmubb%cQD9xNw6dgiS9P2uN@pqVDm5QaQcLIX4*wx zeC7JB`yG0J@kT(A62)D==`v>K=Hy`Uta+~A8@6ta`~^kwqr%0DqIQ!8uKO0woD(R& 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[EceEzsanP;HaMfaR<-gAE4Ah-XvY5Zwmd)g!^ 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

{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_table\_default\_permission](#input\_create\_table\_default\_permission) | Creates a set of default permissions on the table for principals | `any` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [glue\_iam\_component\_name](#input\_glue\_iam\_component\_name) | Glue IAM component name. Used to get the Glue IAM role from the remote state | `string` | `"glue/iam"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lakeformation\_permissions](#input\_lakeformation\_permissions) | List of permissions granted to the principal. Refer to https://docs.aws.amazon.com/lake-formation/latest/dg/lf-permissions-reference.html for more details | `list(string)` |
[
"ALL"
]
| no | +| [lakeformation\_permissions\_enabled](#input\_lakeformation\_permissions\_enabled) | Whether to enable adding Lake Formation permissions to the IAM role that is used to access the Glue database | `bool` | `true` | no | +| [location\_uri](#input\_location\_uri) | Location of the database (for example, an HDFS path) | `string` | `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [parameters](#input\_parameters) | Map of key-value pairs that define parameters and properties of the database | `map(string)` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [target\_database](#input\_target\_database) | Configuration block for a target database for resource linking |
object({
# If `target_database` is provided (not `null`), all these fields are required
catalog_id = string
database_name = string
})
| `null` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [catalog\_database\_arn](#output\_catalog\_database\_arn) | Catalog database ARN | +| [catalog\_database\_id](#output\_catalog\_database\_id) | Catalog database ID | +| [catalog\_database\_name](#output\_catalog\_database\_name) | Catalog database name | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/catalog-database) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/catalog-database/context.tf b/modules/glue/catalog-database/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/catalog-database/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/catalog-database/main.tf b/modules/glue/catalog-database/main.tf new file mode 100644 index 000000000..98156179f --- /dev/null +++ b/modules/glue/catalog-database/main.tf @@ -0,0 +1,30 @@ +module "glue_catalog_database" { + source = "cloudposse/glue/aws//modules/glue-catalog-database" + version = "0.4.0" + + catalog_database_name = var.catalog_database_name + catalog_database_description = var.catalog_database_description + catalog_id = var.catalog_id + create_table_default_permission = var.create_table_default_permission + location_uri = var.location_uri + parameters = var.parameters + target_database = var.target_database + + context = module.this.context +} + +# Grant Lake Formation permissions to the Glue IAM role that is used to access the Glue database. +# This prevents the error: +# Error: error creating Glue crawler: InvalidInputException: Insufficient Lake Formation permission(s) on > (Service: AmazonDataCatalog; Status Code: 400; Error Code: AccessDeniedException +# https://aws.amazon.com/premiumsupport/knowledge-center/glue-insufficient-lakeformation-permissions +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lakeformation_permissions +resource "aws_lakeformation_permissions" "default" { + count = module.this.enabled && var.lakeformation_permissions_enabled ? 1 : 0 + + principal = module.glue_iam_role.outputs.role_arn + permissions = var.lakeformation_permissions + + database { + name = module.glue_catalog_database.name + } +} diff --git a/modules/glue/catalog-database/outputs.tf b/modules/glue/catalog-database/outputs.tf new file mode 100644 index 000000000..29e423401 --- /dev/null +++ b/modules/glue/catalog-database/outputs.tf @@ -0,0 +1,14 @@ +output "catalog_database_id" { + description = "Catalog database ID" + value = module.glue_catalog_database.id +} + +output "catalog_database_name" { + description = "Catalog database name" + value = module.glue_catalog_database.name +} + +output "catalog_database_arn" { + description = "Catalog database ARN" + value = module.glue_catalog_database.arn +} diff --git a/modules/glue/catalog-database/providers.tf b/modules/glue/catalog-database/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/catalog-database/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/catalog-database/remote-state.tf b/modules/glue/catalog-database/remote-state.tf new file mode 100644 index 000000000..040455b66 --- /dev/null +++ b/modules/glue/catalog-database/remote-state.tf @@ -0,0 +1,8 @@ +module "glue_iam_role" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_iam_component_name + + context = module.this.context +} diff --git a/modules/glue/catalog-database/variables.tf b/modules/glue/catalog-database/variables.tf new file mode 100644 index 000000000..e1d8e9c32 --- /dev/null +++ b/modules/glue/catalog-database/variables.tf @@ -0,0 +1,74 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "catalog_database_name" { + type = string + description = "Glue catalog database name. The acceptable characters are lowercase letters, numbers, and the underscore character" + default = null +} + +variable "catalog_database_description" { + type = string + description = "Glue catalog database description" + default = null +} + +variable "catalog_id" { + type = string + description = "ID of the Glue Catalog to create the database in. If omitted, this defaults to the AWS Account ID" + default = null +} + +variable "create_table_default_permission" { + # type = object({ + # permissions = list(string) + # principal = object({ + # data_lake_principal_identifier = string + # }) + # }) + type = any + description = "Creates a set of default permissions on the table for principals" + default = null +} + +variable "location_uri" { + type = string + description = "Location of the database (for example, an HDFS path)" + default = null +} + +variable "parameters" { + type = map(string) + description = "Map of key-value pairs that define parameters and properties of the database" + default = null +} + +variable "target_database" { + type = object({ + # If `target_database` is provided (not `null`), all these fields are required + catalog_id = string + database_name = string + }) + description = " Configuration block for a target database for resource linking" + default = null +} + +variable "glue_iam_component_name" { + type = string + description = "Glue IAM component name. Used to get the Glue IAM role from the remote state" + default = "glue/iam" +} + +variable "lakeformation_permissions_enabled" { + type = bool + description = "Whether to enable adding Lake Formation permissions to the IAM role that is used to access the Glue database" + default = true +} + +variable "lakeformation_permissions" { + type = list(string) + description = "List of permissions granted to the principal. Refer to https://docs.aws.amazon.com/lake-formation/latest/dg/lf-permissions-reference.html for more details" + default = ["ALL"] +} diff --git a/modules/glue/catalog-database/versions.tf b/modules/glue/catalog-database/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/catalog-database/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/catalog-table/README.md b/modules/glue/catalog-table/README.md new file mode 100644 index 000000000..1da4ce397 --- /dev/null +++ b/modules/glue/catalog-table/README.md @@ -0,0 +1,113 @@ +# Component: `glue/catalog-table` + +This component provisions Glue catalog tables. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/catalog-table/example: + metadata: + component: glue/catalog-table + vars: + enabled: true + name: example + catalog_table_description: Glue catalog table example + glue_iam_component_name: glue/iam + glue_catalog_database_component_name: glue/catalog-database/example + lakeformation_permissions_enabled: true + lakeformation_permissions: + - "ALL" + storage_descriptor: + location: "s3://awsglue-datasets/examples/medicare/Medicare_Hospital_Provider.csv" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_catalog\_database](#module\_glue\_catalog\_database) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [glue\_catalog\_table](#module\_glue\_catalog\_table) | cloudposse/glue/aws//modules/glue-catalog-table | 0.4.0 | +| [glue\_iam\_role](#module\_glue\_iam\_role) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_lakeformation_permissions.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lakeformation_permissions) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [catalog\_id](#input\_catalog\_id) | ID of the Glue Catalog and database to create the table in. If omitted, this defaults to the AWS Account ID plus the database name | `string` | `null` | no | +| [catalog\_table\_description](#input\_catalog\_table\_description) | Description of the table | `string` | `null` | no | +| [catalog\_table\_name](#input\_catalog\_table\_name) | Name of the table | `string` | `null` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [glue\_catalog\_database\_component\_name](#input\_glue\_catalog\_database\_component\_name) | Glue catalog database component name where the table metadata resides. Used to get the Glue catalog database from the remote state | `string` | n/a | yes | +| [glue\_iam\_component\_name](#input\_glue\_iam\_component\_name) | Glue IAM component name. Used to get the Glue IAM role from the remote state | `string` | `"glue/iam"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lakeformation\_permissions](#input\_lakeformation\_permissions) | List of permissions granted to the principal. Refer to https://docs.aws.amazon.com/lake-formation/latest/dg/lf-permissions-reference.html for more details | `list(string)` |
[
"ALL"
]
| no | +| [lakeformation\_permissions\_enabled](#input\_lakeformation\_permissions\_enabled) | Whether to enable adding Lake Formation permissions to the IAM role that is used to access the Glue table | `bool` | `true` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [owner](#input\_owner) | Owner of the table | `string` | `null` | no | +| [parameters](#input\_parameters) | Properties associated with this table, as a map of key-value pairs | `map(string)` | `null` | no | +| [partition\_index](#input\_partition\_index) | Configuration block for a maximum of 3 partition indexes |
object({
index_name = string
keys = list(string)
})
| `null` | no | +| [partition\_keys](#input\_partition\_keys) | Configuration block of columns by which the table is partitioned. Only primitive types are supported as partition keys | `map(string)` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [retention](#input\_retention) | Retention time for the table | `number` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [storage\_descriptor](#input\_storage\_descriptor) | Configuration block for information about the physical storage of this table | `any` | `null` | no | +| [table\_type](#input\_table\_type) | Type of this table (`EXTERNAL_TABLE`, `VIRTUAL_VIEW`, etc.). While optional, some Athena DDL queries such as `ALTER TABLE` and `SHOW CREATE TABLE` will fail if this argument is empty | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [target\_table](#input\_target\_table) | Configuration block of a target table for resource linking |
object({
catalog_id = string
database_name = string
name = string
})
| `null` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [view\_expanded\_text](#input\_view\_expanded\_text) | If the table is a view, the expanded text of the view; otherwise null | `string` | `null` | no | +| [view\_original\_text](#input\_view\_original\_text) | If the table is a view, the original text of the view; otherwise null | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [catalog\_table\_arn](#output\_catalog\_table\_arn) | Catalog table ARN | +| [catalog\_table\_id](#output\_catalog\_table\_id) | Catalog table ID | +| [catalog\_table\_name](#output\_catalog\_table\_name) | Catalog table name | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/catalog-table) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/catalog-table/context.tf b/modules/glue/catalog-table/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/catalog-table/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/catalog-table/main.tf b/modules/glue/catalog-table/main.tf new file mode 100644 index 000000000..8f6272159 --- /dev/null +++ b/modules/glue/catalog-table/main.tf @@ -0,0 +1,42 @@ +locals { + database_name = module.glue_catalog_database.outputs.catalog_database_name +} + +module "glue_catalog_table" { + source = "cloudposse/glue/aws//modules/glue-catalog-table" + version = "0.4.0" + + catalog_table_name = var.catalog_table_name + catalog_table_description = var.catalog_table_description + catalog_id = var.catalog_id + database_name = local.database_name + owner = var.owner + parameters = var.parameters + partition_index = var.partition_index + partition_keys = var.partition_keys + retention = var.retention + table_type = var.table_type + target_table = var.target_table + view_expanded_text = var.view_expanded_text + view_original_text = var.view_original_text + storage_descriptor = var.storage_descriptor + + context = module.this.context +} + +# Grant Lake Formation permissions to the Glue IAM role that is used to access the Glue table. +# This prevents the error: +# Error: error creating Glue crawler: InvalidInputException: Insufficient Lake Formation permission(s) on > (Service: AmazonDataCatalog; Status Code: 400; Error Code: AccessDeniedException +# https://aws.amazon.com/premiumsupport/knowledge-center/glue-insufficient-lakeformation-permissions +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lakeformation_permissions +resource "aws_lakeformation_permissions" "default" { + count = module.this.enabled && var.lakeformation_permissions_enabled ? 1 : 0 + + principal = module.glue_iam_role.outputs.role_arn + permissions = var.lakeformation_permissions + + table { + database_name = local.database_name + name = module.glue_catalog_table.name + } +} diff --git a/modules/glue/catalog-table/outputs.tf b/modules/glue/catalog-table/outputs.tf new file mode 100644 index 000000000..99a122b02 --- /dev/null +++ b/modules/glue/catalog-table/outputs.tf @@ -0,0 +1,14 @@ +output "catalog_table_id" { + description = "Catalog table ID" + value = module.glue_catalog_table.id +} + +output "catalog_table_name" { + description = "Catalog table name" + value = module.glue_catalog_table.name +} + +output "catalog_table_arn" { + description = "Catalog table ARN" + value = module.glue_catalog_table.arn +} diff --git a/modules/glue/catalog-table/providers.tf b/modules/glue/catalog-table/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/catalog-table/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/catalog-table/remote-state.tf b/modules/glue/catalog-table/remote-state.tf new file mode 100644 index 000000000..db6ac3ccf --- /dev/null +++ b/modules/glue/catalog-table/remote-state.tf @@ -0,0 +1,17 @@ +module "glue_iam_role" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_iam_component_name + + context = module.this.context +} + +module "glue_catalog_database" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_catalog_database_component_name + + context = module.this.context +} diff --git a/modules/glue/catalog-table/variables.tf b/modules/glue/catalog-table/variables.tf new file mode 100644 index 000000000..ceff37fbb --- /dev/null +++ b/modules/glue/catalog-table/variables.tf @@ -0,0 +1,186 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "catalog_table_name" { + type = string + description = "Name of the table" + default = null +} + +variable "catalog_table_description" { + type = string + description = "Description of the table" + default = null +} + +variable "catalog_id" { + type = string + description = "ID of the Glue Catalog and database to create the table in. If omitted, this defaults to the AWS Account ID plus the database name" + default = null +} + +variable "owner" { + type = string + description = "Owner of the table" + default = null +} + +variable "parameters" { + type = map(string) + description = "Properties associated with this table, as a map of key-value pairs" + default = null +} + +variable "partition_index" { + type = object({ + index_name = string + keys = list(string) + }) + description = "Configuration block for a maximum of 3 partition indexes" + default = null +} + +variable "partition_keys" { + # type = object({ + # comment = string + # name = string + # type = string + # }) + # Using `type = map(string)` since some of the the fields are optional and we don't want to force the caller to specify all of them and set to `null` those not used + type = map(string) + description = "Configuration block of columns by which the table is partitioned. Only primitive types are supported as partition keys" + default = null +} + +variable "retention" { + type = number + description = "Retention time for the table" + default = null +} + +variable "table_type" { + type = string + description = "Type of this table (`EXTERNAL_TABLE`, `VIRTUAL_VIEW`, etc.). While optional, some Athena DDL queries such as `ALTER TABLE` and `SHOW CREATE TABLE` will fail if this argument is empty" + default = null +} + +variable "target_table" { + type = object({ + catalog_id = string + database_name = string + name = string + }) + description = "Configuration block of a target table for resource linking" + default = null +} + +variable "view_expanded_text" { + type = string + description = "If the table is a view, the expanded text of the view; otherwise null" + default = null +} + +variable "view_original_text" { + type = string + description = "If the table is a view, the original text of the view; otherwise null" + default = null +} + +variable "storage_descriptor" { + # type = object({ + # # List of reducer grouping columns, clustering columns, and bucketing columns in the table + # bucket_columns = list(string) + # # Configuration block for columns in the table + # columns = list(object({ + # comment = string + # name = string + # parameters = map(string) + # type = string + # })) + # # Whether the data in the table is compressed + # compressed = bool + # # Input format: SequenceFileInputFormat (binary), or TextInputFormat, or a custom format + # input_format = string + # # Physical location of the table. By default this takes the form of the warehouse location, followed by the database location in the warehouse, followed by the table name + # location = string + # # Must be specified if the table contains any dimension columns + # number_of_buckets = number + # # Output format: SequenceFileOutputFormat (binary), or IgnoreKeyTextOutputFormat, or a custom format + # output_format = string + # # User-supplied properties in key-value form + # parameters = map(string) + # # Object that references a schema stored in the AWS Glue Schema Registry + # # When creating a table, you can pass an empty list of columns for the schema, and instead use a schema reference + # schema_reference = object({ + # # Configuration block that contains schema identity fields. Either this or the schema_version_id has to be provided + # schema_id = object({ + # # Name of the schema registry that contains the schema. Must be provided when schema_name is specified and conflicts with schema_arn + # registry_name = string + # # ARN of the schema. One of schema_arn or schema_name has to be provided + # schema_arn = string + # # Name of the schema. One of schema_arn or schema_name has to be provided + # schema_name = string + # }) + # # Unique ID assigned to a version of the schema. Either this or the schema_id has to be provided + # schema_version_id = string + # schema_version_number = number + # }) + # # Configuration block for serialization and deserialization ("SerDe") information + # ser_de_info = object({ + # # Name of the SerDe + # name = string + # # Map of initialization parameters for the SerDe, in key-value form + # parameters = map(string) + # # Usually the class that implements the SerDe. An example is org.apache.hadoop.hive.serde2.columnar.ColumnarSerDe + # serialization_library = string + # }) + # # Configuration block with information about values that appear very frequently in a column (skewed values) + # skewed_info = object({ + # # List of names of columns that contain skewed values + # skewed_column_names = list(string) + # # List of values that appear so frequently as to be considered skewed + # skewed_column_value_location_maps = list(string) + # # Map of skewed values to the columns that contain them + # skewed_column_values = map(string) + # }) + # # Configuration block for the sort order of each bucket in the table + # sort_columns = object({ + # # Name of the column + # column = string + # # Whether the column is sorted in ascending (1) or descending order (0) + # sort_order = number + # }) + # # Whether the table data is stored in subdirectories + # stored_as_sub_directories = bool + # }) + + # Using `type = any` since some of the the fields are optional and we don't want to force the caller to specify all of them and set to `null` those not used + type = any + description = "Configuration block for information about the physical storage of this table" + default = null +} + +variable "glue_iam_component_name" { + type = string + description = "Glue IAM component name. Used to get the Glue IAM role from the remote state" + default = "glue/iam" +} + +variable "glue_catalog_database_component_name" { + type = string + description = "Glue catalog database component name where the table metadata resides. Used to get the Glue catalog database from the remote state" +} + +variable "lakeformation_permissions_enabled" { + type = bool + description = "Whether to enable adding Lake Formation permissions to the IAM role that is used to access the Glue table" + default = true +} + +variable "lakeformation_permissions" { + type = list(string) + description = "List of permissions granted to the principal. Refer to https://docs.aws.amazon.com/lake-formation/latest/dg/lf-permissions-reference.html for more details" + default = ["ALL"] +} diff --git a/modules/glue/catalog-table/versions.tf b/modules/glue/catalog-table/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/catalog-table/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/connection/README.md b/modules/glue/connection/README.md new file mode 100644 index 000000000..3b7adff52 --- /dev/null +++ b/modules/glue/connection/README.md @@ -0,0 +1,122 @@ +# Component: `glue/connection` + +This component provisions Glue connections. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/connection/example/redshift: + metadata: + component: glue/connection + vars: + connection_name: "jdbc-redshift" + connection_description: "Glue Connection for Redshift" + connection_type: "JDBC" + db_type: "redshift" + connection_db_name: "analytics" + ssm_path_username: "/glue/redshift/admin_user" + ssm_path_password: "/glue/redshift/admin_password" + ssm_path_endpoint: "/glue/redshift/endpoint" + physical_connection_enabled: true + vpc_component_name: "vpc" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_connection](#module\_glue\_connection) | cloudposse/glue/aws//modules/glue-connection | 0.4.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.2.0 | +| [target\_security\_group](#module\_target\_security\_group) | cloudposse/security-group/aws | 2.2.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ssm_parameter.endpoint](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_subnet.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [catalog\_id](#input\_catalog\_id) | The ID of the Data Catalog in which to create the connection. If none is supplied, the AWS account ID is used by default | `string` | `null` | no | +| [connection\_db\_name](#input\_connection\_db\_name) | Database name that the Glue connector will reference | `string` | `null` | no | +| [connection\_description](#input\_connection\_description) | Connection description | `string` | `null` | no | +| [connection\_name](#input\_connection\_name) | Connection name. If not provided, the name will be generated from the context | `string` | `null` | no | +| [connection\_properties](#input\_connection\_properties) | A map of key-value pairs used as parameters for this connection | `map(string)` | `null` | no | +| [connection\_type](#input\_connection\_type) | The type of the connection. Supported are: JDBC, MONGODB, KAFKA, and NETWORK. Defaults to JDBC | `string` | n/a | yes | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [db\_type](#input\_db\_type) | Database type for the connection URL: `postgres` or `redshift` | `string` | `"redshift"` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [match\_criteria](#input\_match\_criteria) | A list of criteria that can be used in selecting this connection | `list(string)` | `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [physical\_connection\_enabled](#input\_physical\_connection\_enabled) | Flag to enable/disable physical connection | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [security\_group\_allow\_all\_egress](#input\_security\_group\_allow\_all\_egress) | A convenience that adds to the rules a rule that allows all egress.
If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. | `bool` | `true` | no | +| [security\_group\_create\_before\_destroy](#input\_security\_group\_create\_before\_destroy) | Set `true` to enable terraform `create_before_destroy` behavior on the created security group.
We only recommend setting this `false` if you are importing an existing security group
that you do not want replaced and therefore need full control over its name.
Note that changing this value will always cause the security group to be replaced. | `bool` | `true` | no | +| [security\_group\_ingress\_cidr\_blocks](#input\_security\_group\_ingress\_cidr\_blocks) | A list of CIDR blocks for the the cluster Security Group to allow ingress to the cluster security group | `list(string)` | `[]` | no | +| [security\_group\_ingress\_from\_port](#input\_security\_group\_ingress\_from\_port) | Start port on which the Glue connection accepts incoming connections | `number` | `0` | no | +| [security\_group\_ingress\_to\_port](#input\_security\_group\_ingress\_to\_port) | End port on which the Glue connection accepts incoming connections | `number` | `0` | no | +| [ssm\_path\_endpoint](#input\_ssm\_path\_endpoint) | Database endpoint SSM path | `string` | `null` | no | +| [ssm\_path\_password](#input\_ssm\_path\_password) | Database password SSM path | `string` | `null` | no | +| [ssm\_path\_username](#input\_ssm\_path\_username) | Database username SSM path | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [target\_security\_group\_rules](#input\_target\_security\_group\_rules) | Additional Security Group rules that allow Glue to communicate with the target database | `list(any)` | `[]` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | VPC component name | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [connection\_arn](#output\_connection\_arn) | Glue connection ARN | +| [connection\_id](#output\_connection\_id) | Glue connection ID | +| [connection\_name](#output\_connection\_name) | Glue connection name | +| [security\_group\_arn](#output\_security\_group\_arn) | The ARN of the Security Group associated with the Glue connection | +| [security\_group\_id](#output\_security\_group\_id) | The ID of the Security Group associated with the Glue connection | +| [security\_group\_name](#output\_security\_group\_name) | The name of the Security Group and associated with the Glue connection | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/connection) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/connection/context.tf b/modules/glue/connection/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/connection/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/connection/main.tf b/modules/glue/connection/main.tf new file mode 100644 index 000000000..8fc89c110 --- /dev/null +++ b/modules/glue/connection/main.tf @@ -0,0 +1,48 @@ +locals { + enabled = module.this.enabled + + physical_connection_enabled = local.enabled && var.physical_connection_enabled + + subnet_id = local.physical_connection_enabled ? module.vpc.outputs.private_subnet_ids[0] : null + + availability_zone = local.physical_connection_enabled ? data.aws_subnet.selected[0].availability_zone : null + + physical_connection_requirements = local.physical_connection_enabled ? { + # List of security group IDs used by the connection + security_group_id_list = [module.security_group.id] + # The availability zone of the connection. This field is redundant and implied by subnet_id, but is currently an API requirement + availability_zone = local.availability_zone + # The subnet ID used by the connection + subnet_id = local.subnet_id + } : null + + username = one(data.aws_ssm_parameter.user.*.value) + password = one(data.aws_ssm_parameter.password.*.value) + endpoint = one(data.aws_ssm_parameter.endpoint.*.value) +} + +data "aws_subnet" "selected" { + count = local.physical_connection_enabled ? 1 : 0 + + id = local.subnet_id +} + +module "glue_connection" { + source = "cloudposse/glue/aws//modules/glue-connection" + version = "0.4.0" + + connection_name = var.connection_name + connection_description = var.connection_description + catalog_id = var.catalog_id + connection_type = var.connection_type + match_criteria = var.match_criteria + physical_connection_requirements = local.physical_connection_requirements + + connection_properties = { + JDBC_CONNECTION_URL = "jdbc:${var.db_type}://${local.endpoint}/${var.connection_db_name}" + USERNAME = local.username + PASSWORD = local.password + } + + context = module.this.context +} diff --git a/modules/glue/connection/outputs.tf b/modules/glue/connection/outputs.tf new file mode 100644 index 000000000..7f2af22c3 --- /dev/null +++ b/modules/glue/connection/outputs.tf @@ -0,0 +1,29 @@ +output "connection_id" { + description = "Glue connection ID" + value = module.glue_connection.id +} + +output "connection_name" { + description = "Glue connection name" + value = module.glue_connection.name +} + +output "connection_arn" { + description = "Glue connection ARN" + value = module.glue_connection.arn +} + +output "security_group_id" { + description = "The ID of the Security Group associated with the Glue connection" + value = module.security_group.id +} + +output "security_group_arn" { + description = "The ARN of the Security Group associated with the Glue connection" + value = module.security_group.arn +} + +output "security_group_name" { + description = "The name of the Security Group and associated with the Glue connection" + value = module.security_group.name +} diff --git a/modules/glue/connection/providers.tf b/modules/glue/connection/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/connection/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/connection/remote-state.tf b/modules/glue/connection/remote-state.tf new file mode 100644 index 000000000..0238c5950 --- /dev/null +++ b/modules/glue/connection/remote-state.tf @@ -0,0 +1,15 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.vpc_component_name + + bypass = !local.physical_connection_enabled + + defaults = { + private_subnet_ids = [] + vpc_id = null + } + + context = module.this.context +} diff --git a/modules/glue/connection/sg.tf b/modules/glue/connection/sg.tf new file mode 100644 index 000000000..1bce26c0c --- /dev/null +++ b/modules/glue/connection/sg.tf @@ -0,0 +1,42 @@ +locals { + ingress_cidr_blocks_enabled = local.physical_connection_enabled && var.security_group_ingress_cidr_blocks != null && length(var.security_group_ingress_cidr_blocks) > 0 + + rules = local.ingress_cidr_blocks_enabled ? [ + { + type = "ingress" + from_port = var.security_group_ingress_from_port + to_port = var.security_group_ingress_to_port + protocol = "all" + cidr_blocks = var.security_group_ingress_cidr_blocks + } + ] : [] +} + +module "security_group" { + source = "cloudposse/security-group/aws" + version = "2.2.0" + + enabled = local.physical_connection_enabled + + vpc_id = module.vpc.outputs.vpc_id + create_before_destroy = var.security_group_create_before_destroy + allow_all_egress = var.security_group_allow_all_egress + rules = local.rules + + context = module.this.context +} + +# This allows adding the necessary Security Group rules for Glue to communicate with Redshift +module "target_security_group" { + source = "cloudposse/security-group/aws" + version = "2.2.0" + + enabled = local.enabled && var.target_security_group_rules != null && length(var.target_security_group_rules) > 0 + + vpc_id = module.vpc.outputs.vpc_id + security_group_name = [module.security_group.name] + target_security_group_id = [module.security_group.id] + rules = var.target_security_group_rules + + context = module.this.context +} diff --git a/modules/glue/connection/ssm.tf b/modules/glue/connection/ssm.tf new file mode 100644 index 000000000..7397013b9 --- /dev/null +++ b/modules/glue/connection/ssm.tf @@ -0,0 +1,17 @@ +data "aws_ssm_parameter" "endpoint" { + count = local.enabled && var.ssm_path_endpoint != null ? 1 : 0 + + name = var.ssm_path_endpoint +} + +data "aws_ssm_parameter" "user" { + count = local.enabled && var.ssm_path_username != null ? 1 : 0 + + name = var.ssm_path_username +} + +data "aws_ssm_parameter" "password" { + count = local.enabled && var.ssm_path_password != null ? 1 : 0 + + name = var.ssm_path_password +} diff --git a/modules/glue/connection/variables.tf b/modules/glue/connection/variables.tf new file mode 100644 index 000000000..6a3c44cff --- /dev/null +++ b/modules/glue/connection/variables.tf @@ -0,0 +1,129 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "vpc_component_name" { + type = string + description = "VPC component name" +} + +variable "connection_name" { + type = string + description = "Connection name. If not provided, the name will be generated from the context" + default = null +} + +variable "connection_description" { + type = string + description = "Connection description" + default = null +} + +variable "catalog_id" { + type = string + description = "The ID of the Data Catalog in which to create the connection. If none is supplied, the AWS account ID is used by default" + default = null +} + +variable "connection_type" { + type = string + description = "The type of the connection. Supported are: JDBC, MONGODB, KAFKA, and NETWORK. Defaults to JDBC" + + validation { + condition = contains(["JDBC", "MONGODB", "KAFKA", "NETWORK"], var.connection_type) + error_message = "Supported are: JDBC, MONGODB, KAFKA, and NETWORK" + } +} + +variable "connection_properties" { + type = map(string) + description = "A map of key-value pairs used as parameters for this connection" + default = null +} + +variable "match_criteria" { + type = list(string) + description = "A list of criteria that can be used in selecting this connection" + default = null +} + +variable "security_group_create_before_destroy" { + type = bool + description = <<-EOT + Set `true` to enable terraform `create_before_destroy` behavior on the created security group. + We only recommend setting this `false` if you are importing an existing security group + that you do not want replaced and therefore need full control over its name. + Note that changing this value will always cause the security group to be replaced. + EOT + default = true +} + +variable "security_group_allow_all_egress" { + type = bool + default = true + description = <<-EOT + A convenience that adds to the rules a rule that allows all egress. + If this is false and no egress rules are specified via `rules` or `rule-matrix`, then no egress will be allowed. + EOT +} + +variable "security_group_ingress_cidr_blocks" { + type = list(string) + default = [] + description = "A list of CIDR blocks for the the cluster Security Group to allow ingress to the cluster security group" +} + +variable "security_group_ingress_from_port" { + type = number + default = 0 + description = "Start port on which the Glue connection accepts incoming connections" +} + +variable "security_group_ingress_to_port" { + type = number + default = 0 + description = "End port on which the Glue connection accepts incoming connections" +} + +variable "physical_connection_enabled" { + type = bool + description = "Flag to enable/disable physical connection" + default = false +} + +variable "connection_db_name" { + type = string + description = "Database name that the Glue connector will reference" + default = null +} + +variable "ssm_path_username" { + type = string + description = "Database username SSM path" + default = null +} + +variable "ssm_path_password" { + type = string + description = "Database password SSM path" + default = null +} + +variable "ssm_path_endpoint" { + type = string + description = "Database endpoint SSM path" + default = null +} + +variable "target_security_group_rules" { + type = list(any) + description = "Additional Security Group rules that allow Glue to communicate with the target database" + default = [] +} + +variable "db_type" { + type = string + description = "Database type for the connection URL: `postgres` or `redshift`" + default = "redshift" +} diff --git a/modules/glue/connection/versions.tf b/modules/glue/connection/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/connection/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/crawler/README.md b/modules/glue/crawler/README.md new file mode 100644 index 000000000..b551ad2db --- /dev/null +++ b/modules/glue/crawler/README.md @@ -0,0 +1,115 @@ +# Component: `glue/crawler` + +This component provisions Glue crawlers. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + # The crawler crawls the data in an S3 bucket and puts the results into a table in the Glue Catalog. + # The crawler will read the first 2 MB of data from the file, and recognize the schema. + # After that, the crawler will sync the table. + glue/crawler/example: + metadata: + component: glue/crawler + vars: + enabled: true + name: example + crawler_description: "Glue crawler example" + glue_iam_component_name: "glue/iam" + glue_catalog_database_component_name: "glue/catalog-database/example" + glue_catalog_table_component_name: "glue/catalog-table/example" + schedule: "cron(0 1 * * ? *)" + schema_change_policy: + delete_behavior: LOG + update_behavior: null +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_catalog\_database](#module\_glue\_catalog\_database) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [glue\_catalog\_table](#module\_glue\_catalog\_table) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [glue\_crawler](#module\_glue\_crawler) | cloudposse/glue/aws//modules/glue-crawler | 0.4.0 | +| [glue\_iam\_role](#module\_glue\_iam\_role) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [catalog\_target](#input\_catalog\_target) | List of nested Glue catalog target arguments |
list(object({
database_name = string
tables = list(string)
}))
| `null` | no | +| [classifiers](#input\_classifiers) | List of custom classifiers. By default, all AWS classifiers are included in a crawl, but these custom classifiers always override the default classifiers for a given classification | `list(string)` | `null` | no | +| [configuration](#input\_configuration) | JSON string of configuration information | `string` | `null` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [crawler\_description](#input\_crawler\_description) | Glue crawler description | `string` | `null` | no | +| [crawler\_name](#input\_crawler\_name) | Glue crawler name. If not provided, the name will be generated from the context | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [delta\_target](#input\_delta\_target) | List of nested Delta target arguments |
list(object({
connection_name = string
delta_tables = list(string)
write_manifest = bool
}))
| `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dynamodb\_target](#input\_dynamodb\_target) | List of nested DynamoDB target arguments | `list(any)` | `null` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [glue\_catalog\_database\_component\_name](#input\_glue\_catalog\_database\_component\_name) | Glue catalog database component name where metadata resides. Used to get the Glue catalog database from the remote state | `string` | n/a | yes | +| [glue\_catalog\_table\_component\_name](#input\_glue\_catalog\_table\_component\_name) | Glue catalog table component name where metadata resides. Used to get the Glue catalog table from the remote state | `string` | `null` | no | +| [glue\_iam\_component\_name](#input\_glue\_iam\_component\_name) | Glue IAM component name. Used to get the Glue IAM role from the remote state | `string` | `"glue/iam"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [jdbc\_target](#input\_jdbc\_target) | List of nested JBDC target arguments | `list(any)` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lineage\_configuration](#input\_lineage\_configuration) | Specifies data lineage configuration settings for the crawler |
object({
crawler_lineage_settings = string
})
| `null` | no | +| [mongodb\_target](#input\_mongodb\_target) | List of nested MongoDB target arguments | `list(any)` | `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [recrawl\_policy](#input\_recrawl\_policy) | A policy that specifies whether to crawl the entire dataset again, or to crawl only folders that were added since the last crawler run |
object({
recrawl_behavior = string
})
| `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [s3\_target](#input\_s3\_target) | List of nested Amazon S3 target arguments | `list(any)` | `null` | no | +| [schedule](#input\_schedule) | A cron expression for the schedule | `string` | `null` | no | +| [schema\_change\_policy](#input\_schema\_change\_policy) | Policy for the crawler's update and deletion behavior | `map(string)` | `null` | no | +| [security\_configuration](#input\_security\_configuration) | The name of Security Configuration to be used by the crawler | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [table\_prefix](#input\_table\_prefix) | The table prefix used for catalog tables that are created | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [crawler\_arn](#output\_crawler\_arn) | Crawler ARN | +| [crawler\_id](#output\_crawler\_id) | Crawler ID | +| [crawler\_name](#output\_crawler\_name) | Crawler name | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/crawler) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/crawler/context.tf b/modules/glue/crawler/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/crawler/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/crawler/main.tf b/modules/glue/crawler/main.tf new file mode 100644 index 000000000..c4763f6dd --- /dev/null +++ b/modules/glue/crawler/main.tf @@ -0,0 +1,38 @@ +locals { + database_name = module.glue_catalog_database.outputs.catalog_database_name + table_name = module.glue_catalog_table.outputs.catalog_table_name + iam_role_arn = module.glue_iam_role.outputs.role_arn + + catalog_target = var.catalog_target != null ? var.catalog_target : [ + { + database_name = local.database_name + tables = [local.table_name] + } + ] +} + +module "glue_crawler" { + source = "cloudposse/glue/aws//modules/glue-crawler" + version = "0.4.0" + + crawler_name = var.crawler_name + crawler_description = var.crawler_description + database_name = local.database_name + role = local.iam_role_arn + schedule = var.schedule + classifiers = var.classifiers + configuration = var.configuration + jdbc_target = var.jdbc_target + dynamodb_target = var.dynamodb_target + s3_target = var.s3_target + mongodb_target = var.mongodb_target + catalog_target = local.catalog_target + delta_target = var.delta_target + table_prefix = var.table_prefix + security_configuration = var.security_configuration + schema_change_policy = var.schema_change_policy + lineage_configuration = var.lineage_configuration + recrawl_policy = var.recrawl_policy + + context = module.this.context +} diff --git a/modules/glue/crawler/outputs.tf b/modules/glue/crawler/outputs.tf new file mode 100644 index 000000000..36ec7dcc6 --- /dev/null +++ b/modules/glue/crawler/outputs.tf @@ -0,0 +1,14 @@ +output "crawler_id" { + description = "Crawler ID" + value = module.glue_crawler.id +} + +output "crawler_name" { + description = "Crawler name" + value = module.glue_crawler.name +} + +output "crawler_arn" { + description = "Crawler ARN" + value = module.glue_crawler.arn +} diff --git a/modules/glue/crawler/providers.tf b/modules/glue/crawler/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/crawler/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/crawler/remote-state.tf b/modules/glue/crawler/remote-state.tf new file mode 100644 index 000000000..1d90fec73 --- /dev/null +++ b/modules/glue/crawler/remote-state.tf @@ -0,0 +1,34 @@ +module "glue_iam_role" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_iam_component_name + + context = module.this.context +} + +module "glue_catalog_database" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_catalog_database_component_name + + context = module.this.context +} + +module "glue_catalog_table" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_catalog_table_component_name + + bypass = var.glue_catalog_table_component_name == null + + defaults = { + catalog_table_id = null + catalog_table_name = null + catalog_table_arn = null + } + + context = module.this.context +} diff --git a/modules/glue/crawler/variables.tf b/modules/glue/crawler/variables.tf new file mode 100644 index 000000000..75988d184 --- /dev/null +++ b/modules/glue/crawler/variables.tf @@ -0,0 +1,165 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "crawler_name" { + type = string + description = "Glue crawler name. If not provided, the name will be generated from the context" + default = null +} + +variable "crawler_description" { + type = string + description = "Glue crawler description" + default = null +} + +variable "schedule" { + type = string + description = "A cron expression for the schedule" + default = null +} + +variable "classifiers" { + type = list(string) + description = "List of custom classifiers. By default, all AWS classifiers are included in a crawl, but these custom classifiers always override the default classifiers for a given classification" + default = null +} + +variable "configuration" { + type = string + description = "JSON string of configuration information" + default = null +} + +variable "jdbc_target" { + # type = list(object({ + # connection_name = string + # path = string + # exclusions = list(string) + # })) + + # Using `type = list(any)` since some of the the fields are optional and we don't want to force the caller to specify all of them and set to `null` those not used + type = list(any) + description = "List of nested JBDC target arguments" + default = null +} + +variable "dynamodb_target" { + # type = list(object({ + # path = string + # scan_all = bool + # scan_rate = number + # })) + + # Using `type = list(any)` since some of the the fields are optional and we don't want to force the caller to specify all of them and set to `null` those not used + type = list(any) + description = "List of nested DynamoDB target arguments" + default = null +} + +variable "s3_target" { + # type = list(object({ + # path = string + # connection_name = string + # exclusions = list(string) + # sample_size = number + # event_queue_arn = string + # dlq_event_queue_arn = string + # })) + + # Using `type = list(any)` since some of the the fields are optional and we don't want to force the caller to specify all of them and set to `null` those not used + type = list(any) + description = "List of nested Amazon S3 target arguments" + default = null +} + +variable "mongodb_target" { + # type = list(object({ + # connection_name = string + # path = string + # scan_all = bool + # })) + + # Using `type = list(any)` since some of the the fields are optional and we don't want to force the caller to specify all of them and set to `null` those not used + type = list(any) + description = "List of nested MongoDB target arguments" + default = null +} + +variable "catalog_target" { + type = list(object({ + database_name = string + tables = list(string) + })) + description = "List of nested Glue catalog target arguments" + default = null +} + +variable "delta_target" { + type = list(object({ + connection_name = string + delta_tables = list(string) + write_manifest = bool + })) + description = "List of nested Delta target arguments" + default = null +} + +variable "table_prefix" { + type = string + description = "The table prefix used for catalog tables that are created" + default = null +} + +variable "security_configuration" { + type = string + description = "The name of Security Configuration to be used by the crawler" + default = null +} + +variable "schema_change_policy" { + # type = object({ + # delete_behavior = string + # update_behavior = string + # }) + + # Using `type = map(string)` since some of the the fields are optional and we don't want to force the caller to specify all of them and set to `null` those not used + type = map(string) + description = "Policy for the crawler's update and deletion behavior" + default = null +} + +variable "lineage_configuration" { + type = object({ + crawler_lineage_settings = string + }) + description = "Specifies data lineage configuration settings for the crawler" + default = null +} + +variable "recrawl_policy" { + type = object({ + recrawl_behavior = string + }) + description = "A policy that specifies whether to crawl the entire dataset again, or to crawl only folders that were added since the last crawler run" + default = null +} + +variable "glue_iam_component_name" { + type = string + description = "Glue IAM component name. Used to get the Glue IAM role from the remote state" + default = "glue/iam" +} + +variable "glue_catalog_database_component_name" { + type = string + description = "Glue catalog database component name where metadata resides. Used to get the Glue catalog database from the remote state" +} + +variable "glue_catalog_table_component_name" { + type = string + description = "Glue catalog table component name where metadata resides. Used to get the Glue catalog table from the remote state" + default = null +} diff --git a/modules/glue/crawler/versions.tf b/modules/glue/crawler/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/crawler/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/iam/README.md b/modules/glue/iam/README.md new file mode 100644 index 000000000..737da185f --- /dev/null +++ b/modules/glue/iam/README.md @@ -0,0 +1,89 @@ +# Component: `glue/iam` + +This component provisions IAM roles for AWS Glue. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/iam: + metadata: + component: glue/iam + vars: + enabled: true + name: glue + iam_role_description: "Role for AWS Glue with access to EC2, S3, and Cloudwatch Logs" + iam_policy_description: "Policy for AWS Glue with access to EC2, S3, and Cloudwatch Logs" + iam_managed_policy_arns: + - "arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_role](#module\_iam\_role) | cloudposse/iam-role/aws | 0.19.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [iam\_managed\_policy\_arns](#input\_iam\_managed\_policy\_arns) | IAM managed policy ARNs | `list(string)` |
[
"arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole"
]
| no | +| [iam\_policy\_description](#input\_iam\_policy\_description) | Glue IAM policy description | `string` | `"Policy for AWS Glue with access to EC2, S3, and Cloudwatch Logs"` | no | +| [iam\_role\_description](#input\_iam\_role\_description) | Glue IAM role description | `string` | `"Role for AWS Glue with access to EC2, S3, and Cloudwatch Logs"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [role\_arn](#output\_role\_arn) | The ARN of the Glue role | +| [role\_id](#output\_role\_id) | The ID of the Glue role | +| [role\_name](#output\_role\_name) | The name of the Glue role | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/iam) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/iam/context.tf b/modules/glue/iam/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/iam/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/iam/main.tf b/modules/glue/iam/main.tf new file mode 100644 index 000000000..246071692 --- /dev/null +++ b/modules/glue/iam/main.tf @@ -0,0 +1,15 @@ +module "iam_role" { + source = "cloudposse/iam-role/aws" + version = "0.19.0" + + principals = { + "Service" = ["glue.amazonaws.com"] + } + + managed_policy_arns = var.iam_managed_policy_arns + role_description = var.iam_role_description + policy_description = var.iam_policy_description + policy_document_count = 0 + + context = module.this.context +} diff --git a/modules/glue/iam/outputs.tf b/modules/glue/iam/outputs.tf new file mode 100644 index 000000000..b48c6deb2 --- /dev/null +++ b/modules/glue/iam/outputs.tf @@ -0,0 +1,14 @@ +output "role_name" { + value = module.iam_role.name + description = "The name of the Glue role" +} + +output "role_id" { + value = module.iam_role.id + description = "The ID of the Glue role" +} + +output "role_arn" { + value = module.iam_role.arn + description = "The ARN of the Glue role" +} diff --git a/modules/glue/iam/providers.tf b/modules/glue/iam/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/iam/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/iam/variables.tf b/modules/glue/iam/variables.tf new file mode 100644 index 000000000..6b423894a --- /dev/null +++ b/modules/glue/iam/variables.tf @@ -0,0 +1,22 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "iam_role_description" { + type = string + description = "Glue IAM role description" + default = "Role for AWS Glue with access to EC2, S3, and Cloudwatch Logs" +} + +variable "iam_policy_description" { + type = string + description = "Glue IAM policy description" + default = "Policy for AWS Glue with access to EC2, S3, and Cloudwatch Logs" +} + +variable "iam_managed_policy_arns" { + type = list(string) + description = "IAM managed policy ARNs" + default = ["arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole"] +} diff --git a/modules/glue/iam/versions.tf b/modules/glue/iam/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/iam/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/job/README.md b/modules/glue/job/README.md new file mode 100644 index 000000000..b81d700df --- /dev/null +++ b/modules/glue/job/README.md @@ -0,0 +1,122 @@ +# Component: `glue/job` + +This component provisions Glue jobs. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/job/example: + metadata: + component: glue/job + vars: + enabled: true + name: example + job_description: Glue job example + glue_version: "2.0" + worker_type: Standard + number_of_workers: 2 + max_retries: 2 + timeout: 20 + glue_iam_component_name: "glue/iam" + glue_job_s3_bucket_component_name: "s3/datalake" + glue_job_s3_bucket_script_path: "glue/glue_job.py" + glue_job_command_name: glueetl + glue_job_command_python_version: 3 +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_iam\_role](#module\_glue\_iam\_role) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [glue\_job](#module\_glue\_job) | cloudposse/glue/aws//modules/glue-job | 0.4.0 | +| [glue\_job\_s3\_bucket](#module\_glue\_job\_s3\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.glue_job_aws_tools_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role_policy_attachment.glue_jobs_aws_tools_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.glue_redshift_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_policy_document.glue_job_aws_tools_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [command](#input\_command) | The command of the job | `map(any)` | `null` | no | +| [connections](#input\_connections) | The list of connections used for this job | `list(string)` | `null` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [default\_arguments](#input\_default\_arguments) | The map of default arguments for the job. You can specify arguments here that your own job-execution script consumes, as well as arguments that AWS Glue itself consumes | `map(string)` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [execution\_property](#input\_execution\_property) | Execution property of the job |
object({
# The maximum number of concurrent runs allowed for the job. The default is 1.
max_concurrent_runs = number
})
| `null` | no | +| [glue\_iam\_component\_name](#input\_glue\_iam\_component\_name) | Glue IAM component name. Used to get the Glue IAM role from the remote state | `string` | `"glue/iam"` | no | +| [glue\_job\_command\_name](#input\_glue\_job\_command\_name) | The name of the job command. Defaults to glueetl. Use pythonshell for Python Shell Job Type, or gluestreaming for Streaming Job Type. max\_capacity needs to be set if pythonshell is chosen | `string` | `"glueetl"` | no | +| [glue\_job\_command\_python\_version](#input\_glue\_job\_command\_python\_version) | The Python version being used to execute a Python shell job. Allowed values are 2, 3 or 3.9. Version 3 refers to Python 3.6 | `number` | `3` | no | +| [glue\_job\_s3\_bucket\_component\_name](#input\_glue\_job\_s3\_bucket\_component\_name) | Glue job S3 bucket component name. Used to get the remote state of the S3 bucket where the Glue job script is located | `string` | `null` | no | +| [glue\_job\_s3\_bucket\_script\_path](#input\_glue\_job\_s3\_bucket\_script\_path) | Glue job script path in the S3 bucket | `string` | `null` | no | +| [glue\_version](#input\_glue\_version) | The version of Glue to use | `string` | `"2.0"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [job\_description](#input\_job\_description) | Glue job description | `string` | `null` | no | +| [job\_name](#input\_job\_name) | Glue job name. If not provided, the name will be generated from the context | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [max\_capacity](#input\_max\_capacity) | The maximum number of AWS Glue data processing units (DPUs) that can be allocated when the job runs. Required when `pythonshell` is set, accept either 0.0625 or 1.0. Use `number_of_workers` and `worker_type` arguments instead with `glue_version` 2.0 and above | `number` | `null` | no | +| [max\_retries](#input\_max\_retries) | The maximum number of times to retry the job if it fails | `number` | `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [non\_overridable\_arguments](#input\_non\_overridable\_arguments) | Non-overridable arguments for this job, specified as name-value pairs | `map(string)` | `null` | no | +| [notification\_property](#input\_notification\_property) | Notification property of the job |
object({
# After a job run starts, the number of minutes to wait before sending a job run delay notification
notify_delay_after = number
})
| `null` | no | +| [number\_of\_workers](#input\_number\_of\_workers) | The number of workers of a defined `worker_type` that are allocated when a job runs | `number` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [security\_configuration](#input\_security\_configuration) | The name of the Security Configuration to be associated with the job | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | The job timeout in minutes. The default is 2880 minutes (48 hours) for `glueetl` and `pythonshell` jobs, and `null` (unlimted) for `gluestreaming` jobs | `number` | `2880` | no | +| [worker\_type](#input\_worker\_type) | The type of predefined worker that is allocated when a job runs. Accepts a value of `Standard`, `G.1X`, or `G.2X` | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [job\_arn](#output\_job\_arn) | Glue job ARN | +| [job\_id](#output\_job\_id) | Glue job ID | +| [job\_name](#output\_job\_name) | Glue job name | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/job) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/job/context.tf b/modules/glue/job/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/job/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/job/iam.tf b/modules/glue/job/iam.tf new file mode 100644 index 000000000..94c3f1725 --- /dev/null +++ b/modules/glue/job/iam.tf @@ -0,0 +1,78 @@ +data "aws_iam_policy_document" "glue_job_aws_tools_access" { + count = local.enabled ? 1 : 0 + + statement { + sid = "S3BucketAccess" + effect = "Allow" + actions = [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "S3:GetBucketAcl", + "s3:PutObjectAcl" + ] + resources = ["*"] + } + + statement { + sid = "ParamStoreReadAccess" + effect = "Allow" + actions = [ + "ssm:GetParameter" + ] + resources = ["*"] + } + + statement { + sid = "SecretsManagerReadAccess" + effect = "Allow" + actions = [ + "secretsmanager:GetResourcePolicy", + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + "secretsmanager:ListSecretVersionIds", + "secretsmanager:ListSecrets" + ] + resources = ["*"] + } + + statement { + sid = "DynamoDBTableAccess" + effect = "Allow" + actions = [ + "dynamodb:BatchGetItem", + "dynamodb:BatchWriteItem", + "dynamodb:ConditionCheckItem", + "dynamodb:PutItem", + "dynamodb:DescribeTable", + "dynamodb:DeleteItem", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:Query", + "dynamodb:UpdateItem" + ] + resources = ["*"] + } +} + +resource "aws_iam_policy" "glue_job_aws_tools_access" { + count = local.enabled ? 1 : 0 + + name = "${module.this.id}-custom-access" + description = "Policy for Glue jobs to interact with S3 buckets, SSM, Systems Manager Parameter Store, DynamoDB tables and Lambda Functions" + policy = one(data.aws_iam_policy_document.glue_job_aws_tools_access.*.json) +} + +resource "aws_iam_role_policy_attachment" "glue_jobs_aws_tools_access" { + count = local.enabled ? 1 : 0 + + role = local.glue_iam_role_name + policy_arn = one(aws_iam_policy.glue_job_aws_tools_access.*.arn) +} + +resource "aws_iam_role_policy_attachment" "glue_redshift_access" { + count = local.enabled ? 1 : 0 + + role = local.glue_iam_role_name + policy_arn = "arn:aws:iam::aws:policy/AmazonRedshiftFullAccess" +} diff --git a/modules/glue/job/main.tf b/modules/glue/job/main.tf new file mode 100644 index 000000000..337c2d240 --- /dev/null +++ b/modules/glue/job/main.tf @@ -0,0 +1,36 @@ +locals { + enabled = module.this.enabled + + glue_iam_role_arn = module.glue_iam_role.outputs.role_arn + glue_iam_role_name = module.glue_iam_role.outputs.role_name + + command = var.command != null ? var.command : { + name = var.glue_job_command_name + script_location = format("s3://%s/%s", module.glue_job_s3_bucket.outputs.bucket_id, var.glue_job_s3_bucket_script_path) + python_version = var.glue_job_command_python_version + } +} + +module "glue_job" { + source = "cloudposse/glue/aws//modules/glue-job" + version = "0.4.0" + + job_name = var.job_name + job_description = var.job_description + role_arn = local.glue_iam_role_arn + connections = var.connections + glue_version = var.glue_version + default_arguments = var.default_arguments + non_overridable_arguments = var.non_overridable_arguments + security_configuration = var.security_configuration + timeout = var.timeout + max_capacity = var.max_capacity + max_retries = var.max_retries + worker_type = var.worker_type + number_of_workers = var.number_of_workers + command = local.command + execution_property = var.execution_property + notification_property = var.notification_property + + context = module.this.context +} diff --git a/modules/glue/job/outputs.tf b/modules/glue/job/outputs.tf new file mode 100644 index 000000000..5c6e7647a --- /dev/null +++ b/modules/glue/job/outputs.tf @@ -0,0 +1,14 @@ +output "job_id" { + description = "Glue job ID" + value = module.glue_job.id +} + +output "job_name" { + description = "Glue job name" + value = module.glue_job.name +} + +output "job_arn" { + description = "Glue job ARN" + value = module.glue_job.arn +} diff --git a/modules/glue/job/providers.tf b/modules/glue/job/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/job/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/job/remote-state.tf b/modules/glue/job/remote-state.tf new file mode 100644 index 000000000..e69f398dc --- /dev/null +++ b/modules/glue/job/remote-state.tf @@ -0,0 +1,26 @@ +module "glue_iam_role" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_iam_component_name + + context = module.this.context +} + +module "glue_job_s3_bucket" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_job_s3_bucket_component_name + bypass = var.glue_job_s3_bucket_component_name == null + + defaults = { + bucket_id = null + bucket_arn = null + bucket_domain_name = null + bucket_regional_domain_name = null + bucket_region = null + } + + context = module.this.context +} diff --git a/modules/glue/job/variables.tf b/modules/glue/job/variables.tf new file mode 100644 index 000000000..8a26521fb --- /dev/null +++ b/modules/glue/job/variables.tf @@ -0,0 +1,142 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "job_name" { + type = string + description = "Glue job name. If not provided, the name will be generated from the context" + default = null +} + +variable "job_description" { + type = string + description = "Glue job description" + default = null +} + +variable "connections" { + type = list(string) + description = "The list of connections used for this job" + default = null +} + +variable "glue_version" { + type = string + description = "The version of Glue to use" + default = "2.0" +} + +variable "default_arguments" { + type = map(string) + description = "The map of default arguments for the job. You can specify arguments here that your own job-execution script consumes, as well as arguments that AWS Glue itself consumes" + default = null +} + +variable "non_overridable_arguments" { + type = map(string) + description = "Non-overridable arguments for this job, specified as name-value pairs" + default = null +} + +variable "security_configuration" { + type = string + description = "The name of the Security Configuration to be associated with the job" + default = null +} + +variable "timeout" { + type = number + description = "The job timeout in minutes. The default is 2880 minutes (48 hours) for `glueetl` and `pythonshell` jobs, and `null` (unlimted) for `gluestreaming` jobs" + default = 2880 +} + +variable "max_capacity" { + type = number + description = "The maximum number of AWS Glue data processing units (DPUs) that can be allocated when the job runs. Required when `pythonshell` is set, accept either 0.0625 or 1.0. Use `number_of_workers` and `worker_type` arguments instead with `glue_version` 2.0 and above" + default = null +} + +variable "max_retries" { + type = number + description = " The maximum number of times to retry the job if it fails" + default = null +} + +variable "worker_type" { + type = string + description = "The type of predefined worker that is allocated when a job runs. Accepts a value of `Standard`, `G.1X`, or `G.2X`" + default = null +} + +variable "number_of_workers" { + type = number + description = "The number of workers of a defined `worker_type` that are allocated when a job runs" + default = null +} + +variable "command" { + # type = object({ + # # The name of the job command. Defaults to glueetl. + # # Use `pythonshell` for Python Shell Job Type, or `gluestreaming` for Streaming Job Type. + # # `max_capacity` needs to be set if `pythonshell` is chosen + # name = string + # # Specifies the S3 path to a script that executes the job + # script_location = string + # # The Python version being used to execute a Python shell job. Allowed values are 2 or 3 + # python_version = number + # }) + + # Using `type = map(any)` since some of the the fields are optional and we don't want to force the caller to specify all of them and set to `null` those not used + type = map(any) + description = "The command of the job" + default = null +} + +variable "execution_property" { + type = object({ + # The maximum number of concurrent runs allowed for the job. The default is 1. + max_concurrent_runs = number + }) + description = "Execution property of the job" + default = null +} + +variable "notification_property" { + type = object({ + # After a job run starts, the number of minutes to wait before sending a job run delay notification + notify_delay_after = number + }) + description = "Notification property of the job" + default = null +} + +variable "glue_iam_component_name" { + type = string + description = "Glue IAM component name. Used to get the Glue IAM role from the remote state" + default = "glue/iam" +} + +variable "glue_job_s3_bucket_component_name" { + type = string + description = "Glue job S3 bucket component name. Used to get the remote state of the S3 bucket where the Glue job script is located" + default = null +} + +variable "glue_job_s3_bucket_script_path" { + type = string + description = "Glue job script path in the S3 bucket" + default = null +} + +variable "glue_job_command_name" { + type = string + description = "The name of the job command. Defaults to glueetl. Use pythonshell for Python Shell Job Type, or gluestreaming for Streaming Job Type. max_capacity needs to be set if pythonshell is chosen" + default = "glueetl" +} + +variable "glue_job_command_python_version" { + type = number + description = "The Python version being used to execute a Python shell job. Allowed values are 2, 3 or 3.9. Version 3 refers to Python 3.6" + default = 3 +} diff --git a/modules/glue/job/versions.tf b/modules/glue/job/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/job/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/registry/README.md b/modules/glue/registry/README.md new file mode 100644 index 000000000..f87f9a8e7 --- /dev/null +++ b/modules/glue/registry/README.md @@ -0,0 +1,86 @@ +# Component: `glue/registry` + +This component provisions Glue registries. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/registry/example: + metadata: + component: glue/registry + vars: + enabled: true + name: example + registry_name: example + registry_description: "Glue registry example" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_registry](#module\_glue\_registry) | cloudposse/glue/aws//modules/glue-registry | 0.4.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [registry\_description](#input\_registry\_description) | Glue registry description | `string` | `null` | no | +| [registry\_name](#input\_registry\_name) | Glue registry name. If not provided, the name will be generated from the context | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [registry\_arn](#output\_registry\_arn) | Glue registry ARN | +| [registry\_id](#output\_registry\_id) | Glue registry ID | +| [registry\_name](#output\_registry\_name) | Glue registry name | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/registry) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/registry/context.tf b/modules/glue/registry/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/registry/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/registry/main.tf b/modules/glue/registry/main.tf new file mode 100644 index 000000000..ae4e7c00a --- /dev/null +++ b/modules/glue/registry/main.tf @@ -0,0 +1,9 @@ +module "glue_registry" { + source = "cloudposse/glue/aws//modules/glue-registry" + version = "0.4.0" + + registry_name = var.registry_name + registry_description = var.registry_description + + context = module.this.context +} diff --git a/modules/glue/registry/outputs.tf b/modules/glue/registry/outputs.tf new file mode 100644 index 000000000..b0a6b01e4 --- /dev/null +++ b/modules/glue/registry/outputs.tf @@ -0,0 +1,14 @@ +output "registry_id" { + description = "Glue registry ID" + value = module.glue_registry.id +} + +output "registry_name" { + description = "Glue registry name" + value = module.glue_registry.name +} + +output "registry_arn" { + description = "Glue registry ARN" + value = module.glue_registry.arn +} diff --git a/modules/glue/registry/providers.tf b/modules/glue/registry/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/registry/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/registry/variables.tf b/modules/glue/registry/variables.tf new file mode 100644 index 000000000..0f7216417 --- /dev/null +++ b/modules/glue/registry/variables.tf @@ -0,0 +1,16 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "registry_name" { + type = string + description = "Glue registry name. If not provided, the name will be generated from the context" + default = null +} + +variable "registry_description" { + type = string + description = "Glue registry description" + default = null +} diff --git a/modules/glue/registry/versions.tf b/modules/glue/registry/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/registry/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/schema/README.md b/modules/glue/schema/README.md new file mode 100644 index 000000000..1dc77e5b0 --- /dev/null +++ b/modules/glue/schema/README.md @@ -0,0 +1,97 @@ +# Component: `glue/schema` + +This component provisions Glue schemas. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/schema/example: + metadata: + component: glue/schema + vars: + enabled: true + name: example + schema_name: example + schema_description: "Glue schema example" + data_format: JSON + glue_registry_component_name: "glue/registry/example" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_registry](#module\_glue\_registry) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [glue\_schema](#module\_glue\_schema) | cloudposse/glue/aws//modules/glue-schema | 0.4.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [compatibility](#input\_compatibility) | The compatibility mode of the schema. Valid values are NONE, DISABLED, BACKWARD, BACKWARD\_ALL, FORWARD, FORWARD\_ALL, FULL, and FULL\_ALL | `string` | `"NONE"` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [data\_format](#input\_data\_format) | The data format of the schema definition. Valid values are `AVRO`, `JSON` and `PROTOBUF` | `string` | `"JSON"` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [glue\_registry\_component\_name](#input\_glue\_registry\_component\_name) | Glue registry component name. Used to get the Glue registry from the remote state | `string` | n/a | yes | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [schema\_definition](#input\_schema\_definition) | The schema definition using the `data_format` setting | `string` | `null` | no | +| [schema\_description](#input\_schema\_description) | Glue schema description | `string` | `null` | no | +| [schema\_name](#input\_schema\_name) | Glue schema name. If not provided, the name will be generated from the context | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [latest\_schema\_version](#output\_latest\_schema\_version) | The latest version of the schema associated with the returned schema definition | +| [next\_schema\_version](#output\_next\_schema\_version) | The next version of the schema associated with the returned schema definition | +| [registry\_name](#output\_registry\_name) | Glue registry name | +| [schema\_arn](#output\_schema\_arn) | Glue schema ARN | +| [schema\_checkpoint](#output\_schema\_checkpoint) | The version number of the checkpoint (the last time the compatibility mode was changed) | +| [schema\_id](#output\_schema\_id) | Glue schema ID | +| [schema\_name](#output\_schema\_name) | Glue schema name | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/schema) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/schema/context.tf b/modules/glue/schema/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/schema/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/schema/main.tf b/modules/glue/schema/main.tf new file mode 100644 index 000000000..aa836076f --- /dev/null +++ b/modules/glue/schema/main.tf @@ -0,0 +1,13 @@ +module "glue_schema" { + source = "cloudposse/glue/aws//modules/glue-schema" + version = "0.4.0" + + schema_name = var.schema_name + schema_description = var.schema_description + registry_arn = module.glue_registry.outputs.registry_arn + data_format = var.data_format + compatibility = var.compatibility + schema_definition = var.schema_definition + + context = module.this.context +} diff --git a/modules/glue/schema/outputs.tf b/modules/glue/schema/outputs.tf new file mode 100644 index 000000000..12cc3a666 --- /dev/null +++ b/modules/glue/schema/outputs.tf @@ -0,0 +1,34 @@ +output "schema_id" { + description = "Glue schema ID" + value = module.glue_schema.id +} + +output "schema_name" { + description = "Glue schema name" + value = module.glue_schema.name +} + +output "schema_arn" { + description = "Glue schema ARN" + value = module.glue_schema.arn +} + +output "registry_name" { + description = "Glue registry name" + value = module.glue_schema.registry_name +} + +output "latest_schema_version" { + description = "The latest version of the schema associated with the returned schema definition" + value = module.glue_schema.latest_schema_version +} + +output "next_schema_version" { + description = "The next version of the schema associated with the returned schema definition" + value = module.glue_schema.next_schema_version +} + +output "schema_checkpoint" { + description = "The version number of the checkpoint (the last time the compatibility mode was changed)" + value = module.glue_schema.schema_checkpoint +} diff --git a/modules/glue/schema/providers.tf b/modules/glue/schema/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/schema/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/schema/remote-state.tf b/modules/glue/schema/remote-state.tf new file mode 100644 index 000000000..51975c614 --- /dev/null +++ b/modules/glue/schema/remote-state.tf @@ -0,0 +1,8 @@ +module "glue_registry" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_registry_component_name + + context = module.this.context +} diff --git a/modules/glue/schema/variables.tf b/modules/glue/schema/variables.tf new file mode 100644 index 000000000..786f64504 --- /dev/null +++ b/modules/glue/schema/variables.tf @@ -0,0 +1,49 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "schema_name" { + type = string + description = "Glue schema name. If not provided, the name will be generated from the context" + default = null +} + +variable "schema_description" { + type = string + description = "Glue schema description" + default = null +} + +variable "data_format" { + type = string + description = "The data format of the schema definition. Valid values are `AVRO`, `JSON` and `PROTOBUF`" + default = "JSON" + + validation { + condition = contains(["AVRO", "JSON", "PROTOBUF"], var.data_format) + error_message = "Supported options are AVRO, JSON or PROTOBUF" + } +} + +variable "compatibility" { + type = string + description = "The compatibility mode of the schema. Valid values are NONE, DISABLED, BACKWARD, BACKWARD_ALL, FORWARD, FORWARD_ALL, FULL, and FULL_ALL" + default = "NONE" + + validation { + condition = contains(["NONE", "DISABLED", "BACKWARD", "BACKWARD_ALL", "FORWARD", "FORWARD_ALL", "FULL", "FULL_ALL"], var.compatibility) + error_message = "Supported options are NONE, DISABLED, BACKWARD, BACKWARD_ALL, FORWARD, FORWARD_ALL, FULL, and FULL_ALL" + } +} + +variable "schema_definition" { + type = string + description = "The schema definition using the `data_format` setting" + default = null +} + +variable "glue_registry_component_name" { + type = string + description = "Glue registry component name. Used to get the Glue registry from the remote state" +} diff --git a/modules/glue/schema/versions.tf b/modules/glue/schema/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/schema/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/trigger/README.md b/modules/glue/trigger/README.md new file mode 100644 index 000000000..88ba77a41 --- /dev/null +++ b/modules/glue/trigger/README.md @@ -0,0 +1,105 @@ +# Component: `glue/trigger` + +This component provisions Glue triggers. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/trigger/example: + metadata: + component: glue/trigger + vars: + enabled: true + name: example + trigger_name: example + trigger_description: "Glue trigger example" + glue_workflow_component_name: "glue/workflow/example" + glue_job_component_name: "glue/job/example" + glue_job_timeout: 10 + trigger_enabled: true + start_on_creation: true + schedule: "cron(15 12 * * ? *)" + type: SCHEDULED +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_job](#module\_glue\_job) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [glue\_trigger](#module\_glue\_trigger) | cloudposse/glue/aws//modules/glue-trigger | 0.4.0 | +| [glue\_workflow](#module\_glue\_workflow) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [actions](#input\_actions) | List of actions initiated by the trigger when it fires | `list(any)` | `null` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [event\_batching\_condition](#input\_event\_batching\_condition) | Batch condition that must be met (specified number of events received or batch time window expired) before EventBridge event trigger fires | `map(number)` | `null` | no | +| [glue\_job\_component\_name](#input\_glue\_job\_component\_name) | Glue workflow job name. Used to get the Glue job from the remote state | `string` | `null` | no | +| [glue\_job\_timeout](#input\_glue\_job\_timeout) | The job run timeout in minutes. It overrides the timeout value of the job | `number` | `null` | no | +| [glue\_workflow\_component\_name](#input\_glue\_workflow\_component\_name) | Glue workflow component name. Used to get the Glue workflow from the remote state | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [predicate](#input\_predicate) | A predicate to specify when the new trigger should fire. Required when trigger type is `CONDITIONAL` | `any` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [schedule](#input\_schedule) | Cron formatted schedule. Required for triggers with type `SCHEDULED` | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [start\_on\_creation](#input\_start\_on\_creation) | Set to `true` to start `SCHEDULED` and `CONDITIONAL` triggers when created. `true` is not supported for `ON_DEMAND` triggers | `bool` | `true` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [trigger\_description](#input\_trigger\_description) | Glue trigger description | `string` | `null` | no | +| [trigger\_enabled](#input\_trigger\_enabled) | Whether to start the created trigger | `bool` | `true` | no | +| [trigger\_name](#input\_trigger\_name) | Glue trigger name. If not provided, the name will be generated from the context | `string` | `null` | no | +| [type](#input\_type) | The type of trigger. Options are CONDITIONAL, SCHEDULED or ON\_DEMAND | `string` | `"CONDITIONAL"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [trigger\_arn](#output\_trigger\_arn) | Glue trigger ARN | +| [trigger\_id](#output\_trigger\_id) | Glue trigger ID | +| [trigger\_name](#output\_trigger\_name) | Glue trigger name | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/trigger) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/trigger/context.tf b/modules/glue/trigger/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/trigger/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/trigger/main.tf b/modules/glue/trigger/main.tf new file mode 100644 index 000000000..c6e9dc5c1 --- /dev/null +++ b/modules/glue/trigger/main.tf @@ -0,0 +1,27 @@ +locals { + actions = var.actions != null ? var.actions : [ + { + job_name = module.glue_job.outputs.job_name + # The job run timeout in minutes. It overrides the timeout value of the job + timeout = var.glue_job_timeout + } + ] +} + +module "glue_trigger" { + source = "cloudposse/glue/aws//modules/glue-trigger" + version = "0.4.0" + + trigger_name = var.trigger_name + trigger_description = var.trigger_description + workflow_name = module.glue_workflow.outputs.workflow_name + type = var.type + actions = local.actions + predicate = var.predicate + event_batching_condition = var.event_batching_condition + schedule = var.schedule + trigger_enabled = var.trigger_enabled + start_on_creation = var.start_on_creation + + context = module.this.context +} diff --git a/modules/glue/trigger/outputs.tf b/modules/glue/trigger/outputs.tf new file mode 100644 index 000000000..d850f4f6b --- /dev/null +++ b/modules/glue/trigger/outputs.tf @@ -0,0 +1,14 @@ +output "trigger_id" { + description = "Glue trigger ID" + value = module.glue_trigger.id +} + +output "trigger_name" { + description = "Glue trigger name" + value = module.glue_trigger.name +} + +output "trigger_arn" { + description = "Glue trigger ARN" + value = module.glue_trigger.arn +} diff --git a/modules/glue/trigger/providers.tf b/modules/glue/trigger/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/trigger/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/trigger/remote-state.tf b/modules/glue/trigger/remote-state.tf new file mode 100644 index 000000000..c5a1fcfc5 --- /dev/null +++ b/modules/glue/trigger/remote-state.tf @@ -0,0 +1,31 @@ +module "glue_workflow" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_workflow_component_name + bypass = var.glue_workflow_component_name == null + + defaults = { + workflow_id = null + workflow_name = null + workflow_arn = null + } + + context = module.this.context +} + +module "glue_job" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.glue_job_component_name + bypass = var.glue_job_component_name == null + + defaults = { + job_id = null + job_name = null + job_arn = null + } + + context = module.this.context +} diff --git a/modules/glue/trigger/variables.tf b/modules/glue/trigger/variables.tf new file mode 100644 index 000000000..39c7f5abc --- /dev/null +++ b/modules/glue/trigger/variables.tf @@ -0,0 +1,107 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "trigger_name" { + type = string + description = "Glue trigger name. If not provided, the name will be generated from the context" + default = null +} + +variable "trigger_description" { + type = string + description = "Glue trigger description" + default = null +} + +variable "type" { + type = string + description = "The type of trigger. Options are CONDITIONAL, SCHEDULED or ON_DEMAND" + default = "CONDITIONAL" + + validation { + condition = contains(["CONDITIONAL", "SCHEDULED", "ON_DEMAND"], var.type) + error_message = "Supported options are CONDITIONAL, SCHEDULED or ON_DEMAND" + } +} + +variable "predicate" { + # type = object({ + # # How to handle multiple conditions. Defaults to `AND`. Valid values are `AND` or `ANY` + # logical = string + # # Conditions for activating the trigger. Required for triggers where type is `CONDITIONAL` + # conditions = list(object({ + # job_name = string + # crawler_name = string + # state = string + # crawl_state = string + # logical_operator = string + # })) + # }) + type = any + description = "A predicate to specify when the new trigger should fire. Required when trigger type is `CONDITIONAL`" + default = null +} + +variable "event_batching_condition" { + # type = object({ + # batch_size = number + # batch_window = number + # }) + type = map(number) + description = "Batch condition that must be met (specified number of events received or batch time window expired) before EventBridge event trigger fires" + default = null +} + +variable "schedule" { + type = string + description = "Cron formatted schedule. Required for triggers with type `SCHEDULED`" + default = null +} + +variable "trigger_enabled" { + type = bool + description = "Whether to start the created trigger" + default = true +} + +variable "start_on_creation" { + type = bool + description = "Set to `true` to start `SCHEDULED` and `CONDITIONAL` triggers when created. `true` is not supported for `ON_DEMAND` triggers" + default = true +} + +variable "glue_workflow_component_name" { + type = string + description = "Glue workflow component name. Used to get the Glue workflow from the remote state" + default = null +} + +variable "glue_job_component_name" { + type = string + description = "Glue workflow job name. Used to get the Glue job from the remote state" + default = null +} + +variable "glue_job_timeout" { + type = number + description = "The job run timeout in minutes. It overrides the timeout value of the job" + default = null +} + +variable "actions" { + # type = list(object({ + # job_name = string + # crawler_name = string + # arguments = map(string) + # security_configuration = string + # notification_property = object({ + # notify_delay_after = number + # }) + # timeout = number + # })) + type = list(any) + description = "List of actions initiated by the trigger when it fires" + default = null +} diff --git a/modules/glue/trigger/versions.tf b/modules/glue/trigger/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/trigger/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/glue/workflow/README.md b/modules/glue/workflow/README.md new file mode 100644 index 000000000..77ebe4235 --- /dev/null +++ b/modules/glue/workflow/README.md @@ -0,0 +1,88 @@ +# Component: `glue/workflow` + +This component provisions Glue workflows. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/workflow/example: + metadata: + component: "glue/workflow" + vars: + enabled: true + name: example + workflow_name: example + workflow_description: "Glue workflow example" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_workflow](#module\_glue\_workflow) | cloudposse/glue/aws//modules/glue-workflow | 0.4.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [default\_run\_properties](#input\_default\_run\_properties) | A map of default run properties for this workflow. These properties are passed to all jobs associated to the workflow | `map(string)` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [max\_concurrent\_runs](#input\_max\_concurrent\_runs) | Maximum number of concurrent runs. If you leave this parameter blank, there is no limit to the number of concurrent workflow runs | `number` | `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [workflow\_description](#input\_workflow\_description) | Glue workflow description | `string` | `null` | no | +| [workflow\_name](#input\_workflow\_name) | Glue workflow name. If not provided, the name will be generated from the context | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [workflow\_arn](#output\_workflow\_arn) | Glue workflow ARN | +| [workflow\_id](#output\_workflow\_id) | Glue workflow ID | +| [workflow\_name](#output\_workflow\_name) | Glue workflow name | + + +## References + +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/workflow) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/glue/workflow/context.tf b/modules/glue/workflow/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/glue/workflow/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/glue/workflow/main.tf b/modules/glue/workflow/main.tf new file mode 100644 index 000000000..66499ae4d --- /dev/null +++ b/modules/glue/workflow/main.tf @@ -0,0 +1,11 @@ +module "glue_workflow" { + source = "cloudposse/glue/aws//modules/glue-workflow" + version = "0.4.0" + + workflow_name = var.workflow_name + workflow_description = var.workflow_description + default_run_properties = var.default_run_properties + max_concurrent_runs = var.max_concurrent_runs + + context = module.this.context +} diff --git a/modules/glue/workflow/outputs.tf b/modules/glue/workflow/outputs.tf new file mode 100644 index 000000000..d6b4779ad --- /dev/null +++ b/modules/glue/workflow/outputs.tf @@ -0,0 +1,14 @@ +output "workflow_id" { + description = "Glue workflow ID" + value = module.glue_workflow.id +} + +output "workflow_name" { + description = "Glue workflow name" + value = module.glue_workflow.name +} + +output "workflow_arn" { + description = "Glue workflow ARN" + value = module.glue_workflow.arn +} diff --git a/modules/glue/workflow/providers.tf b/modules/glue/workflow/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/glue/workflow/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/glue/workflow/variables.tf b/modules/glue/workflow/variables.tf new file mode 100644 index 000000000..5904f2b05 --- /dev/null +++ b/modules/glue/workflow/variables.tf @@ -0,0 +1,28 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "workflow_name" { + type = string + description = "Glue workflow name. If not provided, the name will be generated from the context" + default = null +} + +variable "workflow_description" { + type = string + description = "Glue workflow description" + default = null +} + +variable "default_run_properties" { + type = map(string) + description = "A map of default run properties for this workflow. These properties are passed to all jobs associated to the workflow" + default = null +} + +variable "max_concurrent_runs" { + type = number + description = "Maximum number of concurrent runs. If you leave this parameter blank, there is no limit to the number of concurrent workflow runs" + default = null +} diff --git a/modules/glue/workflow/versions.tf b/modules/glue/workflow/versions.tf new file mode 100644 index 000000000..f4c416ee6 --- /dev/null +++ b/modules/glue/workflow/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + utils = { + source = "cloudposse/utils" + version = ">= 1.15.0" + } + } +} diff --git a/modules/iam-role/README.md b/modules/iam-role/README.md index 2b75d8ff0..7a2c83e96 100644 --- a/modules/iam-role/README.md +++ b/modules/iam-role/README.md @@ -129,6 +129,6 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/iam-role) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/iam-role) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/iam-service-linked-roles/README.md b/modules/iam-service-linked-roles/README.md index 1c2302c99..f5236791c 100644 --- a/modules/iam-service-linked-roles/README.md +++ b/modules/iam-service-linked-roles/README.md @@ -110,6 +110,6 @@ For more details, see: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/iam-service-linked-roles) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/iam-service-linked-roles) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ipam/README.md b/modules/ipam/README.md index 2e9ea5f32..a26449820 100644 --- a/modules/ipam/README.md +++ b/modules/ipam/README.md @@ -122,7 +122,7 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/kinesis-stream/README.md b/modules/kinesis-stream/README.md index 5496e380a..72dc6734c 100644 --- a/modules/kinesis-stream/README.md +++ b/modules/kinesis-stream/README.md @@ -111,6 +111,6 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/kinesis-stream) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/kinesis-stream) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/kms/README.md b/modules/kms/README.md index b13c7b72c..dc8f3a404 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -85,7 +85,7 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/kms) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/kms) - Cloud Posse's upstream component * [cloudposse/terraform-aws-kms-key](https://github.com/cloudposse/terraform-aws-kms-key) - Cloud Posse's upstream module [](https://cpco.io/component) diff --git a/modules/lakeformation/README.md b/modules/lakeformation/README.md index 2ba4b509b..8c1278ed8 100644 --- a/modules/lakeformation/README.md +++ b/modules/lakeformation/README.md @@ -125,6 +125,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/lakeformation) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/lakeformation) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/lambda/README.md b/modules/lambda/README.md index 172c3327b..c61ed6261 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -188,6 +188,6 @@ components: ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/mq-broker/README.md b/modules/mq-broker/README.md index 134413ff0..2b7317c38 100644 --- a/modules/mq-broker/README.md +++ b/modules/mq-broker/README.md @@ -128,7 +128,7 @@ No resources. ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/mq-broker) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/mq-broker) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/mwaa/README.md b/modules/mwaa/README.md index 8b8cb1006..1cc7e069d 100644 --- a/modules/mwaa/README.md +++ b/modules/mwaa/README.md @@ -147,7 +147,7 @@ components: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/network-firewall/README.md b/modules/network-firewall/README.md index 2dedb4d13..f72db4820 100644 --- a/modules/network-firewall/README.md +++ b/modules/network-firewall/README.md @@ -323,6 +323,6 @@ No resources. - [How to deploy AWS Network Firewall by using AWS Firewall Manager](https://aws.amazon.com/blogs/security/how-to-deploy-aws-network-firewall-by-using-aws-firewall-manager) - [A Deep Dive into AWS Transit Gateway](https://www.youtube.com/watch?v=a55Iud-66q0) - [Appliance in a shared services VPC](https://docs.aws.amazon.com/vpc/latest/tgw/transit-gateway-appliance-scenario.html) -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index 7ed43d5ff..cdcd659ef 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -383,7 +383,7 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ - [How to Implement Incident Management with OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie) ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/opsgenie-team) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/opsgenie-team) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/philips-labs-github-runners/README.md b/modules/philips-labs-github-runners/README.md index b3533c185..aa45e22b5 100644 --- a/modules/philips-labs-github-runners/README.md +++ b/modules/philips-labs-github-runners/README.md @@ -142,6 +142,6 @@ This is output by the component, and available via the `webhook` output under `e ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecs) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/rds/README.md b/modules/rds/README.md index e4b44817c..9fb828b86 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -240,7 +240,7 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/rds) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/rds) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/redshift/README.md b/modules/redshift/README.md index cce5ca377..c0045301b 100644 --- a/modules/redshift/README.md +++ b/modules/redshift/README.md @@ -142,7 +142,7 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/redshift) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/redshift) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/route53-resolver-dns-firewall/README.md b/modules/route53-resolver-dns-firewall/README.md index bab5662f6..7206ca7e3 100644 --- a/modules/route53-resolver-dns-firewall/README.md +++ b/modules/route53-resolver-dns-firewall/README.md @@ -156,6 +156,6 @@ No resources. - [Appliance in a shared services VPC](https://docs.aws.amazon.com/vpc/latest/tgw/transit-gateway-appliance-scenario.html) - [Quotas on Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-entities-resolver) - [Unified bad hosts](https://github.com/StevenBlack/hosts) -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index 7c4182500..0647544c6 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -185,7 +185,7 @@ components: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/s3-bucket) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/s3-bucket) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ses/README.md b/modules/ses/README.md index 046ad6b89..4863a0b03 100644 --- a/modules/ses/README.md +++ b/modules/ses/README.md @@ -100,6 +100,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ses) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ses) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/snowflake-database/README.md b/modules/snowflake-database/README.md index 02ebd9beb..e2800c294 100644 --- a/modules/snowflake-database/README.md +++ b/modules/snowflake-database/README.md @@ -125,7 +125,7 @@ No outputs. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/snowflake-database) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/snowflake-database) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/sns-topic/README.md b/modules/sns-topic/README.md index 3fb0a61d9..95c975a88 100644 --- a/modules/sns-topic/README.md +++ b/modules/sns-topic/README.md @@ -146,6 +146,6 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/sns-topic) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sns-topic) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 9d8b7279f..ec7671c0c 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -232,7 +232,7 @@ an extensive explanation for how these preview environments work. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spa-s3-cloudfront) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/spa-s3-cloudfront) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index e13b58015..7727590ab 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -264,6 +264,6 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role ## References - [cloudposse/terraform-spacelift-cloud-infrastructure-automation](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation) - Cloud Posse's related upstream component -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spacelift-worker-pool) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/spacelift-worker-pool) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index 6ab9c7110..f7114ac04 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -91,6 +91,6 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/sqs-queue) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sqs-queue) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ssm-parameters/README.md b/modules/ssm-parameters/README.md index f41fd4a75..45c48d763 100644 --- a/modules/ssm-parameters/README.md +++ b/modules/ssm-parameters/README.md @@ -92,7 +92,7 @@ components: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ssm-parameters) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ssm-parameters) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 40100f0fe..3dd8e61c2 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -177,4 +177,4 @@ Here's an example snippet for how to use this component. ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/tfstate-backend) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tfstate-backend) - Cloud Posse's upstream component diff --git a/modules/tgw/cross-region-hub-connector/README.md b/modules/tgw/cross-region-hub-connector/README.md index f627badd4..5efbd3bb4 100644 --- a/modules/tgw/cross-region-hub-connector/README.md +++ b/modules/tgw/cross-region-hub-connector/README.md @@ -129,6 +129,6 @@ components: ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/tgw/cross-region-hub-connector) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw/cross-region-hub-connector) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 1c39d71d9..135e22281 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -148,6 +148,6 @@ No resources. ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/tgw/hub) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw/hub) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index ee096dfad..de63a4e58 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -167,6 +167,6 @@ No outputs. ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/tgw) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/vpc-flow-logs-bucket/README.md b/modules/vpc-flow-logs-bucket/README.md index e5eb20cfa..7ab0ca610 100644 --- a/modules/vpc-flow-logs-bucket/README.md +++ b/modules/vpc-flow-logs-bucket/README.md @@ -91,7 +91,7 @@ No resources. ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/vpc-flow-logs-bucket) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc-flow-logs-bucket) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/vpc-peering/README.md b/modules/vpc-peering/README.md index 670fdc5bd..a7cc67e65 100644 --- a/modules/vpc-peering/README.md +++ b/modules/vpc-peering/README.md @@ -250,6 +250,6 @@ atmos terraform apply vpc-peering -s ue1-prod | [vpc\_peering](#output\_vpc\_peering) | VPC peering outputs | -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/vpc-peering) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc-peering) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/vpc/README.md b/modules/vpc/README.md index d8d758578..a72901b3c 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -167,6 +167,6 @@ components: ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/vpc) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/waf/README.md b/modules/waf/README.md index 2fc72784c..3a2564dd9 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -131,7 +131,7 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/waf) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/waf) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index 20de9c78e..97843df14 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -108,6 +108,6 @@ import: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/zscaler) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/zscaler) - Cloud Posse's upstream component [](https://cpco.io/component) From b66bdf825208ac1629ec1e1c757af1c236ff2890 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 23 Jan 2024 10:46:54 -0800 Subject: [PATCH 349/501] `ecs-service` additional_targets var (#955) --- modules/ecs-service/README.md | 48 ++++++++++++++++++++++++++++++++ modules/ecs-service/main.tf | 2 +- modules/ecs-service/variables.tf | 6 ++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index d1ff00d8e..3f07c7fae 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -144,6 +144,53 @@ components: task_cpu: 256 ``` +#### Other Domains + +This component supports alternate service names for your ECS Service through a couple of variables: + - `vanity_domain` & `vanity_alias` - This will create a route to the service in the listener rules of the ALB. This will also create a Route 53 alias record in the hosted zone in this account. The hosted zone is looked up by the `vanity_domain` input. + - `additional_targets` - This will create a route to the service in the listener rules of the ALB. This will not create a Route 53 alias record. + +Examples: + +```yaml + ecs/platform/service/echo-server: + vars: + vanity_domain: "dev-acme.com" + vanity_alias: + - "echo-server.dev-acme.com" + additional_targets: + - "echo.acme.com" +``` + +This then creates the following listener rules: + +```text +HTTP Host Header is +echo-server.public-platform.use2.dev.plat.service-discovery.com + OR echo-server.dev-acme.com + OR echo.acme.com +``` + +It will also create the record in Route53 to point `"echo-server.dev-acme.com"` to the ALB. Thus `"echo-server.dev-acme.com"` should resolve. + +We can then create a pointer to this service in the `acme.come` hosted zone. + +```yaml + dns-primary: + vars: + domain_names: + - acme.com + record_config: + - root_zone: acme.com + name: echo. + type: CNAME + ttl: 60 + records: + - echo-server.dev-acme.com +``` + +This will create a CNAME record in the `acme.com` hosted zone that points `echo.acme.com` to `echo-server.dev-acme.com`. + ## Requirements @@ -219,6 +266,7 @@ components: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [additional\_targets](#input\_additional\_targets) | Additional target routes to add to the ALB that point to this service. The only difference between this and `var.vanity_alias` is `var.vanity_alias` will create an alias record in Route 53 in the hosted zone in this account as well. `var.additional_targets` only adds the listener route to this service's target group. | `list(string)` | `[]` | no | | [alb\_configuration](#input\_alb\_configuration) | The configuration to use for the ALB, specifying which cluster alb configuration to use | `string` | `"default"` | no | | [alb\_name](#input\_alb\_name) | The name of the ALB this service should attach to | `string` | `null` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 58f9aefbd..52efcf578 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -284,7 +284,7 @@ module "alb_ingress" { vpc_id = local.vpc_id unauthenticated_listener_arns = [local.lb_listener_https_arn] - unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", var.vanity_domain), local.full_domain] : concat([local.full_domain], var.vanity_alias) + unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", var.vanity_domain), local.full_domain] : concat([local.full_domain], var.vanity_alias, var.additional_targets) unauthenticated_paths = flatten(var.unauthenticated_paths) # When set to catch-all, make priority super high to make sure last to match unauthenticated_priority = var.lb_catch_all ? 99 : var.unauthenticated_priority diff --git a/modules/ecs-service/variables.tf b/modules/ecs-service/variables.tf index 95e1ed903..46204615a 100644 --- a/modules/ecs-service/variables.tf +++ b/modules/ecs-service/variables.tf @@ -196,6 +196,12 @@ variable "vanity_alias" { default = [] } +variable "additional_targets" { + type = list(string) + description = "Additional target routes to add to the ALB that point to this service. The only difference between this and `var.vanity_alias` is `var.vanity_alias` will create an alias record in Route 53 in the hosted zone in this account as well. `var.additional_targets` only adds the listener route to this service's target group." + default = [] +} + variable "kinesis_enabled" { type = bool description = "Enable Kinesis" From b5d7e9cfb7244f23a7dbaa7dad19c83a5d9e350d Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 25 Jan 2024 13:55:48 -0800 Subject: [PATCH 350/501] fix: `argocd-repo` Existing Repo Reference (#959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: screenshot-action 📷 --- .github/banner.png | Bin 1045919 -> 1032325 bytes modules/argocd-repo/main.tf | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/banner.png b/.github/banner.png index ede1e98e0d6e21b0ef2e6536ad113251313b2473..a045735ab731698b194b54d132abceda895b7587 100644 GIT binary patch literal 1032325 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≧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

RLu%Vf~w}2`VvW^zd?tNO4yMake>p8sxCF@ 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

+O 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%0AGCInY{CDg4~r^w-_S8#~yh;+voK+ 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̡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
L
z0*olN9+2m1s^U-OW;KeJRva-_zFi(VcV^pdnZJNrnX>GDCI^sbRK`YGyC
zau56Cbi_ex_V&!yi1ML|+szttYf7HE&wYfpOE=N7&Hu0>!r8@XbyBjGjC%q+irWBH
z6hU`oqZyDSBBWUEFgTCkN?74{^VF=v^SNO~mjPZ{^$w@7hvgeSHd$mJ)E?whnJ+mR
zO5c(U=d_zJo-u^B9Funycp!O}(z(90B-*-?-7ZWQH97f6Sav1$#ydq>HQK4TJ0#f|)IG&r!~nq0F4NB>i?u
z%H-&6@8$5%+L&fe{rf|)=a!kKT;PqIB@OyJx%t}&lqnXRPDPUaV^u7nt`zUR>P%6L
zLg&|(j&UUQEaCi)3xh`n6bucMt4NMLIRYkX<0(fh|3fi04#)D(L9AL28_cf>vtQ6q
zokJ}(8aL+qt1DApSX(emPQs?KQ*ZnZ=is)p-eHC~%2#BV<6EG%4wbs|@WAGWe7$Q8
z9Tmw-_S>98!R8#>eaETd&fnUk#-_R;1f$HOdZKeKuvtiwY&%ebN_Bjn5NfyUcds?O
zr6DA%>FMCCFX)?yyyPZQ)nIEZ?Yvq;CrpyjUx7+M9u|CbR&w|NZhkl359s?`w)NMR
z&z=zBlQ;_ud9>K@GHjkiCiO$d;Wh!!|o@E&_Nb?UZ%%>^w1=SMph{|PABgUGGlGa-*-0SC5s
zxa+TelLyf25e4~7{*_5%uAgO8*iH9_asc+XplQsx``@3A+#!{_m8>5bcMa39U-yU~
zk9~{)$+rk@5R%pQ!hak-%-U&XuCPVv5d#!N@8w=rLH*s7^DCKO9jj%xx312-alyaW
z+>#Uk9m}%oTl3$wCE&G@-{~{U@q&>)LhF)r%C?Bno2k?5{PjZFk{=Ocr?sn)%lX?6
zbI)sWNLgYh@k)Kb^k>|$@Or}R1rl*#g@}wIqSzwj<;=rF-e;){C>gfzIK5Or?rTKUYz!q
zEyO+H6(3=neQjw{o(C
zyk3x=v{dv**~6ebqT)P!MbZss3OSR!LG6=Ie~wNYMq?ayw5n7-igNQ7CIFjXZObtlBi{a`z**v|ui~C4
zPDS71FzEN^T@2o-Vg7LqpltB-?(_$se3LskM&uDg
z+QS|#j>$1XUy5^$u0B2B~rzxf8Q
z)>Qr+ozYySy-~lzM(b2E
zzm5UgE&{p!J!=C8AmbBz7b|SZznt^ORKypfbr6*5Yq=>4YA>I7dd%{U6&{gdDE^Ss}^Uvbd+bmPc60~?8@5mA>m
z`e|IWT;o|GM?E>lzQ$VIfY()kyo~snO{gSlcsD3Atd^=}hIj~hZL=h*{2_w61E4uA
zT)zaD68Gt@Um(QzxaffxLK**Vnk{~_65LgoV2^N*P2VYVJw7IPiMfd1#YP&&syOn6
zTO*6;jnO+RUgmxI!#dX5fyP*cqNv&%oIA@1bD!DHV
zTr)VHnPuE=2wf7=H)9jqAKB?cy>Y()q+7-~9MP@Yndt&o1MWilmYomF=yb(pQ1_mQ
zEsFPm@sel3o)Ea`OJa+v{;*TfT%jQ(#K%NsHVS*RKk$35ya9sQC3ecO&xgM$h}Ngo
zB>913W9_{f_@uphKpgXYK3XRdsrh-{?P9AFw4A25`pDvpwQpt9u?72$TJ~Gx$ap}c
zp?%HRk0}hL;xsKE`J2=(x7?<|PutChIIp%{a-70gGkbri0wR=;zBW*JLfc(({b>)n
zm49&M$uu$=4xF=PS{WR)8{&-4nqB%6!j~cO*P+Qm@l7th
zyM;1UxvM!_kKRQ91XXvkRP_RD%1~MGxF#*p6MK)md|RHCRsV`mIHYwhDQLTMF<|SC
z0V2Q(@v5drXaNR3z^fxf8Ngi35#;KgAhKnGk)U=!eAyAPI1aS?(K-ovw}!{LauJV&
zN1W#=j~RL&-4_IAN08oCd9dmvQu?iGl-Zu%T>a?xRTN+Y4rS-EUpr*TSnt3h4W!4L
zMH_y1PW_M#2lecqD^Q4xHO4Yz85{8_FV*D)q_}bKF5HLhO;aK=`k&j4Wb5TkAK>DR
zx_6~-LmJ&WTqeg_vrfPsBTi2^DTOPv`eGgI=3eJI#={=TB=IS2WHDcD>rw?T9GDPW
zuerm>2=;K*HKp@M?|-}km3}Ghp(*UyHN=y|ct2qs{yW=XmqgO+d5*d+ATk$HXdQgx
z4X+E$de713BE67VN17XQn{5|zUl`e|YAnEMKO5cy#^Wc~7oVW+`X4;ds{hNXhYwUu
zfC6$+h|E9WrBTfY*G%mow(a<=wQv~no=Qf2q1tV$aYQCP^ad|6>p25l;6xo(d
z?oAVdY43gqON+LvU8n1!(Y8jY8uH2ul)etHj3QDL78pFPH2>~d20Q|g`6J5wpvpjQ
zOJGX>rT>md;wCA`h`?kH14m84RERX}t!!-EL})k^Mo@}k6B`wc7xfmvJ&5A~R*#mr
z4=#b&0x-2;5!&|xK%{$vHChq(?3Dt1l=q3o!z0h~1MCkCMBwFTtRlIDxYo=4C!
z|4Wo+iw`YTU>si;RD6zVI#6tV66G+*9LDB6hnPb-Gd43aW*cU+!)Ncy_wxPa`42oVm&fz|
zeB5uh>vixpZ%^po9ME-GYPbwg?X!+S%2gKEOfP#>JZ49t5`DAat-@2D3<9_4R(oZz
z!F$_{p4dQHQF4l>lDB6hL(6zNJQ;ey>D5bFye`|X)d$0afJ>+fjLYo%?b$TWf8chV
zQ2!XrR+Z`c3`iq5o1nTo(3?)Ye_2Y9(=hT4eBivjn|;dg<;~k%st9#6Z{E&qGqItn
zR3~E&>Fd*VU}N+&g7vg{gIm8r68!^y#sWoFIGK$t=)m1|%0+tEcJPUxnVX)mAOGUQ
z3yrjpzU!ZWG_s1-4J%#_1B33M5qBQ7B}wdy!-fXy$Zh`7_HeD$iVyCuX7srn<@;mq
zs}zj9(sp`(xZAA-NVI*d*RSgsbl;4oQ?uFt3LzCoN_B(N5m!r^OUB<+)Zu8Su4SRuO61YRxrKin#MoCgk}n>Zy#nysR2L-UBA@5;H5}rS?`V|sCkD0
zu_3@K{JvsqqPI-t4Eo-0y(=AqP4IxOxzb1{)~$5vx4rBviDptTx!!I6_Uf>Ik-w->Q|X;E^l
z)$QnC^FmSLl4l7u_hGjmUwlU-Tp8+(
z8|^qSbM9-g`PH!;n%MD2XNuiH@bif3zoCySBuUaFm9v=poIfrc#Deo{zfZns!!8W{ah|}q7u$!l~
zd$;@9&D^mYk%_EL*1DUQ)48E5p3|(8+^v(_kNnW=;2=R$vP+d$oznC%zhdT7i)u`g
z^?-Vyz3=zGvZk=jTmFIzdBWbB0gBxJRExbGK9xo~K7g%#+;7m-l)|GE(_$D^-&Y9C
z4{*PKttlKQlHoizk?{W5_L%~N$N_bP3H`y~bQex)Pe-}%SH}xVjTz$kH`OFb{}|$_
zp03!@#rHadwfDp`copCz%lw7$`*CquRlEDJ8GamMZRcYmz)y1&aj}sim>55jgpaDI
zHR;~iQB1-sB>5~7jd~#)#2FrU2{)67A*QGqC~tr0I+`K78`}?n8bZQC1WllYv7)Chm&*0V}tlUiiY%ed~$_4CC`YI|jL-ZbPdluR0Hhgy^>cx-oV
z7t|}N)qO@->OVHr8>t1JOIOl`AHR-CQG%iu)pY!ovn&LW&L5)!=rd*WWeW)_B<(mf
z=+#0T2Zi2MY3uv>RxJzpCv>Ai(uo-+Sa%*qzP!Ef5~(J*CF%T}dZ2@G`F6nZST*ZcFU$VoPq;P3yGCzi+U%rpbT=5Zp03T#ptFLdYqsKx
z=4HDm(*o0gW$eFk)hCSI26SCBK-RY(lylSJ_&VCAy?V!$mr07ts(&quk5Dt&cTP`m
z8K*f^+Lq50-pVX~kr}C|3kAHRsyQx%Nvl5afc3H3`#`PhvK5Im1=n8Hz44_DQ;Oc{
zd(McT@sW5EYgg2a<17q)dZOHmn^q~g=&`R!25|aeTTq?B4lDLL|0(P)>+@Myc8i4X
z?hB3NJ>vR@w98IjZ%gUbwB^}ZQLGfkOvBaScH0xO7Vbqi0OY2QO!0JPrR3+xXS#+b
zaan5fiPk2+%)sgYzT4Opa`up;h?;P@<*d_J9?lso3Ja=;Hoap46G`dEX99oqYVML<
z7I0BpD1^G%pc*0xfAZtWXCXv#N{kt4G$>Tn<6lIT>6?tq3mQ(OyO-f_5C|WHh&wAZ
zI9&{&P<=aI0-tia@@sKz7Y?c_6>oDMsygeTe6RdLi8S;wOp`<4xYI^$l^-RDsn)B^
z0qOQMV>Rvt-g)4w1H!6OH@LZucgAmxWIluM{C%CxbRIaMXT_6$!4A^7TvkgYAJVrq
z6;U`s1$*~OvA&q*5{MtWzwDI;Dt91bf-9Tv0P`l;TWmk>VVK4sqH}w%toZ#Y!b{p!
zFogAbUxijk1c2F+;_*FoJzq3TEf-as*VT2lNd(5Jwa;#{a(tk|69dB?L2&MU8y?e~r^
zw=A{4#vGL)XU_Ws2+1}aKTy+rWYFa1$sXeUqrs{0%FI8oA^n`Up6@a!r}7_4BZz2G
zwDys*Jx`uBXvb~7dQLr--`R3?}O&SM+L`9CK1;8p6(f=LwRKf~uD+^QRDT<_7M#Un=36A8{_wSd_P
z=d7Esi0B0B9?IJxnQOBZl?s*mZiyD_Ao#{P|2tXaDviL*Mov`g7z%XIrhh(uH6Zgz0+_
zUD&$f?w}WKV`3mI7zdKJ@E<&lKq9*;4liHl7cT~p>F)v@kTWj9tR3Gl9|WA@*PMe>
z#Eh>f?CR=iFf)^bWM$Wn>l3>lMJXqDzICPIXaP&M>Gpp^mo6lGZ}o#5I@I0OPar*Ua&~}s=>Wo8u<{p*+nr8E^G;K^Y
zy}5d4RvVVvdj=0GTRXjJ+b6bU$#%XrXuR`BV38_P|G^O|0(Br8YyBIGP1F8g4T
zuw{<%RIY0aEYTi9c{D@rc)s%rEl%yx&>ruFCrQ-d!Z1@?;TvKRoJ3}5GevaWnm)id
zPgN-c>jh>bWd(+(iZe>6Q1g;MHbN$e9kDDe^dH3W!2bdt?(2!bXyDlgWhX0+s5rx{
zd*#V;6aR5$b{Yj^ocy#m+P{;gKbOI>e10J8zhA|z5eN`~yDRSa)v;oG?|n^7GE$zG
z+yYUkY{zS%fJ@Vca690doGRBv<+HHHQ<$$-6|{ACYeBEKC2dgC;c81ma%c@WXtE9Q
z*WZyf&E%(UpT)*&OcY_C;^3XH?=cZu4b!o^uATWe9NWgq$Z*XsP%tgrLmACV;>eYNDkZE0F^v@vAJh0KJYLosinLIY(
z9%bPx$h8JI{L(qM@}e=2CtzQ9`X&8D98uc+8YRR1Y^>-da9(1pNQA
z0Ku{;UuVVUAdhSPYC+QU4qORcALNufR|IV&&g54iTQbM&%>}Ok_
zOvOiY>T%%qhTB1ff&l40n&#By&S>cZeHdY9ZhcHRIhl6M@x^VdY7-iEeb>hHI?e%0THZ6>Es90vb%P*!|_5{A832SbGh4k*oW7K?Ouw(<()l4GU
zu;7(rHkUG|x^StNdZlIGuDb=`cHke01jOKh2~8TGz{fJ
zen>uikH3h0IwS?N(WnT1X(;U+ACgBKHA&tM@c0heoowpCX`sL8XU^!^JR$alqUj&C
zyfQVrOy9=mD(+mK^8M2V%ANytF6NT9ZUDij1UM3FiXHu8cYU8}26aACMia=al6>U(
zuor|44y^CHbEBYCTcg?$77=&$?f#~yU*#Rc3DEd8FQ6iSJQ=~eRni0?I?ZHnUTp*Nl?
z96Te4pNulImH_JIBFiM*{Z@_vGyum@jDPd)!*27Uh`W{d3*i@c!Q4}Wan=Wqm6niI
zpLxVw{x-`yYWp~GSJvoWrEQw+fyOuhaM7(*rGh-hT_A|=wh*?6$5U;4ehP%3HLBbn
zpQnEAaS!UD;jf?j4G#e;^4JdBk^%7xW1Q&05X5;-D?D
zE&mqGzjY1Au=y}JOHGA1y##Ive~wx1(qt2Vx=hYOGr_5dCaBf;-_JPQcLRaVw!pAe6960unVE&XO$CFr
z2cLv(ORKklwM!qyhF(|{7Un)Ng#ERBt_N;h@4!?T2^1y2@>PfZq03IerKeZuo_=fX
zcd-a-p+9~O%^5zm6Wl2Gb8STp1#NyP#M%fMR)R+NG$qzh;T)l~j8)(}sbm1%r@FfSmywAD#xS5!}L$tyR%Ioxj?)61Q0r
zrj+c8+R4t}huVkISVM0}QH=v9)M!SVxE8SdfL|-g?X6m())#d`s!_-s==0bBwe6AY
zad*V%Qq0bE#Wn4F9{CQG%u{PJAKg>pG;ppfAibXh
zp2qK#Vz@x<5Acs)_lHEEtu?S78CW{mFB&n@;{UcTD>%zi0`7{O_uY-M&D{6;`*X`<
z-HbqWpu^d0=#?oEipP}qUzM9^B#Gro=VwHP_)+T%;UKrz)~`GkOmLYOa&E#{IETu=
z&Lt<+AZ!;8P}pXTa3nc+6IEKxC~KLYujd5SN&B4jjx$o3iKmR$FcVZj{G;O*H^XN8
z0Rj8JDBO0K$<5%Lr3W@1$Nnv^o0*t~IB5H`%~1viw~ODLIl(2c&TL^d4On5ZYPuQ)
zzBuAiu6sg*{tt}1W*(viH?FZ&|7A3=_<{EPrKFv+z*_LUot4w+$gCqM(L72l65^+o
zfb`O4j?01R`XK;B|L~hxgOH~Nsoi407fVaUw6QWZ+ceFjs1(D@(#+gp1AW#;>h$x>WbuDepZ(RkRwK|zQ$?zJNZ@I$vGu*EJT=mSr
zTmshI3%*VpTm8HF`(0H{u4+M9+s8ZC`o)3|QMcWyOOuP`Dh9}IR|6DuvD3#T3^c6$
z20Csl%AQk@d70NYZi3=ghA87?8y;i--T~V;mk?4i!$Z)!nP&Ha$9mnxpGPbm!z2$s=fBjC%?oM}wErDa0DjQk
z(x1>@9~h&NU8?rZ@i9G5gD
zgdHwjE4`S&ND2CwG21FXYvQ>cZxNzyte8oOq;ci0!@J-jOF<9lWbyT*y&
z%(n0+pM7D96bDfnxO?7X8&X=do^-y9AjtEOYjM|gI18QSFCH75L(1}FH7t3ki$9JU
z#AS`BHYa{>Q97DQdGNXDgXGv!(W1)LE~>7}rrQW@kzHoks{UK*W!$*ww#-`e@%@ZX
zb3KqcGsC&K7S59bj`@FafvLsCr2A>oZnZ}`k`2#4#L)KRy}Ts1w9nkCOIPIZVm4#Td!IhIB)u-%VAb{EME3)9(xN<>
z>ZgZ&Jz;s!@3m|!>*avYhxChZ9s8RoQFYjvyI*Hc%wju467t3Lo-w)Fj&>u!gM-(f
z-Lz=>cT^rg4L<=);nv*+eYFPuD9|z7XK+0>bK>XhwArJ=%-Kd88TI2>g|)RN-H3Z!
z!cv_2l?hFM?!ayMyA;A^9J_`*fDq{hZDC_~gJSt3(4XzPohILNx%qxpox`VVDgg+P
zy%+=2WnO(oTl0J7v(6AnBA{2AXq$cj)Jm-jWiRWtUYUH|YTLJ(sdyPa1-}QpG*3CP
zrk_0iMlswWK8Vacmz=`38bA#){)UIyE
zP9<1{#Ec*T{kEe*{`slaq?Bml_o8Rq6taq%j{$WS=)@Wh7hgD|Bhh)s8ELGfGJ0l8
zv`2Hn%}_JjA62Y>bwZIO{K@#oOzq?Rso9@9`(TZ#dFII}CjIoc2OlR2*Wo|R_$mH|
z$q}y2fA2i#Q!4cErR--o1aTd}gg%ZE^Bc9^?-0tqG?Te)H8s3*Q(E1DMU5ZLdaDozu#ovPI0A7gBwJh~NHcC$!&7OJ$ib1UNc+&}t#
z_{3h)g?ZV>J2B&y*ZCLm55teOy$P5zl-h-<^5wWa&ejhp7Rh!j(5Ov@rj1bEEiggt
zr2$hnvCjzRLa-G)(>+we{^bJ6|{h;3b#p?
zOwOn#H|+ZprUv!4K5wVUx_PAIe1vtDK%aBwAQnCK=xECSN;V+nQWeLhgDE5B)!D!L
zAucO8-yW=5RG)9kO+d%`1@ZfyXvxon?@-fsuRp(5$U|7qgsIk*_l1H;#K#$f5`Qh5uC-`j|Be!Y2I5HY|HY(Avn)!?N@xt
zVajXkD*>Ug^@ZQ1M{Hi!D$yXM1ChZ55@jhH%_PJR2qxzJ(h>kY`BsVDBs~9XMAcqK
zIOmC)kX;w(DesN6d7Y$F$(@G9sb>YOsJrsAE9pz_Bn)kNQjqN|mHbSiby&Yp@WHh9
z3p12EkIsC5+W(9+y^v_llLx&7m2NT>(nkBjh*b$~tDnl=G%mH+#KqYs!LLwKYi)!a
zLOij%{jul_8+lz2N)z8^RyBvXD{$Xk6OBXIY$H%LeW@2CMcg3L%`I09=-OqR=dF(^
z)1k`#5kEfnj2j$!J3PK{s67VUdP`QrXlL=Y3Cqa`TkO|QW9axx<2Co?B|+$eC_G6n
z9b8WP8Isi<#SBdj?|t7Oq`Asn@3A;_S%K08o-~ZCUlgR4?Z*iL;&2yWR!)4)lmp
zwTSqcm9?aNnb5U@6#bMdm#M^+F^TI9Z**iMe-4dmta}sjk1B%c&Eskhc2f~~V@J;?
z=juBlib-4q(Ze-%G7+?WLgfTHFZef5Lr8vFdRMdZ^~-vnmd!-7bpGQ;n6u_8byN8T
z9S57x#EA>
zt|F0hBBeua6`6_;;-=2e<%4q!HZ2{VaAjKZ>0B#}E@qTgLpH3R{;$OGLAW81m@DU!
z!OYKVnf(sCAC&&DF^D2!`)cJ|h{JqDjFiV5vy~Jw#eR`z!X}%(Z8H?_>3r@gVV0tE
z)c8fYe%XT_ok>ew>g4p?ct=0(6bh~S^R|6gi&=O`00Pk(pT}mwCGF-?s%)XjEXcMlABlm$UsYsY{84vcKKF)vSOrEU
zH!16^(PM1+1#5a5`}6d_v%FaE3C>@0qUcB`^n1LSX8TcFS3waZ?w|0JWM*EdS`APHGiytAKcKz&Y+}h8`ujXq9HqVuS#*mZ7
z!g{xwu6=}@3EhwB$lx3HYDzP8@bUQbX|4&kI~(8EEKRs0*NfB%8k15}DWh(TTnuOA
z@#af8wsJCR`I*vzgk$gF4HN#fRLX;pW)GsAIsWa}F?hffQG`A!RPKYT?fpBscQVu=
z(-%frQ=h1k2fy#89c+3c8|sjlo6dXqAL0IVG7dKAo#7@ZjZcRL^@weG@Vs?S-U;Wr
z5O<-mo0&TvLGJzCy_CWaP*uGJYdyLiWymq!$S*&yyM;_jL^=e%I)m}qE4OjM=AS6^
zfL>Cl&wI-O)%d&Uvg*8VoS(ed8f^tUnzZYO#P)ABnL0p-kwu~A7ePzM$a?zDG1eW
z)>Z-1t8eE}%!}JfJ3mD3YvyJ;s#pnqGM1Rr=+{3tN<3BIHzQpD{%Sj#TkM@3DV?EY
zpy814zdI!vLKN$H1}H>7R5ef_Oql>$m09ZEF1qm-#TcKh443&74AXR?mE|WXa=l`{-~qxE+VDe)HFWLL-~F#|7Q48>2_6?fMrZUcaOSm
z|76=WER5rY7d=;n#Gsa6p@%{2oMIgQe8qEKX?oUu+^DtJV_ka;VdlS$@c7crS2WKZ
z8P%&l6kkn;3pbi=(?3JlJ*#cEVzq?*I#dJ8cW73vkI~(i^wv^ANiVUH)RtY-*Ez_WF~3Q%
zrB}xy;|m{t@C;GlPuOOrJcUP|O27#(GA=~^l&LWPy|tNt+{_8KGlc_(oP
z)Aw8e9Zg-uK5M*^jfCpi#^{&d*_NY@RwIt^Og=Fj{FOX8$+TecN~zxrC0&UbrV%Q9
z>45ZA)cuk}CF#TrfL_zBqB@q9LTAiFunuaYr(9*%^0EX6{yPRJFt27Z^?Kb-GhF(k2qiZ&5
zfEz5=h84zLM_W53i1c<7onhsEtVE>26er#hrSaqfJvz1;Qu}382%+ToTB>3ucPC|1B!TxLTpA)9KD3O}
z)d?RRmFiDdgmM_WtvTOY4_pE?w1Vq3DRe}ZgdPV(czZEig*Oc7DL6uoj+mRzNB|mE
zxQ4_}Z>bGkM7LGM@r;W(+;gYt+I+#-7V%+Qv#+$YZSzC-HfQr|u1?|8D|?EbTye^k
zEeZ5%j~dL*S7Q%w2vB$!EzZu+ulYK$fM$>%fFdb<&mGO?NX6W$elC24n`8*fHMCol
z*c42j{L*nm(Q?^ePa}TUnmTttXtaUyui~;=CHW9uYBW|aTmZd|j
zIpIcy@{ICkKPw`lMLw3edM&@pQxON8m)yf-y_%9op&xIWJVI7z>j+A>=^5i@X>aCs
zUj8HRJM@&vew@F&v9~sJ3O3kkxiBC1jBG`~uhp*Fti4;r=SoRRwjv=^jkrbyu!KO;%b`-$3hXGhQ{BSoj
zX@NSItOJ?UeU(nWs2|8hj1eK6yv4q?dp;MuieN|Nf>t4f`cHa?j(fPmvA&zp<@O>I
zs07)g#-XqRzCZf{N{_VPZFQO=DiGGhy*P)x#LS_XkZpX9@*;ABES4Ty!>vs
zbGT_#)RHZAG~^0n^Z{~q(!50?z0tb)rXbs-j2Q*om{s}sQ>Cxtee{3AsAlh}Q{z{a
zC?s3*3a|3BNXztQ^^Vkj5tRF4eeoVFm|a1y*W>zZi)zzUOKS4qA2eHqL-XdLZ8gD(
z#Xhrv8O4vJ2fcllvr^8o4~K9Su9wnZd=6+l7Z%yE9x#QoMbd6Gi{v~^69p0l3L*|0n&^F;)bPJOCmw+IS%aJPVe2Sdg7
zEC0+x@5P2Gxo!S?t~eB#-vA7BBzMwoa!hyxi~<(c%Z^WHZnJK`olFF`|M-l*k-TN(
z&~I1__j{w;BPRhS1syZi$7Y^)E7Dt>RAs6v6+ThpO;n(G544v5-b3!7#>2tD@v??A
zlD0L^x4UQvUKxEH^lv9^13zCzO`|$sJ1#twy95B1PBfteJhBN$Y{z#A;6{TeAz`l_GSiIU+(-S%VU!VsQUrlyeDW475?|K@8XvRp4
zmM%UT`n&s1?#6H5N!NL@3V3j7i4h)(a{*}9)i3@^?rjLc2LGluG;Pum|
zoq1>Ao{2Px`E#NlG$jlb9#C@I=psf>RX}GHwE-QDNqTL9q)F)W+C`OnmQx?q3Tbzc
z7fF-R3&M8_IbZd4v+9S~pWAMAVSaC!G^8Q3@@i~y0M^57J00_&XwG(*72qZvv
zWbu$~E#7K7%wSlX%?u1TH!}?gZ{i!*L0D`@aq6POs(=*?SzDg7pi*E&?Lp)OPST-S
zun^+f>Kyg8|8#Cn+RTIL68^#Z!{eYO%hlp!JxAWaaHxa*<}I|KghE_j
zc@rA*OS(2fSd94cOru}3`pQMeLPbFb&6ah|-~PLl-V%>=GwsDt%1nc6Nog1BMRjc8
zcPvSw`NxGOkBNfkUI7MOv-gz;JeMUgdG4=W%tH%z@!B660{JwV^ZWs
zBPwl~?(8n$R8P1BISkwDnPHinDn%@hG=a!)hqPMAs_$0>c
z2>28@prK=bjLxv#yyf+v&QLLB)lLwf^9R;!n8O?C_5Gi^{-D5+yTdEy3GHT@@DW$PT3d>qMzO
z%}P2LG=3P+M|M-gecee;=PMZe7#Y&ST>j6kL1Vy-Wbp{(ulK`#XhLj*6jt!R>wHooa9J-(Ux*5+}J
z2?-ePNTH@;x2oxg$$?m#aLO&D)aHqC27xGB+s;aLD#Gw~hAl6z4o7z*#bf#VZG^lY
zw!K>Bg7#^Zzt?_8qcY^5(v;`ljB56HT8Di~QH(InGGNQQNYLAq<1qp6bMa3ES`R?x
zxFNAOM3o!Solmdri~VnhMl@FBMy=V(_*C=-sX4|chgpJNtiVSYeoq(O{b}XS`8i{&
z|1KT@X#R9yyuiqZxNt!A;|cH3(Vrss)z`-ZO?(}QLS@1t0xV;6DdWpEK6_(4vfHoh
z%EBvK<$FijxITz1{I;|aNvht)u#S+`b!`rQ{1m({WKGo*$$jjO(2vuunDiM2(JISd
z-wZV?Y}mhhM$BjFjbcAQMm}jrIq&@Vt`HpO5rwfY*ynFo^WsE>D`mI&dN^hvM2Y!&
z0v7@>%Mr2r$y%)MA2Tm`FRZ@Y^2)=Y;$!rd-!?kNV{-V<&(o*98HuY_AB{fO=r?Y}
zo=*O|d6qvl{BcBYcW!hem?bs}%rVx;Omo05uY7ZZ$hvFSJzJxsu4Sx+U5TKlMI%I6
z7k1Ys@dg^_lP8*tZ~6p>9K!?t2EI$U4EFFt@6(Wn$+!3#H0o$P@uZ6}qPDw1)2G%O
zbxi!MvRw6g2{qHfk#gp&16)1_$Wz}t(1+&~^S3dlB6z5cVsI1Zt1bk-v9iDWyXp7L
z6aVt1WTI@n@+VP?JGkbMw9|S&DrCKuT8?1L8?1nXt3roSx5w82y&DqCn4Z_iHllcI
zFy09V{6E_Gl=k)m1c>%=BEZzk{)7`4<~`LymAy2c6A_N#XnKha!I9%yVzt`-->K`Kg`NmgevS
zTz_A$S8LIjBl!tTUmJ}IXnJVi2QgMr+dZeTxc8Ob2Vz~9nSS$`;R^q}`;_(VgS68%
zp567~D9rUkmAg6usTa(Ww)@($r6R|#Sw6xnSqo3HZOoltOJhS8t%=XO9Q~rZQ8sCe
z=zyE(Du9gh&yuvP$BjoYW*fsFv|#=~;LfumW^PEpm%XuOxo=6iq_u2}eU={i)KmR_
z;a`B%2^PJNxH15}&Ff(ov0`u$M=`WM1c-7rN$Tv}7HtX=ylo{U9HFsq{xooE^|gxe
zjk7^&#f3_ec_s9;+hFa?t#@4OFoT`Tpy1(9SO-mjLCF9p5CE%~W4@ayx%!MTh`u<2
zx#cr1Y6vc2L^(JHvv5AA!d@bk2iJVF*#_*@3z3lyW+?snUF!l031!J}wF4!jk8k^<
z5&+f(*?V7oiH#;9d^I{yIhZWwt||O7OvhoS6H*(yfkyU*w0w9-Waa0t@T5{?ZjEPMlEPe?8%$Hbe!<0Q~8%=2DY5Xq$3%1H}Df>;h$gN
zP0%{LB!{}e&q`>$u2C;Vh2`kY^`cc)A8U2JU%D)_`s>2j`J5Vnonpu7!H8Hj?pgW=&imjJYi{Kc3Z2#
z(!ax`-iS$7mVwnP(?O)urQgfyOt%19VXwC?;w>>8FlQJVv`j54hJ0`S|TF3`*sUDX`8W)ZlK{uFoHG<
z8sDp&rq<83Z{fE3^lc|88e^O=aj^7-7lh1l*s}$Zh2Fw9@AY_neqz3cw&#bWlU*Bf
zCd7!_(sTCqU5B~*ZzbxD9qh%#dkWqx;TxeTbHh*^Lb%>LY9|;(>&zL`8P_k1*=6Xn
zVUoV#%soSv{3?znF#{aBAe$jGck&)%J->kr?<|U&nT+KqbWN6q
z>~?jOHH1#)=pcn*=T`J;R3H-Pn7`3hh(RcTzom1yA$kWWHdS|2#bxF2Y%4Lgp0UFE
z?3b6X-qc77$UE}YxE3`6A?yrNAERVmv3TbxCw%*zIv7O7>f_9A>etx|6(O)ziS|=J
z8BG^3kaVvd@6?5edyIux?v3)fPJrjkFT2jC8@MIntzg)V{N_vRlRJ9)E_$I;m)kV#
zN;>P<1{F15n=A2NnVt~ss&-=|z0(GEFv8YxLb%SKHzD_eur#$D7P)G!dG>YcnQ8M*
z$e=KhEv4q~F;hnPArF#lu?STifAG-w%+_bDQ_eEB+I
zRfE_4Iu`2EL<>;Z3NwPqU>?f-tXqN&{Dvi8?{53eAJuLet&+%>QsrmQrLJq>
zz8#DeVqD}X!Ch@xe%tt?^%KGB#*w5jJ%`F
z+rVmp*T6O;%))x^Y!AE9;#}S^&0UW0*mt$E
z|Qq^bN`yzJWOIb=Z~zGO3rI(mUE1m
ziphunGNe*22ui81nw4yh_LF;UM15Hj?4nkjI1n~sITOt(jt1Y3X*hR%FfzVmVBvH(
z+`;yUxR7BJRCn95y3-CbjmCMW7En)$TJ}qeR}oIx5EN?BN+-!F
z;1hWS*w;0QNPvD|<~_lfm$E_|QWcIT8YyA2eKSnge7GD9R@VV$?dgA78bhap;4Ic9
zm_v-GB4Bf#b8yEPr4&t1FK%C|j$U9`VKGl$wzB5`2n(mo{Ka|mpQxR=$1-xfNVh7H
zDrprZI-~E>!kZ3$N6>bwacI?#6@BR)#O0rFOLUr+RZJ~Vc!0t<;(`USJ(~Oq0JvA6
zvybx%DtAQoZY8~vRzq;=25cbHPsj@(j(+{gn!m_yadlU2*fc1<@#`aB=(+h@PuXLy
z>>g$&cmW2kh9}*RUgaHu*cNziw%@8xTBc<)6cD(Zu#@QYp{Y)8*zxgGFu-8JPZoIB
z(}#PYA;HfqlD~9aDD8DN#Xhd~@F7HVG&UR!5J(_&<
zO(G~xr#FyrfTBWXX3d0QtU*C>7y2xX4+nOpC%Wd%5^SCX%e5RMZWp_w-X_
ziNMq`z8_$d_UJpN(RcFn)xpr_f1?TAb-F`Y-!w@YcFm9eEj#7fQl7=Fx-s}G(xCss
zVKZ?y6AIQa)f?aAyD0Nffxn=@p=_Ry`-5($J42Y?W?>O|RQ`}5?;;y12B?R=+Xeqq
zUpDYSuKjZl?J&T=4&!ZQCwE#1X|Hf^Z4JB_~goKQxjU|1g!rX?e
zC4i0V-J|B)TULAB9)Na)&rM2-Ym#U|aIj>H*Lt*Zh#f!dEAFvCK!xwGJ3-yDR{geL
z5-Jz*K=%3qF6n^xAF#!Nh`J<$6_nhIn_J+8buEG(s|kzS?td@ucXgci?1@1{z*ZzD
z4wk~Yl-R#^LR!MM)s;0K@_YOrz5wJGLkVO3{Y~jR6t_k>F_E=@zL#<-K~~N}y)Bc@
zPV3e@yfv`1ww$R=jO7$<7F?s_GLs~C)7rju7VPD(w+Pnqm
zS?mk4?3%Vp_BkP1@h!w6G|NA~yQXCO4;2)48pdEr(=?KhA^78oI&$O=S9{<(DP8m=
z0RObYa^gCyjpNl|uS=|BuX91R6}C>1hv4OwBl_Ci
z$b+_lXvKBVjm<39xaQ(kINdj_oct+H!H+8bhE=vlImOOrx?(q=
zke=kgb-<)K$EUs^t-g9UR&XNY>L16$2a0wFGRSWAWs0DQ62SXZK?2BiEVMpfe^{M9Yh!|`Hy
zn#EB!&~Rv-p&4!QS@U!kUd_RDKvko&Va|s3d-L6WK+J>;uC}0=q=O^!-!SomyPl1(
zTed-|lje8vlP?XU`XqQI%&l9{bhO6GKQ8-_@Y#1_TE7=Bcg%vbAgntUxHUroPPRxlze68K0995h71NE;mZ)!i
zSIyvWHm%X#!woxa^DS>YZ!tt+W1ygFFkizAcxmn968S^LrkG0HbLc59c=YiseiDmw
zXAS%>v4=6BKCbwGrwd5O&$~E%RtEN=j$->SSOxPf^NL*3PU^2jHRW{|IDR3^%Xee{
ze%!Vtf&Edl`I9hrop}F^=i?y_D8uY_`gs+jz=7=a;HR-mnSRm$m+7PM6^G#My7m6P!R#Id?;4brSs?9^$_lQ(d!L#4g4LT7idmX_r2{e&~=UG3Dqv?Sm+`>7nqW!S4=_wO$JHV{<5H4~XWTcxOn
zv7;T^*w_71uat8>Idd{Xk#A6u^k?1ra`vooD8GuRmHcaafTM<4jF+JV3&FZq-%Lrg
zaQ%20`eZ_KBw*Fmw=+&^_Ev!yWJyH7ck$FMaC&oRZ^A}EjnAvZGrb{Mkv9I`-?TA?
zK^{*R)3&<3ZeUHQF_Ok2yTL0BagPJ86tB>+n~oIq0XP@{x0X7W_4Ys>;{r1iU=oDo2%w5AUe=
zb#Et`M2;U%VT4SXAl0pee$$VyA(7-R9Ant*K#N7me624xG#9hkBUtbj{Sw_dz>sRc
z=76}C8#`Xw*WV9WxZO`mUukTY{_AO4I$LHMME@~w#GqE(&ah^O|AEj08xE!)kMSsd
zzr>ZIn?j%F?<7?s8lX{Xdq2TA#gnRhts1hI~j^UE|2r;LC!~VoU`~G?B-&9
zZT{oeXUlF~g)OlPXEH3_QQn$Y!ejawe<-aLsm6+DBC`C+DuxO8kFD?h8!?TYV;6IO9>0h!vf>fQOZ$4L>bWcjoL_Nf)h
zDBtP5L;M{cw_F@-iM>I55%5#7!%yMoeX%mJ*m!0q-6|1=buHw{xZJjF57?K$_dNPD
zzgU^H%Mik6THg)GQv)AHebxcq;BA3ye^6#={0VZ#CAqu&j;UROI8vor8B_4EEX&f;I~wpk^;
zRq+oh0XF!%(S98;v=ZRcsyxPX7;#MF?*rIYk<(Wy^tkom&ngjWMaF?RB#28okFnxV
zU-ls>e+}rh-wXEP{Y;?3s#OLY0f*y01LkkC_5^7v2B(Z0=&7V-MsuOG!dm(k+poz0ix$jkPb@v@6WFZ&U&kBr{ims@8i
zG|NlyezE@(_k(^wa~tl<_IVZCK!Zu{b3g9yr_V|3=ljpUywmF=i&Vf8_PE@pfFZ(k3td;pGCpG(hlc#{o9X
zT}|gF1N<4+o<9$A^$F{nS$^BvXu1BCq4ar1C4Sz$B*f%YpAQE1|L^uBmvEmHR_SG<
zx3BEt4{2+^sN25|*Dv$=Lj@QPdt6?Nn!Wjmd@bF-l)m1L=aLY4wPp1hJ6QIUkALM0r=-nu*~$aRr%xQ4Oz$G8>N
zr95uVZ*>@LC)S-F4Ic|UDfPIwdz5P$4pY4Ya8-JT;qLr^&A=aa%H+9&zngr^=3B@s
zu~W=vc5!6Oo||7b90!JcUhnM0;$R++?&4$aSBd_^xEz;l0^i#Ij)AiuoN;B}lKnn>
z`aCE;k
zbnDzNDtXp2X=OGr{7pH
zt|bB+BzMx|FYuv$hli7e58q{Sy$awlzVy(m*KZS(`)xeOr%FF6`DX*}#82DzztQW>
zL;PUAZqe%n}df_cz;kWy^BOuajIWJ^V&%c9rc{-0}aj
z>qdT89TXi-$}9b0zWut`uE^2X{9aDyzCX_i;{79l3j@#O7VZDrdHDIfu}hM}od3Ht
v`Cnwp|0L4>i#f-C_OJi9|H*$|@$mluBQ$}s9!ugk00000NkvXXu0mjfo+C=S

literal 1045919
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

$HGR)54{|I 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®!_HwDOA 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

5}D1II^9ctN>WV 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∨S0qwwr;eC{D}1rekI$_NoD^SrV4*1 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

#kd_8=XqOcrye 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

p=w$ww(8Jz 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℘l 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

0VHT6kuINtyjVLT9xv9KeyS!@4jYs@|?mTV@ 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

gi*XKB^Or0;y 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�FviG@ZSTInbKXCmbDlq+xX*pv*Yzpq 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 z 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໯s&IPpo94;%Df>+;wbm&u>%zh$ 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%0DqIQ!8uKO0woD(R& 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[EceEzsanP;HaMfaR<-gAE4Ah-XvY5Zwmd)g!^ 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

map(object({
enabled = optional(bool, true)
addon_version = optional(string, null)
# configuration_values is a JSON string, such as '{"computeType": "Fargate"}'.
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | +| [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
map(object({
enabled = optional(bool, true)
addon_version = optional(string, null)
# configuration_values is a JSON string, such as '{"computeType": "Fargate"}'.
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts_on_create = optional(string, "OVERWRITE")
resolve_conflicts_on_update = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | | [addons\_depends\_on](#input\_addons\_depends\_on) | If set `true` (recommended), all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned.
See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. | `bool` | `true` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | diff --git a/modules/eks/cluster/addons.tf b/modules/eks/cluster/addons.tf index 320e9f0a1..078820a93 100644 --- a/modules/eks/cluster/addons.tf +++ b/modules/eks/cluster/addons.tf @@ -27,14 +27,15 @@ locals { addons = [ for k, v in var.addons : { - addon_name = k - addon_version = lookup(v, "addon_version", null) - configuration_values = lookup(v, "configuration_values", null) - resolve_conflicts = lookup(v, "resolve_conflicts", null) - service_account_role_arn = try(coalesce(lookup(v, "service_account_role_arn", null), lookup(local.final_addon_service_account_role_arn_map, k, null)), null) - create_timeout = lookup(v, "create_timeout", null) - update_timeout = lookup(v, "update_timeout", null) - delete_timeout = lookup(v, "delete_timeout", null) + addon_name = k + addon_version = lookup(v, "addon_version", null) + configuration_values = lookup(v, "configuration_values", null) + resolve_conflicts_on_create = lookup(v, "resolve_conflicts_on_create", null) + resolve_conflicts_on_update = lookup(v, "resolve_conflicts_on_update", null) + service_account_role_arn = try(coalesce(lookup(v, "service_account_role_arn", null), lookup(local.final_addon_service_account_role_arn_map, k, null)), null) + create_timeout = lookup(v, "create_timeout", null) + update_timeout = lookup(v, "update_timeout", null) + delete_timeout = lookup(v, "delete_timeout", null) } if v.enabled ] diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 2415c74c5..ebe8466b2 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -129,7 +129,7 @@ module "utils" { module "eks_cluster" { source = "cloudposse/eks-cluster/aws" - version = "2.9.0" + version = "3.0.0" region = var.region attributes = local.attributes diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 0b671fc70..dc15e9c8e 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -517,11 +517,12 @@ variable "addons" { # Set default resolve_conflicts to OVERWRITE because it is required on initial installation of # add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and # because any custom configuration that you would want to preserve should be managed by Terraform. - resolve_conflicts = optional(string, "OVERWRITE") - service_account_role_arn = optional(string, null) - create_timeout = optional(string, null) - update_timeout = optional(string, null) - delete_timeout = optional(string, null) + resolve_conflicts_on_create = optional(string, "OVERWRITE") + resolve_conflicts_on_update = optional(string, "OVERWRITE") + service_account_role_arn = optional(string, null) + create_timeout = optional(string, null) + update_timeout = optional(string, null) + delete_timeout = optional(string, null) })) description = "Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources" From 054755816f15d597ee248855e0896d892ff256af Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 30 Jan 2024 10:09:38 -0800 Subject: [PATCH 353/501] `ecs-service` volume passthroughs (#963) --- modules/ecs-service/README.md | 4 ++-- modules/ecs-service/main.tf | 8 +++++-- modules/ecs-service/variables.tf | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 3f07c7fae..754586cb2 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -220,7 +220,7 @@ This will create a CNAME record in the `acme.com` hosted zone that points `echo. | [datadog\_container\_definition](#module\_datadog\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | | [datadog\_fluent\_bit\_container\_definition](#module\_datadog\_fluent\_bit\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | | [datadog\_sidecar\_logs](#module\_datadog\_sidecar\_logs) | cloudposse/cloudwatch-logs/aws | 0.6.6 | -| [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.71.0 | +| [ecs\_alb\_service\_task](#module\_ecs\_alb\_service\_task) | cloudposse/ecs-alb-service-task/aws | 0.72.0 | | [ecs\_cloudwatch\_autoscaling](#module\_ecs\_cloudwatch\_autoscaling) | cloudposse/ecs-cloudwatch-autoscaling/aws | 0.7.3 | | [ecs\_cloudwatch\_sns\_alarms](#module\_ecs\_cloudwatch\_sns\_alarms) | cloudposse/ecs-cloudwatch-sns-alarms/aws | 0.12.3 | | [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | @@ -352,7 +352,7 @@ This will create a CNAME record in the `acme.com` hosted zone that points `echo. | [stickiness\_type](#input\_stickiness\_type) | The type of sticky sessions. The only current possible value is `lb_cookie` | `string` | `"lb_cookie"` | no | | [stream\_mode](#input\_stream\_mode) | Stream mode details for the Kinesis stream | `string` | `"PROVISIONED"` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module |
object({
task_cpu = optional(number)
task_memory = optional(number)
task_role_arn = optional(string, "")
pid_mode = optional(string, null)
ipc_mode = optional(string, null)
network_mode = optional(string)
propagate_tags = optional(string)
assign_public_ip = optional(bool, false)
use_alb_security_groups = optional(bool, true)
launch_type = optional(string, "FARGATE")
scheduling_strategy = optional(string, "REPLICA")
capacity_provider_strategies = optional(list(object({
capacity_provider = string
weight = number
base = number
})), [])

deployment_minimum_healthy_percent = optional(number, null)
deployment_maximum_percent = optional(number, null)
desired_count = optional(number, 0)
min_capacity = optional(number, 1)
max_capacity = optional(number, 2)
wait_for_steady_state = optional(bool, true)
circuit_breaker_deployment_enabled = optional(bool, true)
circuit_breaker_rollback_enabled = optional(bool, true)

ecs_service_enabled = optional(bool, true)
bind_mount_volumes = optional(list(object({
name = string
host_path = string
})), [])
})
| `{}` | no | +| [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module |
object({
task_cpu = optional(number)
task_memory = optional(number)
task_role_arn = optional(string, "")
pid_mode = optional(string, null)
ipc_mode = optional(string, null)
network_mode = optional(string)
propagate_tags = optional(string)
assign_public_ip = optional(bool, false)
use_alb_security_groups = optional(bool, true)
launch_type = optional(string, "FARGATE")
scheduling_strategy = optional(string, "REPLICA")
capacity_provider_strategies = optional(list(object({
capacity_provider = string
weight = number
base = number
})), [])

deployment_minimum_healthy_percent = optional(number, null)
deployment_maximum_percent = optional(number, null)
desired_count = optional(number, 0)
min_capacity = optional(number, 1)
max_capacity = optional(number, 2)
wait_for_steady_state = optional(bool, true)
circuit_breaker_deployment_enabled = optional(bool, true)
circuit_breaker_rollback_enabled = optional(bool, true)

ecs_service_enabled = optional(bool, true)
bind_mount_volumes = optional(list(object({
name = string
host_path = string
})), [])
efs_volumes = optional(list(object({
host_path = string
name = string
efs_volume_configuration = list(object({
file_system_id = string
root_directory = string
transit_encryption = string
transit_encryption_port = string
authorization_config = list(object({
access_point_id = string
iam = string
}))
}))
})), [])
docker_volumes = optional(list(object({
host_path = string
name = string
docker_volume_configuration = list(object({
autoprovision = bool
driver = string
driver_opts = map(string)
labels = map(string)
scope = string
}))
})), [])
fsx_volumes = optional(list(object({
host_path = string
name = string
fsx_windows_file_server_volume_configuration = list(object({
file_system_id = string
root_directory = string
authorization_config = list(object({
credentials_parameter = string
domain = string
}))
}))
})), [])
})
| `{}` | no | | [task\_enabled](#input\_task\_enabled) | Whether or not to use the ECS task module | `bool` | `true` | no | | [task\_iam\_role\_component](#input\_task\_iam\_role\_component) | A component that outputs an iam\_role module as 'role' for adding to the service as a whole. | `string` | `null` | no | | [task\_policy\_arns](#input\_task\_policy\_arns) | The IAM policy ARNs to attach to the ECS task IAM role | `list(string)` |
[
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
"arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess"
]
| no | diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 52efcf578..f34c34873 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -214,7 +214,7 @@ locals { module "ecs_alb_service_task" { source = "cloudposse/ecs-alb-service-task/aws" - version = "0.71.0" + version = "0.72.0" count = local.enabled ? 1 : 0 @@ -265,10 +265,14 @@ module "ecs_alb_service_task" { circuit_breaker_rollback_enabled = lookup(local.task, "circuit_breaker_rollback_enabled", true) task_policy_arns = var.iam_policy_enabled ? concat(var.task_policy_arns, aws_iam_policy.default[*].arn) : var.task_policy_arns ecs_service_enabled = lookup(local.task, "ecs_service_enabled", true) - bind_mount_volumes = lookup(local.task, "bind_mount_volumes", []) task_role_arn = lookup(local.task, "task_role_arn", one(module.iam_role[*]["outputs"]["role"]["arn"])) capacity_provider_strategies = lookup(local.task, "capacity_provider_strategies") + efs_volumes = lookup(local.task, "efs_volumes", []) + docker_volumes = lookup(local.task, "docker_volumes", []) + fsx_volumes = lookup(local.task, "fsx_volumes", []) + bind_mount_volumes = lookup(local.task, "bind_mount_volumes", []) + depends_on = [ module.alb_ingress ] diff --git a/modules/ecs-service/variables.tf b/modules/ecs-service/variables.tf index 46204615a..55d5c9274 100644 --- a/modules/ecs-service/variables.tf +++ b/modules/ecs-service/variables.tf @@ -134,6 +134,43 @@ variable "task" { name = string host_path = string })), []) + efs_volumes = optional(list(object({ + host_path = string + name = string + efs_volume_configuration = list(object({ + file_system_id = string + root_directory = string + transit_encryption = string + transit_encryption_port = string + authorization_config = list(object({ + access_point_id = string + iam = string + })) + })) + })), []) + docker_volumes = optional(list(object({ + host_path = string + name = string + docker_volume_configuration = list(object({ + autoprovision = bool + driver = string + driver_opts = map(string) + labels = map(string) + scope = string + })) + })), []) + fsx_volumes = optional(list(object({ + host_path = string + name = string + fsx_windows_file_server_volume_configuration = list(object({ + file_system_id = string + root_directory = string + authorization_config = list(object({ + credentials_parameter = string + domain = string + })) + })) + })), []) }) description = "Feed inputs into ecs_alb_service_task module" default = {} From 0be24760b366b800088b45c424995bbb5117f02d Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 31 Jan 2024 09:10:28 -0800 Subject: [PATCH 354/501] `philips-labs-github-runners` component add `node`, `aws`, `gh` cli (#968) --- .../templates/userdata_post_install.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/modules/philips-labs-github-runners/templates/userdata_post_install.sh b/modules/philips-labs-github-runners/templates/userdata_post_install.sh index b511a316b..3f810387b 100644 --- a/modules/philips-labs-github-runners/templates/userdata_post_install.sh +++ b/modules/philips-labs-github-runners/templates/userdata_post_install.sh @@ -1,3 +1,16 @@ echo "Installing Custom Packages..." yum install -y make + +# Install AWS CLI +curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" +unzip awscliv2.zip +sudo ./aws/install + +# Install `gh` CLI +type -p yum-config-manager >/dev/null || sudo yum install -y yum-utils +sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo +sudo yum install -y gh + +# Install nodejs +sudo yum install -y nodejs-1:18.18.2-1.amzn2023.0.1 From 0f75c9b71a48fb00b9fc3700039be98aff5ff672 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 31 Jan 2024 09:42:37 -0800 Subject: [PATCH 355/501] `aws-backup` Upgrade to 1.0.0 (#964) --- modules/aws-backup/README.md | 67 +++++++++++++++++++++++++++++---- modules/aws-backup/main.tf | 16 +++----- modules/aws-backup/variables.tf | 48 +++++++++++++++++++++++ modules/aws-backup/versions.tf | 2 +- 4 files changed, 114 insertions(+), 19 deletions(-) diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index b8e0b292c..4da76b8ae 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -37,6 +37,13 @@ components: iam_role_enabled: true # this will be reused vault_enabled: true # this will be reused plan_enabled: false + +## Please be careful when enabling backup_vault_lock_configuration, +# backup_vault_lock_configuration: +## `changeable_for_days` enables compliance mode and once the lock is set, the retention policy cannot be changed unless through account deletion! +# changeable_for_days: 36500 +# max_retention_days: 365 +# min_retention_days: 1 ``` Then if we would like to deploy the component into a given stacks we can import the following to deploy our backup plans. @@ -85,7 +92,12 @@ components: vars: plan_name_suffix: aws-backup-daily # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html - schedule: cron(0 0 ? * * *) # Daily at midnight (UTC) + rules: + - name: "plan-daily" + schedule: "cron(0 5 ? * * *)" + start_window: 320 # 60 * 8 # minutes + completion_window: 10080 # 60 * 24 * 7 # minutes + delete_after: 35 # 7 * 5 # days selection_tags: - type: STRINGEQUALS key: aws-backup/efs @@ -102,7 +114,12 @@ components: vars: plan_name_suffix: aws-backup-weekly # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html - schedule: cron(0 0 ? * 1 *) # Weekly on first day of week at midnight (UTC) + rules: + - name: "plan-weekly" + schedule: "cron(0 5 ? * SAT *)" + start_window: 320 # 60 * 8 # minutes + completion_window: 10080 # 60 * 24 * 7 # minutes + delete_after: 90 # 30 * 3 # days selection_tags: - type: STRINGEQUALS key: aws-backup/efs @@ -118,10 +135,15 @@ components: - aws-backup/plan-defaults vars: plan_name_suffix: aws-backup-monthly - # delete monthly snapshots after 60 days - delete_after: 60 # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html - schedule: cron(0 0 1 * ? *) # Monthly on 1st day of the month (doesn't matter which) at midnight UTC + rules: + - name: "plan-monthly" + schedule: "cron(0 5 1 * ? *)" + start_window: 320 # 60 * 8 # minutes + completion_window: 10080 # 60 * 24 * 7 # minutes + delete_after: 2555 # 365 * 7 # days + cold_storage_after: 90 # 30 * 3 # days + selection_tags: - type: STRINGEQUALS key: aws-backup/efs @@ -182,13 +204,41 @@ components: copy_action_delete_after: 14 ``` +### Backup Lock Configuration + +To enable backup lock configuration, you can use the following snippet: + +* [AWS Backup Vault Lock](https://docs.aws.amazon.com/aws-backup/latest/devguide/vault-lock.html) + +#### Compliance Mode +Vaults locked in compliance mode cannot be deleted once the cooling-off period ("grace time") expires. During grace time, you can still remove the vault lock and change the lock configuration. + +To enable **Compliance Mode**, set `changeable_for_days` to a value greater than 0. Once the lock is set, the retention policy cannot be changed unless through account deletion! +```yaml +# Please be careful when enabling backup_vault_lock_configuration, + backup_vault_lock_configuration: +# `changeable_for_days` enables compliance mode and once the lock is set, the retention policy cannot be changed unless through account deletion! + changeable_for_days: 36500 + max_retention_days: 365 + min_retention_days: 1 +``` + +#### Governance Mode +Vaults locked in governance mode can have the lock removed by users with sufficient IAM permissions. + +To enable **governance mode** +```yaml + backup_vault_lock_configuration: + max_retention_days: 365 + min_retention_days: 1 +``` ## Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | ## Providers @@ -199,7 +249,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [backup](#module\_backup) | cloudposse/backup/aws | 0.14.0 | +| [backup](#module\_backup) | cloudposse/backup/aws | 1.0.0 | | [copy\_destination\_vault](#module\_copy\_destination\_vault) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -213,8 +263,10 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [advanced\_backup\_setting](#input\_advanced\_backup\_setting) | An object that specifies backup options for each resource type. |
object({
backup_options = string
resource_type = string
})
| `null` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [backup\_resources](#input\_backup\_resources) | An array of strings that either contain Amazon Resource Names (ARNs) or match patterns of resources to assign to a backup plan | `list(string)` | `[]` | no | +| [backup\_vault\_lock\_configuration](#input\_backup\_vault\_lock\_configuration) | The backup vault lock configuration, each vault can have one vault lock in place. This will enable Backup Vault Lock on an AWS Backup vault it prevents the deletion of backup data for the specified retention period. During this time, the backup data remains immutable and cannot be deleted or modified."
`changeable_for_days` - The number of days before the lock date. If omitted creates a vault lock in `governance` mode, otherwise it will create a vault lock in `compliance` mode. |
object({
changeable_for_days = optional(number)
max_retention_days = optional(number)
min_retention_days = optional(number)
})
| `null` | no | | [cold\_storage\_after](#input\_cold\_storage\_after) | Specifies the number of days after creation that a recovery point is moved to cold storage | `number` | `null` | no | | [completion\_window](#input\_completion\_window) | The amount of time AWS Backup attempts a backup before canceling the job and returning an error. Must be at least 60 minutes greater than `start_window` | `number` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | @@ -241,6 +293,7 @@ No resources. | [plan\_name\_suffix](#input\_plan\_name\_suffix) | The string appended to the plan name | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [rules](#input\_rules) | An array of rule maps used to define schedules in a backup plan |
list(object({
name = string
schedule = optional(string)
enable_continuous_backup = optional(bool)
start_window = optional(number)
completion_window = optional(number)
lifecycle = optional(object({
cold_storage_after = optional(number)
delete_after = optional(number)
opt_in_to_archive_for_supported_resources = optional(bool)
}))
copy_action = optional(object({
destination_vault_arn = optional(string)
lifecycle = optional(object({
cold_storage_after = optional(number)
delete_after = optional(number)
opt_in_to_archive_for_supported_resources = optional(bool)
}))
}))
}))
| `[]` | no | | [schedule](#input\_schedule) | A CRON expression specifying when AWS Backup initiates a backup job | `string` | `null` | no | | [selection\_tags](#input\_selection\_tags) | An array of tag condition objects used to filter resources based on tags for assigning to a backup plan | `list(map(string))` | `[]` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/aws-backup/main.tf b/modules/aws-backup/main.tf index bb371da65..f93e4717d 100644 --- a/modules/aws-backup/main.tf +++ b/modules/aws-backup/main.tf @@ -6,7 +6,7 @@ locals { module "backup" { source = "cloudposse/backup/aws" - version = "0.14.0" + version = "1.0.0" plan_name_suffix = var.plan_name_suffix vault_enabled = var.vault_enabled @@ -16,17 +16,11 @@ module "backup" { backup_resources = var.backup_resources selection_tags = var.selection_tags - schedule = var.schedule - start_window = var.start_window - completion_window = var.completion_window - cold_storage_after = var.cold_storage_after - delete_after = var.delete_after - kms_key_arn = var.kms_key_arn + kms_key_arn = var.kms_key_arn - # Copy config to new region - destination_vault_arn = local.copy_destination_arn - copy_action_cold_storage_after = var.copy_action_cold_storage_after - copy_action_delete_after = var.copy_action_delete_after + rules = var.rules + advanced_backup_setting = var.advanced_backup_setting + backup_vault_lock_configuration = var.backup_vault_lock_configuration context = module.this.context } diff --git a/modules/aws-backup/variables.tf b/modules/aws-backup/variables.tf index c43515f5a..4b8de574e 100644 --- a/modules/aws-backup/variables.tf +++ b/modules/aws-backup/variables.tf @@ -21,6 +21,19 @@ variable "start_window" { default = null } +variable "backup_vault_lock_configuration" { + type = object({ + changeable_for_days = optional(number) + max_retention_days = optional(number) + min_retention_days = optional(number) + }) + description = <<-EOT + The backup vault lock configuration, each vault can have one vault lock in place. This will enable Backup Vault Lock on an AWS Backup vault it prevents the deletion of backup data for the specified retention period. During this time, the backup data remains immutable and cannot be deleted or modified." + `changeable_for_days` - The number of days before the lock date. If omitted creates a vault lock in `governance` mode, otherwise it will create a vault lock in `compliance` mode. + EOT + default = null +} + variable "completion_window" { type = number description = "The amount of time AWS Backup attempts a backup before canceling the job and returning an error. Must be at least 60 minutes greater than `start_window`" @@ -104,3 +117,38 @@ variable "iam_role_enabled" { description = "Whether or not to create a new IAM Role and Policy Attachment" default = true } + + +variable "rules" { + type = list(object({ + name = string + schedule = optional(string) + enable_continuous_backup = optional(bool) + start_window = optional(number) + completion_window = optional(number) + lifecycle = optional(object({ + cold_storage_after = optional(number) + delete_after = optional(number) + opt_in_to_archive_for_supported_resources = optional(bool) + })) + copy_action = optional(object({ + destination_vault_arn = optional(string) + lifecycle = optional(object({ + cold_storage_after = optional(number) + delete_after = optional(number) + opt_in_to_archive_for_supported_resources = optional(bool) + })) + })) + })) + description = "An array of rule maps used to define schedules in a backup plan" + default = [] +} + +variable "advanced_backup_setting" { + type = object({ + backup_options = string + resource_type = string + }) + description = "An object that specifies backup options for each resource type." + default = null +} diff --git a/modules/aws-backup/versions.tf b/modules/aws-backup/versions.tf index cc73ffd35..b5920b7b1 100644 --- a/modules/aws-backup/versions.tf +++ b/modules/aws-backup/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.3.0" required_providers { aws = { From 2bf1db8226b41f1b34e374366e83f502d6c4eea4 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 31 Jan 2024 14:09:49 -0800 Subject: [PATCH 356/501] EFS Volume Lookup support for `ecs-service` (#969) Co-authored-by: Dan Miller --- modules/ecs-service/README.md | 57 ++++++++++++++++++++++++++++- modules/ecs-service/main.tf | 24 +++++++++++- modules/ecs-service/remote-state.tf | 17 +++++++++ modules/ecs-service/variables.tf | 24 ++++++++++-- 4 files changed, 116 insertions(+), 6 deletions(-) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 754586cb2..d6c4d7e65 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -191,6 +191,58 @@ We can then create a pointer to this service in the `acme.come` hosted zone. This will create a CNAME record in the `acme.com` hosted zone that points `echo.acme.com` to `echo-server.dev-acme.com`. +### EFS + +EFS is supported by `ecs-service`. You can use either `efs_volumes` or `efs_component_volumes` in your task definition. + +This example shows how to use `efs_component_volumes` which remote looks up efs component and uses the `efs_id` to mount the volume. +And how to use `efs_volumes` +```yaml +components: + terraform: + ecs-services/my-service: + metadata: + component: ecs-service + inherits: + - ecs-services/defaults + vars: + containers: + service: + name: app + image: my-image:latest + log_configuration: + logDriver: awslogs + options: {} + port_mappings: + - containerPort: 8080 + hostPort: 8080 + protocol: tcp + mount_points: + - containerPath: "/var/lib/" + sourceVolume: "my-volume-mount" + + task: + efs_component_volumes: + - name: "my-volume-mount" + host_path: null + efs_volume_configuration: + - component: efs/my-volume-mount + root_directory: "/var/lib/" + transit_encryption: "ENABLED" + transit_encryption_port: 2999 + authorization_config: [] + efs_volumes: + - name: "my-volume-mount-2" + host_path: null + efs_volume_ configuration: + - file_system_id: "fs-1234" + root_directory: "/var/lib/" + transit_encryption: "ENABLED" + transit_encryption_port: 2998 + authorization_config: [] +``` + + ## Requirements @@ -224,6 +276,7 @@ This will create a CNAME record in the `acme.com` hosted zone that points `echo. | [ecs\_cloudwatch\_autoscaling](#module\_ecs\_cloudwatch\_autoscaling) | cloudposse/ecs-cloudwatch-autoscaling/aws | 0.7.3 | | [ecs\_cloudwatch\_sns\_alarms](#module\_ecs\_cloudwatch\_sns\_alarms) | cloudposse/ecs-cloudwatch-sns-alarms/aws | 0.12.3 | | [ecs\_cluster](#module\_ecs\_cluster) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [efs](#module\_efs) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | | [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | | [iam\_role](#module\_iam\_role) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | @@ -274,7 +327,7 @@ This will create a CNAME record in the `acme.com` hosted zone that points `echo. | [autoscaling\_enabled](#input\_autoscaling\_enabled) | Should this service autoscale using SNS alarams | `bool` | `true` | no | | [chamber\_service](#input\_chamber\_service) | SSM parameter service name for use with chamber. This is used in chamber\_format where /$chamber\_service/$name/$container\_name/$parameter would be the default. | `string` | `"ecs-service"` | no | | [cluster\_attributes](#input\_cluster\_attributes) | The attributes of the cluster name e.g. if the full name is `namespace-tenant-environment-dev-ecs-b2b` then the `cluster_name` is `ecs` and this value should be `b2b`. | `list(string)` | `[]` | no | -| [containers](#input\_containers) | Feed inputs into container definition module |
map(object({
name = string
ecr_image = optional(string)
image = optional(string)
memory = optional(number)
memory_reservation = optional(number)
cpu = optional(number)
essential = optional(bool, true)
readonly_root_filesystem = optional(bool, null)
privileged = optional(bool, null)
container_depends_on = optional(list(object({
containerName = string
condition = string # START, COMPLETE, SUCCESS, HEALTHY
})), null)

port_mappings = optional(list(object({
containerPort = number
hostPort = number
protocol = string
})), [])
command = optional(list(string), null)
entrypoint = optional(list(string), null)
healthcheck = optional(object({
command = list(string)
interval = number
retries = number
startPeriod = number
timeout = number
}), null)
ulimits = optional(list(object({
name = string
softLimit = number
hardLimit = number
})), null)
log_configuration = optional(object({
logDriver = string
options = optional(map(string), {})
}))
docker_labels = optional(map(string), null)
map_environment = optional(map(string), {})
map_secrets = optional(map(string), {})
volumes_from = optional(list(object({
sourceContainer = string
readOnly = bool
})), null)
mount_points = optional(list(object({
sourceVolume = string
containerPath = string
readOnly = bool
})), [])
}))
| `{}` | no | +| [containers](#input\_containers) | Feed inputs into container definition module |
map(object({
name = string
ecr_image = optional(string)
image = optional(string)
memory = optional(number)
memory_reservation = optional(number)
cpu = optional(number)
essential = optional(bool, true)
readonly_root_filesystem = optional(bool, null)
privileged = optional(bool, null)
container_depends_on = optional(list(object({
containerName = string
condition = string # START, COMPLETE, SUCCESS, HEALTHY
})), null)

port_mappings = optional(list(object({
containerPort = number
hostPort = number
protocol = string
})), [])
command = optional(list(string), null)
entrypoint = optional(list(string), null)
healthcheck = optional(object({
command = list(string)
interval = number
retries = number
startPeriod = number
timeout = number
}), null)
ulimits = optional(list(object({
name = string
softLimit = number
hardLimit = number
})), null)
log_configuration = optional(object({
logDriver = string
options = optional(map(string), {})
}))
docker_labels = optional(map(string), null)
map_environment = optional(map(string), {})
map_secrets = optional(map(string), {})
volumes_from = optional(list(object({
sourceContainer = string
readOnly = bool
})), null)
mount_points = optional(list(object({
sourceVolume = optional(string)
containerPath = optional(string)
readOnly = optional(bool)
})), [])
}))
| `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [cpu\_utilization\_high\_alarm\_actions](#input\_cpu\_utilization\_high\_alarm\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization High Alarm action | `list(string)` | `[]` | no | | [cpu\_utilization\_high\_evaluation\_periods](#input\_cpu\_utilization\_high\_evaluation\_periods) | Number of periods to evaluate for the alarm | `number` | `1` | no | @@ -352,7 +405,7 @@ This will create a CNAME record in the `acme.com` hosted zone that points `echo. | [stickiness\_type](#input\_stickiness\_type) | The type of sticky sessions. The only current possible value is `lb_cookie` | `string` | `"lb_cookie"` | no | | [stream\_mode](#input\_stream\_mode) | Stream mode details for the Kinesis stream | `string` | `"PROVISIONED"` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module |
object({
task_cpu = optional(number)
task_memory = optional(number)
task_role_arn = optional(string, "")
pid_mode = optional(string, null)
ipc_mode = optional(string, null)
network_mode = optional(string)
propagate_tags = optional(string)
assign_public_ip = optional(bool, false)
use_alb_security_groups = optional(bool, true)
launch_type = optional(string, "FARGATE")
scheduling_strategy = optional(string, "REPLICA")
capacity_provider_strategies = optional(list(object({
capacity_provider = string
weight = number
base = number
})), [])

deployment_minimum_healthy_percent = optional(number, null)
deployment_maximum_percent = optional(number, null)
desired_count = optional(number, 0)
min_capacity = optional(number, 1)
max_capacity = optional(number, 2)
wait_for_steady_state = optional(bool, true)
circuit_breaker_deployment_enabled = optional(bool, true)
circuit_breaker_rollback_enabled = optional(bool, true)

ecs_service_enabled = optional(bool, true)
bind_mount_volumes = optional(list(object({
name = string
host_path = string
})), [])
efs_volumes = optional(list(object({
host_path = string
name = string
efs_volume_configuration = list(object({
file_system_id = string
root_directory = string
transit_encryption = string
transit_encryption_port = string
authorization_config = list(object({
access_point_id = string
iam = string
}))
}))
})), [])
docker_volumes = optional(list(object({
host_path = string
name = string
docker_volume_configuration = list(object({
autoprovision = bool
driver = string
driver_opts = map(string)
labels = map(string)
scope = string
}))
})), [])
fsx_volumes = optional(list(object({
host_path = string
name = string
fsx_windows_file_server_volume_configuration = list(object({
file_system_id = string
root_directory = string
authorization_config = list(object({
credentials_parameter = string
domain = string
}))
}))
})), [])
})
| `{}` | no | +| [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module |
object({
task_cpu = optional(number)
task_memory = optional(number)
task_role_arn = optional(string, "")
pid_mode = optional(string, null)
ipc_mode = optional(string, null)
network_mode = optional(string)
propagate_tags = optional(string)
assign_public_ip = optional(bool, false)
use_alb_security_groups = optional(bool, true)
launch_type = optional(string, "FARGATE")
scheduling_strategy = optional(string, "REPLICA")
capacity_provider_strategies = optional(list(object({
capacity_provider = string
weight = number
base = number
})), [])

deployment_minimum_healthy_percent = optional(number, null)
deployment_maximum_percent = optional(number, null)
desired_count = optional(number, 0)
min_capacity = optional(number, 1)
max_capacity = optional(number, 2)
wait_for_steady_state = optional(bool, true)
circuit_breaker_deployment_enabled = optional(bool, true)
circuit_breaker_rollback_enabled = optional(bool, true)

ecs_service_enabled = optional(bool, true)
bind_mount_volumes = optional(list(object({
name = string
host_path = string
})), [])
efs_volumes = optional(list(object({
host_path = string
name = string
efs_volume_configuration = list(object({
file_system_id = string
root_directory = string
transit_encryption = string
transit_encryption_port = string
authorization_config = list(object({
access_point_id = string
iam = string
}))
}))
})), [])
efs_component_volumes = optional(list(object({
host_path = string
name = string
efs_volume_configuration = list(object({
component = optional(string, "efs")
tenant = optional(string, null)
environment = optional(string, null)
stage = optional(string, null)

root_directory = string
transit_encryption = string
transit_encryption_port = string
authorization_config = list(object({
access_point_id = string
iam = string
}))
}))
})), [])
docker_volumes = optional(list(object({
host_path = string
name = string
docker_volume_configuration = list(object({
autoprovision = bool
driver = string
driver_opts = map(string)
labels = map(string)
scope = string
}))
})), [])
fsx_volumes = optional(list(object({
host_path = string
name = string
fsx_windows_file_server_volume_configuration = list(object({
file_system_id = string
root_directory = string
authorization_config = list(object({
credentials_parameter = string
domain = string
}))
}))
})), [])
})
| `{}` | no | | [task\_enabled](#input\_task\_enabled) | Whether or not to use the ECS task module | `bool` | `true` | no | | [task\_iam\_role\_component](#input\_task\_iam\_role\_component) | A component that outputs an iam\_role module as 'role' for adding to the service as a whole. | `string` | `null` | no | | [task\_policy\_arns](#input\_task\_policy\_arns) | The IAM policy ARNs to attach to the ECS task IAM role | `list(string)` |
[
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
"arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess"
]
| no | diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index f34c34873..5000fc207 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -40,6 +40,28 @@ locals { } : {} task = merge(var.task, local.task_s3) + + efs_component_volumes = lookup(local.task, "efs_component_volumes", []) + efs_component_map = { + for efs in local.efs_component_volumes : efs["name"] => efs + } + efs_component_remote_state = { + for efs in local.efs_component_volumes : efs["name"] => module.efs[efs["name"]].outputs + } + efs_component_merged = [for efs_volume_name, efs_component_output in local.efs_component_remote_state : { + host_path = local.efs_component_map[efs_volume_name].host_path + name = efs_volume_name + efs_volume_configuration = [ #again this is a hardcoded array because AWS does not support multiple configurations per volume + { + file_system_id = efs_component_output.efs_id + root_directory = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].root_directory + transit_encryption = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].transit_encryption + transit_encryption_port = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].transit_encryption_port + authorization_config = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].authorization_config + } + ] + }] + efs_volumes = concat(lookup(local.task, "efs_volumes", []), local.efs_component_merged) } data "aws_s3_objects" "mirror" { @@ -268,7 +290,7 @@ module "ecs_alb_service_task" { task_role_arn = lookup(local.task, "task_role_arn", one(module.iam_role[*]["outputs"]["role"]["arn"])) capacity_provider_strategies = lookup(local.task, "capacity_provider_strategies") - efs_volumes = lookup(local.task, "efs_volumes", []) + efs_volumes = local.efs_volumes docker_volumes = lookup(local.task, "docker_volumes", []) fsx_volumes = lookup(local.task, "fsx_volumes", []) bind_mount_volumes = lookup(local.task, "bind_mount_volumes", []) diff --git a/modules/ecs-service/remote-state.tf b/modules/ecs-service/remote-state.tf index 4ca467c7f..a32de427f 100644 --- a/modules/ecs-service/remote-state.tf +++ b/modules/ecs-service/remote-state.tf @@ -181,3 +181,20 @@ module "iam_role" { context = module.this.context } + +module "efs" { + for_each = local.efs_component_map + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + # Here we can use [0] because aws only allows one efs volume configuration per volume + component = each.value.efs_volume_configuration[0].component + + context = module.this.context + + tenant = each.value.efs_volume_configuration[0].tenant + stage = each.value.efs_volume_configuration[0].stage + environment = each.value.efs_volume_configuration[0].environment + +} diff --git a/modules/ecs-service/variables.tf b/modules/ecs-service/variables.tf index 55d5c9274..2df296d5f 100644 --- a/modules/ecs-service/variables.tf +++ b/modules/ecs-service/variables.tf @@ -92,9 +92,9 @@ variable "containers" { readOnly = bool })), null) mount_points = optional(list(object({ - sourceVolume = string - containerPath = string - readOnly = bool + sourceVolume = optional(string) + containerPath = optional(string) + readOnly = optional(bool) })), []) })) description = "Feed inputs into container definition module" @@ -148,6 +148,24 @@ variable "task" { })) })) })), []) + efs_component_volumes = optional(list(object({ + host_path = string + name = string + efs_volume_configuration = list(object({ + component = optional(string, "efs") + tenant = optional(string, null) + environment = optional(string, null) + stage = optional(string, null) + + root_directory = string + transit_encryption = string + transit_encryption_port = string + authorization_config = list(object({ + access_point_id = string + iam = string + })) + })) + })), []) docker_volumes = optional(list(object({ host_path = string name = string From 288d6f86742c6a9ec6b1bef5aca4c77ab4e7be80 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 31 Jan 2024 14:52:40 -0800 Subject: [PATCH 357/501] `lambda` Remove formatting of bucket name (#970) --- modules/lambda/README.md | 1 + modules/lambda/main.tf | 15 +++++++++------ modules/lambda/variables.tf | 10 ++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/modules/lambda/README.md b/modules/lambda/README.md index c61ed6261..310a65d03 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -161,6 +161,7 @@ components: | [reserved\_concurrent\_executions](#input\_reserved\_concurrent\_executions) | The amount of reserved concurrent executions for this lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `-1` | no | | [runtime](#input\_runtime) | The runtime environment for the Lambda function you are uploading. | `string` | `null` | no | | [s3\_bucket\_name](#input\_s3\_bucket\_name) | The name suffix of the S3 bucket containing the function's deployment package. Conflicts with filename and image\_uri.
This bucket must reside in the same AWS region where you are creating the Lambda function. | `string` | `null` | no | +| [s3\_full\_bucket\_name](#input\_s3\_full\_bucket\_name) | The full name of the S3 bucket containing the function's deployment package. Conflicts with filename and image\_uri.
This bucket must reside in the same AWS region where you are creating the Lambda function.

This is alternative to `var.s3_bucket_name` which formats the name for the current account. | `string` | `null` | no | | [s3\_key](#input\_s3\_key) | The S3 key of an object containing the function's deployment package. Conflicts with filename and image\_uri. | `string` | `null` | no | | [s3\_object\_version](#input\_s3\_object\_version) | The object version containing the function's deployment package. Conflicts with filename and image\_uri. | `string` | `null` | no | | [sns\_subscriptions](#input\_sns\_subscriptions) | Creates subscriptions to SNS topics which trigger the Lambda Function. Required Lambda invocation permissions will be generated. | `map(any)` | `{}` | no | diff --git a/modules/lambda/main.tf b/modules/lambda/main.tf index 897976789..323f82356 100644 --- a/modules/lambda/main.tf +++ b/modules/lambda/main.tf @@ -1,9 +1,12 @@ locals { - enabled = module.this.enabled - iam_policy_enabled = local.enabled && (try(length(var.iam_policy), 0) > 0 || var.policy_json != null) - s3_bucket_full_name = var.s3_bucket_name != null ? format("%s-%s-%s-%s-%s", module.this.namespace, module.this.tenant, module.this.environment, module.this.stage, var.s3_bucket_name) : null + enabled = module.this.enabled + iam_policy_enabled = local.enabled && (try(length(var.iam_policy), 0) > 0 || var.policy_json != null) + s3_bucket_computed_name = var.s3_bucket_name != null ? format("%s-%s-%s-%s-%s", module.this.namespace, module.this.tenant, module.this.environment, module.this.stage, var.s3_bucket_name) : null - cicd_s3_key_format = var.cicd_s3_key_format != null ? var.cicd_s3_key_format : "stage/${module.this.stage}/lambda/${var.function_name}/%s" + s3_full_bucket_name = coalesce(var.s3_full_bucket_name, local.s3_bucket_computed_name, "none") == "none" ? null : coalesce(var.s3_full_bucket_name, local.s3_bucket_computed_name) + function_name = coalesce(var.function_name, module.label.id) + + cicd_s3_key_format = var.cicd_s3_key_format != null ? var.cicd_s3_key_format : "stage/${module.this.stage}/lambda/${local.function_name}/%s" s3_key = var.s3_bucket_name == null ? null : (var.s3_key != null ? var.s3_key : format(local.cicd_s3_key_format, coalesce(one(data.aws_ssm_parameter.cicd_ssm_param[*].value), "example"))) } @@ -53,7 +56,7 @@ module "lambda" { source = "cloudposse/lambda-function/aws" version = "0.4.1" - function_name = coalesce(var.function_name, module.label.id) + function_name = local.function_name description = var.description handler = var.handler lambda_environment = var.lambda_environment @@ -61,7 +64,7 @@ module "lambda" { image_config = var.image_config filename = var.filename - s3_bucket = local.s3_bucket_full_name + s3_bucket = local.s3_full_bucket_name s3_key = local.s3_key s3_object_version = var.s3_object_version diff --git a/modules/lambda/variables.tf b/modules/lambda/variables.tf index f2f1ccc7b..f0736c8c2 100644 --- a/modules/lambda/variables.tf +++ b/modules/lambda/variables.tf @@ -173,6 +173,16 @@ variable "runtime" { description = "The runtime environment for the Lambda function you are uploading." default = null } +variable "s3_full_bucket_name" { + type = string + description = < Date: Thu, 1 Feb 2024 12:05:05 -0800 Subject: [PATCH 358/501] feat: Multi Org Support for ArgoCD Deployment Repos (#965) Co-authored-by: cloudpossebot --- modules/argocd-repo/README.md | 2 + modules/argocd-repo/applicationset.tf | 7 +- modules/argocd-repo/main.tf | 2 +- .../templates/applicationset.yaml.tpl | 11 +- modules/argocd-repo/variables.tf | 23 ++ modules/eks/argocd/README.md | 55 +++- modules/eks/argocd/github_webhook.tf | 59 ++++ modules/eks/argocd/main.tf | 47 +-- modules/eks/argocd/notifications.tf | 261 ++++++++-------- modules/eks/argocd/outputs.tf | 5 + modules/eks/argocd/provider-github.tf | 13 +- .../argocd/variables-argocd-notifications.tf | 4 - modules/eks/argocd/variables-argocd.tf | 6 - modules/github-webhook/README.md | 148 ++++++++++ modules/github-webhook/context.tf | 279 ++++++++++++++++++ modules/github-webhook/main.tf | 33 +++ modules/github-webhook/provider-github.tf | 33 +++ modules/github-webhook/providers.tf | 19 ++ modules/github-webhook/remote-state.tf | 12 + modules/github-webhook/variables.tf | 49 +++ modules/github-webhook/versions.tf | 14 + 21 files changed, 894 insertions(+), 188 deletions(-) create mode 100644 modules/eks/argocd/github_webhook.tf create mode 100644 modules/github-webhook/README.md create mode 100644 modules/github-webhook/context.tf create mode 100644 modules/github-webhook/main.tf create mode 100644 modules/github-webhook/provider-github.tf create mode 100644 modules/github-webhook/providers.tf create mode 100644 modules/github-webhook/remote-state.tf create mode 100644 modules/github-webhook/variables.tf create mode 100644 modules/github-webhook/versions.tf diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index bc74f4878..ac1931c07 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -140,6 +140,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [github\_base\_url](#input\_github\_base\_url) | This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/` | `string` | `null` | no | | [github\_codeowner\_teams](#input\_github\_codeowner\_teams) | List of teams to use when populating the CODEOWNERS file.

For example: `["@ACME/cloud-admins", "@ACME/cloud-developers"]`. | `list(string)` | n/a | yes | | [github\_default\_notifications\_enabled](#input\_github\_default\_notifications\_enabled) | Enable default GitHub commit statuses notifications (required for CD sync mode) | `string` | `true` | no | +| [github\_notifications](#input\_github\_notifications) | ArgoCD notification annotations for subscribing to GitHub.

The default value given uses the same notification template names as defined in the `eks/argocd` component. If want to add additional notifications, include any existing notifications from this list that you want to keep in addition. | `list(string)` |
[
"notifications.argoproj.io/subscribe.on-deploy-started.app-repo-github-commit-status: \"\"",
"notifications.argoproj.io/subscribe.on-deploy-started.argocd-repo-github-commit-status: \"\"",
"notifications.argoproj.io/subscribe.on-deploy-succeded.app-repo-github-commit-status: \"\"",
"notifications.argoproj.io/subscribe.on-deploy-succeded.argocd-repo-github-commit-status: \"\"",
"notifications.argoproj.io/subscribe.on-deploy-failed.app-repo-github-commit-status: \"\"",
"notifications.argoproj.io/subscribe.on-deploy-failed.argocd-repo-github-commit-status: \"\""
]
| no | | [github\_organization](#input\_github\_organization) | GitHub Organization | `string` | n/a | yes | | [github\_token\_override](#input\_github\_token\_override) | Use the value of this variable as the GitHub token instead of reading it from SSM | `string` | `null` | no | | [github\_user](#input\_github\_user) | Github user | `string` | n/a | yes | @@ -150,6 +151,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [manifest\_kubernetes\_namespace](#input\_manifest\_kubernetes\_namespace) | The namespace used for the ArgoCD application | `string` | `"argocd"` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [permissions](#input\_permissions) | A list of Repository Permission objects used to configure the team permissions of the repository

`team_slug` should be the name of the team without the `@{org}` e.g. `@cloudposse/team` => `team`
`permission` is just one of the available values listed below |
list(object({
team_slug = string,
permission = string
}))
| `[]` | no | diff --git a/modules/argocd-repo/applicationset.tf b/modules/argocd-repo/applicationset.tf index fccb75ec2..21abbf92b 100644 --- a/modules/argocd-repo/applicationset.tf +++ b/modules/argocd-repo/applicationset.tf @@ -1,3 +1,8 @@ +locals { + github_default_notifications_enabled = local.enabled && var.github_default_notifications_enabled + github_notifications = local.github_default_notifications_enabled ? var.github_notifications : [] +} + resource "github_repository_file" "application_set" { for_each = local.environments @@ -11,7 +16,7 @@ resource "github_repository_file" "application_set" { name = module.this.namespace namespace = local.manifest_kubernetes_namespace ssh_url = local.github_repository.ssh_clone_url - notifications = var.github_default_notifications_enabled + notifications = local.github_notifications slack_notifications_channel = var.slack_notifications_channel }) commit_message = "Initialize environment: `${each.key}`." diff --git a/modules/argocd-repo/main.tf b/modules/argocd-repo/main.tf index 631a3e4e3..ede85f1bd 100644 --- a/modules/argocd-repo/main.tf +++ b/modules/argocd-repo/main.tf @@ -12,7 +12,7 @@ locals { )) => env } : {} - manifest_kubernetes_namespace = "argocd" + manifest_kubernetes_namespace = var.manifest_kubernetes_namespace team_slugs = toset(compact([ for permission in var.permissions : lookup(permission, "team_slug", null) diff --git a/modules/argocd-repo/templates/applicationset.yaml.tpl b/modules/argocd-repo/templates/applicationset.yaml.tpl index f4aec1768..e44b750ff 100644 --- a/modules/argocd-repo/templates/applicationset.yaml.tpl +++ b/modules/argocd-repo/templates/applicationset.yaml.tpl @@ -48,14 +48,9 @@ spec: app_repository: '{{app_repository}}' app_commit: '{{app_commit}}' app_hostname: 'https://{{app_hostname}}' -%{if notifications ~} - notifications.argoproj.io/subscribe.on-deploy-started.app-repo-github-commit-status: "" - notifications.argoproj.io/subscribe.on-deploy-started.argocd-repo-github-commit-status: "" - notifications.argoproj.io/subscribe.on-deploy-succeded.app-repo-github-commit-status: "" - notifications.argoproj.io/subscribe.on-deploy-succeded.argocd-repo-github-commit-status: "" - notifications.argoproj.io/subscribe.on-deploy-failed.app-repo-github-commit-status: "" - notifications.argoproj.io/subscribe.on-deploy-failed.argocd-repo-github-commit-status: "" -%{ endif ~} +%{for noti in notifications ~} + ${noti} +%{ endfor ~} %{if length(slack_notifications_channel) > 0 ~} notifications.argoproj.io/subscribe.on-created.slack: ${slack_notifications_channel} notifications.argoproj.io/subscribe.on-deleted.slack: ${slack_notifications_channel} diff --git a/modules/argocd-repo/variables.tf b/modules/argocd-repo/variables.tf index ac6e22187..65b336576 100644 --- a/modules/argocd-repo/variables.tf +++ b/modules/argocd-repo/variables.tf @@ -156,3 +156,26 @@ variable "slack_notifications_channel" { default = "" description = "If given, the Slack channel to for deployment notifications." } + +variable "manifest_kubernetes_namespace" { + type = string + default = "argocd" + description = "The namespace used for the ArgoCD application" +} + +variable "github_notifications" { + type = list(string) + default = [ + "notifications.argoproj.io/subscribe.on-deploy-started.app-repo-github-commit-status: \"\"", + "notifications.argoproj.io/subscribe.on-deploy-started.argocd-repo-github-commit-status: \"\"", + "notifications.argoproj.io/subscribe.on-deploy-succeded.app-repo-github-commit-status: \"\"", + "notifications.argoproj.io/subscribe.on-deploy-succeded.argocd-repo-github-commit-status: \"\"", + "notifications.argoproj.io/subscribe.on-deploy-failed.app-repo-github-commit-status: \"\"", + "notifications.argoproj.io/subscribe.on-deploy-failed.argocd-repo-github-commit-status: \"\"", + ] + description = < [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles\_config\_secrets](#module\_iam\_roles\_config\_secrets) | ../../account-map/modules/iam-roles | n/a | +| [notifications\_notifiers](#module\_notifications\_notifiers) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | +| [notifications\_templates](#module\_notifications\_templates) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | | [saml\_sso\_providers](#module\_saml\_sso\_providers) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -478,6 +524,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"5.19.12"` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_github\_webhook](#input\_create\_github\_webhook) | Enable GitHub webhook creation

Use this to create the GitHub Webhook for the given ArgoCD repo using the value created when `var.github_webhook_enabled` is `true`. | `bool` | `true` | no | | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `false` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | @@ -489,7 +536,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [github\_default\_notifications\_enabled](#input\_github\_default\_notifications\_enabled) | Enable default GitHub commit statuses notifications (required for CD sync mode) | `bool` | `true` | no | | [github\_organization](#input\_github\_organization) | GitHub Organization | `string` | n/a | yes | | [github\_token\_override](#input\_github\_token\_override) | Use the value of this variable as the GitHub token instead of reading it from SSM | `string` | `null` | no | -| [github\_webhook\_enabled](#input\_github\_webhook\_enabled) | Enable GitHub webhook integration | `bool` | `true` | no | +| [github\_webhook\_enabled](#input\_github\_webhook\_enabled) | Enable GitHub webhook integration

Use this to create a secret value and pass it to the argo-cd chart | `bool` | `true` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [host](#input\_host) | Host name to use for ingress and ALB | `string` | `""` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | @@ -510,7 +557,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
# service.webhook.:
webhook = optional(map(
object({
url = string
headers = optional(list(
object({
name = string
value = string
})
), [])
basicAuth = optional(object({
username = string
password = string
}))
insecureSkipVerify = optional(bool, false)
})
))
})
| `{}` | no | +| [notifications\_notifiers](#input\_notifications\_notifiers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
object({
ssm_path_prefix = optional(string, "/argocd/notifications/notifiers")
# service.webhook.:
webhook = optional(map(
object({
url = string
headers = optional(list(
object({
name = string
value = string
})
), [])
insecureSkipVerify = optional(bool, false)
})
))
})
| `{}` | no | | [notifications\_templates](#input\_notifications\_templates) | Notification Templates to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/templates/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L158) |
map(object({
message = string
alertmanager = optional(object({
labels = map(string)
annotations = map(string)
generatorURL = string
}))
webhook = optional(map(
object({
method = optional(string)
path = optional(string)
body = optional(string)
})
))
}))
| `{}` | no | | [notifications\_triggers](#input\_notifications\_triggers) | Notification Triggers to configure.

See: https://argocd-notifications.readthedocs.io/en/stable/triggers/
See: [Example value in argocd-notifications Helm Chart](https://github.com/argoproj/argo-helm/blob/a0a74fb43d147073e41aadc3d88660b312d6d638/charts/argocd-notifications/values.yaml#L352) |
map(list(
object({
oncePer = optional(string)
send = list(string)
when = string
})
))
| `{}` | no | | [oidc\_enabled](#input\_oidc\_enabled) | Toggles OIDC integration in the deployed chart | `bool` | `false` | no | @@ -541,7 +588,9 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec ## Outputs -No outputs. +| Name | Description | +|------|-------------| +| [github\_webhook\_value](#output\_github\_webhook\_value) | The value of the GitHub webhook secret used for ArgoCD | ## References diff --git a/modules/eks/argocd/github_webhook.tf b/modules/eks/argocd/github_webhook.tf new file mode 100644 index 000000000..b0696ee38 --- /dev/null +++ b/modules/eks/argocd/github_webhook.tf @@ -0,0 +1,59 @@ +# The GitHub webhook can be created with this component, with another component (such as github-webhook), or manually. +# However, we need to define the value for the webhook secret now with the ArgoCD chart deployment. Store it in SSM for reference. +# +# We need to create the webhook with a separate component if we're deploying argocd-repos for multiple GitHub Organizations +locals { + github_webhook_enabled = local.enabled && var.github_webhook_enabled + create_github_webhook = local.github_webhook_enabled && var.create_github_webhook + + webhook_github_secret = local.github_webhook_enabled ? try(random_password.webhook["github"].result, null) : "" +} + +variable "github_webhook_enabled" { + type = bool + default = true + description = < { + for k, v in var.argocd_repositories : replace(k, "/", "-") => { clone_url = module.argocd_repo[k].outputs.repository_ssh_clone_url github_deploy_key = data.aws_ssm_parameter.github_deploy_key[k].value + repository = module.argocd_repo[k].outputs.repository } } : {} - webhook_github_secret = try(random_password.webhook["github"].result, null) + credential_templates = flatten(concat([ for k, v in local.argocd_repositories : [ { @@ -159,7 +160,6 @@ module "argocd" { name = module.this.name oidc_enabled = local.oidc_enabled oidc_rbac_scopes = var.oidc_rbac_scopes - organization = var.github_organization saml_enabled = local.saml_enabled saml_rbac_scopes = var.saml_rbac_scopes rbac_default_policy = var.argocd_rbac_default_policy @@ -230,32 +230,3 @@ module "argocd_apps" { module.argocd ] } - -resource "random_password" "webhook" { - for_each = toset(local.github_webhook_enabled ? ["github"] : []) - - # min 16, max 128 - length = 128 - special = true - - min_upper = 3 - min_lower = 3 - min_numeric = 3 - min_special = 3 -} - -resource "github_repository_webhook" "default" { - for_each = local.github_webhook_enabled ? local.argocd_repositories : {} - repository = each.key - - configuration { - url = format("%s/api/webhook", local.url) - content_type = "json" - secret = local.webhook_github_secret - insecure_ssl = false - } - - active = true - - events = ["push"] -} diff --git a/modules/eks/argocd/notifications.tf b/modules/eks/argocd/notifications.tf index 0e9c2ebf3..3e213d90f 100644 --- a/modules/eks/argocd/notifications.tf +++ b/modules/eks/argocd/notifications.tf @@ -12,9 +12,143 @@ data "aws_ssm_parameter" "slack_notifications" { with_decryption = true } +module "notifications_templates" { + source = "cloudposse/config/yaml//modules/deepmerge" + version = "1.0.2" + + count = local.enabled ? 1 : 0 + + maps = [ + var.notifications_templates, + local.github_notifications_enabled ? { + app-deploy-succeded = { + message = "Application {{ .app.metadata.name }} is now running new version of deployments manifests." + webhook = { + app-repo-github-commit-status = { + for k, v in local.notifications_template_app_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "success" })) : tostring(v) + } + argocd-repo-github-commit-status = { + for k, v in local.notifications_template_argocd_repo_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "success" })) : tostring(v) + } + } + } + app-deploy-started = { + message = "Application {{ .app.metadata.name }} is now running new version of deployments manifests." + webhook = { + app-repo-github-commit-status = { + for k, v in local.notifications_template_app_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "pending" })) : tostring(v) + } + argocd-repo-github-commit-status = { + for k, v in local.notifications_template_argocd_repo_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "pending" })) : tostring(v) + } + } + } + app-deploy-failed = { + message = "Application {{ .app.metadata.name }} failed deploying new version." + webhook = { + app-repo-github-commit-status = { + for k, v in local.notifications_template_app_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "error" })) : tostring(v) + } + argocd-repo-github-commit-status = { + for k, v in local.notifications_template_argocd_repo_github_commit_status : + k => k == "body" ? jsonencode(merge(v, { state = "error" })) : tostring(v) + } + } + } + } : {}, + local.slack_notifications_enabled ? { + app-created = { + message = "Application {{ .app.metadata.name }} has been created." + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#00ff00" + } + ) + } + }, + app-deleted = { + message = "Application {{ .app.metadata.name }} was deleted." + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#FFA500" + } + ) + } + }, + app-success = { + message = "Application {{ .app.metadata.name }} deployment was successful!" + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#00ff00" + } + ) + } + }, + app-failure = { + message = "Application {{ .app.metadata.name }} deployment failed!" + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#FF0000" + } + ) + } + }, + app-started = { + message = "Application {{ .app.metadata.name }} started deployment..." + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#0000ff" + } + ) + } + }, + app-health-degraded = { + message = "Application {{ .app.metadata.name }} health has degraded!" + slack = { + attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", + { + color = "#FF0000" + } + ) + } + } + } : {} + ] +} + +module "notifications_notifiers" { + source = "cloudposse/config/yaml//modules/deepmerge" + version = "1.0.2" + + count = local.enabled ? 1 : 0 + + maps = [ + var.notifications_notifiers, + local.github_notifications_enabled ? { + webhook = { + app-repo-github-commit-status = local.notification_default_notifier_github_commit_status + argocd-repo-github-commit-status = local.notification_default_notifier_github_commit_status + } + } : {}, + local.slack_notifications_enabled ? { + slack = local.notification_slack_service + } : {} + ] +} + locals { - github_default_notifications_enabled = local.enabled && var.github_default_notifications_enabled - slack_notifications_enabled = local.enabled && var.slack_notifications_enabled + github_notifications_enabled = local.enabled && var.github_default_notifications_enabled + slack_notifications_enabled = local.enabled && var.slack_notifications_enabled notification_default_notifier_github_commit_status = { url = "https://api.github.com" @@ -24,6 +158,7 @@ locals { value = "token $common_github-token" } ] + insecureSkipVerify = false } notification_slack_service = { @@ -33,18 +168,7 @@ locals { icon = var.slack_notifications.icon } - notifications_notifiers = merge( - var.notifications_notifiers, - local.github_default_notifications_enabled ? { - webhook = { - app-repo-github-commit-status = local.notification_default_notifier_github_commit_status - argocd-repo-github-commit-status = local.notification_default_notifier_github_commit_status - } - } : {}, - local.slack_notifications_enabled ? { - slack = local.notification_slack_service - } : {} - ) + notifications_notifiers = jsondecode(local.enabled ? jsonencode(module.notifications_notifiers[0].merged) : jsonencode({})) ## Get list of notifiers services notifications_notifiers_variables = merge( @@ -106,114 +230,9 @@ locals { path = "/repos/{{call .repo.FullNameByRepoURL .app.spec.source.repoURL}}/statuses/{{.app.status.operationState.operation.sync.revision}}" }) - notifications_default_templates = merge(local.github_default_notifications_enabled ? { - app-deploy-succeded = { - message = "Application {{ .app.metadata.name }} is now running new version of deployments manifests." - webhook = { - app-repo-github-commit-status = { - for k, v in local.notifications_template_app_github_commit_status : - k => k == "body" ? jsonencode(merge(v, { state = "success" })) : tostring(v) - } - argocd-repo-github-commit-status = { - for k, v in local.notifications_template_argocd_repo_github_commit_status : - k => k == "body" ? jsonencode(merge(v, { state = "success" })) : tostring(v) - } - } - } - app-deploy-started = { - message = "Application {{ .app.metadata.name }} is now running new version of deployments manifests." - webhook = { - app-repo-github-commit-status = { - for k, v in local.notifications_template_app_github_commit_status : - k => k == "body" ? jsonencode(merge(v, { state = "pending" })) : tostring(v) - } - argocd-repo-github-commit-status = { - for k, v in local.notifications_template_argocd_repo_github_commit_status : - k => k == "body" ? jsonencode(merge(v, { state = "pending" })) : tostring(v) - } - } - } - app-deploy-failed = { - message = "Application {{ .app.metadata.name }} failed deploying new version." - webhook = { - app-repo-github-commit-status = { - for k, v in local.notifications_template_app_github_commit_status : - k => k == "body" ? jsonencode(merge(v, { state = "error" })) : tostring(v) - } - argocd-repo-github-commit-status = { - for k, v in local.notifications_template_argocd_repo_github_commit_status : - k => k == "body" ? jsonencode(merge(v, { state = "error" })) : tostring(v) - } - } - } - } : {}, - local.slack_notifications_enabled ? { - app-created = { - message = "Application {{ .app.metadata.name }} has been created." - slack = { - attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", - { - color = "#00ff00" - } - ) - } - }, - app-deleted = { - message = "Application {{ .app.metadata.name }} was deleted." - slack = { - attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", - { - color = "#FFA500" - } - ) - } - }, - app-success = { - message = "Application {{ .app.metadata.name }} deployment was successful!" - slack = { - attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", - { - color = "#00ff00" - } - ) - } - }, - app-failure = { - message = "Application {{ .app.metadata.name }} deployment failed!" - slack = { - attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", - { - color = "#FF0000" - } - ) - } - }, - app-started = { - message = "Application {{ .app.metadata.name }} started deployment..." - slack = { - attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", - { - color = "#0000ff" - } - ) - } - }, - app-health-degraded = { - message = "Application {{ .app.metadata.name }} health has degraded!" - slack = { - attachments = templatefile("${path.module}/resources/argocd-slack-message.tpl", - { - color = "#FF0000" - } - ) - } - } - } : {} - ) - - notifications_templates = merge(var.notifications_templates, local.notifications_default_templates) + notifications_templates = jsondecode(local.enabled ? jsonencode(module.notifications_templates[0].merged) : jsonencode({})) - notifications_default_triggers = merge(local.github_default_notifications_enabled ? { + notifications_default_triggers = merge(local.github_notifications_enabled ? { on-deploy-started = [ { when = "app.status.operationState.phase in ['Running'] or ( app.status.operationState.phase == 'Succeeded' and app.status.health.status == 'Progressing' )" diff --git a/modules/eks/argocd/outputs.tf b/modules/eks/argocd/outputs.tf index e69de29bb..043ed3fdf 100644 --- a/modules/eks/argocd/outputs.tf +++ b/modules/eks/argocd/outputs.tf @@ -0,0 +1,5 @@ +output "github_webhook_value" { + description = "The value of the GitHub webhook secret used for ArgoCD" + sensitive = true + value = local.webhook_github_secret +} diff --git a/modules/eks/argocd/provider-github.tf b/modules/eks/argocd/provider-github.tf index 022ae13dd..7c99b6240 100644 --- a/modules/eks/argocd/provider-github.tf +++ b/modules/eks/argocd/provider-github.tf @@ -17,19 +17,20 @@ variable "github_token_override" { } locals { - github_token = local.github_webhook_enabled ? coalesce(var.github_token_override, try(data.aws_ssm_parameter.github_api_key[0].value, null)) : "" + github_token = local.create_github_webhook ? coalesce(var.github_token_override, try(data.aws_ssm_parameter.github_api_key[0].value, null)) : "" } data "aws_ssm_parameter" "github_api_key" { - count = local.github_webhook_enabled ? 1 : 0 + count = local.create_github_webhook ? 1 : 0 name = var.ssm_github_api_key with_decryption = true - provider = aws.config_secrets + #provider = aws.config_secrets } +# We will only need the github provider if we are creating the GitHub webhook with github_repository_webhook. provider "github" { - base_url = local.github_webhook_enabled ? var.github_base_url : null - owner = local.github_webhook_enabled ? var.github_organization : null - token = local.github_webhook_enabled ? local.github_token : null + base_url = local.create_github_webhook ? var.github_base_url : null + owner = local.create_github_webhook ? var.github_organization : null + token = local.create_github_webhook ? local.github_token : null } diff --git a/modules/eks/argocd/variables-argocd-notifications.tf b/modules/eks/argocd/variables-argocd-notifications.tf index 4e5f838be..4a8bce937 100644 --- a/modules/eks/argocd/variables-argocd-notifications.tf +++ b/modules/eks/argocd/variables-argocd-notifications.tf @@ -63,10 +63,6 @@ variable "notifications_notifiers" { value = string }) ), []) - basicAuth = optional(object({ - username = string - password = string - })) insecureSkipVerify = optional(bool, false) }) )) diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index 3c0ddf6a5..cda437076 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -198,9 +198,3 @@ variable "saml_sso_providers" { default = {} description = "SAML SSO providers components" } - -variable "github_webhook_enabled" { - type = bool - default = true - description = "Enable GitHub webhook integration" -} diff --git a/modules/github-webhook/README.md b/modules/github-webhook/README.md new file mode 100644 index 000000000..4f2afc916 --- /dev/null +++ b/modules/github-webhook/README.md @@ -0,0 +1,148 @@ +# Component: `github-webhook` + +This component provisions a GitHub webhook for a single GitHub repository. + +You may want to use this component if you are provisioning webhooks for multiple ArgoCD deployment repositories across GitHub organizations. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. This example pulls the value of the webhook from `remote-state` + +```yaml +components: + terraform: + webhook/cloudposse/argocd: + metadata: + component: github-webhook + vars: + github_organization: cloudposse + github_repository: argocd-deploy-non-prod + webhook_url: "https://argocd.ue2.dev.plat.cloudposse.org/api/webhook" + + remote_state_github_webhook_enabled: true # default value added for visibility + remote_state_component_name: eks/argocd +``` + +### SSM Stored Value Example + +Here's an example snippet for how to use this component with a value stored in SSM + +```yaml +components: + terraform: + webhook/cloudposse/argocd: + metadata: + component: github-webhook + vars: + github_organization: cloudposse + github_repository: argocd-deploy-non-prod + webhook_url: "https://argocd.ue2.dev.plat.cloudposse.org/api/webhook" + + remote_state_github_webhook_enabled: false + ssm_github_webhook_enabled: true + ssm_github_webhook: "/argocd/github/webhook" +``` + +### Input Value Example + +Here's an example snippet for how to use this component with a value stored in Terraform variables. + +```yaml +components: + terraform: + webhook/cloudposse/argocd: + metadata: + component: github-webhook + vars: + github_organization: cloudposse + github_repository: argocd-deploy-non-prod + webhook_url: "https://argocd.ue2.dev.plat.cloudposse.org/api/webhook" + + remote_state_github_webhook_enabled: false + ssm_github_webhook_enabled: false + webhook_github_secret: "abcdefg" +``` + + +### ArgoCD Webhooks + +For usage with the `eks/argocd` component, see [Creating Webhooks with `github-webhook`](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/eks/argocd/README.md#creating-webhooks-with-github-webhook) in that component's README. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [github](#requirement\_github) | >= 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [github](#provider\_github) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [source](#module\_source) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [github_repository_webhook.default](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_webhook) | resource | +| [aws_ssm_parameter.github_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [github\_base\_url](#input\_github\_base\_url) | This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/` | `string` | `null` | no | +| [github\_organization](#input\_github\_organization) | The name of the GitHub Organization where the repository lives | `string` | n/a | yes | +| [github\_repository](#input\_github\_repository) | The name of the GitHub repository where the webhook will be created | `string` | n/a | yes | +| [github\_token\_override](#input\_github\_token\_override) | Use the value of this variable as the GitHub token instead of reading it from SSM | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region. | `string` | n/a | yes | +| [remote\_state\_component\_name](#input\_remote\_state\_component\_name) | If fetching the Github Webhook value from remote-state, set this to the source compoennt name. For example, `eks/argocd`. | `string` | `""` | no | +| [remote\_state\_github\_webhook\_enabled](#input\_remote\_state\_github\_webhook\_enabled) | If `true`, pull the GitHub Webhook value from remote-state | `bool` | `true` | no | +| [ssm\_github\_api\_key](#input\_ssm\_github\_api\_key) | SSM path to the GitHub API key | `string` | `"/argocd/github/api_key"` | no | +| [ssm\_github\_webhook](#input\_ssm\_github\_webhook) | Format string of the SSM parameter path where the webhook will be pulled from. Only used if `var.webhook_github_secret` is not given. | `string` | `"/github/webhook"` | no | +| [ssm\_github\_webhook\_enabled](#input\_ssm\_github\_webhook\_enabled) | If `true`, pull the GitHub Webhook value from AWS SSM Parameter Store using `var.ssm_github_webhook` | `bool` | `false` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [webhook\_github\_secret](#input\_webhook\_github\_secret) | The value to use as the GitHub webhook secret. Set both `var.ssm_github_webhook_enabled` and `var.remote_state_github_webhook_enabled` to `false` in order to use this value | `string` | `""` | no | +| [webhook\_url](#input\_webhook\_url) | The URL for the webhook | `string` | n/a | yes | + +## Outputs + +No outputs. + + +## References + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components) - Cloud Posse's upstream components + +[](https://cpco.io/component) diff --git a/modules/github-webhook/context.tf b/modules/github-webhook/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/github-webhook/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/github-webhook/main.tf b/modules/github-webhook/main.tf new file mode 100644 index 000000000..130a49df4 --- /dev/null +++ b/modules/github-webhook/main.tf @@ -0,0 +1,33 @@ +locals { + enabled = module.this.enabled + + remote_state_github_webhook_enabled = local.enabled && var.remote_state_github_webhook_enabled + ssm_github_webhook_enabled = local.enabled && var.ssm_github_webhook_enabled + + # If remote_state_github_webhook_enabled, get the value from remote-state + # Else if ssm_github_webhook_enabled, get the value from SSM + # Else, get the value given by var.webhook_github_secret + webhook_github_secret = local.remote_state_github_webhook_enabled ? module.source[0].outputs.github_webhook_value : (local.ssm_github_webhook_enabled ? try(data.aws_ssm_parameter.webhook[0].value, null) : var.webhook_github_secret) +} + +data "aws_ssm_parameter" "webhook" { + count = local.ssm_github_webhook_enabled ? 1 : 0 + + name = var.ssm_github_webhook + with_decryption = true +} + +resource "github_repository_webhook" "default" { + repository = var.github_repository + + configuration { + url = var.webhook_url + content_type = "json" + secret = local.webhook_github_secret + insecure_ssl = false + } + + active = true + + events = ["push"] +} diff --git a/modules/github-webhook/provider-github.tf b/modules/github-webhook/provider-github.tf new file mode 100644 index 000000000..8690b6a26 --- /dev/null +++ b/modules/github-webhook/provider-github.tf @@ -0,0 +1,33 @@ +variable "github_base_url" { + type = string + description = "This is the target GitHub base API endpoint. Providing a value is a requirement when working with GitHub Enterprise. It is optional to provide this value and it can also be sourced from the `GITHUB_BASE_URL` environment variable. The value must end with a slash, for example: `https://terraformtesting-ghe.westus.cloudapp.azure.com/`" + default = null +} + +variable "ssm_github_api_key" { + type = string + description = "SSM path to the GitHub API key" + default = "/argocd/github/api_key" +} + +variable "github_token_override" { + type = string + description = "Use the value of this variable as the GitHub token instead of reading it from SSM" + default = null +} + +locals { + github_token = local.enabled ? coalesce(var.github_token_override, try(data.aws_ssm_parameter.github_api_key[0].value, null)) : "" +} + +data "aws_ssm_parameter" "github_api_key" { + count = local.enabled ? 1 : 0 + name = var.ssm_github_api_key + with_decryption = true +} + +provider "github" { + base_url = local.enabled ? var.github_base_url : null + owner = local.enabled ? var.github_organization : null + token = local.enabled ? local.github_token : null +} diff --git a/modules/github-webhook/providers.tf b/modules/github-webhook/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/github-webhook/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/github-webhook/remote-state.tf b/modules/github-webhook/remote-state.tf new file mode 100644 index 000000000..a2a7dab14 --- /dev/null +++ b/modules/github-webhook/remote-state.tf @@ -0,0 +1,12 @@ +# This can be any component that has the required output, `github-webhook-value` +# This is typically eks/argocd +module "source" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.remote_state_github_webhook_enabled ? 1 : 0 + + component = var.remote_state_component_name + + context = module.this.context +} diff --git a/modules/github-webhook/variables.tf b/modules/github-webhook/variables.tf new file mode 100644 index 000000000..701706b36 --- /dev/null +++ b/modules/github-webhook/variables.tf @@ -0,0 +1,49 @@ +variable "region" { + description = "AWS Region." + type = string +} + +variable "github_repository" { + type = string + description = "The name of the GitHub repository where the webhook will be created" +} + +variable "github_organization" { + type = string + description = "The name of the GitHub Organization where the repository lives" +} + +variable "webhook_url" { + type = string + description = "The URL for the webhook" +} + +variable "webhook_github_secret" { + type = string + description = "The value to use as the GitHub webhook secret. Set both `var.ssm_github_webhook_enabled` and `var.remote_state_github_webhook_enabled` to `false` in order to use this value" + default = "" +} + +variable "ssm_github_webhook_enabled" { + type = bool + description = "If `true`, pull the GitHub Webhook value from AWS SSM Parameter Store using `var.ssm_github_webhook`" + default = false +} + +variable "ssm_github_webhook" { + type = string + description = "Format string of the SSM parameter path where the webhook will be pulled from. Only used if `var.webhook_github_secret` is not given." + default = "/github/webhook" +} + +variable "remote_state_github_webhook_enabled" { + type = bool + description = "If `true`, pull the GitHub Webhook value from remote-state" + default = true +} + +variable "remote_state_component_name" { + type = string + description = "If fetching the Github Webhook value from remote-state, set this to the source compoennt name. For example, `eks/argocd`." + default = "" +} diff --git a/modules/github-webhook/versions.tf b/modules/github-webhook/versions.tf new file mode 100644 index 000000000..64cf5005b --- /dev/null +++ b/modules/github-webhook/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + github = { + source = "integrations/github" + version = ">= 4.0" + } + } +} From 4c40af98ce2d8e675d3d664bde8c52e707d29ff6 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Fri, 2 Feb 2024 02:47:02 +0300 Subject: [PATCH 359/501] Consolidate auto-release workflow (#967) --- .github/workflows/auto-release.yml | 12 ++---------- .github/workflows/bats.yml | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 3a38fae08..a0ae99042 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -11,16 +11,8 @@ jobs: publish: runs-on: ubuntu-latest steps: - # Get PR from merged commit to master - - uses: actions-ecosystem/action-get-merged-pull-request@v1 - id: get-merged-pull-request + - uses: cloudposse/github-action-auto-release@v1 with: - github_token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} - # Drafts your next Release notes as Pull Requests are merged into "main" - - uses: release-drafter/release-drafter@v5 - with: - publish: ${{ !contains(steps.get-merged-pull-request.outputs.labels, 'no-release') }} prerelease: false + publish: true config-name: auto-release.yml - env: - GITHUB_TOKEN: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} diff --git a/.github/workflows/bats.yml b/.github/workflows/bats.yml index dfb901a67..4915cdace 100644 --- a/.github/workflows/bats.yml +++ b/.github/workflows/bats.yml @@ -16,7 +16,7 @@ jobs: BATS_SUBMODULE_TESTS: input-descriptions lint output-descriptions steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -32,7 +32,7 @@ jobs: HEAD_REF: ${{ github.head_ref }} run: | # when running in test-harness, need to mark the directory safe for git operations - make safe-directory + make git-safe-directory MODIFIED_MODULES=($(git diff --name-only origin/${BASE_REF} origin/${HEAD_REF} | xargs -n 1 dirname | sort | uniq | grep ^modules/ || true)) if [ -z "$MODIFIED_MODULES" ]; then echo "No modules changed in this PR. Skipping tests." From 942548ef2d2f5a8448f5bba276f927a382b94e66 Mon Sep 17 00:00:00 2001 From: Aayush Harwani <47695284+Aayushadh@users.noreply.github.com> Date: Fri, 2 Feb 2024 09:07:32 +0530 Subject: [PATCH 360/501] Update remote-state.tf in vpc module (#972) --- modules/vpc/remote-state.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/vpc/remote-state.tf b/modules/vpc/remote-state.tf index b9db2205c..ac5ef9f92 100644 --- a/modules/vpc/remote-state.tf +++ b/modules/vpc/remote-state.tf @@ -1,5 +1,5 @@ module "vpc_flow_logs_bucket" { - count = var.vpc_flow_logs_enabled ? 1 : 0 + count = local.vpc_flow_logs_enabled ? 1 : 0 source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" From 633f81082fb13cdf5febebdf5aee77505d585e04 Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Mon, 5 Feb 2024 21:20:43 -0700 Subject: [PATCH 361/501] chore: upgrades child iam-account-settings for hashicorp/template fix (#975) Co-authored-by: cloudpossebot --- modules/account-settings/README.md | 2 +- modules/account-settings/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index ff9dd3f15..7fcd021df 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -87,7 +87,7 @@ components: | Name | Source | Version | |------|--------|---------| | [budgets](#module\_budgets) | cloudposse/budgets/aws | 0.2.1 | -| [iam\_account\_settings](#module\_iam\_account\_settings) | cloudposse/iam-account-settings/aws | 0.4.0 | +| [iam\_account\_settings](#module\_iam\_account\_settings) | cloudposse/iam-account-settings/aws | 0.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [service\_quotas](#module\_service\_quotas) | cloudposse/service-quotas/aws | 0.1.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/account-settings/main.tf b/modules/account-settings/main.tf index 2ee5cdabe..34e392613 100644 --- a/modules/account-settings/main.tf +++ b/modules/account-settings/main.tf @@ -8,7 +8,7 @@ resource "aws_ebs_encryption_by_default" "default" { # It also sets the account alias for the current account. module "iam_account_settings" { source = "cloudposse/iam-account-settings/aws" - version = "0.4.0" + version = "0.5.0" hard_expiry = true minimum_password_length = var.minimum_password_length From 7b2f215145d38b69ba26c0680d4edcbc01206955 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 8 Feb 2024 09:30:09 -0800 Subject: [PATCH 362/501] Refactor Lambda@Edge for `spa-s3-cloudfront` (#978) Co-authored-by: Andriy Knysh Co-authored-by: cloudpossebot --- modules/spa-s3-cloudfront/CHANGELOG.md | 26 ++ modules/spa-s3-cloudfront/README.md | 50 +++- .../github-actions-iam-role.mixin.tf | 2 +- modules/spa-s3-cloudfront/lambda_edge.tf | 133 +++++++++ modules/spa-s3-cloudfront/main.tf | 54 +--- .../modules/lambda-edge-preview/context.tf | 279 ------------------ .../modules/lambda-edge-preview/main.tf | 73 ----- .../modules/lambda-edge-preview/outputs.tf | 4 - .../modules/lambda-edge-preview/variables.tf | 28 -- .../modules/lambda-edge-preview/versions.tf | 11 - .../lambda_edge_redirect_404/context.tf | 279 ------------------ .../modules/lambda_edge_redirect_404/index.js | 119 -------- .../modules/lambda_edge_redirect_404/main.tf | 30 -- .../lambda_edge_redirect_404/outputs.tf | 4 - .../lambda_edge_redirect_404/variables.tf | 28 -- .../lambda_edge_redirect_404/versions.tf | 11 - modules/spa-s3-cloudfront/remote-state.tf | 10 +- modules/spa-s3-cloudfront/variables.tf | 65 +++- 18 files changed, 280 insertions(+), 926 deletions(-) create mode 100644 modules/spa-s3-cloudfront/CHANGELOG.md create mode 100644 modules/spa-s3-cloudfront/lambda_edge.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/context.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/outputs.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda-edge-preview/versions.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/context.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/index.js delete mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/outputs.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf delete mode 100644 modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/versions.tf diff --git a/modules/spa-s3-cloudfront/CHANGELOG.md b/modules/spa-s3-cloudfront/CHANGELOG.md new file mode 100644 index 000000000..29775f308 --- /dev/null +++ b/modules/spa-s3-cloudfront/CHANGELOG.md @@ -0,0 +1,26 @@ +## Components PR [#978](https://github.com/cloudposse/terraform-aws-components/pull/978) + +### Lambda@Edge Submodule Refactor + +This PR has significantly refactored how Lambda@Edge functions are managed by Terraform with this component. Previously, the specific use cases for Lambda@Edge functions were handled by submodules `lambda-edge-preview` and `lambda_edge_redirect_404`. These component submodules both called the same Terraform module, `cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge`. These submodules have been replaced with a single Terraform file, `lambda_edge.tf`. + +The reason a single file is better than submodules is (1) simplification and (2) adding the ability to deep merge function configuration. Cloudfront Distributions support a single Lambda@Edge function for each origin/viewer request or response. With deep merging, we can define default values for function configuration and provide the ability to overwrite specific values for a given deployment. + +Specifically, our own use case is using an authorization Lambda@Edge viewer request only if the paywall is enabled. Other deployments use an alternative viewer request to redirect 404. + +#### Upgrading with `preview_environment_enabled: true` or `lambda_edge_redirect_404_enabled: true` + +If you have `var.preview_environment_enabled` or `var.lambda_edge_redirect_404_enabled` set to `true`, Terraform `moved` will move the previous resource by submodule to the new resource by file. Please give your next Terraform plan a sanity check. Any existing Lambda functions _should not be destroyed_ by this change. + +#### Upgrading with both `preview_environment_enabled: false` and `lambda_edge_redirect_404_enabled: false` + +If you have no Lambda@Edge functions deployed and where both `var.preview_environment_enabled` and `var.lambda_edge_redirect_404_enabled` are `false` (the default value), no change is necessary. + +### Lambda Runtime Version + +The previous PR [#946](https://github.com/cloudposse/terraform-aws-components/pull/946) introduced the `var.lambda_runtime` input. Previously, the version of node in both submodules was hard-coded to be `nodejs12.x`. This PR renames that variable to `var.lambda_edge_runtime` and sets the default to `nodejs16.x`. + +If you want to maintain the previous version of Node, set `var.lambda_edge_runtime` to `nodejs12.x`, though be aware that AWS deprecated that version on March 31, 2023, and lambdas using that environment may no longer work. Otherwise, this component will attempt to deploy the functions with runtime `nodejs16.x`. + +- [See all available runtimes here](https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime) +- [See runtime environment deprecation dates here](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtime-support-policy) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index ec7671c0c..54799e4ea 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -93,6 +93,40 @@ SPA Preview environments (i.e. `subdomain.example.com` mapping to a `/subdomain` are supported via `var.preview_environment_enabled`. See the both the variable description and inline documentation for an extensive explanation for how these preview environments work. +### Customizing Lambda@Edge + +This component supports customizing Lambda@Edge functions for the CloudFront distribution. All Lambda@Edge function configuration is deep merged before being passed to the `cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge` module. You can add additional functions and overwrite existing functions as such: + +```yaml +import: + - catalog/spa-s3-cloudfront/defaults + +components: + terraform: + refarch-docs-site-spa: + metadata: + component: spa-s3-cloudfront + inherits: + - spa-s3-cloudfront-defaults + vars: + enabled: true + lambda_edge_functions: + viewer_request: # overwrite existing function + source: null # this overwrites the 404 viewer request source with deep merging + source_zip: "./dist/lambda_edge_paywall_viewer_request.zip" + runtime: "nodejs16.x" + handler: "index.handler" + event_type: "viewer-request" + include_body: false + viewer_response: # new function + source_zip: "./dist/lambda_edge_paywall_viewer_response.zip" + runtime: "nodejs16.x" + handler: "index.handler" + event_type: "viewer-response" + include_body: false + +``` + ## Requirements @@ -118,8 +152,8 @@ an extensive explanation for how these preview environments work. | [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | | [github\_runners](#module\_github\_runners) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [lambda\_edge\_preview](#module\_lambda\_edge\_preview) | ./modules/lambda-edge-preview | n/a | -| [lambda\_edge\_redirect\_404](#module\_lambda\_edge\_redirect\_404) | ./modules/lambda_edge_redirect_404 | n/a | +| [lambda\_edge](#module\_lambda\_edge) | cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge | 0.92.0 | +| [lambda\_edge\_functions](#module\_lambda\_edge\_functions) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | | [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.92.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | @@ -129,8 +163,11 @@ an extensive explanation for how these preview environments work. | Name | Type | |------|------| +| [aws_iam_policy.additional_lambda_edge_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.additional_lambda_edge_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_shield_protection.shield_protection](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/shield_protection) | resource | +| [aws_iam_policy_document.additional_lambda_edge_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_s3_bucket.failover_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_bucket) | data source | @@ -188,8 +225,12 @@ an extensive explanation for how these preview environments work. | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lambda\_edge\_allowed\_ssm\_parameters](#input\_lambda\_edge\_allowed\_ssm\_parameters) | The Lambda@Edge functions will be allowed to access the list of AWS SSM parameter with these ARNs | `list(string)` | `[]` | no | +| [lambda\_edge\_destruction\_delay](#input\_lambda\_edge\_destruction\_delay) | The delay, in [Golang ParseDuration](https://pkg.go.dev/time#ParseDuration) format, to wait before destroying the Lambda@Edge
functions.

This delay is meant to circumvent Lambda@Edge functions not being immediately deletable following their dissociation from
a CloudFront distribution, since they are replicated to CloudFront Edge servers around the world.

If set to `null`, no delay will be introduced.

By default, the delay is 20 minutes. This is because it takes about 3 minutes to destroy a CloudFront distribution, and
around 15 minutes until the Lambda@Edge function is available for deletion, in most cases.

For more information, see: https://github.com/hashicorp/terraform-provider-aws/issues/1721. | `string` | `"20m"` | no | +| [lambda\_edge\_functions](#input\_lambda\_edge\_functions) | Lambda@Edge functions to create.

The key of this map is the name of the Lambda@Edge function.

This map will be deep merged with each enabled default function. Use deep merge to change or overwrite specific values passed by those function objects. |
map(object({
source = optional(list(object({
filename = string
content = string
})))
source_dir = optional(string)
source_zip = optional(string)
runtime = string
handler = string
event_type = string
include_body = bool
}))
| `{}` | no | +| [lambda\_edge\_handler](#input\_lambda\_edge\_handler) | The default Lambda@Edge handler for all functions.

This value is deep merged in `module.lambda_edge_functions` with `var.lambda_edge_functions` and can be overwritten for any individual function. | `string` | `"index.handler"` | no | | [lambda\_edge\_redirect\_404\_enabled](#input\_lambda\_edge\_redirect\_404\_enabled) | Enable or disable SPA 404 redirects via Lambda@Edge - returns a 302 and a location of `/` if the request returned 404. | `bool` | `false` | no | -| [lambda\_runtime](#input\_lambda\_runtime) | Identifier of the function's runtime. See Runtimes for valid values.
https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime | `string` | `"nodejs16.x"` | no | +| [lambda\_edge\_runtime](#input\_lambda\_edge\_runtime) | The default Lambda@Edge runtime for all functions.

This value is deep merged in `module.lambda_edge_functions` with `var.lambda_edge_functions` and can be overwritten for any individual function. | `string` | `"nodejs16.x"` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [ordered\_cache](#input\_ordered\_cache) | An ordered list of [cache behaviors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#cache-behavior-arguments) resource for this distribution.
List in order of precedence (first match wins). This is in addition to the default cache policy.
Set `target_origin_id` to `""` to specify the S3 bucket origin created by this module. |
list(object({
target_origin_id = string
path_pattern = string

allowed_methods = list(string)
cached_methods = list(string)
compress = bool
trusted_signers = list(string)
trusted_key_groups = list(string)

cache_policy_id = string
origin_request_policy_id = string

viewer_protocol_policy = string
min_ttl = number
default_ttl = number
max_ttl = number
response_headers_policy_id = string

forward_query_string = bool
forward_header_values = list(string)
forward_cookies = string
forward_cookies_whitelisted_names = list(string)

lambda_function_association = list(object({
event_type = string
include_body = bool
lambda_arn = string
}))

function_association = list(object({
event_type = string
function_arn = string
}))
}))
| `[]` | no | @@ -232,7 +273,8 @@ an extensive explanation for how these preview environments work. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/spa-s3-cloudfront) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spa-s3-cloudfront) - Cloud Posse's upstream component +* [How do I use CloudFront to serve a static website hosted on Amazon S3?](https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-serve-static-website/) [](https://cpco.io/component) diff --git a/modules/spa-s3-cloudfront/github-actions-iam-role.mixin.tf b/modules/spa-s3-cloudfront/github-actions-iam-role.mixin.tf index 4294f1aac..de68c6602 100644 --- a/modules/spa-s3-cloudfront/github-actions-iam-role.mixin.tf +++ b/modules/spa-s3-cloudfront/github-actions-iam-role.mixin.tf @@ -29,7 +29,7 @@ variable "github_actions_iam_role_attributes" { locals { - github_actions_iam_role_enabled = module.this.enabled && var.github_actions_iam_role_enabled && length(var.github_actions_allowed_repos) > 0 + github_actions_iam_role_enabled = local.enabled && var.github_actions_iam_role_enabled && length(var.github_actions_allowed_repos) > 0 } module "gha_role_name" { diff --git a/modules/spa-s3-cloudfront/lambda_edge.tf b/modules/spa-s3-cloudfront/lambda_edge.tf new file mode 100644 index 000000000..197170f3c --- /dev/null +++ b/modules/spa-s3-cloudfront/lambda_edge.tf @@ -0,0 +1,133 @@ +locals { + lambda_edge_redirect_404_enabled = local.enabled && var.lambda_edge_redirect_404_enabled + + cloudfront_lambda_function_association = concat(var.cloudfront_lambda_function_association, module.lambda_edge.lambda_function_association) +} + +# See CHANGELOG for PR #978: +# https://github.com/cloudposse/terraform-aws-components/pull/978 +# +# Lambda@Edge was moved from submodules to this file +moved { + from = module.lambda-edge-preview.module.lambda_edge.aws_lambda_function.default["origin_request"] + to = module.lambda_edge.aws_lambda_function.default["origin_request"] +} +moved { + from = module.lambda_edge_redirect_404.module.lambda_edge.aws_lambda_function.default["origin_response"] + to = module.lambda_edge.aws_lambda_function.default["origin_response"] +} +moved { + from = module.lambda_edge_redirect_404.module.lambda_edge.aws_lambda_function.default["viewer_request"] + to = module.lambda_edge.aws_lambda_function.default["viewer_request"] +} + +module "lambda_edge_functions" { + source = "cloudposse/config/yaml//modules/deepmerge" + version = "1.0.2" + + count = local.enabled ? 1 : 0 + + maps = [ + local.preview_environment_enabled ? { + origin_request = { + source = [{ + content = <<-EOT + exports.handler = (event, context, callback) => { + const site_fqdn = "${local.site_fqdn}"; + + const { request } = event.Records[0].cf; + const default_prefix = ""; + + console.log('request:' + JSON.stringify(request)); + const host = request.headers['x-forwarded-host'][0].value; + if (host == site_fqdn) { + request.origin.custom.path = default_prefix; // use default prefix if there is no subdomain + } else { + const subdomain = host.replace('.' + site_fqdn, ''); + request.origin.custom.path = `/$${subdomain}`; // use preview prefix + } + + return callback(null, request); + }; + EOT + filename = "index.js" + }] + runtime = var.lambda_edge_runtime + handler = var.lambda_edge_handler + event_type = "origin-request" + include_body = false + } + } : {}, + local.lambda_edge_redirect_404_enabled ? { + origin_response = { + source = [{ + content = file("${path.module}/dist/lambda_edge_404_redirect.js") + filename = "index.js" + }] + runtime = var.lambda_edge_runtime + handler = var.lambda_edge_handler + event_type = "origin-response" + include_body = false + }, + viewer_request = { + source = [{ + content = <<-EOT + exports.handler = (event, context, callback) => { + const { request } = event.Records[0].cf; + request.headers['x-forwarded-host'] = [ + { + key: 'X-Forwarded-Host', + value: request.headers.host[0].value + } + ]; + return callback(null, request); + }; + EOT + filename = "index.js" + }] + runtime = var.lambda_edge_runtime + handler = var.lambda_edge_handler + event_type = "viewer-request" + include_body = false + } + } : {}, + var.lambda_edge_functions, + ] +} + +module "lambda_edge" { + source = "cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge" + version = "0.92.0" + + functions = jsondecode(local.enabled ? jsonencode(module.lambda_edge_functions[0].merged) : jsonencode({})) + destruction_delay = var.lambda_edge_destruction_delay + + providers = { + aws = aws.us-east-1 + } + + context = module.this.context +} + +data "aws_iam_policy_document" "additional_lambda_edge_permission" { + count = local.enabled && (length(var.lambda_edge_allowed_ssm_parameters) > 0) ? 1 : 0 + statement { + effect = "Allow" + actions = ["ssm:GetParameter*"] + resources = var.lambda_edge_allowed_ssm_parameters + } +} + +resource "aws_iam_policy" "additional_lambda_edge_permission" { + count = local.enabled && (length(var.lambda_edge_allowed_ssm_parameters) > 0) ? 1 : 0 + + name = "${module.this.id}-read-ssm-vars" + policy = data.aws_iam_policy_document.additional_lambda_edge_permission[0].json +} + +resource "aws_iam_role_policy_attachment" "additional_lambda_edge_permission" { + for_each = local.enabled && (length(var.lambda_edge_allowed_ssm_parameters) > 0) ? toset(keys(module.lambda_edge_functions[0].merged)) : toset([]) + + policy_arn = aws_iam_policy.additional_lambda_edge_permission[0].arn + role = split("/", module.lambda_edge.lambda_functions[each.key].role_arn)[1] +} diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf index fd6cd47d8..a6b3072a8 100644 --- a/modules/spa-s3-cloudfront/main.tf +++ b/modules/spa-s3-cloudfront/main.tf @@ -8,24 +8,23 @@ locals { s3_access_log_bucket_name = var.origin_s3_access_log_bucket_name_rendering_enabled ? format("%[1]v-${module.this.tenant != null ? "%[2]v-" : ""}%[3]v-%[4]v-%[5]v", var.namespace, var.tenant, var.environment, var.stage, var.origin_s3_access_log_bucket_name) : var.origin_s3_access_log_bucket_name cloudfront_access_log_bucket_name = var.cloudfront_access_log_bucket_name_rendering_enabled ? format("%[1]v-${module.this.tenant != null ? "%[2]v-" : ""}%[3]v-%[4]v-%[5]v", var.namespace, var.tenant, var.environment, var.stage, var.cloudfront_access_log_bucket_name) : var.cloudfront_access_log_bucket_name cloudfront_access_log_prefix = var.cloudfront_access_log_prefix_rendering_enabled ? "${var.cloudfront_access_log_prefix}${module.this.id}" : var.cloudfront_access_log_prefix - origin_deployment_principal_arns = local.github_runners_enabled ? concat(var.origin_deployment_principal_arns, [module.github_runners[0].outputs.iam_role_arn]) : var.origin_deployment_principal_arns + origin_deployment_principal_arns = local.github_runners_enabled ? concat(var.origin_deployment_principal_arns, [module.github_runners.outputs.iam_role_arn]) : var.origin_deployment_principal_arns # Variables affected by SPA Preview Environments # # In order for preview environments to work, there are some specific CloudFront Distribution settings that need to be in place (in order of local variables set below this list): # 1. A wildcard domain Route53 alias needs to be created for the CloudFront distribution. SANs for the ACM certificate need to be set accordingly. - # 2. The origin must be a custom origin pointing to the S3 website endpoint, not a S3 REST origin (the set of Lambda@Edge functions in modules/lambda-edge-preview do not support the latter). + # 2. The origin must be a custom origin pointing to the S3 website endpoint, not a S3 REST origin (the set of Lambda@Edge functions in lambda_edge.tf do not support the latter). # 3. Because of #2, the bucket in question cannot have a Public Access Block configuration blocking all public ACLs. # 4. Because of #2 and #3, it is best practice to enable a password on the S3 website origin so that CloudFront is the single point of entry. # 5. Object ACLs should be disabled for the origin bucket in the preview environment, otherwise CI/CD jobs uploading to the origin bucket may create object ACLs preventing the content from being served. # 6. The statement in the bucket policy blocking non-TLS requests from CloudFront needs to be disabled. # 7. A custom header 'x-forwarded-host' needs to be forwarded to the origin (it is injected by lambda@edge function associated with the Viewer Request event). # 8. TTL values will be set to 0, because the preview environment is associated with development and debugging, not long term caching. - # 9. The Lambda@Edge functions created by modules/lambda-edge-preview need to be associated with the CloudFront Distribution. + # 9. The Lambda@Edge functions created by lambda_edge.tf need to be associated with the CloudFront Distribution. # # This isn't necessarily the only way to get preview environments to work, but these are the constraints required to achieve the currently tested implementation in modules/lambda-edge-preview. preview_environment_enabled = local.enabled && var.preview_environment_enabled - lambda_edge_redirect_404_enabled = local.enabled && var.lambda_edge_redirect_404_enabled preview_environment_wildcard_domain = format("%v.%v", "*", local.site_fqdn) aliases = concat([local.site_fqdn], local.preview_environment_enabled ? [local.preview_environment_wildcard_domain] : []) external_aliases = local.preview_environment_enabled ? [] : var.external_aliases @@ -44,11 +43,10 @@ locals { # Preview must have website_enabled. origin_allow_ssl_requests_only = var.origin_allow_ssl_requests_only && !local.s3_website_enabled - forward_header_values = local.preview_environment_enabled ? concat(var.forward_header_values, ["x-forwarded-host"]) : var.forward_header_values - cloudfront_default_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_default_ttl - cloudfront_min_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_min_ttl - cloudfront_max_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_max_ttl - cloudfront_lambda_function_association = concat(var.cloudfront_lambda_function_association, local.preview_environment_enabled ? module.lambda_edge_preview.lambda_function_association : [], local.lambda_edge_redirect_404_enabled ? module.lambda_edge_redirect_404.lambda_function_association : []) + forward_header_values = local.preview_environment_enabled ? concat(var.forward_header_values, ["x-forwarded-host"]) : var.forward_header_values + cloudfront_default_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_default_ttl + cloudfront_min_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_min_ttl + cloudfront_max_ttl = local.preview_environment_enabled ? 0 : var.cloudfront_max_ttl } # Create an ACM and explicitly set it to us-east-1 (requirement of CloudFront) @@ -76,7 +74,7 @@ module "spa_web" { encryption_enabled = var.origin_encryption_enabled origin_force_destroy = var.origin_force_destroy versioning_enabled = var.origin_versioning_enabled - web_acl_id = local.aws_waf_enabled ? module.waf[0].outputs.acl.arn : null + web_acl_id = local.aws_waf_enabled ? module.waf.outputs.acl.arn : null cloudfront_access_log_create_bucket = var.cloudfront_access_log_create_bucket cloudfront_access_log_bucket_name = local.cloudfront_access_log_bucket_name @@ -146,39 +144,3 @@ resource "aws_shield_protection" "shield_protection" { name = module.spa_web.cf_id resource_arn = module.spa_web.cf_arn } - -module "lambda_edge_preview" { - source = "./modules/lambda-edge-preview" - - enabled = local.preview_environment_enabled - - cloudfront_distribution_domain_name = module.spa_web.cf_domain_name - cloudfront_distribution_hosted_zone_id = module.spa_web.cf_hosted_zone_id - site_fqdn = local.site_fqdn - parent_zone_name = local.parent_zone_name - runtime = var.lambda_runtime - - context = module.this.context - - providers = { - aws.us-east-1 = aws.us-east-1 - } -} - -module "lambda_edge_redirect_404" { - source = "./modules/lambda_edge_redirect_404" - - enabled = local.lambda_edge_redirect_404_enabled - - cloudfront_distribution_domain_name = module.spa_web.cf_domain_name - cloudfront_distribution_hosted_zone_id = module.spa_web.cf_hosted_zone_id - site_fqdn = local.site_fqdn - parent_zone_name = local.parent_zone_name - runtime = var.lambda_runtime - - context = module.this.context - - providers = { - aws.us-east-1 = aws.us-east-1 - } -} diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/context.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/context.tf deleted file mode 100644 index 5e0ef8856..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/context.tf +++ /dev/null @@ -1,279 +0,0 @@ -# -# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label -# All other instances of this file should be a copy of that one -# -# -# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf -# and then place it in your Terraform module to automatically get -# Cloud Posse's standard configuration inputs suitable for passing -# to Cloud Posse modules. -# -# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf -# -# Modules should access the whole context as `module.this.context` -# to get the input variables with nulls for defaults, -# for example `context = module.this.context`, -# and access individual variables as `module.this.`, -# with final values filled in. -# -# For example, when using defaults, `module.this.context.delimiter` -# will be null, and `module.this.delimiter` will be `-` (hyphen). -# - -module "this" { - source = "cloudposse/label/null" - version = "0.25.0" # requires Terraform >= 0.13.0 - - enabled = var.enabled - namespace = var.namespace - tenant = var.tenant - environment = var.environment - stage = var.stage - name = var.name - delimiter = var.delimiter - attributes = var.attributes - tags = var.tags - additional_tag_map = var.additional_tag_map - label_order = var.label_order - regex_replace_chars = var.regex_replace_chars - id_length_limit = var.id_length_limit - label_key_case = var.label_key_case - label_value_case = var.label_value_case - descriptor_formats = var.descriptor_formats - labels_as_tags = var.labels_as_tags - - context = var.context -} - -# Copy contents of cloudposse/terraform-null-label/variables.tf here - -variable "context" { - type = any - default = { - enabled = true - namespace = null - tenant = null - environment = null - stage = null - name = null - delimiter = null - attributes = [] - tags = {} - additional_tag_map = {} - regex_replace_chars = null - label_order = [] - id_length_limit = null - label_key_case = null - label_value_case = null - descriptor_formats = {} - # Note: we have to use [] instead of null for unset lists due to - # https://github.com/hashicorp/terraform/issues/28137 - # which was not fixed until Terraform 1.0.0, - # but we want the default to be all the labels in `label_order` - # and we want users to be able to prevent all tag generation - # by setting `labels_as_tags` to `[]`, so we need - # a different sentinel to indicate "default" - labels_as_tags = ["unset"] - } - description = <<-EOT - Single object for setting entire context at once. - See description of individual variables for details. - Leave string and numeric variables as `null` to use default value. - Individual variable settings (non-null) override settings in context object, - except for attributes, tags, and additional_tag_map, which are merged. - EOT - - validation { - condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) - error_message = "Allowed values: `lower`, `title`, `upper`." - } - - validation { - condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) - error_message = "Allowed values: `lower`, `title`, `upper`, `none`." - } -} - -variable "enabled" { - type = bool - default = null - description = "Set to false to prevent the module from creating any resources" -} - -variable "namespace" { - type = string - default = null - description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" -} - -variable "tenant" { - type = string - default = null - description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" -} - -variable "environment" { - type = string - default = null - description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" -} - -variable "stage" { - type = string - default = null - description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" -} - -variable "name" { - type = string - default = null - description = <<-EOT - ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. - This is the only ID element not also included as a `tag`. - The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. - EOT -} - -variable "delimiter" { - type = string - default = null - description = <<-EOT - Delimiter to be used between ID elements. - Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. - EOT -} - -variable "attributes" { - type = list(string) - default = [] - description = <<-EOT - ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, - in the order they appear in the list. New attributes are appended to the - end of the list. The elements of the list are joined by the `delimiter` - and treated as a single ID element. - EOT -} - -variable "labels_as_tags" { - type = set(string) - default = ["default"] - description = <<-EOT - Set of labels (ID elements) to include as tags in the `tags` output. - Default is to include all labels. - Tags with empty values will not be included in the `tags` output. - Set to `[]` to suppress all generated tags. - **Notes:** - The value of the `name` tag, if included, will be the `id`, not the `name`. - Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be - changed in later chained modules. Attempts to change it will be silently ignored. - EOT -} - -variable "tags" { - type = map(string) - default = {} - description = <<-EOT - Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). - Neither the tag keys nor the tag values will be modified by this module. - EOT -} - -variable "additional_tag_map" { - type = map(string) - default = {} - description = <<-EOT - Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. - This is for some rare cases where resources want additional configuration of tags - and therefore take a list of maps with tag key, value, and additional configuration. - EOT -} - -variable "label_order" { - type = list(string) - default = null - description = <<-EOT - The order in which the labels (ID elements) appear in the `id`. - Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. - EOT -} - -variable "regex_replace_chars" { - type = string - default = null - description = <<-EOT - Terraform regular expression (regex) string. - Characters matching the regex will be removed from the ID elements. - If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. - EOT -} - -variable "id_length_limit" { - type = number - default = null - description = <<-EOT - Limit `id` to this many characters (minimum 6). - Set to `0` for unlimited length. - Set to `null` for keep the existing setting, which defaults to `0`. - Does not affect `id_full`. - EOT - validation { - condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 - error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." - } -} - -variable "label_key_case" { - type = string - default = null - description = <<-EOT - Controls the letter case of the `tags` keys (label names) for tags generated by this module. - Does not affect keys of tags passed in via the `tags` input. - Possible values: `lower`, `title`, `upper`. - Default value: `title`. - EOT - - validation { - condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) - error_message = "Allowed values: `lower`, `title`, `upper`." - } -} - -variable "label_value_case" { - type = string - default = null - description = <<-EOT - Controls the letter case of ID elements (labels) as included in `id`, - set as tag values, and output by this module individually. - Does not affect values of tags passed in via the `tags` input. - Possible values: `lower`, `title`, `upper` and `none` (no transformation). - Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. - Default value: `lower`. - EOT - - validation { - condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) - error_message = "Allowed values: `lower`, `title`, `upper`, `none`." - } -} - -variable "descriptor_formats" { - type = any - default = {} - description = <<-EOT - Describe additional descriptors to be output in the `descriptors` output map. - Map of maps. Keys are names of descriptors. Values are maps of the form - `{ - format = string - labels = list(string) - }` - (Type is `any` so the map values can later be enhanced to provide additional options.) - `format` is a Terraform format string to be passed to the `format()` function. - `labels` is a list of labels, in order, to pass to `format()` function. - Label values will be normalized before being passed to `format()` so they will be - identical to how they appear in `id`. - Default is `{}` (`descriptors` output will be empty). - EOT -} - -#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf deleted file mode 100644 index 1fc6b98dd..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/main.tf +++ /dev/null @@ -1,73 +0,0 @@ -# https://levelup.gitconnected.com/preview-environments-in-aws-with-cloudfront-and-lambda-edge-7acccb0b67d1 - -locals { - lambda_runtime = var.runtime - lambda_handler = "index.handler" -} - -module "lambda_edge" { - source = "cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge" - version = "0.82.4" - - functions = { - origin_request = { - source = [{ - content = <<-EOT - exports.handler = (event, context, callback) => { - const site_fqdn = "${var.site_fqdn}"; - - const { request } = event.Records[0].cf; - const default_prefix = ""; - - console.log(event); - console.log(request); - console.log(request.headers); - const host = request.headers['x-forwarded-host'][0].value; - if (host == site_fqdn) { - request.origin.custom.path = default_prefix; // use default prefix if there is no subdomain - } else { - const subdomain = host.replace('.' + site_fqdn, ''); - request.origin.custom.path = `/$${subdomain}`; // use preview prefix - } - - return callback(null, request); - }; - EOT - filename = "index.js" - }] - runtime = local.lambda_runtime - handler = local.lambda_handler - event_type = "origin-request" - include_body = false - } - viewer_request = { - source = [{ - content = <<-EOT - exports.handler = (event, context, callback) => { - const { request } = event.Records[0].cf; - - request.headers['x-forwarded-host'] = [ - { - key: 'X-Forwarded-Host', - value: request.headers.host[0].value - } - ]; - - return callback(null, request); - }; - EOT - filename = "index.js" - }] - runtime = local.lambda_runtime - handler = local.lambda_handler - event_type = "viewer-request" - include_body = false - } - } - - providers = { - aws = aws.us-east-1 - } - - context = module.this.context -} diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/outputs.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/outputs.tf deleted file mode 100644 index 315e14b86..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ -output "lambda_function_association" { - description = "The Lambda@Edge function association configuration to pass to `var.cloudfront_lambda_function_association` in the parent module." - value = module.lambda_edge.lambda_function_association -} diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf deleted file mode 100644 index 1b3ea97eb..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/variables.tf +++ /dev/null @@ -1,28 +0,0 @@ -variable "cloudfront_distribution_domain_name" { - type = string - description = "Cloudfront Distribution Domain Name." -} - -variable "cloudfront_distribution_hosted_zone_id" { - type = string - description = "The CloudFront Distribution Hosted Zone ID." -} - -variable "parent_zone_name" { - type = string - description = "The name of the Route53 Hosted Zone where aliases for the CloudFront distribution are created." -} - -variable "site_fqdn" { - type = string - description = "The fully qualified alias for the CloudFront Distribution." -} - -variable "runtime" { - type = string - description = <<-EOT - Identifier of the function's runtime. See Runtimes for valid values. - https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime - EOT - default = "nodejs16.x" -} diff --git a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/versions.tf b/modules/spa-s3-cloudfront/modules/lambda-edge-preview/versions.tf deleted file mode 100644 index 19b91b119..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda-edge-preview/versions.tf +++ /dev/null @@ -1,11 +0,0 @@ -terraform { - required_version = ">= 1.0.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 4.9.0" - configuration_aliases = [aws.us-east-1] - } - } -} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/context.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/context.tf deleted file mode 100644 index 5e0ef8856..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/context.tf +++ /dev/null @@ -1,279 +0,0 @@ -# -# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label -# All other instances of this file should be a copy of that one -# -# -# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf -# and then place it in your Terraform module to automatically get -# Cloud Posse's standard configuration inputs suitable for passing -# to Cloud Posse modules. -# -# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf -# -# Modules should access the whole context as `module.this.context` -# to get the input variables with nulls for defaults, -# for example `context = module.this.context`, -# and access individual variables as `module.this.`, -# with final values filled in. -# -# For example, when using defaults, `module.this.context.delimiter` -# will be null, and `module.this.delimiter` will be `-` (hyphen). -# - -module "this" { - source = "cloudposse/label/null" - version = "0.25.0" # requires Terraform >= 0.13.0 - - enabled = var.enabled - namespace = var.namespace - tenant = var.tenant - environment = var.environment - stage = var.stage - name = var.name - delimiter = var.delimiter - attributes = var.attributes - tags = var.tags - additional_tag_map = var.additional_tag_map - label_order = var.label_order - regex_replace_chars = var.regex_replace_chars - id_length_limit = var.id_length_limit - label_key_case = var.label_key_case - label_value_case = var.label_value_case - descriptor_formats = var.descriptor_formats - labels_as_tags = var.labels_as_tags - - context = var.context -} - -# Copy contents of cloudposse/terraform-null-label/variables.tf here - -variable "context" { - type = any - default = { - enabled = true - namespace = null - tenant = null - environment = null - stage = null - name = null - delimiter = null - attributes = [] - tags = {} - additional_tag_map = {} - regex_replace_chars = null - label_order = [] - id_length_limit = null - label_key_case = null - label_value_case = null - descriptor_formats = {} - # Note: we have to use [] instead of null for unset lists due to - # https://github.com/hashicorp/terraform/issues/28137 - # which was not fixed until Terraform 1.0.0, - # but we want the default to be all the labels in `label_order` - # and we want users to be able to prevent all tag generation - # by setting `labels_as_tags` to `[]`, so we need - # a different sentinel to indicate "default" - labels_as_tags = ["unset"] - } - description = <<-EOT - Single object for setting entire context at once. - See description of individual variables for details. - Leave string and numeric variables as `null` to use default value. - Individual variable settings (non-null) override settings in context object, - except for attributes, tags, and additional_tag_map, which are merged. - EOT - - validation { - condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) - error_message = "Allowed values: `lower`, `title`, `upper`." - } - - validation { - condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) - error_message = "Allowed values: `lower`, `title`, `upper`, `none`." - } -} - -variable "enabled" { - type = bool - default = null - description = "Set to false to prevent the module from creating any resources" -} - -variable "namespace" { - type = string - default = null - description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" -} - -variable "tenant" { - type = string - default = null - description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" -} - -variable "environment" { - type = string - default = null - description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" -} - -variable "stage" { - type = string - default = null - description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" -} - -variable "name" { - type = string - default = null - description = <<-EOT - ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. - This is the only ID element not also included as a `tag`. - The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. - EOT -} - -variable "delimiter" { - type = string - default = null - description = <<-EOT - Delimiter to be used between ID elements. - Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. - EOT -} - -variable "attributes" { - type = list(string) - default = [] - description = <<-EOT - ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, - in the order they appear in the list. New attributes are appended to the - end of the list. The elements of the list are joined by the `delimiter` - and treated as a single ID element. - EOT -} - -variable "labels_as_tags" { - type = set(string) - default = ["default"] - description = <<-EOT - Set of labels (ID elements) to include as tags in the `tags` output. - Default is to include all labels. - Tags with empty values will not be included in the `tags` output. - Set to `[]` to suppress all generated tags. - **Notes:** - The value of the `name` tag, if included, will be the `id`, not the `name`. - Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be - changed in later chained modules. Attempts to change it will be silently ignored. - EOT -} - -variable "tags" { - type = map(string) - default = {} - description = <<-EOT - Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). - Neither the tag keys nor the tag values will be modified by this module. - EOT -} - -variable "additional_tag_map" { - type = map(string) - default = {} - description = <<-EOT - Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. - This is for some rare cases where resources want additional configuration of tags - and therefore take a list of maps with tag key, value, and additional configuration. - EOT -} - -variable "label_order" { - type = list(string) - default = null - description = <<-EOT - The order in which the labels (ID elements) appear in the `id`. - Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. - EOT -} - -variable "regex_replace_chars" { - type = string - default = null - description = <<-EOT - Terraform regular expression (regex) string. - Characters matching the regex will be removed from the ID elements. - If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. - EOT -} - -variable "id_length_limit" { - type = number - default = null - description = <<-EOT - Limit `id` to this many characters (minimum 6). - Set to `0` for unlimited length. - Set to `null` for keep the existing setting, which defaults to `0`. - Does not affect `id_full`. - EOT - validation { - condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 - error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." - } -} - -variable "label_key_case" { - type = string - default = null - description = <<-EOT - Controls the letter case of the `tags` keys (label names) for tags generated by this module. - Does not affect keys of tags passed in via the `tags` input. - Possible values: `lower`, `title`, `upper`. - Default value: `title`. - EOT - - validation { - condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) - error_message = "Allowed values: `lower`, `title`, `upper`." - } -} - -variable "label_value_case" { - type = string - default = null - description = <<-EOT - Controls the letter case of ID elements (labels) as included in `id`, - set as tag values, and output by this module individually. - Does not affect values of tags passed in via the `tags` input. - Possible values: `lower`, `title`, `upper` and `none` (no transformation). - Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. - Default value: `lower`. - EOT - - validation { - condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) - error_message = "Allowed values: `lower`, `title`, `upper`, `none`." - } -} - -variable "descriptor_formats" { - type = any - default = {} - description = <<-EOT - Describe additional descriptors to be output in the `descriptors` output map. - Map of maps. Keys are names of descriptors. Values are maps of the form - `{ - format = string - labels = list(string) - }` - (Type is `any` so the map values can later be enhanced to provide additional options.) - `format` is a Terraform format string to be passed to the `format()` function. - `labels` is a list of labels, in order, to pass to `format()` function. - Label values will be normalized before being passed to `format()` so they will be - identical to how they appear in `id`. - Default is `{}` (`descriptors` output will be empty). - EOT -} - -#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/index.js b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/index.js deleted file mode 100644 index 301c8445d..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/index.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict'; - -const http = require('https'); - -const indexPage = 'index.html'; - -exports.handler = async (event, context, callback) => { - const cf = event.Records[0].cf; - const request = cf.request; - const response = cf.response; - const statusCode = response.status; - - // Only replace 403 and 404 requests typically received - // when loading a page for a SPA that uses client-side routing - const doReplace = request.method === 'GET' - && (statusCode == '403' || statusCode == '404'); - - const result = doReplace - ? await generateResponseAndLog(cf, request, indexPage) - : response; - - response.status = result.status; - response.headers = {...response.headers, ...result.headers}; - response.body = result.body; - - callback(null, response); -}; - -async function generateResponseAndLog(cf, request, indexPage){ - - const domain = cf.config.distributionDomainName; - const indexPath = `/${indexPage}`; - - const response = await generateResponse(domain, indexPath); - console.log('response: ' + JSON.stringify(response)); - return response; -} - -async function generateResponse(domain, path){ - try { - // Load HTML index from the CloudFront cache - const s3Response = await httpGet({ hostname: domain, path: path }); - - const headers = s3Response.headers || - { - 'content-type': [{ value: 'text/html;charset=UTF-8' }] - }; - - return { - status: '200', - statusDescription: 'OK', - headers: wrapAndFilterHeaders(headers), - body: s3Response.body - }; - } catch (error) { - return { - status: '500', - headers:{ - 'content-type': [{ value: 'text/plain' }] - }, - body: 'An error occurred loading the page' - }; - } -} - -function httpGet(params) { - return new Promise((resolve, reject) => { - http.get(params, (resp) => { - let result = { - headers: resp.headers, - body: '' - }; - resp.on('data', (chunk) => { result.body += chunk; }); - resp.on('end', () => { resolve(result); }); - }).on('error', (err) => { - console.log(`Couldn't fetch ${params.hostname}${params.path} : ${err.message}`); - reject(err, null); - }); - }); -} - -// Cloudfront requires header values to be wrapped in an array -function wrapAndFilterHeaders(headers){ - const allowedHeaders = [ - "content-type", - "content-length", - "date", - "last-modified", - "etag", - "cache-control", - "accept-ranges", - "server", - "age" - ]; - - const responseHeaders = {}; - - if(!headers){ - return responseHeaders; - } - - for(var propName in headers) { - // only include allowed headers - if(allowedHeaders.includes(propName.toLowerCase())){ - var header = headers[propName]; - - if (Array.isArray(header)){ - // assume already 'wrapped' format - responseHeaders[propName] = header; - } else { - // fix to required format - responseHeaders[propName] = [{ value: header }]; - } - } - - } - - return responseHeaders; -} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf deleted file mode 100644 index 659d6880a..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/main.tf +++ /dev/null @@ -1,30 +0,0 @@ -# https://levelup.gitconnected.com/preview-environments-in-aws-with-cloudfront-and-lambda-edge-7acccb0b67d1 - -locals { - lambda_runtime = var.runtime - lambda_handler = "index.handler" -} - -module "lambda_edge" { - source = "cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge" - version = "0.82.4" - - functions = { - origin_response = { - source = [{ - content = file("${path.module}/index.js") - filename = "index.js" - }] - runtime = local.lambda_runtime - handler = local.lambda_handler - event_type = "origin-response" - include_body = false - } - } - - providers = { - aws = aws.us-east-1 - } - - context = module.this.context -} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/outputs.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/outputs.tf deleted file mode 100644 index 315e14b86..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ -output "lambda_function_association" { - description = "The Lambda@Edge function association configuration to pass to `var.cloudfront_lambda_function_association` in the parent module." - value = module.lambda_edge.lambda_function_association -} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf deleted file mode 100644 index 1b3ea97eb..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/variables.tf +++ /dev/null @@ -1,28 +0,0 @@ -variable "cloudfront_distribution_domain_name" { - type = string - description = "Cloudfront Distribution Domain Name." -} - -variable "cloudfront_distribution_hosted_zone_id" { - type = string - description = "The CloudFront Distribution Hosted Zone ID." -} - -variable "parent_zone_name" { - type = string - description = "The name of the Route53 Hosted Zone where aliases for the CloudFront distribution are created." -} - -variable "site_fqdn" { - type = string - description = "The fully qualified alias for the CloudFront Distribution." -} - -variable "runtime" { - type = string - description = <<-EOT - Identifier of the function's runtime. See Runtimes for valid values. - https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime - EOT - default = "nodejs16.x" -} diff --git a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/versions.tf b/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/versions.tf deleted file mode 100644 index 19b91b119..000000000 --- a/modules/spa-s3-cloudfront/modules/lambda_edge_redirect_404/versions.tf +++ /dev/null @@ -1,11 +0,0 @@ -terraform { - required_version = ">= 1.0.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 4.9.0" - configuration_aliases = [aws.us-east-1] - } - } -} diff --git a/modules/spa-s3-cloudfront/remote-state.tf b/modules/spa-s3-cloudfront/remote-state.tf index 1f9f90575..8d8e44418 100644 --- a/modules/spa-s3-cloudfront/remote-state.tf +++ b/modules/spa-s3-cloudfront/remote-state.tf @@ -2,8 +2,7 @@ module "waf" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" - count = local.aws_waf_enabled ? 1 : 0 - + bypass = !local.aws_waf_enabled component = var.cloudfront_aws_waf_component_name privileged = false environment = var.cloudfront_aws_waf_environment @@ -31,12 +30,11 @@ module "github_runners" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" - count = local.github_runners_enabled ? 1 : 0 - - component = "github-runners" + bypass = !local.github_runners_enabled + component = var.github_runners_component_name stage = var.github_runners_stage_name environment = var.github_runners_environment_name - tenant = try(var.github_runners_tenant_name, var.tenant) + tenant = try(var.github_runners_tenant_name, module.this.tenant) context = module.this.context } diff --git a/modules/spa-s3-cloudfront/variables.tf b/modules/spa-s3-cloudfront/variables.tf index a99568240..a779a453d 100644 --- a/modules/spa-s3-cloudfront/variables.tf +++ b/modules/spa-s3-cloudfront/variables.tf @@ -490,11 +490,70 @@ variable "github_runners_tenant_name" { default = null } -variable "lambda_runtime" { +variable "lambda_edge_functions" { + type = map(object({ + source = optional(list(object({ + filename = string + content = string + }))) + source_dir = optional(string) + source_zip = optional(string) + runtime = string + handler = string + event_type = string + include_body = bool + })) + description = <<-EOT + Lambda@Edge functions to create. + + The key of this map is the name of the Lambda@Edge function. + + This map will be deep merged with each enabled default function. Use deep merge to change or overwrite specific values passed by those function objects. + EOT + default = {} +} + +variable "lambda_edge_runtime" { type = string description = <<-EOT - Identifier of the function's runtime. See Runtimes for valid values. - https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime + The default Lambda@Edge runtime for all functions. + + This value is deep merged in `module.lambda_edge_functions` with `var.lambda_edge_functions` and can be overwritten for any individual function. EOT default = "nodejs16.x" } + +variable "lambda_edge_handler" { + type = string + description = <<-EOT + The default Lambda@Edge handler for all functions. + + This value is deep merged in `module.lambda_edge_functions` with `var.lambda_edge_functions` and can be overwritten for any individual function. + EOT + default = "index.handler" +} + +variable "lambda_edge_allowed_ssm_parameters" { + type = list(string) + description = "The Lambda@Edge functions will be allowed to access the list of AWS SSM parameter with these ARNs" + default = [] +} + +variable "lambda_edge_destruction_delay" { + type = string + description = <<-EOT + The delay, in [Golang ParseDuration](https://pkg.go.dev/time#ParseDuration) format, to wait before destroying the Lambda@Edge + functions. + + This delay is meant to circumvent Lambda@Edge functions not being immediately deletable following their dissociation from + a CloudFront distribution, since they are replicated to CloudFront Edge servers around the world. + + If set to `null`, no delay will be introduced. + + By default, the delay is 20 minutes. This is because it takes about 3 minutes to destroy a CloudFront distribution, and + around 15 minutes until the Lambda@Edge function is available for deletion, in most cases. + + For more information, see: https://github.com/hashicorp/terraform-provider-aws/issues/1721. + EOT + default = "20m" +} From 4f854e13700921ad481bb8ea229d59046c1bd52c Mon Sep 17 00:00:00 2001 From: "Erik Osterman (CEO @ Cloud Posse)" Date: Fri, 9 Feb 2024 21:21:50 -0600 Subject: [PATCH 363/501] Update README (#974) Co-authored-by: cloudpossebot --- ...e-commit-check-and-autocommit-changes.yaml | 100 --------- README.md | 204 ++++++++++++------ README.yaml | 115 +++++++--- 3 files changed, 222 insertions(+), 197 deletions(-) delete mode 100644 .github/workflows/pre-commit-check-and-autocommit-changes.yaml diff --git a/.github/workflows/pre-commit-check-and-autocommit-changes.yaml b/.github/workflows/pre-commit-check-and-autocommit-changes.yaml deleted file mode 100644 index 63b2f1b36..000000000 --- a/.github/workflows/pre-commit-check-and-autocommit-changes.yaml +++ /dev/null @@ -1,100 +0,0 @@ -name: pre-commit-check-and-autocommit-changes - -on: - pull_request_target: - types: [labeled, opened, synchronize, unlabeled] - -jobs: - run-pre-commit-checks-and-autocommit-changes: - runs-on: ubuntu-latest - if: github.event.pull_request.state == 'open' - steps: - - name: Privileged Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - # Check out the PR commit, not the merge commit - # Use `ref` instead of `sha` to enable pushing back to `ref` - ref: ${{ github.event.pull_request.head.ref }} - - - name: Get List of Modified files - id: get-modified-files - shell: bash -x -e -o pipefail {0} - env: - BASE_REF: ${{ github.base_ref }} - HEAD_REF: ${{ github.head_ref }} - run: | - MODIFIED_FILES=$(git diff --name-only origin/${BASE_REF} origin/${HEAD_REF}) - if [ -z "$MODIFIED_FILES" ]; then - echo "No changed files detected on this branch? This must be an error." - exit 1 - else - echo "Running checks on the following files: ${MODIFIED_FILES}" - echo "::set-output name=modified_files::$(echo $MODIFIED_FILES)" - fi - - - name: Get Terraform Version - id: get-terraform-version - shell: bash -x -e -o pipefail {0} - env: - BASE_REF: ${{ github.base_ref }} - LABELS: ${{ join(github.event.pull_request.labels.*.name, '\n') }} - MODIFIED_FILES: ${{ steps.get-modified-files.outputs.modified_files }} - DEFAULT_TERRAFORM_VERSION: ${{ secrets.DEFAULT_TERRAFORM_VERSION }} - run: | - # Match labels like `terraform/0.12` or nothing (to prevent grep from returning a non-zero exit code) - # Use [0-9] because \d is not standard part of egrep - echo "PR labels: ${LABELS}" - TERRAFORM_VERSION=$(grep -Eo 'terraform/[0-9]+\.[x0-9]+|' <<<${LABELS} | cut -d/ -f2) - # Go through all the possible cases: one compliant label, no compliant labels, and multiple compliant labels - if grep -Ez '^\W*[0-9]+\.[x0-9]+\W*$' <<<${TERRAFORM_VERSION}; then - echo "Terraform version ${TERRAFORM_VERSION} will be used to check the formatting of files that have been modified since this branch diverged from ${BASE_REF}." - elif [ -z "${TERRAFORM_VERSION}" ]; then - TERRAFORM_VERSION="${DEFAULT_TERRAFORM_VERSION:-1.x}" - echo "No Terraform version found in the PR labels. Using the default Terraform version ${TERRAFORM_VERSION}." - else - echo "You have either chosen terraform labels with malformed versions or, more likely, you have chosen multiple terraform version labels." - echo "Please select a single terraform version label that matches the following regular expression: terraform/[0-9]+\.[x0-9]+" - exit 2 - fi - # Construct the actual semver expression that will be passed to Terraform - if grep -q 'x$' <<<${TERRAFORM_VERSION}; then - TERRAFORM_SEMVER=$TERRAFORM_VERSION - else - TERRAFORM_SEMVER=~$TERRAFORM_VERSION - fi - # Create GitHub Actions step output - echo "::set-output name=terraform_semver::$(echo $TERRAFORM_SEMVER)" - - # Install terraform to ensure we're using our expected version - - uses: hashicorp/setup-terraform@v1 - with: - terraform_version: ${{ steps.get-terraform-version.outputs.terraform_semver }} - - # Install terraform-docs for pre-commit hook - - name: Install terraform-docs - shell: bash - env: - INSTALL_PATH: "${{ github.workspace }}/bin" - run: | - make init - mkdir -p "${INSTALL_PATH}" - make packages/install/terraform-docs - echo "$INSTALL_PATH" >> $GITHUB_PATH - - # python setup, in preparation for pre-commit run - - uses: actions/setup-python@v2 - - # pre-commit checks: fmt + terraform-docs - # We skip tf_validate as it requires an init - # of all root modules, which is to be avoided. - - uses: cloudposse/github-action-pre-commit@v2.1.2 - env: - SKIP: tf_validate - with: - token: ${{ secrets.PUBLIC_REPO_ACCESS_TOKEN }} - git_user_name: cloudpossebot - git_user_email: cloudpossebot@users.noreply.github.com - extra_args: --files ${{ steps.get-modified-files.outputs.modified_files }} diff --git a/README.md b/README.md index 94f482358..f604c5195 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ - -[![Project Banner](.github/banner.png?raw=true)](https://cpco.io/homepage) - [![Latest Release](https://img.shields.io/github/release/cloudposse/terraform-aws-components.svg)](https://github.com/cloudposse/terraform-aws-components/releases/latest) [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) +Project Banner
+

+Latest ReleaseLast UpdateSlack Community

- -This is a collection of reusable Terraform components for provisioning infrastructure used by the Cloud Posse [reference architectures](https://cloudposse.com). +This is a collection of reusable [AWS Terraform components](https://atmos.tools/core-concepts/components/) for provisioning infrastructure used by the Cloud Posse [reference architectures](https://cloudposse.com). +They work really well with [Atmos](https://atmos.tools), our open-source tool for managing infrastructure as code with Terraform. --- > [!NOTE] -> This project is part of Cloud Posse's comprehensive ["SweetOps"](https://cpco.io/sweetops) approach towards DevOps. +> This project is part of Cloud Posse's comprehensive ["SweetOps"](https://cpco.io/homepage?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=) approach towards DevOps. +>
Learn More > > It's 100% Open Source and licensed under the [APACHE2](LICENSE). > +>
-[![README Header][readme_header_img]][readme_header_link] + ## Introduction -In this repo you'll find real-world examples of how we've implemented various common patterns using our [terraform modules](https://cpco.io/terraform-modules) for our customers. +In this repo you'll find real-world examples of how we've implemented Terraform "root" modules as native +[Atmos Components](https://atmos.tools/core-concepts/components/) for our customers. These Components +leverage our hundreds of free and open-source [terraform "child" modules](https://cpco.io/terraform-modules). + +The [component library](https://docs.cloudposse.com/components/) captures the business logic, opinions, best practices and +non-functional requirements for an organization. + +It's from this library that other developers in your organization will pick and choose from whenever they need to deploy some new +capability. + +These components make a lot of assumptions (aka ["convention over configuration"](https://en.wikipedia.org/wiki/Convention_over_configuration)) about how we've configured our environments. +That said, they still serve as an excellent reference for others on how to build, organize and distribute enterprise-grade infrastructure +with Terraform that can be used with [Atmos](https://atmos.tools). + + -The [component library](https://docs.cloudposse.com/components/) captures the business logic, opinions, best practices and non-functional requirements. +## Usage -It's from this library that other developers in your organization will pick and choose from anytime they need to deploy some new capability. -These components make a lot of assumptions about how we've configured our environments. That said, they can still serve as an excellent reference for others. -## Deprecations -Terraform components which are no longer actively maintained are now in the `deprecated/` folder. +Please take a look at each [component's README](https://docs.cloudposse.com/components/) for specific usage. -Many of these deprecated components are used in our old reference architectures. +> [!TIP] +> ## 👽 Use Atmos with Terraform +> To orchestrate multiple environments with ease using Terraform, Cloud Posse recommends using [Atmos](https://atmos.tools), +> our open-source tool for Terraform automation. +> +>
+> Watch demo of using Atmos with Terraform +>
+> Example of running atmos to manage infrastructure from our Quick Start tutorial. +> + +Generally, you can use these components in [Atmos](https://atmos.tools/core-concepts/components/) by adding something like the following +code into your [stack manifest](https://atmos.tools/core-concepts/stacks/): + +```yaml +components: # List of components to include in the stack + terraform: # The toolchain being used for configuration + vpc: # The name of the component (e.g. terraform "root" module) + vars: # Terraform variables (e.g. `.tfvars`) + cidr_block: 10.0.0.0/16 # A variable input passed to terraform via `.tfvars` +``` -We intend to eventually delete, but are leaving them for now in the repo. +## Automated Updates of Components using GitHub Actions + +Leverage our [GitHub Action](https://atmos.tools/integrations/github-actions/component-updater) to automate the creation and management of pull requests for component updates. + +This is done by creating a new file (e.g. `atmos-component-updater.yml`) in the `.github/workflows` directory of your repository. + +The file should contain the following: + +```yaml +jobs: +update: + runs-on: + - "ubuntu-latest" + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Update Atmos Components + uses: cloudposse/github-action-atmos-component-updater@v2 + env: + # https://atmos.tools/cli/configuration/#environment-variables + ATMOS_CLI_CONFIG_PATH: ${{ github.workspace }}/rootfs/usr/local/etc/atmos/ + with: + github-access-token: ${{ secrets.GITHUB_TOKEN }} + log-level: INFO + max-number-of-prs: 10 + + - name: Delete abandoned update branches + uses: phpdocker-io/github-actions-delete-abandoned-branches@v2 + with: + github_token: ${{ github.token }} + last_commit_age_days: 0 + allowed_prefixes: "component-update/" + dry_run: no +``` + +For the full documentation on how to use the Component Updater GitHub Action, please see the [Atmos Intergations](https://atmos.tools/integrations/github-actions/component-updater) documentation. ## Using `pre-commit` Hooks @@ -74,13 +145,13 @@ Then run the following command to rebuild the docs for all Terraform components: make rebuild-docs ``` - - -## Usage - - - -Please take a look at each [component's README](https://docs.cloudposse.com/components/) for usage. +> [!IMPORTANT] +> ## Deprecated Components +> Terraform components which are no longer actively maintained are kept in the [`deprecated/`](deprecated/) folder. +> +> Many of these deprecated components are used in our older reference architectures. +> +> We intend to eventually delete, but are leaving them for now in the repo. @@ -134,45 +205,53 @@ Please use the [issue tracker](https://github.com/cloudposse/terraform-aws-compo ### 💻 Developing -If you are interested in being a contributor and want to get involved in developing this project or [help out](https://cpco.io/help-out) with Cloud Posse's other projects, we would love to hear from you! Shoot us an [email][email]. +If you are interested in being a contributor and want to get involved in developing this project or help out with Cloud Posse's other projects, we would love to hear from you! +Hit us up in [Slack](https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=slack), in the `#cloudposse` channel. In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. - - 1. **Fork** the repo on GitHub - 2. **Clone** the project to your own machine - 3. **Commit** changes to your own branch - 4. **Push** your work back up to your fork - 5. Submit a **Pull Request** so that we can review your changes + 1. Review our [Code of Conduct](https://github.com/cloudposse/terraform-aws-components/?tab=coc-ov-file#code-of-conduct) and [Contributor Guidelines](https://github.com/cloudposse/.github/blob/main/CONTRIBUTING.md). + 2. **Fork** the repo on GitHub + 3. **Clone** the project to your own machine + 4. **Commit** changes to your own branch + 5. **Push** your work back up to your fork + 6. Submit a **Pull Request** so that we can review your changes **NOTE:** Be sure to merge the latest changes from "upstream" before making a pull request! ### 🌎 Slack Community -Join our [Open Source Community][slack] on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure. +Join our [Open Source Community](https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=slack) on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure. ### 📰 Newsletter -Sign up for [our newsletter][newsletter] that covers everything on our technology radar. Receive updates on what we're up to on GitHub as well as awesome new projects we discover. +Sign up for [our newsletter](https://cpco.io/newsletter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=newsletter) and join 3,000+ DevOps engineers, CTOs, and founders who get insider access to the latest DevOps trends, so you can always stay in the know. +Dropped straight into your Inbox every week — and usually a 5-minute read. -### 📆 Office Hours +### 📆 Office Hours -[Join us every Wednesday via Zoom][office_hours] for our weekly "Lunch & Learn" sessions. It's **FREE** for everyone! +[Join us every Wednesday via Zoom](https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=office_hours) for your weekly dose of insider DevOps trends, AWS news and Terraform insights, all sourced from our SweetOps community, plus a _live Q&A_ that you can’t find anywhere else. +It's **FREE** for everyone! ## About -This project is maintained and funded by [Cloud Posse, LLC][website]. - +This project is maintained by Cloud Posse, LLC. + -We are a [**DevOps Accelerator**][commercial_support]. We'll help you build your cloud infrastructure from the ground up so you can own it. Then we'll show you how to operate it and stick around for as long as you need us. +We are a [**DevOps Accelerator**](https://cpco.io/commercial-support?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=commercial_support) for funded startups and enterprises. +Use our ready-to-go terraform architecture blueprints for AWS to get up and running quickly. +We build it with you. You own everything. Your team wins. Plus, we stick around until you succeed. -[![Learn More](https://img.shields.io/badge/learn%20more-success.svg?style=for-the-badge)][commercial_support] +Learn More -Work directly with our team of DevOps experts via email, slack, and video conferencing. +*Your team can operate like a pro today.* -We deliver 10x the value for a fraction of the cost of a full-time engineer. Our track record is not even funny. If you want things done right and you need it done FAST, then we're your best bet. +Ensure that your team succeeds by using our proven process and turnkey blueprints. Plus, we stick around until you succeed. + +
+ 📚 See What's Included - **Reference Architecture.** You'll get everything you need from the ground up built using 100% infrastructure as code. -- **Release Engineering.** You'll have end-to-end CI/CD with unlimited staging environments. +- **Deployment Strategy.** You'll have a battle-tested deployment strategy using GitHub Actions that's automated and repeatable. - **Site Reliability Engineering.** You'll have total visibility into your apps and microservices. - **Security Baseline.** You'll have built-in governance with accountability and audit logs for all changes. - **GitOps.** You'll be able to operate your infrastructure via Pull Requests. @@ -181,14 +260,18 @@ We deliver 10x the value for a fraction of the cost of a full-time engineer. Our - **Troubleshooting.** You'll get help to triage when things aren't working. - **Code Reviews.** You'll receive constructive feedback on Pull Requests. - **Bug Fixes.** We'll rapidly work with you to fix any bugs in our projects. +
-[![README Commercial Support][readme_commercial_support_img]][readme_commercial_support_link] + ## License -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) - -See [LICENSE](LICENSE) for full details. +License +
+Preamble to the Apache License, Version 2.0 +
+
+Complete license is available in the [`LICENSE`](LICENSE) file. ```text Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -207,34 +290,15 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` +
## Trademarks All other trademarks referenced herein are the property of their respective owners. --- Copyright © 2017-2024 [Cloud Posse, LLC](https://cpco.io/copyright) -[![README Footer][readme_footer_img]][readme_footer_link] -[![Beacon][beacon]][website] - - [logo]: https://cloudposse.com/logo-300x69.svg - [docs]: https://cpco.io/docs?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=docs - [website]: https://cpco.io/homepage?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=website - [github]: https://cpco.io/github?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=github - [jobs]: https://cpco.io/jobs?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=jobs - [hire]: https://cpco.io/hire?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=hire - [slack]: https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=slack - [twitter]: https://cpco.io/twitter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=twitter - [office_hours]: https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=office_hours - [newsletter]: https://cpco.io/newsletter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=newsletter - [email]: https://cpco.io/email?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=email - [commercial_support]: https://cpco.io/commercial-support?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=commercial_support - [we_love_open_source]: https://cpco.io/we-love-open-source?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=we_love_open_source - [terraform_modules]: https://cpco.io/terraform-modules?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=terraform_modules - [readme_header_img]: https://cloudposse.com/readme/header/img - [readme_header_link]: https://cloudposse.com/readme/header/link?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=readme_header_link - [readme_footer_img]: https://cloudposse.com/readme/footer/img - [readme_footer_link]: https://cloudposse.com/readme/footer/link?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=readme_footer_link - [readme_commercial_support_img]: https://cloudposse.com/readme/commercial-support/img - [readme_commercial_support_link]: https://cloudposse.com/readme/commercial-support/link?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=readme_commercial_support_link - [beacon]: https://ga-beacon.cloudposse.com/UA-76589703-4/cloudposse/terraform-aws-components?pixel&cs=github&cm=readme&an=terraform-aws-components - + + +README footer + +Beacon diff --git a/README.yaml b/README.yaml index 69a0b5ddc..e96844f94 100644 --- a/README.yaml +++ b/README.yaml @@ -36,10 +36,13 @@ github_repo: "cloudposse/terraform-aws-components" # Badges to display badges: - name: "Latest Release" - image: "https://img.shields.io/github/release/cloudposse/terraform-aws-components.svg" + image: "https://img.shields.io/github/release/cloudposse/terraform-aws-components.svg?style=for-the-badge" url: "https://github.com/cloudposse/terraform-aws-components/releases/latest" + - name: "Last Update" + image: https://img.shields.io/github/last-commit/cloudposse/terraform-aws-components/main?style=for-the-badge + url: https://github.com/cloudposse/terraform-aws-components/commits/main/ - name: "Slack Community" - image: "https://slack.cloudposse.com/badge.svg" + image: "https://slack.cloudposse.com/for-the-badge.svg" url: "https://slack.cloudposse.com" references: @@ -61,24 +64,90 @@ related: # Short description of this project description: |- - This is a collection of reusable Terraform components for provisioning infrastructure used by the Cloud Posse [reference architectures](https://cloudposse.com). + This is a collection of reusable [AWS Terraform components](https://atmos.tools/core-concepts/components/) for provisioning infrastructure used by the Cloud Posse [reference architectures](https://cloudposse.com). + They work really well with [Atmos](https://atmos.tools), our open-source tool for managing infrastructure as code with Terraform. introduction: |- - In this repo you'll find real-world examples of how we've implemented various common patterns using our [terraform modules](https://cpco.io/terraform-modules) for our customers. + In this repo you'll find real-world examples of how we've implemented Terraform "root" modules as native + [Atmos Components](https://atmos.tools/core-concepts/components/) for our customers. These Components + leverage our hundreds of free and open-source [terraform "child" modules](https://cpco.io/terraform-modules). - The [component library](https://docs.cloudposse.com/components/) captures the business logic, opinions, best practices and non-functional requirements. + The [component library](https://docs.cloudposse.com/components/) captures the business logic, opinions, best practices and + non-functional requirements for an organization. - It's from this library that other developers in your organization will pick and choose from anytime they need to deploy some new capability. + It's from this library that other developers in your organization will pick and choose from whenever they need to deploy some new + capability. - These components make a lot of assumptions about how we've configured our environments. That said, they can still serve as an excellent reference for others. + These components make a lot of assumptions (aka ["convention over configuration"](https://en.wikipedia.org/wiki/Convention_over_configuration)) about how we've configured our environments. + That said, they still serve as an excellent reference for others on how to build, organize and distribute enterprise-grade infrastructure + with Terraform that can be used with [Atmos](https://atmos.tools). - ## Deprecations +# How to use this project +usage: |- - Terraform components which are no longer actively maintained are now in the `deprecated/` folder. + Please take a look at each [component's README](https://docs.cloudposse.com/components/) for specific usage. + + > [!TIP] + > ## 👽 Use Atmos with Terraform + > To orchestrate multiple environments with ease using Terraform, Cloud Posse recommends using [Atmos](https://atmos.tools), + > our open-source tool for Terraform automation. + > + >
+ > Watch demo of using Atmos with Terraform + >
+ > Example of running atmos to manage infrastructure from our Quick Start tutorial. + > + + Generally, you can use these components in [Atmos](https://atmos.tools/core-concepts/components/) by adding something like the following + code into your [stack manifest](https://atmos.tools/core-concepts/stacks/): + + ```yaml + components: # List of components to include in the stack + terraform: # The toolchain being used for configuration + vpc: # The name of the component (e.g. terraform "root" module) + vars: # Terraform variables (e.g. `.tfvars`) + cidr_block: 10.0.0.0/16 # A variable input passed to terraform via `.tfvars` + ``` - Many of these deprecated components are used in our old reference architectures. + ## Automated Updates of Components using GitHub Actions + + Leverage our [GitHub Action](https://atmos.tools/integrations/github-actions/component-updater) to automate the creation and management of pull requests for component updates. + + This is done by creating a new file (e.g. `atmos-component-updater.yml`) in the `.github/workflows` directory of your repository. + + The file should contain the following: + + ```yaml + jobs: + update: + runs-on: + - "ubuntu-latest" + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Update Atmos Components + uses: cloudposse/github-action-atmos-component-updater@v2 + env: + # https://atmos.tools/cli/configuration/#environment-variables + ATMOS_CLI_CONFIG_PATH: ${{ github.workspace }}/rootfs/usr/local/etc/atmos/ + with: + github-access-token: ${{ secrets.GITHUB_TOKEN }} + log-level: INFO + max-number-of-prs: 10 + + - name: Delete abandoned update branches + uses: phpdocker-io/github-actions-delete-abandoned-branches@v2 + with: + github_token: ${{ github.token }} + last_commit_age_days: 0 + allowed_prefixes: "component-update/" + dry_run: no + ``` - We intend to eventually delete, but are leaving them for now in the repo. + For the full documentation on how to use the Component Updater GitHub Action, please see the [Atmos Intergations](https://atmos.tools/integrations/github-actions/component-updater) documentation. ## Using `pre-commit` Hooks @@ -97,24 +166,16 @@ introduction: |- make rebuild-docs ``` -# How to use this project -usage: |- - Please take a look at each [component's README](https://docs.cloudposse.com/components/) for usage. + > [!IMPORTANT] + > ## Deprecated Components + > Terraform components which are no longer actively maintained are kept in the [`deprecated/`](deprecated/) folder. + > + > Many of these deprecated components are used in our older reference architectures. + > + > We intend to eventually delete, but are leaving them for now in the repo. include: - "docs/targets.md" # Contributors to this project -contributors: - - name: "Erik Osterman" - github: "osterman" - - name: "Igor Rodionov" - github: "goruha" - - name: "Andriy Knysh" - github: "aknysh" - - name: "Matt Gowie" - github: "Gowiem" - - name: "Yonatan Koren" - github: "korenyoni" - - name: "Matt Calhoun" - github: "mcalhoun" +contributors: [] From 51ec634e6c7b9b1495cbac92188401b56389aa0b Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Mon, 12 Feb 2024 16:56:22 -0500 Subject: [PATCH 364/501] Update `spacelift` component. Add `settings.spacelift.space_name_pattern` attribute (#980) --- README.md | 2 +- modules/spacelift/admin-stack/README.md | 2 +- modules/spacelift/admin-stack/child-stacks.tf | 28 ++++++++++++++++++- modules/spacelift/admin-stack/spaces.tf | 26 +++++++++++++++-- modules/spacelift/admin-stack/variables.tf | 2 +- modules/spacelift/spaces/README.md | 2 +- modules/spacelift/spaces/main.tf | 2 +- 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f604c5195..6badcac97 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ Please use the [issue tracker](https://github.com/cloudposse/terraform-aws-compo ### 💻 Developing -If you are interested in being a contributor and want to get involved in developing this project or help out with Cloud Posse's other projects, we would love to hear from you! +If you are interested in being a contributor and want to get involved in developing this project or help out with Cloud Posse's other projects, we would love to hear from you! Hit us up in [Slack](https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=slack), in the `#cloudposse` channel. In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index 6251199fa..08a33fd7e 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -235,7 +235,7 @@ components: | [root\_stack\_policy\_attachments](#input\_root\_stack\_policy\_attachments) | List of policy attachments to attach to the root admin stack | `set(string)` | `[]` | no | | [runner\_image](#input\_runner\_image) | The full image name and tag of the Docker image to use in Spacelift | `string` | `null` | no | | [showcase](#input\_showcase) | Showcase settings | `map(any)` | `null` | no | -| [space\_id](#input\_space\_id) | Place the stack in the specified space\_id. | `string` | `"root"` | no | +| [space\_id](#input\_space\_id) | Place the stack in the specified space\_id | `string` | `"root"` | no | | [spacelift\_run\_enabled](#input\_spacelift\_run\_enabled) | Enable/disable creation of the `spacelift_run` resource | `bool` | `false` | no | | [spacelift\_spaces\_component\_name](#input\_spacelift\_spaces\_component\_name) | The component name of the spacelift spaces component | `string` | `"spacelift/spaces"` | no | | [spacelift\_spaces\_environment\_name](#input\_spacelift\_spaces\_environment\_name) | The environment name of the spacelift spaces component | `string` | `null` | no | diff --git a/modules/spacelift/admin-stack/child-stacks.tf b/modules/spacelift/admin-stack/child-stacks.tf index 6bad152c7..0d094e2e2 100644 --- a/modules/spacelift/admin-stack/child-stacks.tf +++ b/modules/spacelift/admin-stack/child-stacks.tf @@ -108,7 +108,6 @@ module "child_stack" { protect_from_deletion = try(each.value.settings.spacelift.protect_from_deletion, var.protect_from_deletion) repository = var.repository runner_image = try(each.value.settings.spacelift.runner_image, var.runner_image) - space_id = local.spaces[try(each.value.settings.spacelift.space_name, var.space_id)] spacelift_run_enabled = try(each.value.settings.spacelift.spacelift_run_enabled, var.spacelift_run_enabled) spacelift_stack_dependency_enabled = try(each.value.settings.spacelift.spacelift_stack_dependency_enabled, var.spacelift_stack_dependency_enabled) stack_destructor_enabled = try(each.value.settings.spacelift.stack_destructor_enabled, var.stack_destructor_enabled) @@ -130,6 +129,33 @@ module "child_stack" { pulumi = try(each.value.settings.spacelift.pulumi, var.pulumi) showcase = try(each.value.settings.spacelift.showcase, var.showcase) + # Process `spacelift.space_name` and `spacelift.space_name_pattern` + space_id = local.spaces[ + try( + coalesce( + # if `space_name` is specified, use it + each.value.settings.spacelift.space_name, + # otherwise, try to replace the context tokens in `space_name_template` and use it + # `space_name_template` accepts the following context tokens: {namespace}, {tenant}, {environment}, {stage} + each.value.settings.spacelift.space_name_pattern != "" && each.value.settings.spacelift.space_name_pattern != null ? ( + replace( + replace( + replace( + replace( + each.value.settings.spacelift.space_name_pattern, + "{namespace}", module.this.namespace + ), + "{tenant}", module.this.tenant + ), + "{environment}", module.this.environment + ), + "{stage}", module.this.stage) + ) : "" + ), + var.space_id + ) + ] + depends_on = [ null_resource.spaces_precondition, null_resource.workers_precondition, diff --git a/modules/spacelift/admin-stack/spaces.tf b/modules/spacelift/admin-stack/spaces.tf index 648a93b6a..b66a208e6 100644 --- a/modules/spacelift/admin-stack/spaces.tf +++ b/modules/spacelift/admin-stack/spaces.tf @@ -3,7 +3,29 @@ locals { # spacelift.settings metadata. It then creates a set of all of the unique space_names so we can use that to look up # their IDs from remote state. unique_spaces_from_config = toset([for k, v in { - for k, v in module.child_stacks_config.spacelift_stacks : k => try(v.settings.spacelift.space_name, "root") + for k, v in module.child_stacks_config.spacelift_stacks : k => try( + coalesce( + # if `space_name` is specified, use it + v.settings.spacelift.space_name, + # otherwise, try to replace the context tokens in `space_name_template` and use it + # `space_name_template` accepts the following context tokens: {namespace}, {tenant}, {environment}, {stage} + v.settings.spacelift.space_name_pattern != "" && v.settings.spacelift.space_name_pattern != null ? ( + replace( + replace( + replace( + replace( + v.settings.spacelift.space_name_pattern, + "{namespace}", module.this.namespace + ), + "{tenant}", module.this.tenant + ), + "{environment}", module.this.environment + ), + "{stage}", module.this.stage) + ) : "" + ), + "root" + ) if try(v.settings.spacelift.workspace_enabled, false) == true } : v if v != "root"]) @@ -18,7 +40,7 @@ locals { missing_spaces = setunion(setsubtract(local.unique_spaces_from_config, keys(local.spaces))) } -# Ensure all of the spaces referenced in the atmos config exist in Spacelift +# Ensure all of the spaces referenced in the Atmos config exist in Spacelift resource "null_resource" "spaces_precondition" { count = local.enabled ? 1 : 0 diff --git a/modules/spacelift/admin-stack/variables.tf b/modules/spacelift/admin-stack/variables.tf index eb8e3af91..883383c66 100644 --- a/modules/spacelift/admin-stack/variables.tf +++ b/modules/spacelift/admin-stack/variables.tf @@ -239,7 +239,7 @@ variable "showcase" { variable "space_id" { type = string - description = "Place the stack in the specified space_id." + description = "Place the stack in the specified space_id" default = "root" } diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index 5db0f2244..dafcf34cc 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -81,7 +81,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.4.0 | +| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.6.0 | | [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.6.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/spacelift/spaces/main.tf b/modules/spacelift/spaces/main.tf index 92c525185..99a641663 100644 --- a/modules/spacelift/spaces/main.tf +++ b/modules/spacelift/spaces/main.tf @@ -53,7 +53,7 @@ module "space" { module "policy" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy" - version = "1.4.0" + version = "1.6.0" for_each = local.all_policies_inputs From 6afdd9aa6fde9d857b04a96f5dd101a617159cd6 Mon Sep 17 00:00:00 2001 From: "John C. Bland II" Date: Wed, 14 Feb 2024 16:09:46 -0600 Subject: [PATCH 365/501] Added DynamoDB import_table support (#981) --- modules/dynamodb/main.tf | 3 ++- modules/dynamodb/variables.tf | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/modules/dynamodb/main.tf b/modules/dynamodb/main.tf index d7f7e21f1..6bd309b44 100644 --- a/modules/dynamodb/main.tf +++ b/modules/dynamodb/main.tf @@ -6,11 +6,12 @@ locals { module "dynamodb_table" { source = "cloudposse/dynamodb/aws" - version = "0.31.0" + version = "0.35.0" billing_mode = var.billing_mode replicas = var.replicas dynamodb_attributes = var.dynamodb_attributes + import_table = var.import_table global_secondary_index_map = var.global_secondary_index_map local_secondary_index_map = var.local_secondary_index_map diff --git a/modules/dynamodb/variables.tf b/modules/dynamodb/variables.tf index 59f19c149..764cc51d8 100644 --- a/modules/dynamodb/variables.tf +++ b/modules/dynamodb/variables.tf @@ -167,3 +167,25 @@ variable "replicas" { default = [] description = "List of regions to create a replica table in" } + +variable "import_table" { + type = object({ + # Valid values are GZIP, ZSTD and NONE + input_compression_type = optional(string, null) + # Valid values are CSV, DYNAMODB_JSON, and ION. + input_format = string + input_format_options = optional(object({ + csv = object({ + delimiter = string + header_list = list(string) + }) + }), null) + s3_bucket_source = object({ + bucket = string + bucket_owner = optional(string) + key_prefix = optional(string) + }) + }) + default = null + description = "Import Amazon S3 data into a new table." +} From 807aa1d279b84fb633092ac7bf4382ff68638e45 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 20 Feb 2024 09:40:37 -0800 Subject: [PATCH 366/501] Update `EFS` & `ECS` components to allow using EFS in ECS (#979) --- modules/ecs-service/README.md | 4 +++- modules/ecs-service/main.tf | 4 +++- modules/ecs-service/variables.tf | 11 +++++++++++ modules/efs/README.md | 11 ++++++++++- modules/efs/main.tf | 3 ++- modules/efs/variables.tf | 13 +++++++++++++ 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index d6c4d7e65..0e17417a7 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -193,7 +193,8 @@ This will create a CNAME record in the `acme.com` hosted zone that points `echo. ### EFS -EFS is supported by `ecs-service`. You can use either `efs_volumes` or `efs_component_volumes` in your task definition. +EFS is supported by this ecs service, you can use either `efs_volumes` or `efs_component_volumes` in your task definition. + This example shows how to use `efs_component_volumes` which remote looks up efs component and uses the `efs_id` to mount the volume. And how to use `efs_volumes` @@ -407,6 +408,7 @@ components: | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [task](#input\_task) | Feed inputs into ecs\_alb\_service\_task module |
object({
task_cpu = optional(number)
task_memory = optional(number)
task_role_arn = optional(string, "")
pid_mode = optional(string, null)
ipc_mode = optional(string, null)
network_mode = optional(string)
propagate_tags = optional(string)
assign_public_ip = optional(bool, false)
use_alb_security_groups = optional(bool, true)
launch_type = optional(string, "FARGATE")
scheduling_strategy = optional(string, "REPLICA")
capacity_provider_strategies = optional(list(object({
capacity_provider = string
weight = number
base = number
})), [])

deployment_minimum_healthy_percent = optional(number, null)
deployment_maximum_percent = optional(number, null)
desired_count = optional(number, 0)
min_capacity = optional(number, 1)
max_capacity = optional(number, 2)
wait_for_steady_state = optional(bool, true)
circuit_breaker_deployment_enabled = optional(bool, true)
circuit_breaker_rollback_enabled = optional(bool, true)

ecs_service_enabled = optional(bool, true)
bind_mount_volumes = optional(list(object({
name = string
host_path = string
})), [])
efs_volumes = optional(list(object({
host_path = string
name = string
efs_volume_configuration = list(object({
file_system_id = string
root_directory = string
transit_encryption = string
transit_encryption_port = string
authorization_config = list(object({
access_point_id = string
iam = string
}))
}))
})), [])
efs_component_volumes = optional(list(object({
host_path = string
name = string
efs_volume_configuration = list(object({
component = optional(string, "efs")
tenant = optional(string, null)
environment = optional(string, null)
stage = optional(string, null)

root_directory = string
transit_encryption = string
transit_encryption_port = string
authorization_config = list(object({
access_point_id = string
iam = string
}))
}))
})), [])
docker_volumes = optional(list(object({
host_path = string
name = string
docker_volume_configuration = list(object({
autoprovision = bool
driver = string
driver_opts = map(string)
labels = map(string)
scope = string
}))
})), [])
fsx_volumes = optional(list(object({
host_path = string
name = string
fsx_windows_file_server_volume_configuration = list(object({
file_system_id = string
root_directory = string
authorization_config = list(object({
credentials_parameter = string
domain = string
}))
}))
})), [])
})
| `{}` | no | | [task\_enabled](#input\_task\_enabled) | Whether or not to use the ECS task module | `bool` | `true` | no | +| [task\_exec\_policy\_arns\_map](#input\_task\_exec\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task execution role.
The names are arbitrary, but must be known at plan time. The purpose of the name
is so that changes to one ARN do not cause a ripple effect on the other ARNs.
If you cannot provide unique names known at plan time, use `task_exec_policy_arns` instead. | `map(string)` | `{}` | no | | [task\_iam\_role\_component](#input\_task\_iam\_role\_component) | A component that outputs an iam\_role module as 'role' for adding to the service as a whole. | `string` | `null` | no | | [task\_policy\_arns](#input\_task\_policy\_arns) | The IAM policy ARNs to attach to the ECS task IAM role | `list(string)` |
[
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
"arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess"
]
| no | | [task\_security\_group\_component](#input\_task\_security\_group\_component) | A component that outputs security\_group\_id for adding to the service as a whole. | `string` | `null` | no | diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 5000fc207..97e560ab1 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -217,7 +217,7 @@ module "container_definition" { options = tomap({ awslogs-region = var.region, awslogs-group = local.awslogs_group, - awslogs-stream-prefix = var.name, + awslogs-stream-prefix = coalesce(each.value["name"], each.key), }) # if we are not using awslogs, we execute this line, which if we have dd enabled, means we are using firelens, so merge that config in. }) : merge(lookup(each.value, "log_configuration", {}), local.datadog_logconfiguration_firelens) @@ -290,6 +290,8 @@ module "ecs_alb_service_task" { task_role_arn = lookup(local.task, "task_role_arn", one(module.iam_role[*]["outputs"]["role"]["arn"])) capacity_provider_strategies = lookup(local.task, "capacity_provider_strategies") + task_exec_policy_arns_map = var.task_exec_policy_arns_map + efs_volumes = local.efs_volumes docker_volumes = lookup(local.task, "docker_volumes", []) fsx_volumes = lookup(local.task, "fsx_volumes", []) diff --git a/modules/ecs-service/variables.tf b/modules/ecs-service/variables.tf index 2df296d5f..f61b4b017 100644 --- a/modules/ecs-service/variables.tf +++ b/modules/ecs-service/variables.tf @@ -568,3 +568,14 @@ variable "task_iam_role_component" { description = "A component that outputs an iam_role module as 'role' for adding to the service as a whole." default = null } + +variable "task_exec_policy_arns_map" { + type = map(string) + description = <<-EOT + A map of name to IAM Policy ARNs to attach to the generated task execution role. + The names are arbitrary, but must be known at plan time. The purpose of the name + is so that changes to one ARN do not cause a ripple effect on the other ARNs. + If you cannot provide unique names known at plan time, use `task_exec_policy_arns` instead. + EOT + default = {} +} diff --git a/modules/efs/README.md b/modules/efs/README.md index e6483a916..722714068 100644 --- a/modules/efs/README.md +++ b/modules/efs/README.md @@ -17,6 +17,14 @@ components: name: shared-files dns_name: shared-files provisioned_throughput_in_mibps: 10 + # additional_security_group_rules: + # - key: "fargate_efs" + # type: "ingress" + # from_port: 2049 + # to_port: 2049 + # protocol: "tcp" + # description: "Allow Fargate EFS Volume mounts" + # cidr_blocks: ["0.0.0.0/0"] ``` @@ -37,7 +45,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [efs](#module\_efs) | cloudposse/efs/aws | 0.32.7 | +| [efs](#module\_efs) | cloudposse/efs/aws | 0.35.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [gbl\_dns\_delegated](#module\_gbl\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | @@ -57,6 +65,7 @@ components: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [additional\_security\_group\_rules](#input\_additional\_security\_group\_rules) | A list of Security Group rule objects to add to the created security group, in addition to the ones
this module normally creates. (To suppress the module's rules, set `create_security_group` to false
and supply your own security group via `associated_security_group_ids`.)
The keys and values of the objects are fully compatible with the `aws_security_group_rule` resource, except
for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique and known at "plan" time.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` | `[]` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | diff --git a/modules/efs/main.tf b/modules/efs/main.tf index d5f5a2c9b..948f2b68f 100644 --- a/modules/efs/main.tf +++ b/modules/efs/main.tf @@ -17,12 +17,13 @@ locals { module "efs" { source = "cloudposse/efs/aws" - version = "0.32.7" + version = "0.35.0" region = var.region vpc_id = local.vpc_id subnets = local.private_subnet_ids allowed_security_group_ids = local.allowed_security_groups + additional_security_group_rules = var.additional_security_group_rules performance_mode = var.performance_mode provisioned_throughput_in_mibps = var.provisioned_throughput_in_mibps throughput_mode = var.throughput_mode diff --git a/modules/efs/variables.tf b/modules/efs/variables.tf index 130b0bf58..c2fd666fd 100644 --- a/modules/efs/variables.tf +++ b/modules/efs/variables.tf @@ -46,3 +46,16 @@ variable "eks_component_names" { description = "The names of the eks components" default = ["eks/cluster"] } + +variable "additional_security_group_rules" { + type = list(any) + default = [] + description = <<-EOT + A list of Security Group rule objects to add to the created security group, in addition to the ones + this module normally creates. (To suppress the module's rules, set `create_security_group` to false + and supply your own security group via `associated_security_group_ids`.) + The keys and values of the objects are fully compatible with the `aws_security_group_rule` resource, except + for `security_group_id` which will be ignored, and the optional "key" which, if provided, must be unique and known at "plan" time. + To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . + EOT +} From 6fa8fae90ab25eaa0eaca16060ebe3cacd02c4c5 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 21 Feb 2024 17:24:32 -0500 Subject: [PATCH 367/501] feat: eventbridge component (#944) --- modules/eventbridge/README.md | 108 +++++++++++ modules/eventbridge/context.tf | 279 ++++++++++++++++++++++++++++ modules/eventbridge/main.tf | 26 +++ modules/eventbridge/outputs.tf | 19 ++ modules/eventbridge/policies.tf | 33 ++++ modules/eventbridge/providers.tf | 19 ++ modules/eventbridge/remote-state.tf | 0 modules/eventbridge/variables.tf | 26 +++ modules/eventbridge/versions.tf | 10 + 9 files changed, 520 insertions(+) create mode 100644 modules/eventbridge/README.md create mode 100644 modules/eventbridge/context.tf create mode 100644 modules/eventbridge/main.tf create mode 100644 modules/eventbridge/outputs.tf create mode 100644 modules/eventbridge/policies.tf create mode 100644 modules/eventbridge/providers.tf create mode 100644 modules/eventbridge/remote-state.tf create mode 100644 modules/eventbridge/variables.tf create mode 100644 modules/eventbridge/versions.tf diff --git a/modules/eventbridge/README.md b/modules/eventbridge/README.md new file mode 100644 index 000000000..302d4d009 --- /dev/null +++ b/modules/eventbridge/README.md @@ -0,0 +1,108 @@ +# Component: `eventbridge` + +The `eventbridge` component is a Terraform module that defines a CloudWatch EventBridge rule. +The rule is pointed at cloudwatch by default. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + eventbridge/ecs-alerts: + metadata: + component: eventbridge + vars: + name: ecs-faults + enabled: true + cloudwatch_event_rule_description: "ECS failures and warnings" + cloudwatch_event_rule_pattern: + source: + - aws.ecs + detail: + $or: + - eventType: + - WARN + - ERROR + - agentConnected: + - false + - containers: + exitCode: + - anything-but: + - 0 +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [cloudwatch\_event](#module\_cloudwatch\_event) | cloudposse/cloudwatch-events/aws | 0.7.0 | +| [cloudwatch\_logs](#module\_cloudwatch\_logs) | cloudposse/cloudwatch-logs/aws | 0.6.8 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_resource_policy.eventbridge_cloudwatch_logs_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_resource_policy) | resource | +| [aws_iam_policy_document.eventbridge_cloudwatch_logs_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [cloudwatch\_event\_rule\_description](#input\_cloudwatch\_event\_rule\_description) | Description of the CloudWatch Event Rule. If empty, will default to `module.this.id` | `string` | `""` | no | +| [cloudwatch\_event\_rule\_pattern](#input\_cloudwatch\_event\_rule\_pattern) | Pattern of the CloudWatch Event Rule | `any` |
{
"source": [
"aws.ec2"
]
}
| no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [event\_log\_retention\_in\_days](#input\_event\_log\_retention\_in\_days) | Number of days to retain the event logs | `number` | `3` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [cloudwatch\_event\_rule\_arn](#output\_cloudwatch\_event\_rule\_arn) | The ARN of the CloudWatch Event Rule | +| [cloudwatch\_event\_rule\_name](#output\_cloudwatch\_event\_rule\_name) | The name of the CloudWatch Event Rule | +| [cloudwatch\_logs\_log\_group\_arn](#output\_cloudwatch\_logs\_log\_group\_arn) | The ARN of the CloudWatch Log Group | +| [cloudwatch\_logs\_log\_group\_name](#output\_cloudwatch\_logs\_log\_group\_name) | The name of the CloudWatch Log Group | + + +## References +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eventbridge) - Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/eventbridge/context.tf b/modules/eventbridge/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eventbridge/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eventbridge/main.tf b/modules/eventbridge/main.tf new file mode 100644 index 000000000..7f34f42d5 --- /dev/null +++ b/modules/eventbridge/main.tf @@ -0,0 +1,26 @@ +locals { + enabled = module.this.enabled + description = var.cloudwatch_event_rule_description != "" ? var.cloudwatch_event_rule_description : module.this.id +} + +module "cloudwatch_logs" { + source = "cloudposse/cloudwatch-logs/aws" + version = "0.6.8" + count = local.enabled ? 1 : 0 + + retention_in_days = var.event_log_retention_in_days + + context = module.this.context +} + +module "cloudwatch_event" { + source = "cloudposse/cloudwatch-events/aws" + version = "0.7.0" + count = local.enabled ? 1 : 0 + + cloudwatch_event_rule_description = local.description + cloudwatch_event_rule_pattern = var.cloudwatch_event_rule_pattern + cloudwatch_event_target_arn = one(module.cloudwatch_logs[*].log_group_arn) + + context = module.this.context +} diff --git a/modules/eventbridge/outputs.tf b/modules/eventbridge/outputs.tf new file mode 100644 index 000000000..335d63148 --- /dev/null +++ b/modules/eventbridge/outputs.tf @@ -0,0 +1,19 @@ +output "cloudwatch_logs_log_group_arn" { + description = "The ARN of the CloudWatch Log Group" + value = one(module.cloudwatch_logs[*].log_group_arn) +} + +output "cloudwatch_logs_log_group_name" { + description = "The name of the CloudWatch Log Group" + value = one(module.cloudwatch_logs[*].log_group_name) +} + +output "cloudwatch_event_rule_arn" { + description = "The ARN of the CloudWatch Event Rule" + value = one(module.cloudwatch_event[*].cloudwatch_event_rule_arn) +} + +output "cloudwatch_event_rule_name" { + description = "The name of the CloudWatch Event Rule" + value = one(module.cloudwatch_event[*].cloudwatch_event_rule_id) +} diff --git a/modules/eventbridge/policies.tf b/modules/eventbridge/policies.tf new file mode 100644 index 000000000..e43dbd106 --- /dev/null +++ b/modules/eventbridge/policies.tf @@ -0,0 +1,33 @@ + +# Note, we need to allow the eventbridge to write to cloudwatch logs +# we use aws_cloudwatch_log_resource_policy to do this + +locals { + log_group_arn = one(module.cloudwatch_logs[*].log_group_arn) +} +data "aws_iam_policy_document" "eventbridge_cloudwatch_logs_policy" { + statement { + principals { + type = "Service" + identifiers = [ + "events.amazonaws.com", + "delivery.logs.amazonaws.com", + ] + } + + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + + resources = [ + "${local.log_group_arn}:*", + ] + } +} + +resource "aws_cloudwatch_log_resource_policy" "eventbridge_cloudwatch_logs_policy" { + count = local.enabled ? 1 : 0 + policy_document = data.aws_iam_policy_document.eventbridge_cloudwatch_logs_policy.json + policy_name = module.this.id +} diff --git a/modules/eventbridge/providers.tf b/modules/eventbridge/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/eventbridge/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/eventbridge/remote-state.tf b/modules/eventbridge/remote-state.tf new file mode 100644 index 000000000..e69de29bb diff --git a/modules/eventbridge/variables.tf b/modules/eventbridge/variables.tf new file mode 100644 index 000000000..d53dd014b --- /dev/null +++ b/modules/eventbridge/variables.tf @@ -0,0 +1,26 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "cloudwatch_event_rule_description" { + type = string + description = "Description of the CloudWatch Event Rule. If empty, will default to `module.this.id`" + default = "" +} + +variable "cloudwatch_event_rule_pattern" { + type = any + description = "Pattern of the CloudWatch Event Rule" + default = { + "source" = [ + "aws.ec2" + ] + } +} + +variable "event_log_retention_in_days" { + type = number + description = "Number of days to retain the event logs" + default = 3 +} diff --git a/modules/eventbridge/versions.tf b/modules/eventbridge/versions.tf new file mode 100644 index 000000000..4c8603db1 --- /dev/null +++ b/modules/eventbridge/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} From e50b920e21ec3c1622a8e47693ea019cb20de54b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 21 Feb 2024 15:06:20 -0800 Subject: [PATCH 368/501] fix: ECS Cluster (#987) --- modules/ecs/README.md | 3 +-- modules/ecs/main.tf | 24 +++++++++++++----------- modules/ecs/variables.tf | 18 ------------------ 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/modules/ecs/README.md b/modules/ecs/README.md index 89726f3d0..b7ce3168c 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -68,7 +68,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [alb](#module\_alb) | cloudposse/alb/aws | 1.5.0 | +| [alb](#module\_alb) | cloudposse/alb/aws | 1.11.1 | | [cluster](#module\_cluster) | cloudposse/ecs-cluster/aws | 0.4.1 | | [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | @@ -107,7 +107,6 @@ components: | [capacity\_providers\_fargate\_spot](#input\_capacity\_providers\_fargate\_spot) | Use FARGATE\_SPOT capacity provider | `bool` | `false` | no | | [container\_insights\_enabled](#input\_container\_insights\_enabled) | Whether or not to enable container insights | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [default\_capacity\_strategy](#input\_default\_capacity\_strategy) | The capacity provider strategy to use by default for the cluster |
object({
base = object({
provider = string
value = number
})
weights = map(number)
})
|
{
"base": {
"provider": "FARGATE",
"value": 1
},
"weights": {}
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_delegated\_component\_name](#input\_dns\_delegated\_component\_name) | Use this component name to read from the remote state to get the dns\_delegated zone ID | `string` | `"dns-delegated"` | no | diff --git a/modules/ecs/main.tf b/modules/ecs/main.tf index 1912df97e..909f7a831 100644 --- a/modules/ecs/main.tf +++ b/modules/ecs/main.tf @@ -3,7 +3,10 @@ locals { dns_enabled = local.enabled && var.route53_enabled - acm_certificate_domain = try(length(var.acm_certificate_domain_suffix) > 0, false) ? format("%s.%s.%s", var.acm_certificate_domain_suffix, var.environment, module.dns_delegated.outputs.default_domain_name) : coalesce(var.acm_certificate_domain, module.dns_delegated.outputs.default_domain_name) + # If var.acm_certificate_domain is defined, use it. + # Else if var.acm_certificate_domain_suffix is defined, use {{ var.acm_certificate_domain_suffix }}.{{ environment }}.{{ domain }} + # Else, use {{ environment }}.{{ domain }} + acm_certificate_domain = try(length(var.acm_certificate_domain) > 0, false) ? var.acm_certificate_domain : try(length(var.acm_certificate_domain_suffix) > 0, false) ? format("%s.%s.%s", var.acm_certificate_domain_suffix, var.environment, module.dns_delegated.outputs.default_domain_name) : format("%s.%s", var.environment, module.dns_delegated.outputs.default_domain_name) maintenance_page_fixed_response = { content_type = "text/html" @@ -41,7 +44,7 @@ resource "aws_security_group_rule" "ingress_cidr" { to_port = 65535 protocol = "tcp" cidr_blocks = [each.value] - security_group_id = join("", aws_security_group.default.*.id) + security_group_id = join("", aws_security_group.default[*].id) } resource "aws_security_group_rule" "ingress_security_groups" { @@ -51,7 +54,7 @@ resource "aws_security_group_rule" "ingress_security_groups" { to_port = 65535 protocol = "tcp" source_security_group_id = each.value - security_group_id = join("", aws_security_group.default.*.id) + security_group_id = join("", aws_security_group.default[*].id) } resource "aws_security_group_rule" "egress" { @@ -61,7 +64,7 @@ resource "aws_security_group_rule" "egress" { to_port = 65535 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] - security_group_id = join("", aws_security_group.default.*.id) + security_group_id = join("", aws_security_group.default[*].id) } module "cluster" { @@ -78,7 +81,7 @@ module "cluster" { name => merge( provider, { - security_group_ids = concat(aws_security_group.default.*.id, provider.security_group_ids) + security_group_ids = concat(aws_security_group.default[*].id, provider.security_group_ids) subnet_ids = var.internal_enabled ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids associate_public_ip_address = !var.internal_enabled } @@ -121,7 +124,7 @@ module "cluster" { # # image_id = data.aws_ssm_parameter.ami.value # instance_type = "t3.medium" -# security_group_ids = aws_security_group.default.*.id +# security_group_ids = aws_security_group.default[*].id # subnet_ids = var.internal_enabled ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids # health_check_type = "EC2" # desired_capacity = 1 @@ -162,7 +165,7 @@ data "aws_acm_certificate" "default" { module "alb" { source = "cloudposse/alb/aws" - version = "1.5.0" + version = "1.11.1" for_each = local.enabled ? var.alb_configuration : {} @@ -185,9 +188,8 @@ module "alb" { https_ingress_cidr_blocks = lookup(each.value, "https_ingress_cidr_blocks", var.alb_ingress_cidr_blocks_https) certificate_arn = lookup(each.value, "certificate_arn", one(data.aws_acm_certificate.default[*].arn)) - access_logs_enabled = lookup(each.value, "access_logs_enabled", true) - alb_access_logs_s3_bucket_force_destroy = lookup(each.value, "alb_access_logs_s3_bucket_force_destroy", true) - alb_access_logs_s3_bucket_force_destroy_enabled = lookup(each.value, "alb_access_logs_s3_bucket_force_destroy_enabled", true) + access_logs_enabled = lookup(each.value, "access_logs_enabled", true) + alb_access_logs_s3_bucket_force_destroy = lookup(each.value, "alb_access_logs_s3_bucket_force_destroy", true) lifecycle_rule_enabled = lookup(each.value, "lifecycle_rule_enabled", true) @@ -232,7 +234,7 @@ locals { certificate_domains = merge([ for config_key, config in var.alb_configuration : { for domain in config.additional_certs : - "${config_key}_${domain}" => domain } if lookup(config, "additional_certs", []) != [] + "${config_key}_${domain}" => domain } if length(lookup(config, "additional_certs", [])) > 0 ]...) } diff --git a/modules/ecs/variables.tf b/modules/ecs/variables.tf index 0c8a80d7c..a1727ab60 100644 --- a/modules/ecs/variables.tf +++ b/modules/ecs/variables.tf @@ -232,21 +232,3 @@ variable "capacity_providers_ec2" { error_message = "'FARGATE' and 'FARGATE_SPOT' name is reserved" } } - -variable "default_capacity_strategy" { - description = "The capacity provider strategy to use by default for the cluster" - type = object({ - base = object({ - provider = string - value = number - }) - weights = map(number) - }) - default = { - base = { - provider = "FARGATE" - value = 1 - } - weights = {} - } -} From 3cc699f4feffb859307708087501a96c5fe23f25 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 21 Feb 2024 16:46:39 -0800 Subject: [PATCH 369/501] chore: bumped `alb` module version (#988) --- modules/alb/README.md | 2 +- modules/alb/main.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/alb/README.md b/modules/alb/README.md index 351997e17..0905867c7 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -35,7 +35,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [acm](#module\_acm) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [alb](#module\_alb) | cloudposse/alb/aws | 1.10.0 | +| [alb](#module\_alb) | cloudposse/alb/aws | 1.11.1 | | [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | diff --git a/modules/alb/main.tf b/modules/alb/main.tf index 91f9c530b..2ccb5e15f 100644 --- a/modules/alb/main.tf +++ b/modules/alb/main.tf @@ -10,7 +10,7 @@ locals { module "alb" { source = "cloudposse/alb/aws" - version = "1.10.0" + version = "1.11.1" vpc_id = module.vpc.outputs.vpc_id subnet_ids = module.vpc.outputs.public_subnet_ids From 1676a93b42e573f5b14b6d1384e8df8715f25a21 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 23 Feb 2024 09:46:18 -0800 Subject: [PATCH 370/501] update `ecs` and add `cloudmap-namespace` component (#984) Co-authored-by: Dan Miller --- modules/cloudmap-namespace/context.tf | 279 +++++++++++++++++++++ modules/cloudmap-namespace/main.tf | 22 ++ modules/cloudmap-namespace/outputs.tf | 14 ++ modules/cloudmap-namespace/providers.tf | 19 ++ modules/cloudmap-namespace/remote-state.tf | 8 + modules/cloudmap-namespace/variables.tf | 19 ++ modules/cloudmap-namespace/versions.tf | 10 + modules/ecs-service/README.md | 12 +- modules/ecs-service/cloud-map.tf | 66 +++++ modules/ecs-service/main.tf | 17 +- modules/ecs-service/variables.tf | 70 +++++- 11 files changed, 531 insertions(+), 5 deletions(-) create mode 100644 modules/cloudmap-namespace/context.tf create mode 100644 modules/cloudmap-namespace/main.tf create mode 100644 modules/cloudmap-namespace/outputs.tf create mode 100644 modules/cloudmap-namespace/providers.tf create mode 100644 modules/cloudmap-namespace/remote-state.tf create mode 100644 modules/cloudmap-namespace/variables.tf create mode 100644 modules/cloudmap-namespace/versions.tf create mode 100644 modules/ecs-service/cloud-map.tf diff --git a/modules/cloudmap-namespace/context.tf b/modules/cloudmap-namespace/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/cloudmap-namespace/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/cloudmap-namespace/main.tf b/modules/cloudmap-namespace/main.tf new file mode 100644 index 000000000..4cd7cbc21 --- /dev/null +++ b/modules/cloudmap-namespace/main.tf @@ -0,0 +1,22 @@ +locals { + enabled = module.this.enabled +} + +resource "aws_service_discovery_private_dns_namespace" "default" { + count = local.enabled && var.type == "private" ? 1 : 0 + name = module.this.id + description = var.description + vpc = module.vpc.outputs.vpc_id +} + +resource "aws_service_discovery_public_dns_namespace" "default" { + count = local.enabled && var.type == "public" ? 1 : 0 + name = module.this.id + description = var.description +} + +resource "aws_service_discovery_http_namespace" "default" { + count = local.enabled && var.type == "http" ? 1 : 0 + name = module.this.id + description = var.description +} diff --git a/modules/cloudmap-namespace/outputs.tf b/modules/cloudmap-namespace/outputs.tf new file mode 100644 index 000000000..40210a4e4 --- /dev/null +++ b/modules/cloudmap-namespace/outputs.tf @@ -0,0 +1,14 @@ +output "name" { + value = module.this.id + description = "The name of the namespace" +} + +output "id" { + value = coalesce(one(aws_service_discovery_http_namespace.default[*].id), one(aws_service_discovery_private_dns_namespace.default[*].id), one(aws_service_discovery_public_dns_namespace.default[*].id)) + description = "The ID of the namespace" +} + +output "arn" { + value = coalesce(one(aws_service_discovery_http_namespace.default[*].arn), one(aws_service_discovery_private_dns_namespace.default[*].arn), one(aws_service_discovery_public_dns_namespace.default[*].arn)) + description = "The ARN of the namespace" +} diff --git a/modules/cloudmap-namespace/providers.tf b/modules/cloudmap-namespace/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/cloudmap-namespace/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/cloudmap-namespace/remote-state.tf b/modules/cloudmap-namespace/remote-state.tf new file mode 100644 index 000000000..757ef9067 --- /dev/null +++ b/modules/cloudmap-namespace/remote-state.tf @@ -0,0 +1,8 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "vpc" + + context = module.this.context +} diff --git a/modules/cloudmap-namespace/variables.tf b/modules/cloudmap-namespace/variables.tf new file mode 100644 index 000000000..c127b391b --- /dev/null +++ b/modules/cloudmap-namespace/variables.tf @@ -0,0 +1,19 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "description" { + type = string + description = "Description of the Cloud Map Namespace" +} + +variable "type" { + type = string + description = "Type of the Cloud Map Namespace" + default = "http" + validation { + condition = contains(["http", "private", "public"], var.type) + error_message = "Invalid namespace type, must be one of `http` or `private` or `public`" + } +} diff --git a/modules/cloudmap-namespace/versions.tf b/modules/cloudmap-namespace/versions.tf new file mode 100644 index 000000000..f33ede77f --- /dev/null +++ b/modules/cloudmap-namespace/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 0e17417a7..d96f3ca89 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -268,7 +268,9 @@ components: |------|--------|---------| | [alb](#module\_alb) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [alb\_ingress](#module\_alb\_ingress) | cloudposse/alb-ingress/aws | 0.28.0 | -| [container\_definition](#module\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.60.0 | +| [cloudmap\_namespace](#module\_cloudmap\_namespace) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [cloudmap\_namespace\_service\_discovery](#module\_cloudmap\_namespace\_service\_discovery) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [container\_definition](#module\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.61.1 | | [datadog\_configuration](#module\_datadog\_configuration) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [datadog\_container\_definition](#module\_datadog\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | | [datadog\_fluent\_bit\_container\_definition](#module\_datadog\_fluent\_bit\_container\_definition) | cloudposse/ecs-container-definition/aws | 0.58.1 | @@ -300,6 +302,8 @@ components: | [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_kinesis_stream.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_stream) | resource | +| [aws_security_group_rule.custom_sg_rules](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_service_discovery_service.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_service) | resource | | [aws_ssm_parameter.full_urls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.github_actions_iam_ecspresso_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -328,7 +332,7 @@ components: | [autoscaling\_enabled](#input\_autoscaling\_enabled) | Should this service autoscale using SNS alarams | `bool` | `true` | no | | [chamber\_service](#input\_chamber\_service) | SSM parameter service name for use with chamber. This is used in chamber\_format where /$chamber\_service/$name/$container\_name/$parameter would be the default. | `string` | `"ecs-service"` | no | | [cluster\_attributes](#input\_cluster\_attributes) | The attributes of the cluster name e.g. if the full name is `namespace-tenant-environment-dev-ecs-b2b` then the `cluster_name` is `ecs` and this value should be `b2b`. | `list(string)` | `[]` | no | -| [containers](#input\_containers) | Feed inputs into container definition module |
map(object({
name = string
ecr_image = optional(string)
image = optional(string)
memory = optional(number)
memory_reservation = optional(number)
cpu = optional(number)
essential = optional(bool, true)
readonly_root_filesystem = optional(bool, null)
privileged = optional(bool, null)
container_depends_on = optional(list(object({
containerName = string
condition = string # START, COMPLETE, SUCCESS, HEALTHY
})), null)

port_mappings = optional(list(object({
containerPort = number
hostPort = number
protocol = string
})), [])
command = optional(list(string), null)
entrypoint = optional(list(string), null)
healthcheck = optional(object({
command = list(string)
interval = number
retries = number
startPeriod = number
timeout = number
}), null)
ulimits = optional(list(object({
name = string
softLimit = number
hardLimit = number
})), null)
log_configuration = optional(object({
logDriver = string
options = optional(map(string), {})
}))
docker_labels = optional(map(string), null)
map_environment = optional(map(string), {})
map_secrets = optional(map(string), {})
volumes_from = optional(list(object({
sourceContainer = string
readOnly = bool
})), null)
mount_points = optional(list(object({
sourceVolume = optional(string)
containerPath = optional(string)
readOnly = optional(bool)
})), [])
}))
| `{}` | no | +| [containers](#input\_containers) | Feed inputs into container definition module |
map(object({
name = string
ecr_image = optional(string)
image = optional(string)
memory = optional(number)
memory_reservation = optional(number)
cpu = optional(number)
essential = optional(bool, true)
readonly_root_filesystem = optional(bool, null)
privileged = optional(bool, null)
container_depends_on = optional(list(object({
containerName = string
condition = string # START, COMPLETE, SUCCESS, HEALTHY
})), null)

port_mappings = optional(list(object({
containerPort = number
hostPort = optional(number)
protocol = optional(string)
name = optional(string)
appProtocol = optional(string)
})), [])
command = optional(list(string), null)
entrypoint = optional(list(string), null)
healthcheck = optional(object({
command = list(string)
interval = number
retries = number
startPeriod = number
timeout = number
}), null)
ulimits = optional(list(object({
name = string
softLimit = number
hardLimit = number
})), null)
log_configuration = optional(object({
logDriver = string
options = optional(map(string), {})
}))
docker_labels = optional(map(string), null)
map_environment = optional(map(string), {})
map_secrets = optional(map(string), {})
volumes_from = optional(list(object({
sourceContainer = string
readOnly = bool
})), null)
mount_points = optional(list(object({
sourceVolume = optional(string)
containerPath = optional(string)
readOnly = optional(bool)
})), [])
}))
| `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [cpu\_utilization\_high\_alarm\_actions](#input\_cpu\_utilization\_high\_alarm\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization High Alarm action | `list(string)` | `[]` | no | | [cpu\_utilization\_high\_evaluation\_periods](#input\_cpu\_utilization\_high\_evaluation\_periods) | Number of periods to evaluate for the alarm | `number` | `1` | no | @@ -340,6 +344,7 @@ components: | [cpu\_utilization\_low\_ok\_actions](#input\_cpu\_utilization\_low\_ok\_actions) | A list of ARNs (i.e. SNS Topic ARN) to notify on CPU Utilization Low OK action | `list(string)` | `[]` | no | | [cpu\_utilization\_low\_period](#input\_cpu\_utilization\_low\_period) | Duration in seconds to evaluate for the alarm | `number` | `300` | no | | [cpu\_utilization\_low\_threshold](#input\_cpu\_utilization\_low\_threshold) | The minimum percentage of CPU utilization average | `number` | `20` | no | +| [custom\_security\_group\_rules](#input\_custom\_security\_group\_rules) | The list of custom security group rules to add to the service security group |
list(object({
type = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = optional(string)
}))
| `[]` | no | | [datadog\_agent\_sidecar\_enabled](#input\_datadog\_agent\_sidecar\_enabled) | Enable the Datadog Agent Sidecar | `bool` | `false` | no | | [datadog\_log\_method\_is\_firelens](#input\_datadog\_log\_method\_is\_firelens) | Datadog logs can be sent via cloudwatch logs (and lambda) or firelens, set this to true to enable firelens via a sidecar container for fluentbit | `bool` | `false` | no | | [datadog\_logging\_default\_tags\_enabled](#input\_datadog\_logging\_default\_tags\_enabled) | Add Default tags to all logs sent to Datadog | `bool` | `true` | no | @@ -352,6 +357,7 @@ components: | [ecs\_cluster\_name](#input\_ecs\_cluster\_name) | The name of the ECS Cluster this belongs to | `any` | `"ecs"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [exec\_enabled](#input\_exec\_enabled) | Specifies whether to enable Amazon ECS Exec for the tasks within the service | `bool` | `false` | no | | [github\_actions\_allowed\_repos](#input\_github\_actions\_allowed\_repos) | A list of the GitHub repositories that are allowed to assume this role from GitHub Actions. For example,
["cloudposse/infra-live"]. Can contain "*" as wildcard.
If org part of repo name is omitted, "cloudposse" will be assumed. | `list(string)` | `[]` | no | | [github\_actions\_ecspresso\_enabled](#input\_github\_actions\_ecspresso\_enabled) | Create IAM policies required for deployments with Ecspresso | `bool` | `false` | no | | [github\_actions\_iam\_role\_attributes](#input\_github\_actions\_iam\_role\_attributes) | Additional attributes to add to the role name | `list(string)` | `[]` | no | @@ -395,6 +401,8 @@ components: | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [retention\_period](#input\_retention\_period) | Length of time data records are accessible after they are added to the stream | `number` | `48` | no | | [s3\_mirror\_name](#input\_s3\_mirror\_name) | The name of the S3 mirror component | `string` | `null` | no | +| [service\_connect\_configurations](#input\_service\_connect\_configurations) | The list of Service Connect configurations.
See `service_connect_configuration` docs https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#service_connect_configuration |
list(object({
enabled = bool
namespace = optional(string, null)
log_configuration = optional(object({
log_driver = string
options = optional(map(string), null)
secret_option = optional(list(object({
name = string
value_from = string
})), [])
}), null)
service = optional(list(object({
client_alias = list(object({
dns_name = string
port = number
}))
discovery_name = optional(string, null)
ingress_port_override = optional(number, null)
port_name = string
})), [])
}))
| `[]` | no | +| [service\_registries](#input\_service\_registries) | The list of Service Registries.
See `service_registries` docs https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#service_registries |
list(object({
namespace = string
registry_arn = optional(string)
port = optional(number)
container_name = optional(string)
container_port = optional(number)
}))
| `[]` | no | | [shard\_count](#input\_shard\_count) | Number of shards that the stream will use | `number` | `1` | no | | [shard\_level\_metrics](#input\_shard\_level\_metrics) | List of shard-level CloudWatch metrics which can be enabled for the stream | `list(string)` |
[
"IncomingBytes",
"IncomingRecords",
"IteratorAgeMilliseconds",
"OutgoingBytes",
"OutgoingRecords",
"ReadProvisionedThroughputExceeded",
"WriteProvisionedThroughputExceeded"
]
| no | | [ssm\_enabled](#input\_ssm\_enabled) | If `true` create SSM keys for the database user and password. | `bool` | `false` | no | diff --git a/modules/ecs-service/cloud-map.tf b/modules/ecs-service/cloud-map.tf new file mode 100644 index 000000000..f5d169d47 --- /dev/null +++ b/modules/ecs-service/cloud-map.tf @@ -0,0 +1,66 @@ +// Service Connect + +module "cloudmap_namespace" { + for_each = { for service_connect in var.service_connect_configurations : service_connect.namespace => service_connect } + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = each.key + + # we ignore errors because the namespace may be a name or an arn of a namespace for the service. + ignore_errors = true + context = module.this.context +} + +locals { + valid_cloudmap_namespaces = { for k, v in module.cloudmap_namespace : k => v if v.outputs != null } + service_connect_configurations = [for service_connect in var.service_connect_configurations : merge(service_connect, { namespace = try(local.valid_cloudmap_namespaces[service_connect.namespace].outputs.name, service_connect.namespace) })] +} +// ------------------------------ + +// Service Discovery + +module "cloudmap_namespace_service_discovery" { + for_each = { for service_connect in var.service_registries : service_connect.namespace => service_connect } + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = each.key + + # we ignore errors because the namespace may be a name or an arn of a namespace for the service. + ignore_errors = true + context = module.this.context +} + +locals { + valid_cloudmap_service_discovery_namespaces = { for k, v in module.cloudmap_namespace_service_discovery : k => v if v.outputs != null } + service_discovery_configurations = [for service_registry in var.service_registries : merge(service_registry, { namespace = try(local.valid_cloudmap_service_discovery_namespaces[service_registry.namespace].outputs.name, service_registry.namespace) })] + service_config_with_id = { for service_registry in var.service_registries : service_registry.namespace => merge(service_registry, { id = try(local.valid_cloudmap_service_discovery_namespaces[service_registry.namespace].outputs.id, null) }) } + service_discovery = [for value in var.service_registries : merge(value, { + registry_arn = aws_service_discovery_service.default[value.namespace].arn + })] +} + +resource "aws_service_discovery_service" "default" { + for_each = local.service_config_with_id + name = module.this.name + + dns_config { + namespace_id = each.value.id + + dns_records { + ttl = 10 + type = "A" + } + + routing_policy = "MULTIVALUE" + } + + health_check_custom_config { + failure_threshold = 1 + } +} + +// ------------------------------ diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 97e560ab1..4830ebc7b 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -163,7 +163,7 @@ locals { module "container_definition" { source = "cloudposse/ecs-container-definition/aws" - version = "0.60.0" + version = "0.61.1" for_each = { for k, v in local.containers : k => v if local.enabled } @@ -297,6 +297,10 @@ module "ecs_alb_service_task" { fsx_volumes = lookup(local.task, "fsx_volumes", []) bind_mount_volumes = lookup(local.task, "bind_mount_volumes", []) + exec_enabled = var.exec_enabled + service_connect_configurations = local.service_connect_configurations + service_registries = local.service_discovery + depends_on = [ module.alb_ingress ] @@ -304,6 +308,17 @@ module "ecs_alb_service_task" { context = module.this.context } +resource "aws_security_group_rule" "custom_sg_rules" { + for_each = local.enabled && var.custom_security_group_rules != [] ? { for sg_rule in var.custom_security_group_rules : format("%s_%s_%s", sg_rule.protocol, sg_rule.from_port, sg_rule.to_port) => sg_rule } : {} + description = each.value.description + type = each.value.type + from_port = each.value.from_port + to_port = each.value.to_port + protocol = each.value.protocol + cidr_blocks = each.value.cidr_blocks + security_group_id = one(module.ecs_alb_service_task[*].service_security_group_id) +} + module "alb_ingress" { source = "cloudposse/alb-ingress/aws" version = "0.28.0" diff --git a/modules/ecs-service/variables.tf b/modules/ecs-service/variables.tf index f61b4b017..55e06af68 100644 --- a/modules/ecs-service/variables.tf +++ b/modules/ecs-service/variables.tf @@ -63,8 +63,10 @@ variable "containers" { port_mappings = optional(list(object({ containerPort = number - hostPort = number - protocol = string + hostPort = optional(number) + protocol = optional(string) + name = optional(string) + appProtocol = optional(string) })), []) command = optional(list(string), null) entrypoint = optional(list(string), null) @@ -579,3 +581,67 @@ variable "task_exec_policy_arns_map" { EOT default = {} } + + +variable "exec_enabled" { + type = bool + description = "Specifies whether to enable Amazon ECS Exec for the tasks within the service" + default = false +} + +variable "service_connect_configurations" { + type = list(object({ + enabled = bool + namespace = optional(string, null) + log_configuration = optional(object({ + log_driver = string + options = optional(map(string), null) + secret_option = optional(list(object({ + name = string + value_from = string + })), []) + }), null) + service = optional(list(object({ + client_alias = list(object({ + dns_name = string + port = number + })) + discovery_name = optional(string, null) + ingress_port_override = optional(number, null) + port_name = string + })), []) + })) + description = <<-EOT + The list of Service Connect configurations. + See `service_connect_configuration` docs https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#service_connect_configuration + EOT + default = [] +} + +variable "service_registries" { + type = list(object({ + namespace = string + registry_arn = optional(string) + port = optional(number) + container_name = optional(string) + container_port = optional(number) + })) + description = <<-EOT + The list of Service Registries. + See `service_registries` docs https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service#service_registries + EOT + default = [] +} + +variable "custom_security_group_rules" { + type = list(object({ + type = string + from_port = number + to_port = number + protocol = string + cidr_blocks = list(string) + description = optional(string) + })) + description = "The list of custom security group rules to add to the service security group" + default = [] +} From a9065c1422d40dcf09e2251a45bb0fe02c989f3e Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 23 Feb 2024 09:46:51 -0800 Subject: [PATCH 371/501] `philips-labs-github-runners` install terraform docs (#989) --- .../templates/userdata_post_install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/philips-labs-github-runners/templates/userdata_post_install.sh b/modules/philips-labs-github-runners/templates/userdata_post_install.sh index 3f810387b..6150351c1 100644 --- a/modules/philips-labs-github-runners/templates/userdata_post_install.sh +++ b/modules/philips-labs-github-runners/templates/userdata_post_install.sh @@ -14,3 +14,6 @@ sudo yum install -y gh # Install nodejs sudo yum install -y nodejs-1:18.18.2-1.amzn2023.0.1 + +# Install terraform-docs +curl -L "$(curl -s https://api.github.com/repos/terraform-docs/terraform-docs/releases/latest | grep -o -E -m 1 "https://.+?-linux-amd64.tar.gz")" > terraform-docs.tgz && tar -xzf terraform-docs.tgz terraform-docs && rm terraform-docs.tgz && chmod +x terraform-docs && sudo mv terraform-docs /usr/bin/ From f32372b6a55797bdead5676bccf75ffc448e2687 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 23 Feb 2024 18:21:59 -0800 Subject: [PATCH 372/501] feat: Philips Labs GitHub Runners Alternate Instance Types (#990) --- modules/philips-labs-github-runners/README.md | 6 +-- modules/philips-labs-github-runners/main.tf | 11 +++--- .../philips-labs-github-runners/variables.tf | 37 ++++++++++++------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/modules/philips-labs-github-runners/README.md b/modules/philips-labs-github-runners/README.md index aa45e22b5..83ad0b33e 100644 --- a/modules/philips-labs-github-runners/README.md +++ b/modules/philips-labs-github-runners/README.md @@ -105,6 +105,7 @@ This is output by the component, and available via the `webhook` output under `e | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_service\_linked\_role\_spot](#input\_create\_service\_linked\_role\_spot) | (optional) create the service linked role for spot instances that is required by the scale-up lambda. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enable\_update\_github\_app\_webhook](#input\_enable\_update\_github\_app\_webhook) | Enable updating the github app webhook | `bool` | `false` | no | @@ -113,20 +114,19 @@ This is output by the component, and available via the `webhook` output under `e | [github\_app\_id\_ssm\_path](#input\_github\_app\_id\_ssm\_path) | Path to the github app id in SSM | `string` | `"/pl-github-runners/id"` | no | | [github\_app\_key\_ssm\_path](#input\_github\_app\_key\_ssm\_path) | Path to the github key in SSM | `string` | `"/pl-github-runners/key"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [instance\_types](#input\_instance\_types) | List of instance types for the action runner. Defaults are based on runner\_os (al2023 for linux and Windows Server Core for win). | `list(string)` |
[
"m5.large",
"c5.large"
]
| no | +| [instance\_target\_capacity\_type](#input\_instance\_target\_capacity\_type) | Default lifecycle used for runner instances, can be either `spot` or `on-demand`. | `string` | `"spot"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [lambda\_repo\_url](#input\_lambda\_repo\_url) | URL of the lambda repository | `string` | `"https://github.com/philips-labs/terraform-aws-github-runner"` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS region | `string` | n/a | yes | | [release\_version](#input\_release\_version) | Version of the application | `string` | `"v5.4.0"` | no | -| [repository\_white\_list](#input\_repository\_white\_list) | List of github repository full names (owner/repo\_name) that will be allowed to use the github app. Leave empty for no filtering. | `list(string)` | `[]` | no | | [runner\_extra\_labels](#input\_runner\_extra\_labels) | Extra (custom) labels for the runners (GitHub). Labels checks on the webhook can be enforced by setting `enable_workflow_job_labels_check`. GitHub read-only labels should not be provided. | `list(string)` |
[
"default"
]
| no | | [scale\_up\_reserved\_concurrent\_executions](#input\_scale\_up\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `-1` | no | +| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = optional(string, "github-action-runners")
app = optional(string, "app")
runners = optional(string, "runners")
use_prefix = optional(bool, true)
})
| `{}` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/philips-labs-github-runners/main.tf b/modules/philips-labs-github-runners/main.tf index b79b4420c..2818f5fd4 100644 --- a/modules/philips-labs-github-runners/main.tf +++ b/modules/philips-labs-github-runners/main.tf @@ -1,9 +1,8 @@ locals { - enabled = var.enabled - version = var.enabled ? var.release_version : null - lambda_repo = "https://github.com/philips-labs/terraform-aws-github-runner" + enabled = module.this.enabled + version = local.enabled ? var.release_version : null - lambdas = var.enabled ? { + lambdas = local.enabled ? { webhook = { name = "webhook.zip" tag = local.version @@ -77,7 +76,9 @@ module "github_runner" { enable_organization_runners = true enable_ssm_on_runners = true - create_service_linked_role_spot = true + ssm_paths = var.ssm_paths + instance_target_capacity_type = var.instance_target_capacity_type + create_service_linked_role_spot = var.create_service_linked_role_spot enable_fifo_build_queue = true scale_up_reserved_concurrent_executions = var.scale_up_reserved_concurrent_executions diff --git a/modules/philips-labs-github-runners/variables.tf b/modules/philips-labs-github-runners/variables.tf index 7824b14e1..9eef9607d 100644 --- a/modules/philips-labs-github-runners/variables.tf +++ b/modules/philips-labs-github-runners/variables.tf @@ -9,12 +9,6 @@ variable "enable_update_github_app_webhook" { default = false } -variable "lambda_repo_url" { - type = string - description = "URL of the lambda repository" - default = "https://github.com/philips-labs/terraform-aws-github-runner" -} - variable "release_version" { type = string description = "Version of the application" @@ -47,14 +41,29 @@ variable "scale_up_reserved_concurrent_executions" { default = -1 } -variable "instance_types" { - description = "List of instance types for the action runner. Defaults are based on runner_os (al2023 for linux and Windows Server Core for win)." - type = list(string) - default = ["m5.large", "c5.large"] +variable "instance_target_capacity_type" { + description = "Default lifecycle used for runner instances, can be either `spot` or `on-demand`." + type = string + default = "spot" + validation { + condition = contains(["spot", "on-demand"], var.instance_target_capacity_type) + error_message = "The instance target capacity should be either spot or on-demand." + } } -variable "repository_white_list" { - description = "List of github repository full names (owner/repo_name) that will be allowed to use the github app. Leave empty for no filtering." - type = list(string) - default = [] +variable "create_service_linked_role_spot" { + description = "(optional) create the service linked role for spot instances that is required by the scale-up lambda." + type = bool + default = true +} + +variable "ssm_paths" { + description = "The root path used in SSM to store configuration and secrets." + type = object({ + root = optional(string, "github-action-runners") + app = optional(string, "app") + runners = optional(string, "runners") + use_prefix = optional(bool, true) + }) + default = {} } From 0521f5dfba9e608f3046822a331c2c0ce8fc6906 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 27 Feb 2024 16:57:21 -0500 Subject: [PATCH 373/501] chore: ecs-service policies should allow tagging now (#994) --- modules/ecs-service/github-actions-iam-policy.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ecs-service/github-actions-iam-policy.tf b/modules/ecs-service/github-actions-iam-policy.tf index 99a3d3a98..be17d952e 100644 --- a/modules/ecs-service/github-actions-iam-policy.tf +++ b/modules/ecs-service/github-actions-iam-policy.tf @@ -111,6 +111,7 @@ data "aws_iam_policy_document" "github_actions_iam_ecspresso_policy" { effect = "Allow" actions = [ "ecs:RegisterTaskDefinition", + "ecs:TagResource", "ecs:DescribeTaskDefinition", "ecs:DescribeTasks", "application-autoscaling:DescribeScalableTargets" From 78af9d95dfcb3b5f774607d44d02f3a4822e6936 Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Tue, 27 Feb 2024 20:42:09 -0500 Subject: [PATCH 374/501] Feat/add macie (#790) Co-authored-by: cloudpossebot Co-authored-by: Dan Miller Co-authored-by: Andriy Knysh --- modules/macie/README.md | 201 ++++++++++++++++++++++++ modules/macie/context.tf | 279 ++++++++++++++++++++++++++++++++++ modules/macie/main.tf | 41 +++++ modules/macie/outputs.tf | 19 +++ modules/macie/providers.tf | 19 +++ modules/macie/remote-state.tf | 12 ++ modules/macie/variables.tf | 78 ++++++++++ modules/macie/versions.tf | 15 ++ 8 files changed, 664 insertions(+) create mode 100644 modules/macie/README.md create mode 100644 modules/macie/context.tf create mode 100644 modules/macie/main.tf create mode 100644 modules/macie/outputs.tf create mode 100644 modules/macie/providers.tf create mode 100644 modules/macie/remote-state.tf create mode 100644 modules/macie/variables.tf create mode 100644 modules/macie/versions.tf diff --git a/modules/macie/README.md b/modules/macie/README.md new file mode 100644 index 000000000..3bf9b211a --- /dev/null +++ b/modules/macie/README.md @@ -0,0 +1,201 @@ +# Component: `macie` + +This component is responsible for configuring Macie within an AWS Organization. + +Amazon Macie is a data security service that discovers sensitive data by using machine learning and pattern matching, +provides visibility into data security risks, and enables automated protection against those risks. + +To help you manage the security posture of your organization's Amazon Simple Storage Service (Amazon S3) data estate, +Macie provides you with an inventory of your S3 buckets, and automatically evaluates and monitors the buckets for +security and access control. If Macie detects a potential issue with the security or privacy of your data, such as a +bucket that becomes publicly accessible, Macie generates a finding for you to review and remediate as necessary. + +Macie also automates discovery and reporting of sensitive data to provide you with a better understanding of the data +that your organization stores in Amazon S3. To detect sensitive data, you can use built-in criteria and techniques that +Macie provides, custom criteria that you define, or a combination of the two. If Macie detects sensitive data in an S3 +object, Macie generates a finding to notify you of the sensitive data that Macie found. + +In addition to findings, Macie provides statistics and other data that offer insight into the security posture of your +Amazon S3 data, and where sensitive data might reside in your data estate. The statistics and data can guide your +decisions to perform deeper investigations of specific S3 buckets and objects. You can review and analyze findings, +statistics, and other data by using the Amazon Macie console or the Amazon Macie API. You can also leverage Macie +integration with Amazon EventBridge and AWS Security Hub to monitor, process, and remediate findings by using other +services, applications, and systems. + +## Usage + +**Stack Level**: Regional + +## Deployment Overview + +This component is complex in that it must be deployed multiple times with different variables set to configure the AWS +Organization successfully. + +In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization +Delegated Administrator account is `security`, both in the `core` tenant. + +### Deploy to Delegated Administrator Account + +First, the component is deployed to the [Delegated +Administrator](https://docs.aws.amazon.com/macie/latest/user/accounts-mgmt-ao-integrate.html) account to configure the +central Macie account∑. + +```yaml +# core-ue1-security +components: + terraform: + macie/delegated-administrator: + metadata: + component: macie + vars: + enabled: true + delegated_administrator_account_name: core-security + environment: ue1 + region: us-east-1 +``` + +```bash +atmos terraform apply macie/delegated-administrator -s core-ue1-security +``` + +### Deploy to Organization Management (root) Account + +Next, the component is deployed to the AWS Organization Management, a/k/a `root`, Account in order to set the AWS +Organization Designated Admininstrator account. + +Note that you must `SuperAdmin` permissions as we are deploying to the AWS Organization Management account. Since +we are using the `SuperAdmin` user, it will already have access to the state bucket, so we set the `role_arn` of the +backend config to null and set `var.privileged` to `true`. + +```yaml +# core-ue1-root +components: + terraform: + guardduty/root: + metadata: + component: macie + backend: + s3: + role_arn: null + vars: + enabled: true + delegated_administrator_account_name: core-security + environment: ue1 + region: us-east-1 + privileged: true +``` + +```bash +atmos terraform apply macie/root -s core-ue1-root +``` + +### Deploy Organization Settings in Delegated Administrator Account + +Finally, the component is deployed to the Delegated Administrator Account again in order to create the +organization-wide configuration for the AWS Organization, but with `var.admin_delegated` set to `true` to indicate that +the delegation has already been performed from the Organization Management account. + +```yaml +# core-ue1-security +components: + terraform: + macie/org-settings: + metadata: + component: macie + vars: + enabled: true + delegated_administrator_account_name: core-security + environment: use1 + region: us-east-1 + admin_delegated: true +``` + +```bash +atmos terraform apply macie/org-settings/ue1 -s core-ue1-security +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 5.0 | +| [awsutils](#requirement\_awsutils) | >= 0.17.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.0 | +| [awsutils](#provider\_awsutils) | >= 0.17.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_macie2_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/macie2_account) | resource | +| [aws_macie2_organization_admin_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/macie2_organization_admin_account) | resource | +| [awsutils_macie2_organization_settings.this](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/resources/macie2_organization_settings) | resource | +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `"core"` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Admininstrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delegated\_admininstrator\_component\_name](#input\_delegated\_admininstrator\_component\_name) | The name of the component that created the Macie account. | `string` | `"macie/delegated-administrator"` | no | +| [delegated\_administrator\_account\_name](#input\_delegated\_administrator\_account\_name) | The name of the account that is the AWS Organization Delegated Administrator account | `string` | `"core-security"` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [finding\_publishing\_frequency](#input\_finding\_publishing\_frequency) | Specifies how often to publish updates to policy findings for the account. This includes publishing updates to AWS
Security Hub and Amazon EventBridge (formerly called Amazon CloudWatch Events). For more information, see:

https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings_cloudwatch.html#guardduty_findings_cloudwatch_notification_frequency | `string` | `"FIFTEEN_MINUTES"` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [member\_accounts](#input\_member\_accounts) | List of member account names to enable Macie on | `list(string)` | `[]` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [organization\_management\_account\_name](#input\_organization\_management\_account\_name) | The name of the AWS Organization management account | `string` | `null` | no | +| [privileged](#input\_privileged) | true if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account. This is used to lookup account IDs from account names
using the `account-map` component. | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [delegated\_administrator\_account\_id](#output\_delegated\_administrator\_account\_id) | The AWS Account ID of the AWS Organization delegated administrator account | +| [macie\_account\_id](#output\_macie\_account\_id) | The ID of the Macie account created by the component | +| [macie\_service\_role\_arn](#output\_macie\_service\_role\_arn) | The Amazon Resource Name (ARN) of the service-linked role that allows Macie to monitor and analyze data in AWS resources for the account. | +| [member\_account\_ids](#output\_member\_account\_ids) | The AWS Account IDs of the member accounts | + + +## References + +- [AWS GuardDuty Documentation](https://aws.amazon.com/guardduty/) +- [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/guardduty/common/) + +[](https://cpco.io/component) diff --git a/modules/macie/context.tf b/modules/macie/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/macie/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/macie/main.tf b/modules/macie/main.tf new file mode 100644 index 000000000..594f08e3c --- /dev/null +++ b/modules/macie/main.tf @@ -0,0 +1,41 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map + + current_account_id = one(data.aws_caller_identity.this[*].account_id) + member_account_id_list = [for a in keys(local.account_map) : (local.account_map[a]) if contains(var.member_accounts, a) && local.account_map[a] != local.org_delegated_administrator_account_id] + org_delegated_administrator_account_id = local.account_map[var.delegated_administrator_account_name] + org_management_account_id = var.organization_management_account_name == null ? local.account_map[module.account_map.outputs.root_account_account_name] : local.account_map[var.organization_management_account_name] + is_org_delegated_administrator_account = local.current_account_id == local.org_delegated_administrator_account_id + is_org_management_account = local.current_account_id == local.org_management_account_id + + is_root_account_member_account = local.is_org_management_account && contains(local.member_account_id_list, local.org_management_account_id) + create_macie_account = local.enabled && ((local.is_org_delegated_administrator_account && !var.admin_delegated) || local.is_root_account_member_account) + create_org_delegation = local.enabled && local.is_org_management_account + create_org_settings = local.enabled && local.is_org_delegated_administrator_account && var.admin_delegated +} + +data "aws_caller_identity" "this" { + count = local.enabled ? 1 : 0 +} + +# If we are are in the AWS Org management account, delegate Macie to the org administrator account +# (usually the security account) +resource "aws_macie2_organization_admin_account" "this" { + count = local.create_org_delegation ? 1 : 0 + admin_account_id = local.org_delegated_administrator_account_id +} + +resource "awsutils_macie2_organization_settings" "this" { + count = local.create_org_settings ? 1 : 0 + member_accounts = local.member_account_id_list +} + +# If we are are in the AWS Org designated administrator account, enable macie detector and optionally create an +# SNS topic for notifications and CloudWatch event rules for findings +resource "aws_macie2_account" "this" { + count = local.create_macie_account ? 1 : 0 + + finding_publishing_frequency = var.finding_publishing_frequency + status = "ENABLED" +} diff --git a/modules/macie/outputs.tf b/modules/macie/outputs.tf new file mode 100644 index 000000000..47f350e15 --- /dev/null +++ b/modules/macie/outputs.tf @@ -0,0 +1,19 @@ +output "delegated_administrator_account_id" { + value = local.org_delegated_administrator_account_id + description = "The AWS Account ID of the AWS Organization delegated administrator account" +} + +output "member_account_ids" { + value = local.create_org_settings ? local.member_account_id_list : null + description = "The AWS Account IDs of the member accounts" +} + +output "macie_account_id" { + value = local.create_macie_account ? try(aws_macie2_account.this[0].id, null) : null + description = "The ID of the Macie account created by the component" +} + +output "macie_service_role_arn" { + value = local.create_macie_account ? try(aws_macie2_account.this[0].service_role, null) : null + description = "The Amazon Resource Name (ARN) of the service-linked role that allows Macie to monitor and analyze data in AWS resources for the account." +} diff --git a/modules/macie/providers.tf b/modules/macie/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/macie/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/macie/remote-state.tf b/modules/macie/remote-state.tf new file mode 100644 index 000000000..d9c31bca2 --- /dev/null +++ b/modules/macie/remote-state.tf @@ -0,0 +1,12 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "account-map" + tenant = var.account_map_tenant != "" ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + privileged = var.privileged + + context = module.this.context +} diff --git a/modules/macie/variables.tf b/modules/macie/variables.tf new file mode 100644 index 000000000..f962620cb --- /dev/null +++ b/modules/macie/variables.tf @@ -0,0 +1,78 @@ +variable "account_map_tenant" { + type = string + default = "core" + description = "The tenant where the `account_map` component required by remote-state is deployed" +} + +variable "admin_delegated" { + type = bool + default = false + description = < Date: Wed, 28 Feb 2024 09:12:34 -0500 Subject: [PATCH 375/501] remove unused vars in `aws-backup` component to reflect the module upgrade (#992) --- modules/aws-backup/README.md | 54 +++++++++++++-------------- modules/aws-backup/main.tf | 6 --- modules/aws-backup/outputs.tf | 5 --- modules/aws-backup/remote-state.tf | 11 ------ modules/aws-backup/variables.tf | 60 ------------------------------ 5 files changed, 26 insertions(+), 110 deletions(-) delete mode 100644 modules/aws-backup/remote-state.tf diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index 4da76b8ae..66cdc0748 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -75,14 +75,6 @@ components: vault_enabled: false # reuse from aws-backup-vault plan_enabled: true plan_name_suffix: aws-backup-defaults - # in minutes - start_window: 60 - completion_window: 240 - # in days - cold_storage_after: null - delete_after: 30 # 1 month - copy_action_cold_storage_after: null - copy_action_delete_after: null aws-backup/daily-plan: metadata: @@ -97,7 +89,8 @@ components: schedule: "cron(0 5 ? * * *)" start_window: 320 # 60 * 8 # minutes completion_window: 10080 # 60 * 24 * 7 # minutes - delete_after: 35 # 7 * 5 # days + lifecycle: + delete_after: 35 # 7 * 5 # days selection_tags: - type: STRINGEQUALS key: aws-backup/efs @@ -119,7 +112,8 @@ components: schedule: "cron(0 5 ? * SAT *)" start_window: 320 # 60 * 8 # minutes completion_window: 10080 # 60 * 24 * 7 # minutes - delete_after: 90 # 30 * 3 # days + lifecycle: + delete_after: 90 # 30 * 3 # days selection_tags: - type: STRINGEQUALS key: aws-backup/efs @@ -141,9 +135,9 @@ components: schedule: "cron(0 5 1 * ? *)" start_window: 320 # 60 * 8 # minutes completion_window: 10080 # 60 * 24 * 7 # minutes - delete_after: 2555 # 365 * 7 # days - cold_storage_after: 90 # 30 * 3 # days - + lifecycle: + delete_after: 2555 # 365 * 7 # days + cold_storage_after: 90 # 30 * 3 # days selection_tags: - type: STRINGEQUALS key: aws-backup/efs @@ -194,14 +188,29 @@ components: plan_enabled: false # disables the plan (which schedules resource backups) ``` -This will output an arn - which you can then use as the copy destination, as seen in the following snippet: +This will output an ARN - which you can then use as the destination in the rule object's `copy_action` (it will be specific to that particular plan), as seen in the following snippet: ```yaml components: terraform: - aws-backup: + aws-backup/plan-with-cross-region-replication: + metadata: + component: aws-backup + inherits: + - aws-backup/plan-defaults vars: - destination_vault_arn: arn:aws:backup::111111111111:backup-vault:-- - copy_action_delete_after: 14 + plan_name_suffix: aws-backup-cross-region + # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html + rules: + - name: "plan-cross-region" + schedule: "cron(0 5 ? * * *)" + start_window: 320 # 60 * 8 # minutes + completion_window: 10080 # 60 * 24 * 7 # minutes + lifecycle: + delete_after: 35 # 7 * 5 # days + copy_action: + destination_vault_arn: "arn:aws:backup::111111111111:backup-vault:--" + lifecycle: + delete_after: 35 ``` ### Backup Lock Configuration @@ -267,17 +276,9 @@ No resources. | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [backup\_resources](#input\_backup\_resources) | An array of strings that either contain Amazon Resource Names (ARNs) or match patterns of resources to assign to a backup plan | `list(string)` | `[]` | no | | [backup\_vault\_lock\_configuration](#input\_backup\_vault\_lock\_configuration) | The backup vault lock configuration, each vault can have one vault lock in place. This will enable Backup Vault Lock on an AWS Backup vault it prevents the deletion of backup data for the specified retention period. During this time, the backup data remains immutable and cannot be deleted or modified."
`changeable_for_days` - The number of days before the lock date. If omitted creates a vault lock in `governance` mode, otherwise it will create a vault lock in `compliance` mode. |
object({
changeable_for_days = optional(number)
max_retention_days = optional(number)
min_retention_days = optional(number)
})
| `null` | no | -| [cold\_storage\_after](#input\_cold\_storage\_after) | Specifies the number of days after creation that a recovery point is moved to cold storage | `number` | `null` | no | -| [completion\_window](#input\_completion\_window) | The amount of time AWS Backup attempts a backup before canceling the job and returning an error. Must be at least 60 minutes greater than `start_window` | `number` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [copy\_action\_cold\_storage\_after](#input\_copy\_action\_cold\_storage\_after) | For copy operation, specifies the number of days after creation that a recovery point is moved to cold storage | `number` | `null` | no | -| [copy\_action\_delete\_after](#input\_copy\_action\_delete\_after) | For copy operation, specifies the number of days after creation that a recovery point is deleted. Must be 90 days greater than `copy_action_cold_storage_after` | `number` | `null` | no | -| [delete\_after](#input\_delete\_after) | Specifies the number of days after creation that a recovery point is deleted. Must be 90 days greater than `cold_storage_after` | `number` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [destination\_vault\_arn](#input\_destination\_vault\_arn) | An Amazon Resource Name (ARN) that uniquely identifies the destination backup vault for the copied backup | `string` | `null` | no | -| [destination\_vault\_component\_name](#input\_destination\_vault\_component\_name) | The name of the component to be used to look up the destination vault | `string` | `"aws-backup/common"` | no | -| [destination\_vault\_region](#input\_destination\_vault\_region) | The short region of the destination backup vault | `string` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [iam\_role\_enabled](#input\_iam\_role\_enabled) | Whether or not to create a new IAM Role and Policy Attachment | `bool` | `true` | no | @@ -294,10 +295,8 @@ No resources. | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [rules](#input\_rules) | An array of rule maps used to define schedules in a backup plan |
list(object({
name = string
schedule = optional(string)
enable_continuous_backup = optional(bool)
start_window = optional(number)
completion_window = optional(number)
lifecycle = optional(object({
cold_storage_after = optional(number)
delete_after = optional(number)
opt_in_to_archive_for_supported_resources = optional(bool)
}))
copy_action = optional(object({
destination_vault_arn = optional(string)
lifecycle = optional(object({
cold_storage_after = optional(number)
delete_after = optional(number)
opt_in_to_archive_for_supported_resources = optional(bool)
}))
}))
}))
| `[]` | no | -| [schedule](#input\_schedule) | A CRON expression specifying when AWS Backup initiates a backup job | `string` | `null` | no | | [selection\_tags](#input\_selection\_tags) | An array of tag condition objects used to filter resources based on tags for assigning to a backup plan | `list(map(string))` | `[]` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [start\_window](#input\_start\_window) | The amount of time in minutes before beginning a backup. Minimum value is 60 minutes | `number` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [vault\_enabled](#input\_vault\_enabled) | Whether or not a new Vault should be created | `bool` | `true` | no | @@ -311,7 +310,6 @@ No resources. | [backup\_selection\_id](#output\_backup\_selection\_id) | Backup Selection ID | | [backup\_vault\_arn](#output\_backup\_vault\_arn) | Backup Vault ARN | | [backup\_vault\_id](#output\_backup\_vault\_id) | Backup Vault ID | -| [copy\_destination\_backup\_vault\_arn](#output\_copy\_destination\_backup\_vault\_arn) | ARN of the destination Backup Vault copy | diff --git a/modules/aws-backup/main.tf b/modules/aws-backup/main.tf index f93e4717d..3a64342fd 100644 --- a/modules/aws-backup/main.tf +++ b/modules/aws-backup/main.tf @@ -1,9 +1,3 @@ -locals { - copy_action_enabled = module.this.enabled && (var.destination_vault_arn != null || (var.destination_vault_component_name != null && var.destination_vault_region != null)) - copy_destination_arn_selection = var.destination_vault_arn != null ? var.destination_vault_arn : try(module.copy_destination_vault[0].outputs.backup_vault_arn, null) - copy_destination_arn = local.copy_action_enabled ? local.copy_destination_arn_selection : null -} - module "backup" { source = "cloudposse/backup/aws" version = "1.0.0" diff --git a/modules/aws-backup/outputs.tf b/modules/aws-backup/outputs.tf index 2ac27e34a..69fa779e5 100644 --- a/modules/aws-backup/outputs.tf +++ b/modules/aws-backup/outputs.tf @@ -22,8 +22,3 @@ output "backup_selection_id" { value = module.backup.backup_selection_id description = "Backup Selection ID" } - -output "copy_destination_backup_vault_arn" { - value = local.copy_destination_arn - description = "ARN of the destination Backup Vault copy" -} diff --git a/modules/aws-backup/remote-state.tf b/modules/aws-backup/remote-state.tf deleted file mode 100644 index c4661026e..000000000 --- a/modules/aws-backup/remote-state.tf +++ /dev/null @@ -1,11 +0,0 @@ -module "copy_destination_vault" { - source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.5.0" - - count = local.copy_action_enabled ? 1 : 0 - - component = var.destination_vault_component_name - environment = var.destination_vault_region - - context = module.this.context -} diff --git a/modules/aws-backup/variables.tf b/modules/aws-backup/variables.tf index 4b8de574e..1c5755152 100644 --- a/modules/aws-backup/variables.tf +++ b/modules/aws-backup/variables.tf @@ -9,18 +9,6 @@ variable "kms_key_arn" { default = null } -variable "schedule" { - type = string - description = "A CRON expression specifying when AWS Backup initiates a backup job" - default = null -} - -variable "start_window" { - type = number - description = "The amount of time in minutes before beginning a backup. Minimum value is 60 minutes" - default = null -} - variable "backup_vault_lock_configuration" { type = object({ changeable_for_days = optional(number) @@ -34,54 +22,6 @@ variable "backup_vault_lock_configuration" { default = null } -variable "completion_window" { - type = number - description = "The amount of time AWS Backup attempts a backup before canceling the job and returning an error. Must be at least 60 minutes greater than `start_window`" - default = null -} - -variable "cold_storage_after" { - type = number - description = "Specifies the number of days after creation that a recovery point is moved to cold storage" - default = null -} - -variable "delete_after" { - type = number - description = "Specifies the number of days after creation that a recovery point is deleted. Must be 90 days greater than `cold_storage_after`" - default = null -} - -variable "destination_vault_arn" { - type = string - description = "An Amazon Resource Name (ARN) that uniquely identifies the destination backup vault for the copied backup" - default = null -} - -variable "destination_vault_component_name" { - type = string - description = "The name of the component to be used to look up the destination vault" - default = "aws-backup/common" -} - -variable "destination_vault_region" { - type = string - description = "The short region of the destination backup vault" - default = null -} - -variable "copy_action_cold_storage_after" { - type = number - description = "For copy operation, specifies the number of days after creation that a recovery point is moved to cold storage" - default = null -} - -variable "copy_action_delete_after" { - type = number - description = "For copy operation, specifies the number of days after creation that a recovery point is deleted. Must be 90 days greater than `copy_action_cold_storage_after`" - default = null -} - variable "selection_tags" { type = list(map(string)) description = "An array of tag condition objects used to filter resources based on tags for assigning to a backup plan" From 056a5cdd6149edba76717a560c080c67ad93271c Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 28 Feb 2024 17:49:58 +0100 Subject: [PATCH 376/501] Use native cloudfront 404 page (#991) --- modules/spa-s3-cloudfront/CHANGELOG.md | 14 ++++++++++ modules/spa-s3-cloudfront/README.md | 1 - modules/spa-s3-cloudfront/lambda_edge.tf | 35 ------------------------ modules/spa-s3-cloudfront/variables.tf | 10 +------ 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/modules/spa-s3-cloudfront/CHANGELOG.md b/modules/spa-s3-cloudfront/CHANGELOG.md index 29775f308..6c38d679f 100644 --- a/modules/spa-s3-cloudfront/CHANGELOG.md +++ b/modules/spa-s3-cloudfront/CHANGELOG.md @@ -1,3 +1,17 @@ +## Component PR [#991]() + +### Drop `lambda_edge_redirect_404` + +This PR removes the `lambda_edge_redirect_404` functionality because it leads to significat costs. +Use native CloudFront error pages configs instead. + +```yaml +cloudfront_custom_error_response: + - error_code: 404 + response_code: 404 + response_page_path: /404.html +``` + ## Components PR [#978](https://github.com/cloudposse/terraform-aws-components/pull/978) ### Lambda@Edge Submodule Refactor diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 54799e4ea..0badadf0a 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -229,7 +229,6 @@ components: | [lambda\_edge\_destruction\_delay](#input\_lambda\_edge\_destruction\_delay) | The delay, in [Golang ParseDuration](https://pkg.go.dev/time#ParseDuration) format, to wait before destroying the Lambda@Edge
functions.

This delay is meant to circumvent Lambda@Edge functions not being immediately deletable following their dissociation from
a CloudFront distribution, since they are replicated to CloudFront Edge servers around the world.

If set to `null`, no delay will be introduced.

By default, the delay is 20 minutes. This is because it takes about 3 minutes to destroy a CloudFront distribution, and
around 15 minutes until the Lambda@Edge function is available for deletion, in most cases.

For more information, see: https://github.com/hashicorp/terraform-provider-aws/issues/1721. | `string` | `"20m"` | no | | [lambda\_edge\_functions](#input\_lambda\_edge\_functions) | Lambda@Edge functions to create.

The key of this map is the name of the Lambda@Edge function.

This map will be deep merged with each enabled default function. Use deep merge to change or overwrite specific values passed by those function objects. |
map(object({
source = optional(list(object({
filename = string
content = string
})))
source_dir = optional(string)
source_zip = optional(string)
runtime = string
handler = string
event_type = string
include_body = bool
}))
| `{}` | no | | [lambda\_edge\_handler](#input\_lambda\_edge\_handler) | The default Lambda@Edge handler for all functions.

This value is deep merged in `module.lambda_edge_functions` with `var.lambda_edge_functions` and can be overwritten for any individual function. | `string` | `"index.handler"` | no | -| [lambda\_edge\_redirect\_404\_enabled](#input\_lambda\_edge\_redirect\_404\_enabled) | Enable or disable SPA 404 redirects via Lambda@Edge - returns a 302 and a location of `/` if the request returned 404. | `bool` | `false` | no | | [lambda\_edge\_runtime](#input\_lambda\_edge\_runtime) | The default Lambda@Edge runtime for all functions.

This value is deep merged in `module.lambda_edge_functions` with `var.lambda_edge_functions` and can be overwritten for any individual function. | `string` | `"nodejs16.x"` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | diff --git a/modules/spa-s3-cloudfront/lambda_edge.tf b/modules/spa-s3-cloudfront/lambda_edge.tf index 197170f3c..95877acfe 100644 --- a/modules/spa-s3-cloudfront/lambda_edge.tf +++ b/modules/spa-s3-cloudfront/lambda_edge.tf @@ -1,6 +1,4 @@ locals { - lambda_edge_redirect_404_enabled = local.enabled && var.lambda_edge_redirect_404_enabled - cloudfront_lambda_function_association = concat(var.cloudfront_lambda_function_association, module.lambda_edge.lambda_function_association) } @@ -58,39 +56,6 @@ module "lambda_edge_functions" { include_body = false } } : {}, - local.lambda_edge_redirect_404_enabled ? { - origin_response = { - source = [{ - content = file("${path.module}/dist/lambda_edge_404_redirect.js") - filename = "index.js" - }] - runtime = var.lambda_edge_runtime - handler = var.lambda_edge_handler - event_type = "origin-response" - include_body = false - }, - viewer_request = { - source = [{ - content = <<-EOT - exports.handler = (event, context, callback) => { - const { request } = event.Records[0].cf; - request.headers['x-forwarded-host'] = [ - { - key: 'X-Forwarded-Host', - value: request.headers.host[0].value - } - ]; - return callback(null, request); - }; - EOT - filename = "index.js" - }] - runtime = var.lambda_edge_runtime - handler = var.lambda_edge_handler - event_type = "viewer-request" - include_body = false - } - } : {}, var.lambda_edge_functions, ] } diff --git a/modules/spa-s3-cloudfront/variables.tf b/modules/spa-s3-cloudfront/variables.tf index a779a453d..e13b02fb8 100644 --- a/modules/spa-s3-cloudfront/variables.tf +++ b/modules/spa-s3-cloudfront/variables.tf @@ -173,7 +173,7 @@ variable "cloudfront_custom_error_response" { # http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/custom-error-pages.html#custom-error-pages-procedure # https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#custom-error-response-arguments type = list(object({ - error_caching_min_ttl = string + error_caching_min_ttl = optional(string, "10") error_code = string response_code = string response_page_path = string @@ -452,14 +452,6 @@ variable "preview_environment_enabled" { default = false } -variable "lambda_edge_redirect_404_enabled" { - type = bool - description = <<-EOT - Enable or disable SPA 404 redirects via Lambda@Edge - returns a 302 and a location of `/` if the request returned 404. - EOT - default = false -} - variable "github_runners_deployment_principal_arn_enabled" { type = bool description = "A flag that is used to decide whether or not to include the GitHub Runner's IAM role in origin_deployment_principal_arns list" From 3727f96af1ed4a81c00445290e23360af3ee0cfe Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 28 Feb 2024 19:06:33 +0100 Subject: [PATCH 377/501] [spa-s3-cloudfront] Fix preview enviornments (#995) --- modules/spa-s3-cloudfront/CHANGELOG.md | 6 ++--- modules/spa-s3-cloudfront/lambda_edge.tf | 32 +++++++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/modules/spa-s3-cloudfront/CHANGELOG.md b/modules/spa-s3-cloudfront/CHANGELOG.md index 6c38d679f..250187cd3 100644 --- a/modules/spa-s3-cloudfront/CHANGELOG.md +++ b/modules/spa-s3-cloudfront/CHANGELOG.md @@ -1,8 +1,8 @@ -## Component PR [#991]() +## Component PRs [#991](https://github.com/cloudposse/terraform-aws-components/pull/991) and [#995](https://github.com/cloudposse/terraform-aws-components/pull/995) -### Drop `lambda_edge_redirect_404` +### Drop `lambda_edge_redirect_404` -This PR removes the `lambda_edge_redirect_404` functionality because it leads to significat costs. +This PRs removes the `lambda_edge_redirect_404` functionality because it leads to significat costs. Use native CloudFront error pages configs instead. ```yaml diff --git a/modules/spa-s3-cloudfront/lambda_edge.tf b/modules/spa-s3-cloudfront/lambda_edge.tf index 95877acfe..b3043b7fc 100644 --- a/modules/spa-s3-cloudfront/lambda_edge.tf +++ b/modules/spa-s3-cloudfront/lambda_edge.tf @@ -37,7 +37,14 @@ module "lambda_edge_functions" { const default_prefix = ""; console.log('request:' + JSON.stringify(request)); - const host = request.headers['x-forwarded-host'][0].value; + + let host = null; + + if (request.headers.hasOwnProperty('x-forwarded-host')) { + host = request.headers['x-forwarded-host'][0].value; + } else { + host = site_fqdn; + } if (host == site_fqdn) { request.origin.custom.path = default_prefix; // use default prefix if there is no subdomain } else { @@ -54,6 +61,29 @@ module "lambda_edge_functions" { handler = var.lambda_edge_handler event_type = "origin-request" include_body = false + }, + viewer_request = { + source = [{ + content = <<-EOT + exports.handler = (event, context, callback) => { + const { request } = event.Records[0].cf; + if ('host' in request.headers) { + request.headers['x-forwarded-host'] = [ + { + key: 'X-Forwarded-Host', + value: request.headers.host[0].value + } + ]; + } + return callback(null, request); + }; + EOT + filename = "index.js" + }] + runtime = var.lambda_edge_runtime + handler = var.lambda_edge_handler + event_type = "viewer-request" + include_body = false } } : {}, var.lambda_edge_functions, From 00304f4b81969d9428d90384900dea224b2dc55c Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 11 Mar 2024 10:39:05 -0700 Subject: [PATCH 378/501] feat: `prettier` in READMEs (#998) --- .github/ISSUE_TEMPLATE/bug_report.md | 18 +- .github/ISSUE_TEMPLATE/feature_request.md | 13 +- .pre-commit-config.yaml | 14 + CHANGELOG.md | 2238 +++++++++-------- README.md | 2 +- docs/targets.md | 3 + mixins/README.md | 7 +- .../README-github-action-iam-role.md | 69 +- modules/account-map/README.md | 19 +- .../account-map/modules/iam-roles/README.md | 20 +- .../modules/roles-to-principals/README.md | 21 +- .../modules/team-assume-role-policy/README.md | 11 +- modules/account-quotas/README.md | 33 +- modules/account-settings/README.md | 15 +- modules/account/README.md | 156 +- modules/acm/README.md | 21 +- modules/alb/README.md | 9 +- modules/amplify/README.md | 31 +- .../api-gateway-account-settings/README.md | 14 +- modules/api-gateway-rest-api/README.md | 6 +- modules/argocd-repo/CHANGELOG.md | 30 +- modules/argocd-repo/README.md | 11 +- modules/athena/README.md | 33 +- modules/aurora-mysql-resources/README.md | 23 +- modules/aurora-mysql/README.md | 34 +- modules/aurora-postgres-resources/README.md | 61 +- modules/aurora-postgres/README.md | 37 +- modules/aws-backup/README.md | 62 +- modules/aws-config/README.md | 78 +- modules/aws-inspector/README.md | 57 +- modules/aws-inspector2/README.md | 19 +- modules/aws-saml/README.md | 10 +- modules/aws-shield/README.md | 28 +- modules/aws-sso/CHANGELOG.md | 46 +- modules/aws-sso/README.md | 214 +- modules/aws-ssosync/README.md | 97 +- modules/aws-team-roles/README.md | 108 +- modules/aws-teams/README.md | 104 +- modules/bastion/README.md | 37 +- modules/cloudtrail-bucket/README.md | 12 +- modules/cloudtrail/README.md | 12 +- modules/cloudwatch-logs/README.md | 6 +- modules/cognito/README.md | 20 +- modules/config-bucket/README.md | 12 +- modules/datadog-configuration/README.md | 30 +- .../modules/datadog_keys/README.md | 2 + modules/datadog-integration/CHANGELOG.md | 25 +- modules/datadog-integration/README.md | 18 +- modules/datadog-lambda-forwarder/CHANGELOG.md | 12 +- modules/datadog-lambda-forwarder/README.md | 15 +- modules/datadog-logs-archive/README.md | 141 +- modules/datadog-monitor/CHANGELOG.md | 4 +- modules/datadog-monitor/README.md | 40 +- .../datadog-private-location-ecs/README.md | 7 +- .../CHANGELOG.md | 20 +- .../README.md | 21 +- modules/datadog-synthetics/CHANGELOG.md | 15 +- modules/datadog-synthetics/README.md | 48 +- modules/dms/endpoint/README.md | 6 +- modules/dms/iam/README.md | 6 +- modules/dms/replication-instance/README.md | 6 +- modules/dms/replication-task/README.md | 6 +- modules/dns-delegated/README.md | 69 +- modules/dns-primary/README.md | 54 +- modules/documentdb/README.md | 6 +- modules/dynamodb/README.md | 10 +- modules/ec2-client-vpn/README.md | 48 +- modules/ecr/README.md | 25 +- modules/ecs-service/README.md | 65 +- modules/ecs/README.md | 8 +- modules/efs/README.md | 10 +- .../eks/actions-runner-controller/README.md | 214 +- .../alb-controller-ingress-class/README.md | 12 +- .../alb-controller-ingress-group/README.md | 17 +- modules/eks/alb-controller/CHANGELOG.md | 24 +- modules/eks/alb-controller/README.md | 21 +- modules/eks/argocd/CHANGELOG.md | 66 +- modules/eks/argocd/README.md | 149 +- .../aws-node-termination-handler/README.md | 10 +- modules/eks/cert-manager/README.md | 68 +- modules/eks/cluster/CHANGELOG.md | 355 ++- modules/eks/cluster/README.md | 118 +- modules/eks/datadog-agent/CHANGELOG.md | 77 +- modules/eks/datadog-agent/README.md | 53 +- modules/eks/echo-server/CHANGELOG.md | 33 +- modules/eks/echo-server/README.md | 76 +- modules/eks/external-dns/README.md | 6 +- .../external-secrets-operator/CHANGELOG.md | 6 +- .../eks/external-secrets-operator/README.md | 22 +- .../eks/github-actions-runner/CHANGELOG.md | 117 +- modules/eks/github-actions-runner/README.md | 353 ++- modules/eks/idp-roles/README.md | 8 +- modules/eks/karpenter-provisioner/README.md | 103 +- modules/eks/karpenter/CHANGELOG.md | 47 +- modules/eks/karpenter/README.md | 270 +- modules/eks/keda/README.md | 7 +- modules/eks/metrics-server/README.md | 5 +- modules/eks/platform/README.md | 12 +- modules/eks/redis-operator/README.md | 12 +- modules/eks/redis/README.md | 9 +- modules/eks/reloader/README.md | 12 +- modules/eks/storage-class/README.md | 83 +- modules/elasticache-redis/README.md | 9 +- modules/elasticsearch/README.md | 9 +- modules/eventbridge/README.md | 14 +- modules/github-action-token-rotator/README.md | 15 +- modules/github-oidc-provider/README.md | 34 +- modules/github-oidc-role/README.md | 60 +- modules/github-runners/README.md | 163 +- modules/github-webhook/README.md | 14 +- modules/gitops/README.md | 12 +- .../README.md | 9 +- modules/global-accelerator/README.md | 6 +- modules/glue/catalog-database/README.md | 5 +- modules/glue/catalog-table/README.md | 5 +- modules/glue/connection/README.md | 5 +- modules/glue/crawler/README.md | 5 +- modules/glue/iam/README.md | 5 +- modules/glue/job/README.md | 5 +- modules/glue/registry/README.md | 5 +- modules/glue/schema/README.md | 5 +- modules/glue/trigger/README.md | 5 +- modules/glue/workflow/README.md | 5 +- modules/guardduty/README.md | 20 +- modules/iam-role/README.md | 10 +- modules/iam-service-linked-roles/README.md | 18 +- modules/ipam/README.md | 6 +- modules/kinesis-stream/README.md | 9 +- modules/kms/README.md | 91 +- modules/lakeformation/README.md | 11 +- modules/lambda/README.md | 8 +- modules/macie/README.md | 22 +- modules/mq-broker/README.md | 6 +- modules/msk/README.md | 7 +- modules/mwaa/README.md | 10 +- modules/network-firewall/README.md | 21 +- modules/opsgenie-team/CHANGELOG.md | 13 +- modules/opsgenie-team/README.md | 91 +- .../modules/escalation/README.md | 6 +- .../modules/integration/README.md | 2 + .../opsgenie-team/modules/routing/README.md | 7 +- modules/philips-labs-github-runners/README.md | 44 +- .../modules/README.md | 10 +- .../modules/webhook-github-app/README.md | 31 +- modules/rds/README.md | 36 +- modules/redshift/CHANGELOG.md | 6 +- modules/redshift/README.md | 10 +- .../route53-resolver-dns-firewall/README.md | 11 +- modules/s3-bucket/README.md | 9 +- modules/security-hub/README.md | 14 +- modules/ses/README.md | 8 +- modules/sftp/README.md | 5 +- modules/snowflake-account/README.md | 31 +- modules/snowflake-database/README.md | 9 +- modules/sns-topic/README.md | 10 +- modules/spa-s3-cloudfront/CHANGELOG.md | 35 +- modules/spa-s3-cloudfront/README.md | 39 +- modules/spacelift/README.md | 239 +- modules/spacelift/admin-stack/README.md | 5 +- modules/spacelift/spaces/README.md | 2 + modules/spacelift/worker-pool/README.md | 27 +- modules/sqs-queue/README.md | 6 +- modules/ssm-parameters/README.md | 9 +- modules/sso-saml-provider/README.md | 2 +- modules/strongdm/README.md | 7 +- modules/tfstate-backend/README.md | 142 +- modules/tgw/CHANGELOG.md | 20 +- modules/tgw/README.md | 49 +- .../tgw/cross-region-hub-connector/README.md | 17 +- modules/tgw/hub/README.md | 8 +- modules/tgw/spoke/README.md | 8 +- modules/vpc-flow-logs-bucket/README.md | 9 +- modules/vpc-peering/README.md | 80 +- modules/vpc/README.md | 9 +- modules/waf/README.md | 36 +- modules/zscaler/README.md | 16 +- 176 files changed, 4888 insertions(+), 3817 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index baddda8e7..1722d9473 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' -labels: 'bug' -assignees: '' - +title: "" +labels: "bug" +assignees: "" --- Found a bug? Maybe our [Slack Community](https://slack.cloudposse.com) can help. @@ -12,26 +11,33 @@ Found a bug? Maybe our [Slack Community](https://slack.cloudposse.com) can help. [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) ## Describe the Bug + A clear and concise description of what the bug is. ## Expected Behavior + A clear and concise description of what you expected to happen. ## Steps to Reproduce + Steps to reproduce the behavior: + 1. Go to '...' 2. Run '....' 3. Enter '....' 4. See error ## Screenshots + If applicable, add screenshots or logs to help explain your problem. ## Environment (please complete the following information): Anything that will help us triage the bug will help. Here are some ideas: - - OS: [e.g. Linux, OSX, WSL, etc] - - Version [e.g. 10.15] + +- OS: [e.g. Linux, OSX, WSL, etc] +- Version [e.g. 10.15] ## Additional Context + Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 44cdd4a43..5ec5bfc31 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,13 +1,13 @@ --- name: Feature Request about: Suggest an idea for this project -title: '' -labels: 'feature request' -assignees: '' - +title: "" +labels: "feature request" +assignees: "" --- -Have a question? Please checkout our [Slack Community](https://slack.cloudposse.com) or visit our [Slack Archive](https://archive.sweetops.com/). +Have a question? Please checkout our [Slack Community](https://slack.cloudposse.com) or visit our +[Slack Archive](https://archive.sweetops.com/). [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) @@ -21,7 +21,8 @@ A clear and concise description of what you expected to happen. ## Use Case -Is your feature request related to a problem/challenge you are trying to solve? Please provide some additional context of why this feature or capability will be valuable. +Is your feature request related to a problem/challenge you are trying to solve? Please provide some additional context +of why this feature or capability will be valuable. ## Describe Ideal Solution diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e73fb4263..bce86cb2a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,6 +36,20 @@ repos: - id: terraform_fmt - id: terraform_docs args: ["--args=--lockfile=false"] + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + name: prettier + entry: prettier --write --prose-wrap always --print-width 120 + types: ["markdown"] + exclude: | + (?x)^( + README.md | + deprecated/.*.md + )$ + - repo: local hooks: - id: rebuild-mixins-docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f1a70dc6..b9c7cf94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,28 +6,32 @@ Aurora Postgres Engine Options @milldr (#845) ### what + - Add scaling configuration variables for both Serverless and Serverless v2 to `aurora-postgres` - Update `aurora-postgres` README ### why + - Support both serverless options - Add an explanation for how to configure each, and where to find valid engine options ### references + - n/a
- ## 1.297.0 (2023-08-28T18:06:11Z)
AWS provider V5 dependency updates @max-lobur (#729) ### what -* Update component dependencies for the AWS provider V5 + +- Update component dependencies for the AWS provider V5 Requested components: + - cloudtrail-bucket - config-bucket - datadog-logs-archive @@ -38,13 +42,11 @@ Requested components: - eks/external-secrets-operator ### why -* Maintenance - +- Maintenance
- ## 1.296.0 (2023-08-28T16:24:05Z)
@@ -62,51 +64,54 @@ Requested components: ### references -
- ## 1.295.0 (2023-08-26T00:51:10Z)
TGW FAQ and Spoke Alternate VPC Support @milldr (#840) ### what + - Added FAQ to the TGW upgrade guide for replacing attachments - Added note about destroying TGW components - Added option to not create TGW propagation and association when connecting an alternate VPC ### why -- When connecting an alternate VPC in the same region as the primary VPC, we do not want to create a duplicate TGW propagation and association + +- When connecting an alternate VPC in the same region as the primary VPC, we do not want to create a duplicate TGW + propagation and association ### references -- n/a +- n/a
- ## 1.294.0 (2023-08-26T00:07:42Z)
Aurora Upstream: Serverless, Tags, Enabled: False @milldr (#841) ### what + - Set `module.context` to `module.cluster` across all resources - Only set parameter for replica if cluster size is > 0 - `enabled: false` support ### why + - Missing tags for SSM parameters for cluster attributes -- Serverless clusters set `cluster_size: 0`, which will break the SSM parameter for replica hostname (since it does not exist) +- Serverless clusters set `cluster_size: 0`, which will break the SSM parameter for replica hostname (since it does not + exist) - Support enabled false for `aurora-*-resources` components ### references + - n/a
- ## 1.293.2 (2023-08-24T15:50:53Z) ### 🚀 Enhancements @@ -115,18 +120,19 @@ Requested components: Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` @aknysh (#837) ### what -* Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` + +- Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` ### why -* Fix the issue described in https://github.com/cloudposse/terraform-aws-components/issues/771 + +- Fix the issue described in https://github.com/cloudposse/terraform-aws-components/issues/771 ### related -* Closes https://github.com/cloudposse/terraform-aws-components/issues/771 +- Closes https://github.com/cloudposse/terraform-aws-components/issues/771
- ## 1.293.1 (2023-08-24T11:24:46Z) ### 🐛 Bug Fixes @@ -142,46 +148,46 @@ Requested components: - Fixes #828 - - ## 1.293.0 (2023-08-23T01:18:53Z)
Add visibility to default VPC component name @milldr (#833) ### what + - Set the default component name for `vpc` in variables, not remote-state ### why + - Bring visibility to where the default is set ### references -- Follow up on comments on #832 +- Follow up on comments on #832
- ## 1.292.0 (2023-08-22T21:33:18Z)
Aurora Optional `vpc` Component Names @milldr (#832) ### what + - Allow optional VPC component names in the aurora components ### why + - Support deploying the clusters for other VPC components than `"vpc"` ### references -- n/a +- n/a
- ## 1.291.1 (2023-08-22T20:25:17Z) ### 🐛 Bug Fixes @@ -192,6 +198,7 @@ Requested components: ### what For `aws-sso`: + - Fix root provider, improperly restored in #740 - Restore `SetSourceIdentity` permission inadvertently removed in #740 @@ -205,47 +212,46 @@ For `aws-sso`: - #740 - #738 - - ## 1.291.0 (2023-08-22T17:08:27Z)
chore: remove defaults from components @dudymas (#831) ### what -* remove `defaults.auto.tfvars` from component modules + +- remove `defaults.auto.tfvars` from component modules ### why -* in favor of drying up configuration using atmos + +- in favor of drying up configuration using atmos ### Notes -* Some defaults may not be captured yet. Regressions might occur. +- Some defaults may not be captured yet. Regressions might occur.
- ## 1.290.0 (2023-08-21T18:57:43Z)
Upgrade aws-config and conformance pack modules to 1.1.0 @johncblandii (#829) ### what -* Upgrade aws-config and conformance pack modules to 1.1.0 + +- Upgrade aws-config and conformance pack modules to 1.1.0 ### why -* They're outdated. + +- They're outdated. ### references - #771 -
- ## 1.289.2 (2023-08-21T08:53:08Z) ### 🐛 Bug Fixes @@ -255,7 +261,8 @@ For `aws-sso`: ### what -- [eks/alb-controller] Change name of local variable from `distributed_iam_policy_overridable` to `overridable_distributed_iam_policy` +- [eks/alb-controller] Change name of local variable from `distributed_iam_policy_overridable` to + `overridable_distributed_iam_policy` ### why @@ -263,7 +270,6 @@ For `aws-sso`: - ## 1.289.1 (2023-08-19T05:20:26Z) ### 🐛 Bug Fixes @@ -279,49 +285,50 @@ For `aws-sso`: - Previous policy had error preventing the creation of the ELB service-linked role - - - ## 1.289.0 (2023-08-18T20:18:12Z)
Spacelift Alternate git Providers @milldr (#825) ### what + - set alternate git provider blocks to filter under `settings.spacelift` ### why + - Debugging GitLab support specifically - These settings should be defined under `settings.spacelift`, not as a top-level configuration ### references -- n/a +- n/a
- ## 1.288.0 (2023-08-18T15:12:16Z)
Placeholder for `upgrade-guide.md` @milldr (#823) ### what + - Added a placeholder file for `docs/upgrade-guide.md` with a basic explanation of what is to come ### why -- With #811 we moved the contents of this upgrade-guide file to the individual component. We plan to continue adding upgrade guides for individual components, and in addition, create a higher-level upgrade guide here -- However, the build steps for refarch-scaffold expect `docs/upgrade-guide.md` to exist and are failing without it. We need a placeholder until the `account-map`, etc changes are added to this file + +- With #811 we moved the contents of this upgrade-guide file to the individual component. We plan to continue adding + upgrade guides for individual components, and in addition, create a higher-level upgrade guide here +- However, the build steps for refarch-scaffold expect `docs/upgrade-guide.md` to exist and are failing without it. We + need a placeholder until the `account-map`, etc changes are added to this file ### references -- Example of failing release: https://github.com/cloudposse/refarch-scaffold/actions/runs/5885022872 +- Example of failing release: https://github.com/cloudposse/refarch-scaffold/actions/runs/5885022872
- ## 1.287.2 (2023-08-18T14:42:49Z) ### 🚀 Enhancements @@ -330,11 +337,13 @@ For `aws-sso`: update boolean logic @mcalhoun (#822) ### what -* Update the GuardDuty component to enable GuardDuty on the root account + +- Update the GuardDuty component to enable GuardDuty on the root account ### why -The API call to designate organization members now fails with the following if GuardDuty was not already enabled in the organization management (root) account : +The API call to designate organization members now fails with the following if GuardDuty was not already enabled in the +organization management (root) account : ``` Error: error designating guardduty administrator account members: [{ @@ -343,10 +352,8 @@ Error: error designating guardduty administrator account members: [{ │ }] ``` - - ## 1.287.1 (2023-08-17T16:41:24Z) ### 🚀 Enhancements @@ -355,7 +362,7 @@ Error: error designating guardduty administrator account members: [{ chore: Remove unused @MaxymVlasov (#818) - # why +# why ``` TFLint in components/terraform/eks/cluster/: @@ -376,26 +383,24 @@ Warning: [Fixable] variable "aws_teams_rbac" is declared but not used (terraform Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md ``` - - ## 1.287.0 (2023-08-17T15:52:57Z)
Update `remote-states` modules to the latest version @aknysh (#820) ### what -* Update `remote-states` modules to the latest version -### why -* `remote-state` version `1.5.0` uses the latest version of `terraform-provider-utils` which uses the latest version of Atmos with many new features and improvements +- Update `remote-states` modules to the latest version +### why +- `remote-state` version `1.5.0` uses the latest version of `terraform-provider-utils` which uses the latest version of + Atmos with many new features and improvements
- ## 1.286.0 (2023-08-17T05:49:45Z)
@@ -422,24 +427,23 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0
- ## 1.285.0 (2023-08-17T05:49:09Z)
Update api-gateway-account-settings README.md @johncblandii (#819) ### what -* Updated the title + +- Updated the title ### why -* It was an extra helping of copy/pasta -### references +- It was an extra helping of copy/pasta +### references
- ## 1.284.0 (2023-08-17T02:10:47Z)
@@ -448,24 +452,26 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 ### what - Update Datadog components: - - `eks/datadog-agent` see `eks/datadog-agent/CHANGELOG.md` - - `datadog-configuration` better handling of `enabled = false` - - `datadog-integration` move "module count" back to "module" for better compatibility and maintainability, see `datadog-integration/CHANGELOG.md` - - `datadog-lambda-forwared` fix issues around `enable = false` and incomplete destruction of resources (particularly log groups) see `datadog-lambda-forwarder/CHANGELOG.md` - - Cleanup `datadog-monitor` see `datadog-monitor/CHANGELOG.md` for details. Possible breaking change in that several inputs have been removed, but they were previously ignored anyway, so no infrastructure change should result from you simply removing any inputs you had for the removed inputs. - - Update `datadog-sythetics` dependency `remote-state` version - - `datadog-synthetics-private-location` migrate control of namespace to `helm-release` module. Possible destruction and recreation of component on upgrade. See CHANGELOG.md + - `eks/datadog-agent` see `eks/datadog-agent/CHANGELOG.md` + - `datadog-configuration` better handling of `enabled = false` + - `datadog-integration` move "module count" back to "module" for better compatibility and maintainability, see + `datadog-integration/CHANGELOG.md` + - `datadog-lambda-forwared` fix issues around `enable = false` and incomplete destruction of resources (particularly + log groups) see `datadog-lambda-forwarder/CHANGELOG.md` + - Cleanup `datadog-monitor` see `datadog-monitor/CHANGELOG.md` for details. Possible breaking change in that several + inputs have been removed, but they were previously ignored anyway, so no infrastructure change should result from + you simply removing any inputs you had for the removed inputs. + - Update `datadog-sythetics` dependency `remote-state` version + - `datadog-synthetics-private-location` migrate control of namespace to `helm-release` module. Possible destruction + and recreation of component on upgrade. See CHANGELOG.md ### why - More reliable deployments, especially when destroying or disabling them - Bug fixes and new features - -
- ## 1.283.0 (2023-08-16T17:23:39Z)
@@ -492,16 +498,15 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 Update storage-class efs component documentation @max-lobur (#817) ### what -* Update storage-class efs component defaults -### why -* Follow component move outside of eks dir +- Update storage-class efs component defaults +### why +- Follow component move outside of eks dir
- ## 1.282.1 (2023-08-15T21:48:02Z) ### 🐛 Bug Fixes @@ -517,28 +522,27 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 ### why - Bug fix: Karpenter did not work when legacy mode disabled -- Originally we expected to use Karpenter-only clusters and the documentation and defaults aligned with this. Now we recommend all Add-Ons be deployed to a managed node group, but the defaults and documentation did not reflect this. - - +- Originally we expected to use Karpenter-only clusters and the documentation and defaults aligned with this. Now we + recommend all Add-Ons be deployed to a managed node group, but the defaults and documentation did not reflect this. - ## 1.282.0 (2023-08-14T16:05:08Z)
Upstream the latest ecs-service module @goruha (#810) ### what -* Upsteam the latest `ecs-service` component + +- Upsteam the latest `ecs-service` component ### why -* Support ecspresso deployments -* Support s3 task definition mirroring -* Support external ALB/NLN components -
+- Support ecspresso deployments +- Support s3 task definition mirroring +- Support external ALB/NLN components + ## 1.281.0 (2023-08-14T09:10:42Z) @@ -546,20 +550,21 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 Refactor Changelog @milldr (#811) ### what + - moved changelog for individual components - changed title ### why + - Title changelogs consistently by components version - Separate changes by affected components ### references -- https://github.com/cloudposse/knowledge-base/discussions/132 +- https://github.com/cloudposse/knowledge-base/discussions/132 - ## 1.280.1 (2023-08-14T08:06:42Z) ### 🚀 Enhancements @@ -569,7 +574,8 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 ### what -- Fix eks/cluster `node_group_defaults` to default to legal (empty) values for `kubernetes_labels` and `kubernetes_taints` +- Fix eks/cluster `node_group_defaults` to default to legal (empty) values for `kubernetes_labels` and + `kubernetes_taints` - Increase eks/cluster managed node group default disk size from 20 to 50 GB ### why @@ -579,7 +585,6 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 - ## 1.280.0 (2023-08-11T20:13:45Z)
@@ -588,7 +593,8 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 ### Why: - `cloudposse/ssm-parameter-store/aws` was out of date -- There are no new [changes](https://github.com/cloudposse/terraform-aws-ssm-parameter-store/releases/tag/0.11.0) incorporated but just wanted to standardize new modules to updated version +- There are no new [changes](https://github.com/cloudposse/terraform-aws-ssm-parameter-store/releases/tag/0.11.0) + incorporated but just wanted to standardize new modules to updated version ### What: @@ -607,120 +613,124 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0
- ## 1.279.0 (2023-08-11T16:39:01Z)
fix: restore argocd notification ssm lookups @dudymas (#764) ### what -* revert some changes to `argocd` component -* connect argocd notifications with ssm secrets -* remove `deployment_id` from `argocd-repo` component -* correct `app_hostname` since gha usually adds protocol + +- revert some changes to `argocd` component +- connect argocd notifications with ssm secrets +- remove `deployment_id` from `argocd-repo` component +- correct `app_hostname` since gha usually adds protocol ### why -* regressions with argocd notifications caused github actions to timeout -* `deployment_id` no longer needed for fascilitating communication between gha -and ArgoCD -* application urls were incorrect and problematic during troubleshooting +- regressions with argocd notifications caused github actions to timeout +- `deployment_id` no longer needed for fascilitating communication between gha and ArgoCD +- application urls were incorrect and problematic during troubleshooting
- ## 1.278.0 (2023-08-09T21:54:09Z)
Upstream `eks/keda` @milldr (#808) ### what + - Added the component `eks/keda` ### why + - We've deployed KEDA for a few customers now and the component should be upstreamed ### references -- n/a +- n/a
- ## 1.277.0 (2023-08-09T20:39:21Z)
Added Inputs for `elasticsearch` and `cognito` @milldr (#786) ### what + - Added `deletion_protection` for `cognito` - Added options for dedicated master for `elasticsearch` ### why + - Allow the default options to be customized ### references -- Customer requested additions +- Customer requested additions
- ## 1.276.1 (2023-08-09T20:30:36Z)
Update upgrade-guide.md Version @milldr (#807) ### what + - Set the version to the correct updated release ### why + - Needs to match correct version ### references + #804
- ### 🚀 Enhancements
feat: allow email to be configured at account level @sgtoj (#799) ### what -* allow email to be configured at account level + +- allow email to be configured at account level ### why -* to allow importing existing accounts with email address that does not met the organization standard naming format + +- to allow importing existing accounts with email address that does not met the organization standard naming format ### references -* n/a +- n/a
- ## 1.276.0 (2023-08-09T16:38:40Z)
Transit Gateway Cross-Region Support @milldr (#804) ### what + - Upgraded `tgw` components to support cross region connections - Added back `tgw/cross-region-hub-connector` with overhaul to support updated `tgw/hub` component ### why + - Deploy `tgw/cross-region-hub-connector` to create peered TGW hubs - Use `tgw/hub` both for in region and intra region connections ### references -- n/a +- n/a
- ## 1.275.0 (2023-08-09T02:53:39Z)
@@ -734,13 +744,11 @@ and ArgoCD - Fixes #797 - Supersedes and closes #798 -- Cloud Posse standard requires error-free operation and no resources created when `enabled` is `false`, but previously this component had several errors - - +- Cloud Posse standard requires error-free operation and no resources created when `enabled` is `false`, but previously + this component had several errors
- ## 1.274.2 (2023-08-09T00:13:36Z) ### 🚀 Enhancements @@ -750,17 +758,15 @@ and ArgoCD ### What: -- Added `enabled` parameter for `modules/aws-saml/modules/okta-user/main.tf` and `modules/datadog-private-location-ecs/main.tf` +- Added `enabled` parameter for `modules/aws-saml/modules/okta-user/main.tf` and + `modules/datadog-private-location-ecs/main.tf` ### Why: - No support for disabling the creation of the resources - - - ## 1.274.1 (2023-08-09T00:11:55Z) ### 🚀 Enhancements @@ -770,7 +776,8 @@ and ArgoCD ### What: -- Updated `bastion`, `redshift`, `rds`, `spacelift`, and `vpc` to utilize the newest version of `cloudposse/security-group/aws` +- Updated `bastion`, `redshift`, `rds`, `spacelift`, and `vpc` to utilize the newest version of + `cloudposse/security-group/aws` ### Why: @@ -780,27 +787,27 @@ and ArgoCD - [AWS Security Group Component](https://github.com/cloudposse/terraform-aws-security-group/compare/2.0.0-rc1...2.2.0) - - ## 1.274.0 (2023-08-08T17:03:41Z)
bug: update descriptions *_account_account_name variables @sgtoj (#801) ### what -* update descriptions `*_account_account_name` variables + +- update descriptions `*_account_account_name` variables - I replaced `stage` with `short` because that is the description used for the respective `outputs` entries ### why -* to help future implementors of CloudPosse's architectures + +- to help future implementors of CloudPosse's architectures ### references -* n/a -
+- n/a + ## 1.273.0 (2023-08-08T17:01:23Z) @@ -808,54 +815,57 @@ and ArgoCD docs: fix issue with eks/cluster usage snippet @sgtoj (#796) ### what + - update usage snippet in readme for `eks/cluster` component ### why + - fix incorrect shape for one of the items in `aws_team_roles_rbac` - improve consistency - remove variables that are not appliable for the component ### references -- n/a +- n/a - ## 1.272.0 (2023-08-08T17:00:32Z)
feat: filter out “SUSPENDED” accounts for account-map @sgtoj (#800) ### what -* filter out “SUSPENDED” accounts (aka accounts in waiting period for termination) for `account-map` component + +- filter out “SUSPENDED” accounts (aka accounts in waiting period for termination) for `account-map` component ### why -* suspended account cannot be used, so therefore it should not exist in the account-map -* allows for new _active_ accounts with same exact name of suspended account to exists and work with `account-map` + +- suspended account cannot be used, so therefore it should not exist in the account-map +- allows for new _active_ accounts with same exact name of suspended account to exists and work with `account-map` ### references -* n/a +- n/a
- ## 1.271.0 (2023-08-08T16:44:18Z)
`eks/karpenter` Readme.md update @Benbentwo (#792) ### what -* Adding Karpenter troubleshooting to readme -* Adding https://endoflife.date/amazon-eks to `EKS/Cluster` + +- Adding Karpenter troubleshooting to readme +- Adding https://endoflife.date/amazon-eks to `EKS/Cluster` ### references -* https://karpenter.sh/docs/troubleshooting/ -* https://endoflife.date/amazon-eks -
+- https://karpenter.sh/docs/troubleshooting/ +- https://endoflife.date/amazon-eks + ## 1.270.0 (2023-08-07T21:54:49Z) @@ -882,10 +892,6 @@ and ArgoCD - Replace with add-ons - Was not being maintained or used - - - -
@@ -897,14 +903,18 @@ and ArgoCD ### why -- Until now, we provisioned StorageClasses as a part of deploying [eks/ebs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/ebs-controller/main.tf#L20-L56) and [eks/efs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/efs-controller/main.tf#L48-L60). However, with the switch from deploying "self-managed" controllers to EKS add-ons, we no longer deploy `eks/ebs-controller` or `eks/efs-controller`. Therefore, we need a new component to manage StorageClasses independently of controllers. - +- Until now, we provisioned StorageClasses as a part of deploying + [eks/ebs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/ebs-controller/main.tf#L20-L56) + and + [eks/efs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/efs-controller/main.tf#L48-L60). + However, with the switch from deploying "self-managed" controllers to EKS add-ons, we no longer deploy + `eks/ebs-controller` or `eks/efs-controller`. Therefore, we need a new component to manage StorageClasses + independently of controllers. ### references - #723 -
@@ -916,179 +926,186 @@ and ArgoCD ### why -- Upgrading Karpenter to v0.28.0 requires updating CRDs, which is not handled by current Helm chart. This script updates them by modifying the existing CRDs to be labeled as being managed by Helm, then installing the `karpenter-crd` Helm chart. +- Upgrading Karpenter to v0.28.0 requires updating CRDs, which is not handled by current Helm chart. This script updates + them by modifying the existing CRDs to be labeled as being managed by Helm, then installing the `karpenter-crd` Helm + chart. ### references - Karpenter [CRD Upgrades](https://karpenter.sh/docs/upgrade-guide/#custom-resource-definition-crd-upgrades) - - -
- ## 1.269.0 (2023-08-03T20:47:56Z)
upstream `api-gateway` and `api-gateway-settings` @Benbentwo (#788) ### what -* Upstream api-gateway and it's corresponding settings component +- Upstream api-gateway and it's corresponding settings component
- ## 1.268.0 (2023-08-01T05:04:37Z)
Added new variable into `argocd-repo` component to configure ArgoCD's `ignore-differences` @zdmytriv (#785) ### what -* Added new variable into `argocd-repo` component to configure ArcoCD `ignore-differences` + +- Added new variable into `argocd-repo` component to configure ArcoCD `ignore-differences` ### why -* There are cases when application and/or third-party operators might want to change k8s API objects. For example, change the number of replicas in deployment. This will conflict with ArgoCD application because the ArgoCD controller will spot drift and will try to make an application in sync with the codebase. + +- There are cases when application and/or third-party operators might want to change k8s API objects. For example, + change the number of replicas in deployment. This will conflict with ArgoCD application because the ArgoCD controller + will spot drift and will try to make an application in sync with the codebase. ### references -* https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs +- https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs
- ## 1.267.0 (2023-07-31T19:41:43Z)
Spacelift `admin-stack` `var.description` @milldr (#787) ### what + - added missing description option ### why + - Variable is defined, but never passed to the modules ### references -n/a +n/a
- ## 1.266.0 (2023-07-29T18:00:25Z)
Use s3_object_ownership variable @sjmiller609 (#779) ### what -* Pass s3_object_ownership variable into s3 module + +- Pass s3_object_ownership variable into s3 module ### why -* I think it was accidentally not included -* Make possible to disable ACL from stack config -### references +- I think it was accidentally not included +- Make possible to disable ACL from stack config -* https://github.com/cloudposse/terraform-aws-s3-bucket/releases/tag/3.1.0 +### references +- https://github.com/cloudposse/terraform-aws-s3-bucket/releases/tag/3.1.0
- ## 1.265.0 (2023-07-28T21:35:14Z)
`bastion` support for `availability_zones` and public IP and subnets @milldr (#783) ### what + - Add support for `availability_zones` - Fix issue with public IP and subnets - `tflint` requirements -- removed all unused locals, variables, formatting ### why + - All instance types are not available in all AZs in a region - Bug fix ### references -- [Internal Slack reference](https://cloudposse.slack.com/archives/C048LCN8LKT/p1689085395494969) +- [Internal Slack reference](https://cloudposse.slack.com/archives/C048LCN8LKT/p1689085395494969)
- ## 1.264.0 (2023-07-28T18:57:28Z)
Aurora Resource Submodule Requirements @milldr (#775) ### what -- Removed unnecessary requirement for aurora resources for the service name not to equal the user name for submodules of both aurora resource components + +- Removed unnecessary requirement for aurora resources for the service name not to equal the user name for submodules of + both aurora resource components ### why -- This conditional doesn't add any value besides creating an unnecessary restriction. We should be able to create a user name as the service name if we want + +- This conditional doesn't add any value besides creating an unnecessary restriction. We should be able to create a user + name as the service name if we want ### references -- n/a +- n/a
- ## 1.263.0 (2023-07-28T18:12:30Z)
fix: restore notifications config in argocd @dudymas (#782) ### what -* Restore ssm configuration options for argocd notifications + +- Restore ssm configuration options for argocd notifications ### why -* notifications were not firing and tasks time out in some installations +- notifications were not firing and tasks time out in some installations
- ## 1.262.0 (2023-07-27T17:05:37Z)
Upstream `spa-s3-cloudfront` @milldr (#780) ### what + - Update module - Add Cloudfront Invalidation permission to GitHub policy ### why + - Corrected bug in the module - Allow GitHub Actions to run invalidations ### references -- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/288 +- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/288
- ## 1.261.0 (2023-07-26T16:20:37Z)
Upstream `spa-s3-cloudfront` @milldr (#778) ### what + - Upstream changes to `spa-s3-cloudfront` ### why + - Updated the included modules to support Terraform v5 - Handle disabled WAF from remote-state ### references -- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/284 +- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/284
- ## 1.260.1 (2023-07-25T05:10:20Z) ### 🚀 Enhancements @@ -1108,11 +1125,8 @@ n/a - tflint fix - tflint fix - - - ### 🐛 Bug Fixes
@@ -1130,182 +1144,184 @@ n/a - tflint fix - tflint fix - -
- ## 1.260.0 (2023-07-23T23:08:53Z)
Update `alb` component @aknysh (#773) ### what -* Update `alb` component + +- Update `alb` component ### why -* Fixes after provisioning and testing on AWS +- Fixes after provisioning and testing on AWS
- ## 1.259.0 (2023-07-20T04:32:13Z)
`elasticsearch` DNS Component Lookup @milldr (#769) ### what + - add environment for `dns-delegated` component lookup ### why + - `elasticsearch` is deployed in a regional environment, but `dns-delegated` is deployed to `gbl` ### references -- n/a +- n/a
- ## 1.258.0 (2023-07-20T04:17:31Z)
Bump `lambda-elasticsearch-cleanup` module @milldr (#768) ### what + - bump version of `lambda-elasticsearch-cleanup` module ### why + - Support Terraform provider v5 ### references + - https://github.com/cloudposse/terraform-aws-lambda-elasticsearch-cleanup/pull/48
- ## 1.257.0 (2023-07-20T03:04:51Z)
Bump ECS cluster module @max-lobur (#752) ### what -* Update ECS cluster module - -### why -* Maintenance +- Update ECS cluster module +### why +- Maintenance
- ## 1.256.0 (2023-07-18T23:57:44Z)
Bump `elasticache-redis` Module @milldr (#767) ### what + - Bump `elasticache-redis` module ### why + - Resolve issues with terraform provider v5 ### references -- https://github.com/cloudposse/terraform-aws-elasticache-redis/issues/199 +- https://github.com/cloudposse/terraform-aws-elasticache-redis/issues/199
- ## 1.255.0 (2023-07-18T22:53:51Z)
Aurora Postgres Enhanced Monitoring Input @milldr (#766) ### what + - Added `enhanced_monitoring_attributes` as option - Set default `aurora-mysql` component name ### why + - Set this var with a custom value to avoid IAM role length restrictions (default unchanged) - Set common value as default ### references -- n/a +- n/a
- ## 1.254.0 (2023-07-18T21:00:30Z)
feat: acm no longer requires zone @dudymas (#765) ### what -* `acm` only looks up zones if `process_domain_validation_options` is true + +- `acm` only looks up zones if `process_domain_validation_options` is true ### why -* Allow external validation of acm certs +- Allow external validation of acm certs
- ## 1.253.0 (2023-07-18T17:45:16Z)
`alb` and `ssm-parameters` Upstream for Basic Use @milldr (#763) ### what + - `alb` component can get the ACM cert from either `dns-delegated` or `acm` - Support deploying `ssm-parameters` without SOPS - `waf` requires a value for `visibility_config` in the stack catalog ### why + - resolving bugs while deploying example components ### references + - https://cloudposse.atlassian.net/browse/JUMPSTART-1185
- ## 1.252.0 (2023-07-18T16:14:23Z)
fix: argocd flags, versions, and expressions @dudymas (#753) ### what -* adjust expressions in argocd -* update helmchart module -* tidy up variables + +- adjust expressions in argocd +- update helmchart module +- tidy up variables ### why -* component wouldn't run +- component wouldn't run
- ## 1.251.0 (2023-07-15T03:47:29Z)
fix: ecs capacity provider typing @dudymas (#762) ### what -* Adjust typing of `capacity_providers_ec2` + +- Adjust typing of `capacity_providers_ec2` ### why -* Component doesn't work without these fixes +- Component doesn't work without these fixes
- ## 1.250.3 (2023-07-15T00:31:40Z) ### 🚀 Enhancements @@ -1314,16 +1330,16 @@ n/a Update `alb` and `eks/alb-controller` components @aknysh (#760) ### what -* Update `alb` and `eks/alb-controller` components + +- Update `alb` and `eks/alb-controller` components ### why -* Remove unused variables and locals -* Apply variables that are defined in `variables.tf` but were not used +- Remove unused variables and locals +- Apply variables that are defined in `variables.tf` but were not used - ## 1.250.2 (2023-07-14T23:34:14Z) ### 🚀 Enhancements @@ -1337,16 +1353,16 @@ n/a ### why -Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy against assuming roles in the `identity` account. - -AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the previous restriction is both not needed and actually hinders desired operation. - - +Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the +originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy +against assuming roles in the `identity` account. +AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team +structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the +previous restriction is both not needed and actually hinders desired operation. - ### 🐛 Bug Fixes
@@ -1358,16 +1374,16 @@ AWS has removed that implied permission and now requires all roles to have expli ### why -Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy against assuming roles in the `identity` account. - -AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the previous restriction is both not needed and actually hinders desired operation. - - +Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the +originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy +against assuming roles in the `identity` account. +AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team +structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the +previous restriction is both not needed and actually hinders desired operation.
- ## 1.250.1 (2023-07-14T02:14:46Z) ### 🚀 Enhancements @@ -1388,43 +1404,52 @@ AWS has removed that implied permission and now requires all roles to have expli ### why - Bug Fix: Input was there, but was being ignored, leading to unexpected behavior -- If a requirement that had a default value was not supplied, Terraform would fail with an error about inconsistent plans because Karpenter would fill in the default +- If a requirement that had a default value was not supplied, Terraform would fail with an error about inconsistent + plans because Karpenter would fill in the default - Show some default values and how to override them - Reduce the burden of supplying empty fields - - ## 1.250.0 (2023-07-14T02:10:46Z)
Add EKS addons and the required IRSA to the `eks` component @aknysh (#723) ### what -* Deprecate the `eks-iam` component -* Add EKS addons and the required IRSA for the addons to the `eks` component -* Add ability to specify configuration values and timeouts for addons -* Add ability to deploy addons to Fargate when necessary -* Add ability to omit specifying Availability Zones and infer them from private subnets -* Add recommended but optional and requiring opt-in: use a single Fargate Pod Execution Role for all Fargate Profiles + +- Deprecate the `eks-iam` component +- Add EKS addons and the required IRSA for the addons to the `eks` component +- Add ability to specify configuration values and timeouts for addons +- Add ability to deploy addons to Fargate when necessary +- Add ability to omit specifying Availability Zones and infer them from private subnets +- Add recommended but optional and requiring opt-in: use a single Fargate Pod Execution Role for all Fargate Profiles ### why -* The `eks-iam` component is not in use (we now create the IAM roles for Kubernetes Service Accounts in the https://github.com/cloudposse/terraform-aws-helm-release module), and has very old and outdated code -* AWS recommends to provision the required EKS addons and not to rely on the managed addons (some of which are automatically provisioned by EKS on a cluster) +- The `eks-iam` component is not in use (we now create the IAM roles for Kubernetes Service Accounts in the + https://github.com/cloudposse/terraform-aws-helm-release module), and has very old and outdated code + +- AWS recommends to provision the required EKS addons and not to rely on the managed addons (some of which are + automatically provisioned by EKS on a cluster) -* Some EKS addons (e.g. `vpc-cni` and `aws-ebs-csi-driver`) require an IAM Role for Kubernetes Service Account (IRSA) with specific permissions. Since these addons are critical for cluster functionality, we create the IRSA roles for the addons in the `eks` component and provide the role ARNs to the addons +- Some EKS addons (e.g. `vpc-cni` and `aws-ebs-csi-driver`) require an IAM Role for Kubernetes Service Account (IRSA) + with specific permissions. Since these addons are critical for cluster functionality, we create the IRSA roles for the + addons in the `eks` component and provide the role ARNs to the addons -* Some EKS addons can be configured. In particular, `coredns` requires configuration to enable it to be deployed to Fargate. +- Some EKS addons can be configured. In particular, `coredns` requires configuration to enable it to be deployed to + Fargate. -* Users relying on Karpenter to deploy all nodes and wanting to deploy `coredns` or `aws-ebs-csi-driver` addons need to deploy them to Fargate or else the EKS deployment will fail. +- Users relying on Karpenter to deploy all nodes and wanting to deploy `coredns` or `aws-ebs-csi-driver` addons need to + deploy them to Fargate or else the EKS deployment will fail. -* Enable DRY specification of Availability Zones, and use of AZ IDs, by reading the VPCs AZs. +- Enable DRY specification of Availability Zones, and use of AZ IDs, by reading the VPCs AZs. -* A cluster needs only one Fargate Pod Execution Role, and it was a mistake to provision one for every profile. However, making the change would break existing clusters, so it is optional and requires opt-in. +- A cluster needs only one Fargate Pod Execution Role, and it was a mistake to provision one for every profile. However, + making the change would break existing clusters, so it is optional and requires opt-in. ### references + - https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html - https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on - https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html @@ -1436,10 +1461,8 @@ AWS has removed that implied permission and now requires all roles to have expli - https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role - https://github.com/kubernetes-sigs/aws-ebs-csi-driver -
- ## 1.249.0 (2023-07-14T01:23:37Z)
@@ -1451,134 +1474,142 @@ AWS has removed that implied permission and now requires all roles to have expli ### why -- When setting `default_ingress_enabled = true` it is a reasonable expectation that the deployed Ingress be marked as the Default Ingress. The previous code suggests this was the intended behavior, but does not work with the current Helm chart and may have never worked. - - +- When setting `default_ingress_enabled = true` it is a reasonable expectation that the deployed Ingress be marked as + the Default Ingress. The previous code suggests this was the intended behavior, but does not work with the current + Helm chart and may have never worked.
- ## 1.248.0 (2023-07-13T00:21:29Z)
Upstream `gitops` Policy Update @milldr (#757) ### what + - allow actions on table resources ### why + - required to be able to query using a global secondary index ### references -- https://github.com/cloudposse/github-action-terraform-plan-storage/pull/16 +- https://github.com/cloudposse/github-action-terraform-plan-storage/pull/16
- ## 1.247.0 (2023-07-12T19:32:33Z)
Update `waf` and `alb` components @aknysh (#755) ### what -* Update `waf` component -* Update `alb` component + +- Update `waf` component +- Update `alb` component ### why -* For `waf` component, add missing features supported by the following resources: + +- For `waf` component, add missing features supported by the following resources: + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl - - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration + +- For `waf` component, remove deprecated features not supported by Terraform `aws` provider v5: -* For `waf` component, remove deprecated features not supported by Terraform `aws` provider v5: - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade#resourceaws_wafv2_web_acl - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade#resourceaws_wafv2_web_acl_logging_configuration -* For `waf` component, allow specifying a list of Atmos components to read from the remote state and associate their ARNs with the web ACL +- For `waf` component, allow specifying a list of Atmos components to read from the remote state and associate their + ARNs with the web ACL -* For `alb` component, update the modules to the latest versions and allow specifying Atmos component names for the remote state in the variables (for the cases where the Atmos component names are not standard) +- For `alb` component, update the modules to the latest versions and allow specifying Atmos component names for the + remote state in the variables (for the cases where the Atmos component names are not standard) ### references -* https://github.com/cloudposse/terraform-aws-waf/pull/45 +- https://github.com/cloudposse/terraform-aws-waf/pull/45
- ## 1.246.0 (2023-07-12T18:57:58Z)
`acm` Upstream @Benbentwo (#756) ### what -* Upstream ACM -### why -* New Variables - * `subject_alternative_names_prefixes` - * `domain_name_prefix` +- Upstream ACM +### why +- New Variables + - `subject_alternative_names_prefixes` + - `domain_name_prefix`
- ## 1.245.0 (2023-07-11T19:36:11Z)
Bump `spaces` module versions @milldr (#754) ### what + - bumped module version for `terraform-spacelift-cloud-infrastructure-automation` ### why + - New policy added to `spaces` ### references -- https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/1.1.0 +- https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/1.1.0
- ## 1.244.0 (2023-07-11T17:50:19Z)
Upstream Spacelift and Documentation @milldr (#732) ### what + - Minor corrections to spacelift components - Documentation ### why + - Deployed this at a customer and resolved the changed errors - Adding documentation for updated Spacelift design ### references -- n/a +- n/a
- ## 1.243.0 (2023-07-06T20:04:08Z)
Upstream `gitops` @milldr (#735) ### what + - Upstream new component, `gitops` ### why -- This component is used to create a role for GitHub to assume. This role is used to assume the `gitops` team and is required for enabling GitHub Action Terraform workflows + +- This component is used to create a role for GitHub to assume. This role is used to assume the `gitops` team and is + required for enabling GitHub Action Terraform workflows ### references -- JUMPSTART-904 +- JUMPSTART-904
- ## 1.242.1 (2023-07-05T19:46:08Z) ### 🚀 Enhancements @@ -1587,16 +1618,15 @@ AWS has removed that implied permission and now requires all roles to have expli Use the new subnets data source @max-lobur (#737) ### what -* Use the new subnets data source -### why -* Planned migration according to https://github.com/hashicorp/terraform-provider-aws/pull/18803 +- Use the new subnets data source +### why +- Planned migration according to https://github.com/hashicorp/terraform-provider-aws/pull/18803 - ## 1.242.0 (2023-07-05T17:05:57Z)
@@ -1608,29 +1638,32 @@ AWS has removed that implied permission and now requires all roles to have expli ### why -- PR #715 removed outputs from `account-map` that `iam-roles` relied on. Although it removed the references in `iam-roles`, this imposed an ordering on the upgrade: the `iam-roles` code had to be deployed before the module could be applied. That proved to be inconvenient. Furthermore, if a future `account-map` upgrade added outputs that iam-roles` required, neither order of operations would go smoothly. With this update, the standard practice of applying `account-map` before deploying code will work again. - - +- PR #715 removed outputs from `account-map` that `iam-roles` relied on. Although it removed the references in + `iam-roles`, this imposed an ordering on the upgrade: the `iam-roles` code had to be deployed before the module could + be applied. That proved to be inconvenient. Furthermore, if a future `account-map` upgrade added outputs that + iam-roles`required, neither order of operations would go smoothly. With this update, the standard practice of applying`account-map` + before deploying code will work again.
- ## 1.241.0 (2023-07-05T16:52:58Z)
Fixed broken links in READMEs @zdmytriv (#749) ### what -* Fixed broken links in READMEs + +- Fixed broken links in READMEs ### why -* Fixed broken links in READMEs + +- Fixed broken links in READMEs ### references -* https://github.com/cloudposse/terraform-aws-components/issues/747 -
+- https://github.com/cloudposse/terraform-aws-components/issues/747 + ## 1.240.1 (2023-07-04T04:54:28Z) @@ -1638,10 +1671,12 @@ AWS has removed that implied permission and now requires all roles to have expli This fixes issues with `aws-sso` and `github-oidc-provider`. Versions from v1.227 through v1.240 should not be used. -After installing this version of `aws-sso`, you may need to change the configuration in your stacks. See [modules/aws-sso/changelog](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/aws-sso/CHANGELOG.md) for more information. Note: this release is from PR #740 +After installing this version of `aws-sso`, you may need to change the configuration in your stacks. See +[modules/aws-sso/changelog](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/aws-sso/CHANGELOG.md) +for more information. Note: this release is from PR #740 - -After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. See the release notes for v1.238.1 for more information. +After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. See +the release notes for v1.238.1 for more information. ### 🐛 Bug Fixes @@ -1649,19 +1684,21 @@ After installing this version of `github-oidc-provider`, you may need to change bugfix `aws-sso`, `github-oidc-provider` @Benbentwo (#740) ### what -* Bugfixes `filter` depreciation issue via module update to `1.1.1` -* Bugfixes missing `aws.root` provider -* Bugfixes `github-oidc-provider` v1.238.1 + +- Bugfixes `filter` depreciation issue via module update to `1.1.1` +- Bugfixes missing `aws.root` provider +- Bugfixes `github-oidc-provider` v1.238.1 ### why -* Bugfixes + +- Bugfixes ### references -* https://github.com/cloudposse/terraform-aws-sso/pull/44 -* closes #744 - +- https://github.com/cloudposse/terraform-aws-sso/pull/44 +- closes #744 + ## 1.240.0 (2023-07-03T18:14:14Z) @@ -1673,32 +1710,32 @@ After installing this version of `github-oidc-provider`, you may need to change I'm too lazy to fix it each time when we get module updates via `atmos vendor` GHA ### References -* https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_deprecated_index.md -* https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_comment_syntax.md -* https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md +- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_deprecated_index.md +- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_comment_syntax.md +- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md - ## 1.239.0 (2023-06-29T23:34:53Z)
Bump `cloudposse/ec2-autoscale-group/aws` to `0.35.0` @milldr (#734) ### what + - bumped ASG module version, `cloudposse/ec2-autoscale-group/aws` to `0.35.0` ### why + - Recent versions of this module resolve errors for these components ### references -- https://github.com/cloudposse/terraform-aws-ec2-autoscale-group +- https://github.com/cloudposse/terraform-aws-ec2-autoscale-group
- ## 1.238.1 (2023-06-29T21:15:50Z) ### Upgrade notes: @@ -1707,24 +1744,29 @@ There is a bug in this version of `github-oidc-provider`. Upgrade to version v1. After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. -- If you have dynamic Terraform roles enabled, then this should be configured like a normal component. The previous component may have required you to set - - ```yaml - backend: - s3: - role_arn: null - ```` -and **that configuration should be removed** everywhere. -- If you only use SuperAdmin to deploy things to the `identity` account, then for the `identity` (and `root`, if applicable) account ***only***, set - - ```yaml - backend: - s3: - role_arn: null - vars: - superadmin: true - ```` -**Deployments to other accounts should not have any of those settings**. +- If you have dynamic Terraform roles enabled, then this should be configured like a normal component. The previous + component may have required you to set + + ```yaml + backend: + s3: + role_arn: null + ```` + + and **that configuration should be removed** everywhere. + +- If you only use SuperAdmin to deploy things to the `identity` account, then for the `identity` (and `root`, if + applicable) account **_only_**, set + + ```yaml + backend: + s3: + role_arn: null + vars: + superadmin: true + ```` + + **Deployments to other accounts should not have any of those settings**. ### 🚀 Enhancements @@ -1733,14 +1775,12 @@ and **that configuration should be removed** everywhere. ### what && why -- This updates `provider.tf` to provide compatibility with various legacy configurations as well as the current reference architecture +- This updates `provider.tf` to provide compatibility with various legacy configurations as well as the current + reference architecture - This update does NOT require updating `account-map` - - - ## 1.238.0 (2023-06-29T19:39:15Z)
@@ -1754,7 +1794,8 @@ and **that configuration should be removed** everywhere. ### why -- Reduce the friction between SSO permission sets and SAML roles by allowing people to use either interchangeably. (Almost. SSO permission sets do not yet have the same permissions as SAML roles in the `identity` account itself.) +- Reduce the friction between SSO permission sets and SAML roles by allowing people to use either interchangeably. + (Almost. SSO permission sets do not yet have the same permissions as SAML roles in the `identity` account itself.) - Enable continued access in the event of a regional outage in us-east-1 as happened recently - Enable auditing of who is using assumed roles @@ -1767,70 +1808,84 @@ and **that configuration should be removed** everywhere. ### Upgrade notes -The regional endpoints and Source Identity support are non-controversial and cannot be disabled. They do, however, require running `terraform apply` against `aws-saml`, `aws-teams`, and `aws-team-roles` in all accounts. +The regional endpoints and Source Identity support are non-controversial and cannot be disabled. They do, however, +require running `terraform apply` against `aws-saml`, `aws-teams`, and `aws-team-roles` in all accounts. #### AWS SSO updates -To enable SSO Permission Sets to function as teams, you need to update `account-map` and `aws-sso`, then apply changes to +To enable SSO Permission Sets to function as teams, you need to update `account-map` and `aws-sso`, then apply changes +to + - `tfstate-backend` - `aws-teams` - `aws-team-roles` - `aws-sso` -This is all enabled by default. If you do not want it, you only need to update `account-map`, and add `account-map/modules/roles-to-principles/variables_override.tf` in which you set +This is all enabled by default. If you do not want it, you only need to update `account-map`, and add +`account-map/modules/roles-to-principles/variables_override.tf` in which you set `overridable_team_permission_sets_enabled` to default to `false` -Under the old `iam-primary-roles` component, corresponding permission sets were named `IdentityRoleAccess`. Under the current `aws-teams` component, they are named `IdentityTeamAccess`. The current `account-map` defaults to the latter convention. To use the earlier convention, add `account-map/modules/roles-to-principles/variables_override.tf` in which you set `overridable_team_permission_set_name_pattern` to default to `"Identity%sRoleAccess"` +Under the old `iam-primary-roles` component, corresponding permission sets were named `IdentityRoleAccess`. Under +the current `aws-teams` component, they are named `IdentityTeamAccess`. The current `account-map` defaults to the +latter convention. To use the earlier convention, add `account-map/modules/roles-to-principles/variables_override.tf` in +which you set `overridable_team_permission_set_name_pattern` to default to `"Identity%sRoleAccess"` -There is a chance the resulting trust policies will be too big, especially for `tfstate-backend`. If you get an error like +There is a chance the resulting trust policies will be too big, especially for `tfstate-backend`. If you get an error +like ``` Cannot exceed quota for ACLSizePerRole: 2048 ``` -You need to request a quota increase (Quota Code L-C07B4B0D), which will be automatically granted, usually in about 5 minutes. The max quota is 4096, but we recommend increasing it to 3072 first, so you retain some breathing room for the future. - +You need to request a quota increase (Quota Code L-C07B4B0D), which will be automatically granted, usually in about 5 +minutes. The max quota is 4096, but we recommend increasing it to 3072 first, so you retain some breathing room for the +future.
- ## 1.237.0 (2023-06-27T22:27:49Z)
Add Missing `github-oidc-provider` Thumbprint @milldr (#736) ### what + - include both thumbprints for GitHub OIDC ### why -- There are two possible intermediary certificates for the Actions SSL certificate and either can be returned by Github's servers, requiring customers to trust both. This is a known behavior when the intermediary certificates are cross-signed by the CA. + +- There are two possible intermediary certificates for the Actions SSL certificate and either can be returned by + Github's servers, requiring customers to trust both. This is a known behavior when the intermediary certificates are + cross-signed by the CA. ### references -- https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/ +- https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/
- ## 1.236.0 (2023-06-26T18:14:29Z)
Update `eks/echo-server` and `eks/alb-controller-ingress-group` components @aknysh (#733) ### what -* Update `eks/echo-server` and `eks/alb-controller-ingress-group` components -* Allow specifying [alb.ingress.kubernetes.io/scheme](https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/#scheme) (`internal` or `internet-facing`) + +- Update `eks/echo-server` and `eks/alb-controller-ingress-group` components +- Allow specifying + [alb.ingress.kubernetes.io/scheme](https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/#scheme) + (`internal` or `internet-facing`) ### why -* Allow the echo server to work with internal load balancers + +- Allow the echo server to work with internal load balancers ### references -* https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/ +- https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/
- ## 1.235.0 (2023-06-22T21:06:18Z)
@@ -1844,14 +1899,13 @@ You need to request a quota increase (Quota Code L-C07B4B0D), which will be auto ### why -- Previously, when the global `account-map` `profiles_enabled` flag was `true`, `iam_roles.terraform_role_arn` would be null. However, `eks/cluster` requires `terraform_role_arn` regardless. -- Changes made in #728 work in environments that have not adopted dynamic Terraform roles but would fail in environments that have (when using SuperAdmin) - - +- Previously, when the global `account-map` `profiles_enabled` flag was `true`, `iam_roles.terraform_role_arn` would be + null. However, `eks/cluster` requires `terraform_role_arn` regardless. +- Changes made in #728 work in environments that have not adopted dynamic Terraform roles but would fail in environments + that have (when using SuperAdmin)
- ## 1.234.0 (2023-06-21T22:44:55Z)
@@ -1863,53 +1917,56 @@ You need to request a quota increase (Quota Code L-C07B4B0D), which will be auto ### why -- Historically, the `terraform` roles in `root` and `identity` were not used for Terraform plan/apply, but for other things, and so the `terraform_roles` map output selected the `admin` roles for those accounts. This "wart" has been remove in current `aws-team-roles` and `tfstate-backend` configurations, but for people who do not want to migrate to the new conventions, this feature flag enables them to maintain the status quo with respect to role usage while taking advantage of other updates to `account-map` and other components. +- Historically, the `terraform` roles in `root` and `identity` were not used for Terraform plan/apply, but for other + things, and so the `terraform_roles` map output selected the `admin` roles for those accounts. This "wart" has been + remove in current `aws-team-roles` and `tfstate-backend` configurations, but for people who do not want to migrate to + the new conventions, this feature flag enables them to maintain the status quo with respect to role usage while taking + advantage of other updates to `account-map` and other components. ### references -This update is recommended for all customers wanting to use ***any*** component version 1.227 or later. +This update is recommended for all customers wanting to use **_any_** component version 1.227 or later. - #715 -
- ## 1.233.0 (2023-06-21T20:03:36Z)
[lambda] feat: allows to use YAML instead of JSON for IAM policy @gberenice (#692) ### what -* BREAKING CHANGE: Actually use variable `function_name` to set the lambda function name. -* Make the variable `function_name` optional. When not set, the old null-lable-derived name will be use. -* Allow IAM policy to be specified in a custom terraform object as an alternative to JSON. - -### why -* `function_name` was required to set, but it wasn't actually passed to `module "lambda"` inputs. -* Allow callers to stop providing `function_name` and preserve old behavior of using automatically generated name. -* When using [Atmos](https://atmos.tools/) to generate inputs from "stack" YAML files, having the ability to pass the statements in as a custom object means specifying them via YAML, which makes the policy declaration in stack more readable compared to embedding a JSON string in the YAML. - +- BREAKING CHANGE: Actually use variable `function_name` to set the lambda function name. +- Make the variable `function_name` optional. When not set, the old null-lable-derived name will be use. +- Allow IAM policy to be specified in a custom terraform object as an alternative to JSON. +### why +- `function_name` was required to set, but it wasn't actually passed to `module "lambda"` inputs. +- Allow callers to stop providing `function_name` and preserve old behavior of using automatically generated name. +- When using [Atmos](https://atmos.tools/) to generate inputs from "stack" YAML files, having the ability to pass the + statements in as a custom object means specifying them via YAML, which makes the policy declaration in stack more + readable compared to embedding a JSON string in the YAML.
- ## 1.232.0 (2023-06-21T15:49:06Z)
refactor securityhub component @mcalhoun (#728) ### what -* Refactor the Security Hub components into a single component + +- Refactor the Security Hub components into a single component ### why -* To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. -
+- To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. + ## 1.231.0 (2023-06-21T14:54:50Z) @@ -1917,10 +1974,12 @@ This update is recommended for all customers wanting to use ***any*** component roll guard duty back to previous providers logic @mcalhoun (#727) ### what -* Roll the Guard Duty component back to using the previous logic for role assumption. + +- Roll the Guard Duty component back to using the previous logic for role assumption. ### why -* The newer method is causing the provider to try to assume the role twice. We get the error: + +- The newer method is causing the provider to try to assume the role twice. We get the error: ``` AWS Error: operation error STS: AssumeRole, https response error StatusCode: 403, RequestID: 00000000-0000-0000-0000-00000000, api error AccessDenied: User: arn:aws:sts::000000000000:assumed-role/acme-core-gbl-security-terraform/aws-go-sdk-1687312396297825294 is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::000000000000:role/acme-core-gbl-security-terraform @@ -1928,20 +1987,20 @@ AWS Error: operation error STS: AssumeRole, https response error StatusCode: 403 - ## 1.230.0 (2023-06-21T01:49:52Z)
refactor guardduty module @mcalhoun (#725) ### what -* Refactor the GuardDuty components into a single component + +- Refactor the GuardDuty components into a single component ### why -* To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. -
+- To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. + ## 1.229.0 (2023-06-20T19:37:35Z) @@ -1949,16 +2008,15 @@ AWS Error: operation error STS: AssumeRole, https response error StatusCode: 403 upstream `github-action-runners` dockerhub authentication @Benbentwo (#726) ### what -* Adds support for dockerhub authentication -### why -* Dockerhub limits are unrealistically low for actually using dockerhub as an image registry for automated builds +- Adds support for dockerhub authentication +### why +- Dockerhub limits are unrealistically low for actually using dockerhub as an image registry for automated builds - ## 1.228.0 (2023-06-15T20:57:45Z)
@@ -1966,24 +2024,22 @@ AWS Error: operation error STS: AssumeRole, https response error StatusCode: 403 ### what -* Apply the HTTPS policy +- Apply the HTTPS policy ### why -* The policy was unused so it was defaulting to an old policy +- The policy was unused so it was defaulting to an old policy ### references -
- ## 1.227.0 (2023-06-12T23:41:45Z) - Possibly breaking change: -In this update, `account-map/modules/iam-roles` acquired a provider, making it no longer able to be used with `count`. If you have code like +In this update, `account-map/modules/iam-roles` acquired a provider, making it no longer able to be used with `count`. +If you have code like ```hcl module "optional_role" { @@ -1995,7 +2051,9 @@ module "optional_role" { } ``` -You will need to rewrite it, removing the `count` parameter. It will be fine to always instantiate the module. If there are problems with ensuring appropriate settings with the module is disabled, you can always replace them with the component's inputs: +You will need to rewrite it, removing the `count` parameter. It will be fine to always instantiate the module. If there +are problems with ensuring appropriate settings with the module is disabled, you can always replace them with the +component's inputs: ```hcl module "optional_role" { @@ -2005,17 +2063,27 @@ module "optional_role" { } ``` - The update to components 1.227.0 is huge, and you have options. -- Enable, or not, dynamic Terraform IAM roles, which allow you to give some people (and Spacelift) the ability to run Terraform plan in some accounts without allowing apply. Note that these users will still have read/write access to Terraform state, but will not have IAM permissions to make changes in accounts. [terraform_dynamic_role_enabled](https://github.com/cloudposse/terraform-aws-components/blob/1b338fe664e5debc5bbac30cfe42003f7458575a/modules/account-map/variables.tf#L96-L100) -- Update to new `aws-teams` team names. The new names are (except for support) distinct from team-roles, making it easier to keep track. Also, the new managers team can run Terraform for identity and root in most (but not all) cases. -- Update to new `aws-team-roles`, including new permissions. The custom policies that have been removed are replaced in the `aws-team-roles` configuration with AWS managed policy ARNs. This is required to add the `planner` role and support the `terraform plan` restriction. -- Update the `providers.tf for` all components. Or some of them now, some later. Most components do not require updates, but all of them have updates. The new `providers.tf`, when used with dynamic Terraform roles, allows users directly logged into target accounts (rather than having roles in the `identity` account) to use Terraform in that account, and also allows SuperAdmin to run Terraform in more cases (almost everywhere). - -**If you do not want any new features**, you only need to update `account-map` to v1.235 or later, to be compatible with future components. Note that when updating `account-map` this way, you should update the code everywhere (all open PRs and branches) before applying the Terraform changes, because the applied changes break the old code. - -If you want all the new features, we recommend updating all of the following to the current release in 1 PR: +- Enable, or not, dynamic Terraform IAM roles, which allow you to give some people (and Spacelift) the ability to run + Terraform plan in some accounts without allowing apply. Note that these users will still have read/write access to + Terraform state, but will not have IAM permissions to make changes in accounts. + [terraform_dynamic_role_enabled](https://github.com/cloudposse/terraform-aws-components/blob/1b338fe664e5debc5bbac30cfe42003f7458575a/modules/account-map/variables.tf#L96-L100) +- Update to new `aws-teams` team names. The new names are (except for support) distinct from team-roles, making it + easier to keep track. Also, the new managers team can run Terraform for identity and root in most (but not all) cases. +- Update to new `aws-team-roles`, including new permissions. The custom policies that have been removed are replaced in + the `aws-team-roles` configuration with AWS managed policy ARNs. This is required to add the `planner` role and + support the `terraform plan` restriction. +- Update the `providers.tf for` all components. Or some of them now, some later. Most components do not require updates, + but all of them have updates. The new `providers.tf`, when used with dynamic Terraform roles, allows users directly + logged into target accounts (rather than having roles in the `identity` account) to use Terraform in that account, and + also allows SuperAdmin to run Terraform in more cases (almost everywhere). + +**If you do not want any new features**, you only need to update `account-map` to v1.235 or later, to be compatible with +future components. Note that when updating `account-map` this way, you should update the code everywhere (all open PRs +and branches) before applying the Terraform changes, because the applied changes break the old code. + +If you want all the new features, we recommend updating all of the following to the current release in 1 PR: - account-map - aws-teams @@ -2027,35 +2095,35 @@ If you want all the new features, we recommend updating all of the following to ### Reviewers, please note: -The PR changes a lot of files. In particular, the `providers.tf` and therefore the `README.md` for nearly every component. Therefore it will likely be easier to review this PR one commit at a time. +The PR changes a lot of files. In particular, the `providers.tf` and therefore the `README.md` for nearly every +component. Therefore it will likely be easier to review this PR one commit at a time. -`import_role_arn` and `import_profile_name` have been removed as they are no longer needed. Current versions of Terraform (probably beginning with v1.1.0, but maybe as late as 1.3.0, I have not found authoritative information) can read data sources during plan and so no longer need a role to be explicitly specified while importing. Feel free to perform your own tests to make yourself more comfortable that this is correct. +`import_role_arn` and `import_profile_name` have been removed as they are no longer needed. Current versions of +Terraform (probably beginning with v1.1.0, but maybe as late as 1.3.0, I have not found authoritative information) can +read data sources during plan and so no longer need a role to be explicitly specified while importing. Feel free to +perform your own tests to make yourself more comfortable that this is correct. ### what -* Updates to allow Terraform to dynamically assume a role based on the user, to allow some users to run `terraform plan` but not `terraform apply` - * Deploy standard `providers.tf` to all components that need an `aws` provider - * Move extra provider configurations to separate file, so that `providers.tf` can - remain consistent/identical among components and thus be easily updated - * Create `provider-awsutils.mixin.tf` to provide consistent, maintainable implementation -* Make `aws-sso` vendor safe -* Deprecate `sso` module in favor of `aws-saml` - +- Updates to allow Terraform to dynamically assume a role based on the user, to allow some users to run `terraform plan` + but not `terraform apply` + - Deploy standard `providers.tf` to all components that need an `aws` provider + - Move extra provider configurations to separate file, so that `providers.tf` can remain consistent/identical among + components and thus be easily updated + - Create `provider-awsutils.mixin.tf` to provide consistent, maintainable implementation +- Make `aws-sso` vendor safe +- Deprecate `sso` module in favor of `aws-saml` ### why -- Allow users to try new code or updated configurations by running `terraform plan` without giving them permission to make changes with Terraform +- Allow users to try new code or updated configurations by running `terraform plan` without giving them permission to + make changes with Terraform - Make it easier for people directly logged into target accounts to still run Terraform - Follow-up to #697, which updated `aws-teams` and `aws-team-roles`, to make `aws-sso` consistent - Reduce confusion by moving deprecated code to `deprecated/` - - - - - ## 1.226.0 (2023-06-12T17:42:51Z)
@@ -2067,44 +2135,43 @@ Fix common issues in the repo ### why -It violates our basic checks, which adds a headache to using https://github.com/cloudposse/github-action-atmos-component-updater as is +It violates our basic checks, which adds a headache to using +https://github.com/cloudposse/github-action-atmos-component-updater as is ![image](https://github.com/cloudposse/terraform-aws-components/assets/11096782/248febbe-b65f-4080-8078-376ef576b457) -> **Note**: It is much simpler to review PR if [hide whitespace changes](https://github.com/cloudposse/terraform-aws-components/pull/714/files?w=1) +> **Note**: It is much simpler to review PR if +> [hide whitespace changes](https://github.com/cloudposse/terraform-aws-components/pull/714/files?w=1)
- ## 1.225.0 (2023-06-12T14:57:20Z)
Removed list of components from main README.md @zdmytriv (#721) ### what -* Removed list of components from main README.md -### why -* That list is outdated - -### references +- Removed list of components from main README.md +### why +- That list is outdated +### references
- ## 1.224.0 (2023-06-09T19:52:51Z)
upstream argocd @Benbentwo (#634) ### what -* Upstream fixes that allow for Google OIDC -
+- Upstream fixes that allow for Google OIDC + ## 1.223.0 (2023-06-09T14:28:08Z) @@ -2112,53 +2179,60 @@ It violates our basic checks, which adds a headache to using https://github.com/ add new spacelift components @mcalhoun (#717) ### what -* Add the newly developed spacelift components -* Deprecate the previous components + +- Add the newly developed spacelift components +- Deprecate the previous components ### why -* We undertook a process of decomposing a monolithic module and broke it into smaller, composable pieces for a better developer experience + +- We undertook a process of decomposing a monolithic module and broke it into smaller, composable pieces for a better + developer experience ### references -* Corresponding [Upstream Module PR](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/143) +- Corresponding + [Upstream Module PR](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/143) - ## 1.222.0 (2023-06-08T23:28:34Z)
Karpenter Node Interruption Handler @milldr (#713) ### what + - Added Karpenter Interruption Handler to existing component ### why + - Interruption is supported by karpenter, but we need to deploy sqs queue and event bridge rules to enable ### references -- https://github.com/cloudposse/knowledge-base/discussions/127 - +- https://github.com/cloudposse/knowledge-base/discussions/127
- ## 1.221.0 (2023-06-07T18:11:23Z)
feat: New Component `aws-ssosync` @dudymas (#625) ### what -* adds a fork of [aws-ssosync](https://github.com/awslabs/ssosync) as a lambda on a 15m cronjob + +- adds a fork of [aws-ssosync](https://github.com/awslabs/ssosync) as a lambda on a 15m cronjob ### Why -Google is one of those identity providers that doesn't have good integration with AWS SSO. In order to sync groups and users across we need to use some API calls, luckily AWS Built [aws-ssosync](https://github.com/awslabs/ssosync) to handle that. -Unfortunately, it required ASM so we use [Benbentwo/ssosync](https://github.com/Benbentwo/ssosync) as it removes that requirement. +Google is one of those identity providers that doesn't have good integration with AWS SSO. In order to sync groups and +users across we need to use some API calls, luckily AWS Built [aws-ssosync](https://github.com/awslabs/ssosync) to +handle that. -
+Unfortunately, it required ASM so we use [Benbentwo/ssosync](https://github.com/Benbentwo/ssosync) as it removes that +requirement. + ## 1.220.0 (2023-06-05T22:31:10Z) @@ -2167,41 +2241,41 @@ Unfortunately, it required ASM so we use [Benbentwo/ssosync](https://github.com/ ### what -* Set `helm_manifest_experiment_enabled` to `false` by default -* Block Kubernetes provider 2.21.0 +- Set `helm_manifest_experiment_enabled` to `false` by default +- Block Kubernetes provider 2.21.0 ### why -* The `helm_manifest_experiment_enabled` reliably breaks when a Helm chart installs CRDs. The initial reason for enabling it was for better drift detection, but the provider seems to have fixed most if not all of the drift detection issues since then. -* Kubernetes provider 2.21.0 had breaking changes which were reverted in 2.21.1. +- The `helm_manifest_experiment_enabled` reliably breaks when a Helm chart installs CRDs. The initial reason for + enabling it was for better drift detection, but the provider seems to have fixed most if not all of the drift + detection issues since then. +- Kubernetes provider 2.21.0 had breaking changes which were reverted in 2.21.1. ### references -* https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084#issuecomment-1576711378 - - +- https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084#issuecomment-1576711378 - ## 1.219.0 (2023-06-05T20:23:17Z)
Expand ECR GH OIDC Default Policy @milldr (#711) ### what + - updated default ECR GH OIDC policy ### why + - This policy should grant GH OIDC access both public and private ECR repos ### references -- https://cloudposse.slack.com/archives/CA4TC65HS/p1685993698149499?thread_ts=1685990234.560589&cid=CA4TC65HS +- https://cloudposse.slack.com/archives/CA4TC65HS/p1685993698149499?thread_ts=1685990234.560589&cid=CA4TC65HS
- ## 1.218.0 (2023-06-05T01:59:49Z)
@@ -2217,13 +2291,8 @@ Unfortunately, it required ASM so we use [Benbentwo/ssosync](https://github.com/ - Prepare for `providers.tf` updates to support dynamic Terraform roles - ARB decision on customization compatible with vendoring - - - -
- ## 1.217.0 (2023-06-04T23:11:44Z)
@@ -2233,23 +2302,20 @@ Unfortunately, it required ASM so we use [Benbentwo/ssosync](https://github.com/ For `eks/external-secrets-operator`: -* Normalize variables, update dependencies -* Exclude Kubernetes provider v2.21.0 +- Normalize variables, update dependencies +- Exclude Kubernetes provider v2.21.0 ### why -* Bring in line with other Helm-based modules -* Take advantage of improvements in dependencies +- Bring in line with other Helm-based modules +- Take advantage of improvements in dependencies ### references -* [Breaking change in Kubernetes provider v2.21.0](https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084) - - +- [Breaking change in Kubernetes provider v2.21.0](https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084)
- ## 1.216.2 (2023-06-04T23:08:39Z) ### 🚀 Enhancements @@ -2270,10 +2336,8 @@ For `eks/external-secrets-operator`: - [v5 upgrade guide](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade) - [v5.0.0 Release Notes](https://github.com/hashicorp/terraform-provider-aws/releases/tag/v5.0.0) - - ## 1.216.1 (2023-06-04T01:18:31Z) ### 🚀 Enhancements @@ -2287,15 +2351,11 @@ For `eks/external-secrets-operator`: ### why -- Currently, custom polices have to be manually added to the map in `main.tf`, but that gets overwritten with every vendor update. Putting that map in a separate, optional file allows for the custom code to survive vendoring. - - - - +- Currently, custom polices have to be manually added to the map in `main.tf`, but that gets overwritten with every + vendor update. Putting that map in a separate, optional file allows for the custom code to survive vendoring. - ## 1.216.0 (2023-06-02T18:02:01Z)
@@ -2303,98 +2363,119 @@ For `eks/external-secrets-operator`: ### what -* Added support for ssm param tiers -* Updated the minimum version to `>= 1.3.0` to support `optional` parameters +- Added support for ssm param tiers +- Updated the minimum version to `>= 1.3.0` to support `optional` parameters ### why -* `Standard` tier only supports 4096 characters. This allows Advanced and Intelligent Tiering support. +- `Standard` tier only supports 4096 characters. This allows Advanced and Intelligent Tiering support. ### references -
- ## 1.215.0 (2023-06-02T14:28:29Z)
`.editorconfig` Typo @milldr (#704) ### what + fixed intent typo ### why + should be spelled "indent" ### references -https://cloudposse.slack.com/archives/C01EY65H1PA/p1685638634845009 - +https://cloudposse.slack.com/archives/C01EY65H1PA/p1685638634845009
- ## 1.214.0 (2023-05-31T17:46:35Z)
Transit Gateway `var.connections` Redesign @milldr (#685) ### what + - Updated how the connection variables for `tgw/hub` and `tgw/spoke` are defined - Moved the old versions of `tgw` to `deprecated/tgw` ### why + - We want to be able to define multiple or alternately named `vpc` or `eks/cluster` components for both hub and spoke -- The cross-region components are not updated yet with this new design, since the current customers requesting these updates do not need cross-region access at this time. But we want to still support the old design s.t. customers using cross-region components can access the old components. We will need to update the cross-region components with follow up effort +- The cross-region components are not updated yet with this new design, since the current customers requesting these + updates do not need cross-region access at this time. But we want to still support the old design s.t. customers using + cross-region components can access the old components. We will need to update the cross-region components with follow + up effort ### references -- https://github.com/cloudposse/knowledge-base/discussions/112 - +- https://github.com/cloudposse/knowledge-base/discussions/112
- ## 1.213.0 (2023-05-31T14:50:16Z)
Introducing Security Hub @zdmytriv (#683) ### what -* Introducing Security Hub component + +- Introducing Security Hub component ### why -Amazon Security Hub enables users to centrally manage and monitor the security and compliance of their AWS accounts and resources. It aggregates, organizes, and prioritizes security findings from various AWS services, third-party tools, and integrated partner solutions. +Amazon Security Hub enables users to centrally manage and monitor the security and compliance of their AWS accounts and +resources. It aggregates, organizes, and prioritizes security findings from various AWS services, third-party tools, and +integrated partner solutions. Here are the key features and capabilities of Amazon Security Hub: -- Centralized security management: Security Hub provides a centralized dashboard where users can view and manage security findings from multiple AWS accounts and regions. This allows for a unified view of the security posture across the entire AWS environment. +- Centralized security management: Security Hub provides a centralized dashboard where users can view and manage + security findings from multiple AWS accounts and regions. This allows for a unified view of the security posture + across the entire AWS environment. -- Automated security checks: Security Hub automatically performs continuous security checks on AWS resources, configurations, and security best practices. It leverages industry standards and compliance frameworks, such as AWS CIS Foundations Benchmark, to identify potential security issues. +- Automated security checks: Security Hub automatically performs continuous security checks on AWS resources, + configurations, and security best practices. It leverages industry standards and compliance frameworks, such as AWS + CIS Foundations Benchmark, to identify potential security issues. -- Integrated partner solutions: Security Hub integrates with a wide range of AWS native services, as well as third-party security products and solutions. This integration enables the ingestion and analysis of security findings from diverse sources, offering a comprehensive security view. +- Integrated partner solutions: Security Hub integrates with a wide range of AWS native services, as well as third-party + security products and solutions. This integration enables the ingestion and analysis of security findings from diverse + sources, offering a comprehensive security view. -- Security standards and compliance: Security Hub provides compliance checks against industry standards and regulatory frameworks, such as PCI DSS, HIPAA, and GDPR. It identifies non-compliant resources and provides guidance on remediation actions to ensure adherence to security best practices. +- Security standards and compliance: Security Hub provides compliance checks against industry standards and regulatory + frameworks, such as PCI DSS, HIPAA, and GDPR. It identifies non-compliant resources and provides guidance on + remediation actions to ensure adherence to security best practices. -- Prioritized security findings: Security Hub analyzes and prioritizes security findings based on severity, enabling users to focus on the most critical issues. It assigns severity levels and generates a consolidated view of security alerts, allowing for efficient threat response and remediation. +- Prioritized security findings: Security Hub analyzes and prioritizes security findings based on severity, enabling + users to focus on the most critical issues. It assigns severity levels and generates a consolidated view of security + alerts, allowing for efficient threat response and remediation. -- Custom insights and event aggregation: Security Hub supports custom insights, allowing users to create their own rules and filters to focus on specific security criteria or requirements. It also provides event aggregation and correlation capabilities to identify related security findings and potential attack patterns. +- Custom insights and event aggregation: Security Hub supports custom insights, allowing users to create their own rules + and filters to focus on specific security criteria or requirements. It also provides event aggregation and correlation + capabilities to identify related security findings and potential attack patterns. -- Integration with other AWS services: Security Hub seamlessly integrates with other AWS services, such as AWS CloudTrail, Amazon GuardDuty, AWS Config, and AWS IAM Access Analyzer. This integration allows for enhanced visibility, automated remediation, and streamlined security operations. +- Integration with other AWS services: Security Hub seamlessly integrates with other AWS services, such as AWS + CloudTrail, Amazon GuardDuty, AWS Config, and AWS IAM Access Analyzer. This integration allows for enhanced + visibility, automated remediation, and streamlined security operations. -- Alert notifications and automation: Security Hub supports alert notifications through Amazon SNS, enabling users to receive real-time notifications of security findings. It also facilitates automation and response through integration with AWS Lambda, allowing for automated remediation actions. +- Alert notifications and automation: Security Hub supports alert notifications through Amazon SNS, enabling users to + receive real-time notifications of security findings. It also facilitates automation and response through integration + with AWS Lambda, allowing for automated remediation actions. -By utilizing Amazon Security Hub, organizations can improve their security posture, gain insights into security risks, and effectively manage security compliance across their AWS accounts and resources. +By utilizing Amazon Security Hub, organizations can improve their security posture, gain insights into security risks, +and effectively manage security compliance across their AWS accounts and resources. ### references + - https://aws.amazon.com/security-hub/ - https://github.com/cloudposse/terraform-aws-security-hub/
- ## 1.212.0 (2023-05-31T14:45:30Z)
@@ -2402,66 +2483,95 @@ By utilizing Amazon Security Hub, organizations can improve their security postu ### what -* Introducing GuardDuty component +- Introducing GuardDuty component ### why -AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by continuously monitoring for malicious activities and unauthorized behaviors. GuardDuty analyzes various data sources within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS logs, to detect potential security threats. +AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by +continuously monitoring for malicious activities and unauthorized behaviors. GuardDuty analyzes various data sources +within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS logs, to detect potential security +threats. Key features and components of AWS GuardDuty include: -- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event logs and network traffic data to detect patterns, anomalies, and known attack techniques. +- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence + to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event + logs and network traffic data to detect patterns, anomalies, and known attack techniques. -- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, domains, and other indicators of compromise. +- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global + community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, + domains, and other indicators of compromise. -- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS Lambda for immediate action or custom response workflows. +- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be + delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS + Lambda for immediate action or custom response workflows. -- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security policies and practices. +- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and + monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security + policies and practices. -- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of security incidents and reduces the need for manual intervention. +- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS + Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of + security incidents and reduces the need for manual intervention. -- Security findings and reports: GuardDuty provides detailed security findings and reports that include information about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed through the AWS Management Console or retrieved via APIs for further analysis and reporting. +- Security findings and reports: GuardDuty provides detailed security findings and reports that include information + about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed + through the AWS Management Console or retrieved via APIs for further analysis and reporting. -GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations with an additional layer of security to proactively identify and respond to potential security risks. +GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations +with an additional layer of security to proactively identify and respond to potential security risks. ### references + - https://aws.amazon.com/guardduty/ - https://github.com/cloudposse/terraform-aws-guardduty - -
- ## 1.211.0 (2023-05-30T16:30:47Z)
Upstream `aws-inspector` @milldr (#700) ### what + Upstream `aws-inspector` from past engagement ### why -* This component was never upstreamed and now were want to use it again -* AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security vulnerabilities and deviations from security best practices. Here are some key features and functionalities of AWS Inspector: - - Security Assessments: AWS Inspector performs security assessments by analyzing the behavior of your resources and identifying potential security vulnerabilities. It examines the network configuration, operating system settings, and installed software to detect common security issues. - - Vulnerability Detection: AWS Inspector uses a predefined set of rules to identify common vulnerabilities, misconfigurations, and security exposures. It leverages industry-standard security best practices and continuously updates its knowledge base to stay current with emerging threats. +- This component was never upstreamed and now were want to use it again +- AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate + the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically + assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security + vulnerabilities and deviations from security best practices. Here are some key features and functionalities of AWS + Inspector: - - Agent-Based Architecture: AWS Inspector utilizes an agent-based approach, where you install an Inspector agent on your EC2 instances. The agent collects data about the system and its configuration, securely sends it to AWS Inspector, and allows for more accurate and detailed assessments. + - Security Assessments: AWS Inspector performs security assessments by analyzing the behavior of your resources and + identifying potential security vulnerabilities. It examines the network configuration, operating system settings, + and installed software to detect common security issues. - - Security Findings: After performing an assessment, AWS Inspector generates detailed findings that highlight security vulnerabilities, including their severity level, impact, and remediation steps. These findings can help you prioritize and address security issues within your AWS environment. + - Vulnerability Detection: AWS Inspector uses a predefined set of rules to identify common vulnerabilities, + misconfigurations, and security exposures. It leverages industry-standard security best practices and continuously + updates its knowledge base to stay current with emerging threats. - - Integration with AWS Services: AWS Inspector seamlessly integrates with other AWS services, such as AWS CloudFormation, AWS Systems Manager, and AWS Security Hub. This allows you to automate security assessments, manage findings, and centralize security information across your AWS infrastructure. + - Agent-Based Architecture: AWS Inspector utilizes an agent-based approach, where you install an Inspector agent on + your EC2 instances. The agent collects data about the system and its configuration, securely sends it to AWS + Inspector, and allows for more accurate and detailed assessments. -### references -DEV-942 + - Security Findings: After performing an assessment, AWS Inspector generates detailed findings that highlight security + vulnerabilities, including their severity level, impact, and remediation steps. These findings can help you + prioritize and address security issues within your AWS environment. + + - Integration with AWS Services: AWS Inspector seamlessly integrates with other AWS services, such as AWS + CloudFormation, AWS Systems Manager, and AWS Security Hub. This allows you to automate security assessments, manage + findings, and centralize security information across your AWS infrastructure. +### references +DEV-942
- ## 1.210.1 (2023-05-27T18:52:11Z) ### 🚀 Enhancements @@ -2470,49 +2580,49 @@ DEV-942 Fix tags @aknysh (#701) ### what -* Fix tags + +- Fix tags ### why -* Typo +- Typo - ### 🐛 Bug Fixes
Fix tags @aknysh (#701) ### what -* Fix tags + +- Fix tags ### why -* Typo +- Typo
- ## 1.210.0 (2023-05-25T22:06:24Z)
EKS FAQ for Addons @milldr (#699) ### what + Added docs for EKS Cluster Addons ### why + FAQ, requested for documentation ### references -DEV-846 - +DEV-846
- ## 1.209.0 (2023-05-25T19:05:53Z)
@@ -2520,19 +2630,20 @@ DEV-846 ### what -* Update `eks/alb-controller` controller IAM policy - +- Update `eks/alb-controller` controller IAM policy ### why -* Email from AWS: -> On June 1, 2023, we will be adding an additional layer of security to ELB ‘Create*' API calls where API callers must have explicit access to add tags in their Identity and Access Management (IAM) policy. Currently, access to attach tags was implicitly granted with access to 'Create*' APIs. +- Email from AWS: + > On June 1, 2023, we will be adding an additional layer of security to ELB ‘Create*' API calls where API callers must + > have explicit access to add tags in their Identity and Access Management (IAM) policy. Currently, access to attach + > tags was implicitly granted with access to 'Create*' APIs. ### references -* [Updated IAM policy](https://github.com/kubernetes-sigs/aws-load-balancer-controller/pull/3068) -
+- [Updated IAM policy](https://github.com/kubernetes-sigs/aws-load-balancer-controller/pull/3068) + ## 1.208.0 (2023-05-24T11:12:15Z) @@ -2540,74 +2651,75 @@ DEV-846 Managed rules for AWS Config @zdmytriv (#690) ### what -* Added option to specify Managed Rules for AWS Config in addition to Conformance Packs + +- Added option to specify Managed Rules for AWS Config in addition to Conformance Packs ### why -* Managed rules will allows to add and tune AWS predefined rules in addition to Conformance Packs + +- Managed rules will allows to add and tune AWS predefined rules in addition to Conformance Packs ### references -* [About AWS Config Manager Rules](https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config_use-managed-rules.html) -* [List of AWS Config Managed Rules](https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html) +- [About AWS Config Manager Rules](https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config_use-managed-rules.html) +- [List of AWS Config Managed Rules](https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html) - ## 1.207.0 (2023-05-22T18:40:06Z)
Corrections to `dms` components @milldr (#658) ### what + - Corrections to `dms` components ### why + - outputs were incorrect - set pass and username with ssm ### references -- n/a - +- n/a
- ## 1.206.0 (2023-05-20T19:41:35Z)
Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL @zdmytriv (#688) ### what -* Upgraded S3 Bucket module version + +- Upgraded S3 Bucket module version ### why -* Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL + +- Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL ### references -* https://github.com/cloudposse/terraform-aws-s3-bucket/pull/178 +- https://github.com/cloudposse/terraform-aws-s3-bucket/pull/178
- ## 1.205.0 (2023-05-19T23:55:14Z)
feat: add lambda monitors to datadog-monitor @dudymas (#686) ### what -* add lambda error monitor -* add datadog lambda log forwarder config monitor -### why -* Observability +- add lambda error monitor +- add datadog lambda log forwarder config monitor +### why +- Observability
- ## 1.204.1 (2023-05-19T19:54:05Z) ### 🚀 Enhancements @@ -2616,28 +2728,27 @@ DEV-846 Update `module "datadog_configuration"` modules @aknysh (#684) ### what -* Update `module "datadog_configuration"` modules -### why -* The module does not accept the `region` variable -* The module must be always enabled to be able to read the Datadog API keys even if the component is disabled +- Update `module "datadog_configuration"` modules +### why +- The module does not accept the `region` variable +- The module must be always enabled to be able to read the Datadog API keys even if the component is disabled - ## 1.204.0 (2023-05-18T20:31:49Z)
`datadog-agent` bugfixes @Benbentwo (#681) ### what -* update datadog agent to latest -* remove variable in datadog configuration -
+- update datadog agent to latest +- remove variable in datadog configuration + ## 1.203.0 (2023-05-18T19:44:08Z) @@ -2645,14 +2756,20 @@ DEV-846 Update `vpc` and `eks/cluster` components @aknysh (#677) ### what -* Update `vpc` and `eks/cluster` components + +- Update `vpc` and `eks/cluster` components ### why -* Use latest module versions -* Take into account `var.availability_zones` for the EKS cluster itself. Only the `node-group` module was using `var.availability_zones` to use the subnets from the provided AZs. The EKS cluster (control plane) was using all the subnets provisioned in a VPC. This caused issues because EKS is not available in all AZs in a region, e.g. it's not available in `us-east-1e` b/c of a limited capacity, and when using all AZs from `us-east-1`, the deployment fails +- Use latest module versions + +- Take into account `var.availability_zones` for the EKS cluster itself. Only the `node-group` module was using + `var.availability_zones` to use the subnets from the provided AZs. The EKS cluster (control plane) was using all the + subnets provisioned in a VPC. This caused issues because EKS is not available in all AZs in a region, e.g. it's not + available in `us-east-1e` b/c of a limited capacity, and when using all AZs from `us-east-1`, the deployment fails -* The latest version of the `vpc` component (which was updated in this PR as well) has the outputs to get a map of AZs to the subnet IDs in each AZ +- The latest version of the `vpc` component (which was updated in this PR as well) has the outputs to get a map of AZs + to the subnet IDs in each AZ ``` # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` @@ -2664,47 +2781,45 @@ DEV-846 private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k)]) ``` - - - ## 1.202.0 (2023-05-18T16:15:12Z)
feat: adds ability to list principals of Lambdas allowed to access ECR @gberenice (#680) ### what -* This change allows listing IDs of the accounts allowed to consume ECR. + +- This change allows listing IDs of the accounts allowed to consume ECR. ### why -* This is supported by [terraform-aws-ecr](https://github.com/cloudposse/terraform-aws-ecr/tree/main), but not the component. + +- This is supported by [terraform-aws-ecr](https://github.com/cloudposse/terraform-aws-ecr/tree/main), but not the + component. ### references -* N/A +- N/A
- ## 1.201.0 (2023-05-18T15:08:54Z)
Introducing AWS Config component @zdmytriv (#675) ### what -* Added AWS Config and related `config-bucket` components -### why -* Added AWS Config and related `config-bucket` components +- Added AWS Config and related `config-bucket` components -### references +### why +- Added AWS Config and related `config-bucket` components +### references
- ## 1.200.1 (2023-05-18T14:52:10Z) ### 🚀 Enhancements @@ -2713,19 +2828,18 @@ DEV-846 Fix `datadog` components @aknysh (#679) ### what -* Fix all `datadog` components + +- Fix all `datadog` components ### why -* Variable `region` is not supported by the `datadog-configuration/modules/datadog_keys` submodule +- Variable `region` is not supported by the `datadog-configuration/modules/datadog_keys` submodule - ## 1.200.0 (2023-05-17T09:19:40Z) -* No changes - +- No changes ## 1.199.0 (2023-05-16T15:01:56Z) @@ -2733,19 +2847,20 @@ DEV-846 `eks/alb-controller-ingress-group`: Corrected Tags to pull LB Data Resource @milldr (#676) ### what + - corrected tag reference for pull lb data resource ### why -- the tags that are used to pull the ALB that's created should be filtering using the same group_name that is given when the LB is created -### references -- n/a +- the tags that are used to pull the ALB that's created should be filtering using the same group_name that is given when + the LB is created +### references +- n/a - ## 1.198.3 (2023-05-15T20:01:18Z) ### 🐛 Bug Fixes @@ -2754,24 +2869,26 @@ DEV-846 Correct `cloudtrail` Account-Map Reference @milldr (#673) ### what + - Correctly pull Audit account from `account-map` for `cloudtrail` - Remove `SessionName` from EKS RBAC user name wrongly added in #668 ### why + - account-map remote state was missing from the `cloudtrail` component - Account names should be pulled from account-map, not using a variable -- Session Name automatically logged in `user.extra.sessionName.0` starting at Kubernetes 1.20, plus addition had a typo and was only on Teams, not Team Roles +- Session Name automatically logged in `user.extra.sessionName.0` starting at Kubernetes 1.20, plus addition had a typo + and was only on Teams, not Team Roles ### references -- Resolves change requests https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193297727 and https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193298107 + +- Resolves change requests https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193297727 and + https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193298107 - Closes #672 - [Internal Slack thread](https://cloudposse.slack.com/archives/CA4TC65HS/p1684122388801769) - - - ## 1.198.2 (2023-05-15T19:47:39Z) ### 🚀 Enhancements @@ -2780,19 +2897,21 @@ DEV-846 bump config yaml dependency on account component as it still depends on hashicorp template provider @lantier (#671) ### what -* Bump [cloudposse/config/yaml](https://github.com/cloudposse/terraform-yaml-config) module dependency from version 1.0.1 to 1.0.2 + +- Bump [cloudposse/config/yaml](https://github.com/cloudposse/terraform-yaml-config) module dependency from version + 1.0.1 to 1.0.2 ### why -* 1.0.1 still uses hashicorp/template provider, which has no M1 binary equivalent, 1.0.2 already uses the cloudposse version which has the binary -### references -* (https://github.com/cloudposse/terraform-yaml-config/releases/tag/1.0.2) +- 1.0.1 still uses hashicorp/template provider, which has no M1 binary equivalent, 1.0.2 already uses the cloudposse + version which has the binary +### references +- (https://github.com/cloudposse/terraform-yaml-config/releases/tag/1.0.2) - ## 1.198.1 (2023-05-15T18:55:09Z) ### 🐛 Bug Fixes @@ -2801,40 +2920,40 @@ DEV-846 Fixed `route53-resolver-dns-firewall` for the case when logging is disabled @zdmytriv (#669) ### what -* Fixed `route53-resolver-dns-firewall` for the case when logging is disabled + +- Fixed `route53-resolver-dns-firewall` for the case when logging is disabled ### why -* Component still required bucket when logging disabled -### references +- Component still required bucket when logging disabled +### references - ## 1.198.0 (2023-05-15T17:37:47Z)
Add `aws-shield` component @aknysh (#670) ### what -* Add `aws-shield` component + +- Add `aws-shield` component ### why -* The component is responsible for enabling AWS Shield Advanced Protection for the following resources: - * Application Load Balancers (ALBs) - * CloudFront Distributions - * Elastic IPs - * Route53 Hosted Zones +- The component is responsible for enabling AWS Shield Advanced Protection for the following resources: -This component also requires that the account where the component is being provisioned to has -been [subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/enable-ddos-prem.html). + - Application Load Balancers (ALBs) + - CloudFront Distributions + - Elastic IPs + - Route53 Hosted Zones +This component also requires that the account where the component is being provisioned to has been +[subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/enable-ddos-prem.html).
- ## 1.197.2 (2023-05-15T15:25:39Z) ### 🚀 Enhancements @@ -2848,13 +2967,12 @@ been [subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/ ### why -- using `string` makes the [if .Values.pvc_enabled](https://github.com/SpotOnInc/cloudposse-actions-runner-controller-tf-module-bugfix/blob/f224c7a4ee8b2ab4baf6929710d6668bd8fc5e8c/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml#L1) condition always true and creates persistent volumes even if they're not intended to use - - +- using `string` makes the + [if .Values.pvc_enabled](https://github.com/SpotOnInc/cloudposse-actions-runner-controller-tf-module-bugfix/blob/f224c7a4ee8b2ab4baf6929710d6668bd8fc5e8c/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml#L1) + condition always true and creates persistent volumes even if they're not intended to use - ## 1.197.1 (2023-05-11T20:39:03Z) ### 🐛 Bug Fixes @@ -2869,7 +2987,8 @@ been [subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/ ### why -- Test code granting access to all `root` users and roles was accidentally left in #645 and breaks when Tenants are part of account names +- Test code granting access to all `root` users and roles was accidentally left in #645 and breaks when Tenants are part + of account names - There is no reason to allow `root` users to access EKS clusters, so even when this code worked it was wrong - Audit trail can keep track of who is performing actions @@ -2877,20 +2996,18 @@ been [subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/ - https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-iam-roles-when-multiple-users-need-identical-access-to-the-cluster - - ## 1.197.0 (2023-05-11T17:59:40Z)
`rds` Component readme update @Benbentwo (#667) ### what -* Updating default example from mssql to postgres -
+- Updating default example from mssql to postgres + ## 1.196.0 (2023-05-11T17:56:41Z) @@ -2898,158 +3015,157 @@ been [subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/ Update `vpc-flow-logs` @milldr (#649) ### what + - Modernized `vpc-flow-logs` with latest conventions ### why + - Old version of the component was significantly out of date - #498 ### references -- DEV-880 - +- DEV-880 - ## 1.195.0 (2023-05-11T07:27:29Z)
Add `iam-policy` to `ecs-service` @milldr (#663) ### what + Add an option to attach the `iam-policy` resource to `ecs-service` ### why + This policy is already created, but is missing its attachment. We should attach this to the resource when enabled ### references -https://cloudposse.slack.com/archives/CA4TC65HS/p1683729972134479 - +https://cloudposse.slack.com/archives/CA4TC65HS/p1683729972134479
- ## 1.194.0 (2023-05-10T18:36:37Z)
upstream `acm` and `datadog-integration` @Benbentwo (#666) ### what -* ACM allows disabling `*.my.domain` -* Datadog-Integration supports allow-list'ing regions +- ACM allows disabling `*.my.domain` +- Datadog-Integration supports allow-list'ing regions
- ## 1.193.0 (2023-05-09T16:00:08Z)
Add `route53-resolver-dns-firewall` and `network-firewall` components @aknysh (#651) ### what -* Add `route53-resolver-dns-firewall` component -* Add `network-firewall` component + +- Add `route53-resolver-dns-firewall` component +- Add `network-firewall` component ### why -* The `route53-resolver-dns-firewall` component is responsible for provisioning [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging configuration - -* The `network-firewall` component is responsible for provisioning [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, including Network Firewall, firewall policy, rule groups, and logging configuration - +- The `route53-resolver-dns-firewall` component is responsible for provisioning + [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) + resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging + configuration +- The `network-firewall` component is responsible for provisioning + [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, including Network Firewall, firewall policy, + rule groups, and logging configuration
- ## 1.192.0 (2023-05-09T15:40:43Z)
[ecs-service] Added IAM policies for ecspresso deployments @goruha (#659) ### what -* [ecs-service] Added IAM policies for [Ecspresso](https://github.com/kayac/ecspresso) deployments +- [ecs-service] Added IAM policies for [Ecspresso](https://github.com/kayac/ecspresso) deployments
- ## 1.191.0 (2023-05-05T22:16:44Z)
`elasticsearch` Corrections @milldr (#662) ### what + - Modernize Elasticsearch component ### why + - `elasticsearch` was not deployable as is. Added up-to-date config ### references -- n/a - +- n/a
- ## 1.190.0 (2023-05-05T18:46:26Z)
fix: remove stray component.yaml in lambda @dudymas (#661) ### what -* Remove the `component.yaml` in the lambda component -### why -* Vendoring would potentially cause conflicts +- Remove the `component.yaml` in the lambda component +### why +- Vendoring would potentially cause conflicts
- ## 1.189.0 (2023-05-05T18:22:04Z)
fix: eks/efs-controller iam policy updates @dudymas (#660) ### what -* Update the iam policy for eks/efs-controller + +- Update the iam policy for eks/efs-controller ### why -* Older permissions will not work with new versions of the controller -### references -* [official iam policy -sample](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/iam-policy-example.json) +- Older permissions will not work with new versions of the controller +### references +- [official iam policy sample](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/iam-policy-example.json)
- ## 1.188.0 (2023-05-05T17:05:23Z)
Move `eks/efs` to `efs` @milldr (#653) ### what + - Moved `eks/efs` to `efs` ### why -- `efs` shouldn't be a submodule of `eks`. You can deploy EFS without EKS -### references -- n/a +- `efs` shouldn't be a submodule of `eks`. You can deploy EFS without EKS +### references +- n/a
- ## 1.187.0 (2023-05-04T23:04:26Z)
@@ -3071,85 +3187,82 @@ sample](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/i - https://github.com/actions/actions-runner-controller/issues/2562 - -
- ## 1.186.0 (2023-05-04T18:15:31Z)
Update `RDS` @Benbentwo (#657) ### what -* Update RDS Modules -* Allow disabling Monitoring Role - -### why -* Monitoring not always needed -* Context.tf Updates in modules +- Update RDS Modules +- Allow disabling Monitoring Role +### why +- Monitoring not always needed +- Context.tf Updates in modules
- ## 1.185.0 (2023-04-26T21:30:24Z)
Add `amplify` component @aknysh (#650) ### what -* Add `amplify` component + +- Add `amplify` component ### why -* Terraform component to provision AWS Amplify apps, backend environments, branches, domain associations, and webhooks + +- Terraform component to provision AWS Amplify apps, backend environments, branches, domain associations, and webhooks ### references -* https://aws.amazon.com/amplify +- https://aws.amazon.com/amplify
- ## 1.184.0 (2023-04-25T14:29:29Z)
Upstream: `eks/ebs-controller` @milldr (#640) ### what + - Added component for `eks/ebs-controller` ### why + - Upstreaming this component for general use ### references -- n/a +- n/a
- ## 1.183.0 (2023-04-24T23:21:17Z)
GitHub OIDC FAQ @milldr (#648) ### what + Added common question for GHA ### why + This is asked frequently ### references -https://cloudposse.slack.com/archives/C04N39YPVAS/p1682355553255269 - +https://cloudposse.slack.com/archives/C04N39YPVAS/p1682355553255269
- ## 1.182.1 (2023-04-24T19:37:31Z) ### 🚀 Enhancements @@ -3160,102 +3273,100 @@ https://cloudposse.slack.com/archives/C04N39YPVAS/p1682355553255269 ### what Update `aws-config` command: -- Add `teams` command and suggest "aws-config-teams" file name instead of "aws-config-saml" because we want to use "aws-config-teams" for both SAML and SSO logins with Leapp handling the difference. + +- Add `teams` command and suggest "aws-config-teams" file name instead of "aws-config-saml" because we want to use + "aws-config-teams" for both SAML and SSO logins with Leapp handling the difference. - Add `help` command - Add more extensive help - Do not rely on script generated by `account-map` for command `main()` function ### why + - Reflect latest design pattern - Improved user experience - - ## 1.182.0 (2023-04-21T17:20:14Z)
Athena CloudTrail Queries @milldr (#638) ### what + - added cloudtrail integration to athena - conditionally allow audit account to decrypt kms key used for cloudtrail ### why + - allow queries against cloudtrail logs from a centralized account (audit) ### references + n/a
- ## 1.181.0 (2023-04-20T22:00:24Z)
Format Identity Team Access Permission Set Name @milldr (#646) ### what + - format permission set roles with hyphens ### why + - pretty Permission Set naming. We want `devops-super` to format to `IdentityDevopsSuperTeamAccess` ### references -https://github.com/cloudposse/refarch-scaffold/pull/127 - +https://github.com/cloudposse/refarch-scaffold/pull/127
- ## 1.180.0 (2023-04-20T21:12:28Z)
Fix `s3-bucket` `var.bucket_name` @milldr (#637) ### what + changed default value for bucket name to empty string not null ### why + default bucket name should be empty string not null. Module checks against name length ### references -n/a - +n/a
- ## 1.179.0 (2023-04-20T20:26:20Z)
ecs-service: fix lint issues @kevcube (#636) - -
- ## 1.178.0 (2023-04-20T20:23:10Z)
fix:aws-team-roles have stray locals @dudymas (#642) ### what -* remove locals from modules/aws-team-roles -### why -* breaks component when it tries to configure locals (the remote state for -account_map isn't around) +- remove locals from modules/aws-team-roles +### why +- breaks component when it tries to configure locals (the remote state for account_map isn't around)
- ## 1.177.0 (2023-04-20T05:13:53Z)
@@ -3272,13 +3383,8 @@ account_map isn't around) - Keep in sync with other modules - #567 is a silent privilege escalation and not needed to accomplish desired goals - - - -
- ## 1.176.1 (2023-04-19T14:20:27Z) ### 🚀 Enhancements @@ -3302,42 +3408,42 @@ account_map isn't around) │ arguments. ``` - - ## 1.176.0 (2023-04-18T18:46:38Z)
feat: cloudtrail-bucket can have acl configured @dudymas (#643) ### what -* add `acl` var to `cloudtrail-bucket` component + +- add `acl` var to `cloudtrail-bucket` component ### why -* Creating new cloudtrail buckets will fail if the acl isn't set to private -### references -* This is part of [a security update from AWS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-faq.html) +- Creating new cloudtrail buckets will fail if the acl isn't set to private +### references +- This is part of + [a security update from AWS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-faq.html)
- ## 1.175.0 (2023-04-11T12:11:46Z)
[argocd-repo] Added ArgoCD git commit notifications @goruha (#633) ### what -* [argocd-repo] Added ArgoCD git commit notifications + +- [argocd-repo] Added ArgoCD git commit notifications ### why -* ArgoCD sync deployment -
+- ArgoCD sync deployment + ## 1.174.0 (2023-04-11T08:53:06Z) @@ -3345,13 +3451,14 @@ account_map isn't around) [argocd] Added github commit status notifications @goruha (#631) ### what -* [argocd] Added github commit status notifications + +- [argocd] Added github commit status notifications ### why -* ArgoCD sync deployment fix concurrent issue - +- ArgoCD sync deployment fix concurrent issue + ## 1.173.0 (2023-04-06T19:21:23Z) @@ -3359,32 +3466,33 @@ account_map isn't around) Missing Version Pins for Bats @milldr (#629) ### what + added missing provider version pins ### why + missing provider versions, required for bats ### references -#626 -#628, #627 +#626 #628, #627 - ## 1.172.0 (2023-04-06T18:32:04Z)
update datadog_lambda_forwarder ref for darwin_arm64 @kevcube (#626) ### what -* update datadog-lambda-forwarder module for darwin_arm64 + +- update datadog-lambda-forwarder module for darwin_arm64 ### why -* run on Darwin_arm64 hardware -
+- run on Darwin_arm64 hardware + ## 1.171.0 (2023-04-06T18:11:40Z) @@ -3392,48 +3500,51 @@ missing provider versions, required for bats Version Pinning Requirements @milldr (#628) ### what + - missing bats requirements resolved ### why + - PR #627 missed a few bats requirements in submodules ### references + - #627 - #626 - - ## 1.170.0 (2023-04-06T17:38:24Z)
Bats Version Pinning @milldr (#627) ### what + - upgraded pattern for version pinning ### why + - bats would fail for all of these components unless these versions are pinned as such ### references -- https://github.com/cloudposse/terraform-aws-components/pull/626 - +- https://github.com/cloudposse/terraform-aws-components/pull/626
- ## 1.169.0 (2023-04-05T20:28:39Z)
[eks/actions-runner-controller]: support Runner Group, webhook queue size @Nuru (#621) ### what + - `eks/actions-runner-controller` - - Support [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups) - - Enable configuration of the webhook queue size limit - - Change runner controller Docker image designation + - Support + [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups) + - Enable configuration of the webhook queue size limit + - Change runner controller Docker image designation - Add documentation on Runner Groups and Autoscaler configuration ### why @@ -3448,24 +3559,25 @@ missing provider versions, required for bats
- ## 1.168.0 (2023-04-04T21:48:58Z)
s3-bucket: use cloudposse template provider for arm64 @kevcube (#618) ### what -* use cloud posse's template provider + +- use cloud posse's template provider ### why -* arm64 -* also this provider was not pinned in versions.tf so that had to be fixed somehow + +- arm64 +- also this provider was not pinned in versions.tf so that had to be fixed somehow ### references -* closes #617 -
+- closes #617 + ## 1.167.0 (2023-04-04T18:14:45Z) @@ -3473,57 +3585,57 @@ missing provider versions, required for bats chore: aws-sso modules updated to 1.0.0 @dudymas (#623) ### what -* upgrade aws-sso modules: permission_sets, sso_account_assignments, and -sso_account_assignments_root -### why -* upstream updates +- upgrade aws-sso modules: permission_sets, sso_account_assignments, and sso_account_assignments_root +### why +- upstream updates - ## 1.166.0 (2023-04-03T13:39:53Z)
Add `datadog-synthetics` component @aknysh (#619) ### what -* Add `datadog-synthetics` component + +- Add `datadog-synthetics` component ### why -* This component is responsible for provisioning Datadog synthetic tests -* Supports Datadog synthetics private locations - - https://docs.datadoghq.com/getting_started/synthetics/private_location - - https://docs.datadoghq.com/synthetics/private_locations +- This component is responsible for provisioning Datadog synthetic tests -* Synthetic tests allow you to observe how your systems and applications are performing using simulated requests and actions from the AWS managed locations around the globe and to monitor internal endpoints from private locations +- Supports Datadog synthetics private locations + - https://docs.datadoghq.com/getting_started/synthetics/private_location + - https://docs.datadoghq.com/synthetics/private_locations +- Synthetic tests allow you to observe how your systems and applications are performing using simulated requests and + actions from the AWS managed locations around the globe and to monitor internal endpoints from private locations
- ## 1.165.0 (2023-03-31T22:11:26Z)
Update `eks/cluster` README @milldr (#616) ### what + - Updated the README with EKS cluster ### why + The example stack is outdated. Add notes for Github OIDC and karpenter ### references -https://cloudposse.atlassian.net/browse/DEV-835 +https://cloudposse.atlassian.net/browse/DEV-835
- ## 1.164.1 (2023-03-30T20:03:15Z) ### 🚀 Enhancements @@ -3532,30 +3644,30 @@ https://cloudposse.atlassian.net/browse/DEV-835 spacelift: Update README.md example login policy @johncblandii (#597) ### what -* Added support for allowing spaces read access to all members -* Added a reference for allowing spaces write access to the "Developers" group + +- Added support for allowing spaces read access to all members +- Added a reference for allowing spaces write access to the "Developers" group ### why -* Spacelift moved to Spaces Access Control -### references -* https://docs.spacelift.io/concepts/spaces/access-control +- Spacelift moved to Spaces Access Control +### references +- https://docs.spacelift.io/concepts/spaces/access-control - ## 1.164.0 (2023-03-30T16:25:28Z)
Update several component Readmes @Benbentwo (#611) ### what -* Update Readmes of many components from Refarch Docs -
+- Update Readmes of many components from Refarch Docs + ## 1.163.0 (2023-03-29T19:52:46Z) @@ -3563,28 +3675,29 @@ https://cloudposse.atlassian.net/browse/DEV-835 add providers to `mixins` folder @Benbentwo (#613) ### what -* Copies some common providers to the mixins folder + +- Copies some common providers to the mixins folder ### why -* Have a central place where our common providers are held. +- Have a central place where our common providers are held. - ## 1.162.0 (2023-03-29T19:30:15Z)
Added ArgoCD GitHub notification subscription @goruha (#615) ### what -* Added ArgoCD GitHub notification subscription + +- Added ArgoCD GitHub notification subscription ### why -* To use synchronous deployment pattern -
+- To use synchronous deployment pattern + ## 1.161.1 (2023-03-29T17:20:27Z) @@ -3594,73 +3707,72 @@ https://cloudposse.atlassian.net/browse/DEV-835 waf component, update dependency versions for aws provider and waf terraform module @arcaven (#612) ### what -* updates to waf module: - * aws provider from ~> 4.0 to => 4.0 - * module cloudposse/waf/aws from 0.0.4 to 0.2.0 - * different recommended catalog entry + +- updates to waf module: + - aws provider from ~> 4.0 to => 4.0 + - module cloudposse/waf/aws from 0.0.4 to 0.2.0 + - different recommended catalog entry ### why -* @aknysh suggested some updates before we start using waf module +- @aknysh suggested some updates before we start using waf module - ## 1.161.0 (2023-03-28T19:51:27Z)
Quick fixes to EKS/ARC arm64 Support @Nuru (#610) ### what + - While supporting EKS/ARC `arm64`, continue to deploy `amd64` by default - Make `tolerations.value` optional ### why + - Majority of echosystem support is currently `amd64` - `tolerations.value` is option in Kubernetes spec ### references -- Corrects issue which escaped review in #609 +- Corrects issue which escaped review in #609
- ## 1.160.0 (2023-03-28T18:26:20Z)
Upstream EKS/ARC amd64 Support @milldr (#609) ### what + Added arm64 support for eks/arc ### why + when supporting both amd64 and arm64, we need to select the correct architecture ### references -https://github.com/cloudposse/infra-live/pull/265 - +https://github.com/cloudposse/infra-live/pull/265
- ## 1.159.0 (2023-03-27T16:19:29Z)
Update account-map to output account information for aws-config script @Nuru (#608) ### what -* Update `account-map` to output account information for `aws-config` script -* Output AWS profile name for root of credential chain - -### why -* Enable `aws-config` to output account IDs and to generate configuration for "AWS Extend Switch Roles" browser plugin -* Support multiple namespaces in a single infrastructure repo - +- Update `account-map` to output account information for `aws-config` script +- Output AWS profile name for root of credential chain +### why +- Enable `aws-config` to output account IDs and to generate configuration for "AWS Extend Switch Roles" browser plugin +- Support multiple namespaces in a single infrastructure repo
@@ -3668,104 +3780,107 @@ https://github.com/cloudposse/infra-live/pull/265 Update CODEOWNERS to remove contributors @Nuru (#607) ### what -* Update CODEOWNERS to remove contributors + +- Update CODEOWNERS to remove contributors ### why -* Require approval from engineering team (or in some cases admins) for all changes, to keep better quality control on this repo +- Require approval from engineering team (or in some cases admins) for all changes, to keep better quality control on + this repo - ## 1.158.0 (2023-03-27T03:41:43Z)
Upstream latest datadog-agent and datadog-configuration updates @nitrocode (#598) ### what -* Upstream latest datadog-agent and datadog-configuration updates + +- Upstream latest datadog-agent and datadog-configuration updates ### why -* datadog irsa role -* removing unused input vars -* default to `public.ecr.aws` images -* ignore deprecated `default.auto.tfvars` -* move `datadog-agent` to `eks/` subfolder for consistency with other helm charts -### references -N/A +- datadog irsa role +- removing unused input vars +- default to `public.ecr.aws` images +- ignore deprecated `default.auto.tfvars` +- move `datadog-agent` to `eks/` subfolder for consistency with other helm charts +### references +N/A
- ## 1.157.0 (2023-03-24T19:12:17Z)
Remove `root_account_tenant_name` @milldr (#605) ### what + - bumped ecr - remove unnecssary variable ### why + - ECR version update - We shouldn't need to set `root_account_tenant_name` in providers - Some Terraform docs are out-of-date ### references -- n/a +- n/a
- ## 1.156.0 (2023-03-23T21:03:46Z)
exposing variables from 2.0.0 of `VPC` module @Benbentwo (#604) ### what -* Adding vars for vpc module and sending them directly to module -### references -* https://github.com/cloudposse/terraform-aws-vpc/blob/master/variables.tf#L10-L44 +- Adding vars for vpc module and sending them directly to module +### references +- https://github.com/cloudposse/terraform-aws-vpc/blob/master/variables.tf#L10-L44
- ## 1.155.0 (2023-03-23T02:01:29Z)
Add Privileged Option for GH OIDC @milldr (#603) ### what + - allow gh oidc role to use privileged as option for reading tf backend ### why -- If deploying GH OIDC with a component that needs to be applied with SuperAdmin (aws-teams) we need to set privileged here + +- If deploying GH OIDC with a component that needs to be applied with SuperAdmin (aws-teams) we need to set privileged + here ### references -- https://cloudposse.slack.com/archives/C04N39YPVAS/p1679409325357119 +- https://cloudposse.slack.com/archives/C04N39YPVAS/p1679409325357119
- ## 1.154.0 (2023-03-22T17:40:35Z)
update `opsgenie-team` to be delete-able via `enabled: false` @Benbentwo (#589) ### what -* Uses Datdaog Configuration as it's source of datadog variables -* Now supports `enabled: false` on a team to destroy it. -
+- Uses Datdaog Configuration as it's source of datadog variables +- Now supports `enabled: false` on a team to destroy it. + ## 1.153.0 (2023-03-21T19:22:03Z) @@ -3773,35 +3888,38 @@ N/A Upstream AWS Teams components @milldr (#600) ### what + - added eks view only policy ### why + - Provided updates from recent contracts ### references -- https://github.com/cloudposse/refarch-scaffold/pull/99 - +- https://github.com/cloudposse/refarch-scaffold/pull/99 - ## 1.152.0 (2023-03-21T15:42:51Z)
upstream 'datadog-lambda-forwarder' @gberenice (#601) ### what -* Upgrade 'datadog-lambda-forwarder' component to v1.3.0 + +- Upgrade 'datadog-lambda-forwarder' component to v1.3.0 ### why -* Be able [to forward Cloudwatch Events](https://github.com/cloudposse/terraform-aws-datadog-lambda-forwarder/pull/48) via components. + +- Be able [to forward Cloudwatch Events](https://github.com/cloudposse/terraform-aws-datadog-lambda-forwarder/pull/48) + via components. ### references -* N/A -
+- N/A + ## 1.151.0 (2023-03-15T15:56:20Z) @@ -3809,108 +3927,106 @@ N/A Upstream `eks/external-secrets-operator` @milldr (#595) ### what + - Adding new module for `eks/external-secrets-operator` ### why + - Other customers want to use this module now, and it needs to be upstreamed ### references -- n/a - +- n/a - ## 1.150.0 (2023-03-14T20:20:41Z)
chore(spacelift): update with dependency resource @dudymas (#594) ### what -* update spacelift component to 0.55.0 + +- update spacelift component to 0.55.0 ### why -* support feature flag for spacelift_stack_dependency resource -### references -* [spacelift module 0.55.0](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/0.55.0) +- support feature flag for spacelift_stack_dependency resource +### references +- [spacelift module 0.55.0](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/0.55.0)
- ## 1.149.0 (2023-03-13T15:25:25Z)
Fix SSO SAML provider fixes @goruha (#592) ### what -* Fix SSO SAML provider fixes +- Fix SSO SAML provider fixes
- ## 1.148.0 (2023-03-10T18:07:36Z)
ArgoCD SSO improvements @goruha (#590) ### what -* ArgoCD SSO improvements +- ArgoCD SSO improvements
- ## 1.147.0 (2023-03-10T17:52:18Z)
Upstream: `eks/echo-server` @milldr (#591) ### what + - Adding the `ingress.alb.group_name` annotation to Echo Server ### why + - Required to set the ALB specifically, rather than using the default ### references -- n/a - +- n/a
- ## 1.146.0 (2023-03-08T23:13:13Z)
Improve platform and external-dns for release engineering @goruha (#588) ### what -* `eks/external-dns` support `dns-primary` -* `eks/platform` support json query remote components outputs + +- `eks/external-dns` support `dns-primary` +- `eks/platform` support json query remote components outputs ### why -* `vanity domain` pattern support by `eks/external-dns` -* Improve flexibility of `eks/platform` +- `vanity domain` pattern support by `eks/external-dns` +- Improve flexibility of `eks/platform`
- ## 1.145.0 (2023-03-07T00:28:25Z)
`eks/actions-runner-controller`: use coalesce @Benbentwo (#586) ### what -* use coalesce instead of try, as we need a value passed in here -
+- use coalesce instead of try, as we need a value passed in here + ## 1.144.0 (2023-03-05T20:24:09Z) @@ -3918,103 +4034,101 @@ N/A Upgrade Remote State to `1.4.1` @milldr (#585) ### what + - Upgrade _all_ remote state modules (`cloudposse/stack-config/yaml//modules/remote-state`) to version `1.4.1` ### why -- In order to use go templating with Atmos, we need to use the latest cloudposse/utils version. This version is specified by `1.4.1` -### references -- https://github.com/cloudposse/terraform-yaml-stack-config/releases/tag/1.4.1 +- In order to use go templating with Atmos, we need to use the latest cloudposse/utils version. This version is + specified by `1.4.1` +### references +- https://github.com/cloudposse/terraform-yaml-stack-config/releases/tag/1.4.1 - ## 1.143.0 (2023-03-02T18:07:53Z)
bugfix: rds anomalies monitor not sending team information @Benbentwo (#583) ### what -* Update monitor to have default CP tags -
+- Update monitor to have default CP tags + ## 1.142.0 (2023-03-02T17:49:40Z)
datadog-lambda-forwarder: if s3_buckets not set, module fails @kevcube (#581) - This module attempts to do length() on the value for s3_buckets. +This module attempts to do length() on the value for s3_buckets. We are not using s3_buckets, and it defaults to null, so length() fails.
- ## 1.141.0 (2023-03-01T19:10:07Z)
`datadog-monitors`: Team Grouping @Benbentwo (#580) ### what -* grouping by team helps ensure the team tag is sent to Opsgenie -### why -* ensures most data is fed to a valid team tag instead of `@opsgenie-` +- grouping by team helps ensure the team tag is sent to Opsgenie +### why +- ensures most data is fed to a valid team tag instead of `@opsgenie-`
- ## 1.140.0 (2023-02-28T18:47:44Z)
`spacelift` add missing `var.region` @johncblandii (#574) ### what -* Added the missing `var.region` + +- Added the missing `var.region` ### why -* The AWS provider requires it and it was not available -### references +- The AWS provider requires it and it was not available +### references
- ## 1.139.0 (2023-02-28T18:46:35Z)
datadog monitors improvements @Benbentwo (#579) ### what -* Datadog monitor improvements - * Prepends `()` e.g. `(tenant-environment-stage)` - * Fixes some messages that had improper syntax - dd uses `{{ var.name }}` -### why -* Datadog monitor improvements +- Datadog monitor improvements + - Prepends `()` e.g. `(tenant-environment-stage)` + - Fixes some messages that had improper syntax - dd uses `{{ var.name }}` +### why +- Datadog monitor improvements
- ## 1.138.0 (2023-02-28T18:45:48Z)
update `account` readme.md @Benbentwo (#570) ### what -* Updated account readme -
+- Updated account readme + ## 1.137.0 (2023-02-27T20:39:34Z) @@ -4022,10 +4136,10 @@ We are not using s3_buckets, and it defaults to null, so length() fails. Update `eks/cluster` @Benbentwo (#578) ### what -* Update EKS Cluster Module to re-include addons - +- Update EKS Cluster Module to re-include addons + ## 1.136.0 (2023-02-27T17:36:47Z) @@ -4033,52 +4147,58 @@ We are not using s3_buckets, and it defaults to null, so length() fails. Set spacelift-worker-pool ami explicitly to x86_64 @arcaven (#577) ### why + - autoscaling group for spacelift-worker-pool will fail to launch when new arm64 images return first - arm64 ami image is being returned first at the moment in us-east-1 ### what + - set spacelift-worker-pool ami statically to return only x86_64 results ### references + - Spacelift Worker Pool ASG may fail to scale due to ami/instance type mismatch #575 -- Note: this is an alternative to spacelift-worker-pool README update and AMI limits #573 which I read after, but I think this filter approach will be more easily be refactored into setting this as an attribute in variables.tf in the near future +- Note: this is an alternative to spacelift-worker-pool README update and AMI limits #573 which I read after, but I + think this filter approach will be more easily be refactored into setting this as an attribute in variables.tf in the + near future - ## 1.135.0 (2023-02-27T13:56:48Z)
github-runners add support for runner groups @johncblandii (#569) ### what -* Added optional support for separating runners by groups + +- Added optional support for separating runners by groups NOTE: I don't know if the default of `default` is valid or if it is `Default`. I'll confirm this soon. ### why -* Groups are supported by GitHub and allow for Actions to target specific runners by group vs by label + +- Groups are supported by GitHub and allow for Actions to target specific runners by group vs by label ### references -* https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups +- https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups
- ## 1.134.0 (2023-02-24T20:59:40Z)
[account-map] Update remote config module version @goruha (#572) ### what -* Update remote config module version `1.4.1` + +- Update remote config module version `1.4.1` ### why -* Solve terraform module version conflict -
+- Solve terraform module version conflict + ## 1.133.0 (2023-02-24T17:55:52Z) @@ -4086,72 +4206,76 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I Fix ArgoCD minor issues @goruha (#571) ### what -* Fix slack notification annotations -* Fix CRD creation order + +- Fix slack notification annotations +- Fix CRD creation order ### why -* Fix ArgoCD bootstrap +- Fix ArgoCD bootstrap - ## 1.132.0 (2023-02-23T04:33:29Z)
Add spacelift-policy component @nitrocode (#556) ### what -* Add spacelift-policy component + +- Add spacelift-policy component ### why + - De-couple policy creation from admin and child stacks - Auto attach policies to remove additional terraform management of resources ### references -- Depends on PR https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/134 - +- Depends on PR https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/134
- ## 1.131.0 (2023-02-23T01:13:58Z)
SSO upgrades and Support for Assume Role from Identity Users @johncblandii (#567) ### what -* Upgraded `aws-sso` to use `0.7.1` modules -* Updated `account-map/modules/roles-to-principals` to support assume role from SSO users in the identity account -* Adjusted `aws-sso/policy-Identity-role-RoleAccess.tf` to use the identity account name vs the stage so it supports names like `core-identity` instead of just `identity` + +- Upgraded `aws-sso` to use `0.7.1` modules +- Updated `account-map/modules/roles-to-principals` to support assume role from SSO users in the identity account +- Adjusted `aws-sso/policy-Identity-role-RoleAccess.tf` to use the identity account name vs the stage so it supports + names like `core-identity` instead of just `identity` ### why -* `aws-sso` users could not assume role to plan/apply terraform locally -* using `core-identity` as a name broke the `aws-sso` policy since account `identity` does not exist in `full_account_map` -### references +- `aws-sso` users could not assume role to plan/apply terraform locally +- using `core-identity` as a name broke the `aws-sso` policy since account `identity` does not exist in + `full_account_map` +### references
- ## 1.130.0 (2023-02-21T18:33:53Z)
Add Redshift component @max-lobur (#563) ### what -* Add Redshift + +- Add Redshift ### why -* Fulfilling the AWS catalog + +- Fulfilling the AWS catalog ### references -* https://github.com/cloudposse/terraform-aws-redshift-cluster -
+- https://github.com/cloudposse/terraform-aws-redshift-cluster + ## 1.129.0 (2023-02-21T16:45:43Z) @@ -4159,10 +4283,10 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I update dd agent docs @Benbentwo (#565) ### what -* Update Datadog Docs to be more clear on catalog entry - +- Update Datadog Docs to be more clear on catalog entry + ## 1.128.0 (2023-02-18T16:28:11Z) @@ -4170,46 +4294,44 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I feat: updates spacelift to support policies outside of the comp folder @Gowiem (#522) ### what -* Adds back `policies_by_name_path` variable to spacelift component + +- Adds back `policies_by_name_path` variable to spacelift component ### why -* Allows specifying spacelift policies outside of the component folder -### references -* N/A +- Allows specifying spacelift policies outside of the component folder +### references +- N/A - ## 1.127.0 (2023-02-16T17:53:31Z)
[sso-saml-provider] Upstream SSO SAML provider component @goruha (#562) ### what -* [sso-saml-provider] Upstream SSO SAML provider component - -### why -* Required for ArgoCD +- [sso-saml-provider] Upstream SSO SAML provider component +### why +- Required for ArgoCD
- ## 1.126.0 (2023-02-14T23:01:00Z)
upstream `opsgenie-team` @Benbentwo (#561) ### what -* Upstreams latest opsgenie-team component -
+- Upstreams latest opsgenie-team component + ## 1.125.0 (2023-02-14T21:45:32Z) @@ -4217,22 +4339,21 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I [eks/argocd] Upstream ArgoCD @goruha (#560) ### what -* Upstream `eks/argocd` +- Upstream `eks/argocd` - ## 1.124.0 (2023-02-14T17:34:29Z)
`aws-backup` upstream @Benbentwo (#559) ### what -* Update `aws-backup` to latest -
+- Update `aws-backup` to latest + ## 1.123.0 (2023-02-13T22:42:56Z) @@ -4240,164 +4361,155 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I upstream lambda pt2 @Benbentwo (#558) ### what -* Add archive zip -* Change to python (no compile) +- Add archive zip +- Change to python (no compile) - ## 1.122.0 (2023-02-13T21:24:02Z)
upstream `lambda` @Benbentwo (#557) ### what -* Upstream `lambda` component + +- Upstream `lambda` component ### why -* Quickly deploy serverless code +- Quickly deploy serverless code
- ## 1.121.0 (2023-02-13T16:59:16Z)
Upstream `ACM` and `eks/Platform` for release_engineering @Benbentwo (#555) ### what -* ACM Component outputs it's acm url -* EKS/Platform will deploy many terraform outputs to SSM -### why -* These components are required for CP Release Engineering Setup +- ACM Component outputs it's acm url +- EKS/Platform will deploy many terraform outputs to SSM +### why +- These components are required for CP Release Engineering Setup
- ## 1.120.0 (2023-02-08T16:34:25Z)
Upstream datadog logs archive @Benbentwo (#552) ### what -* Upstream DD Logs Archive - - +- Upstream DD Logs Archive
- ## 1.119.0 (2023-02-07T21:32:25Z)
Upstream `dynamodb` @milldr (#512) ### what + - Updated the `dynamodb` component ### why + - maintaining up-to-date upstream component ### references -- N/A +- N/A
- ## 1.118.0 (2023-02-07T20:15:17Z)
fix dd-forwarder: datadog service config depends on lambda arn config @raybotha (#531) - -
- ## 1.117.0 (2023-02-07T19:44:32Z)
Upstream `spa-s3-cloudfront` @milldr (#500) ### what + - Added missing component from upstream `spa-s3-cloudfront` ### why + - We use this component to provision Cloudfront and related resources ### references -- N/A +- N/A
- ## 1.116.0 (2023-02-07T00:52:27Z)
Upstream `aurora-mysql` @milldr (#517) ### what + - Upstreaming both `aurora-mysql` and `aurora-mysql-resources` ### why + - Added option for allowing ingress by account name, rather than requiring CIDR blocks copy and pasted - Replaced the deprecated provider for MySQL - Resolved issues with Terraform perma-drift for the resources component with granting "ALL" ### references + - Old provider, archived: https://github.com/hashicorp/terraform-provider-mysql - New provider: https://github.com/petoju/terraform-provider-mysql - -
- ## 1.115.0 (2023-02-07T00:49:59Z)
Upstream `aurora-postgres` @milldr (#518) ### what + - Upstreaming `aurora-postgres` and `aurora-postgres-resources` ### why + - TLC for these components - Added options for adding ingress by account - Cleaned up the submodule for the resources component - Support creating schemas - Support conditionally pulling passwords from SSM, similar to `aurora-mysql` - -
- ## 1.114.0 (2023-02-06T17:09:31Z)
`datadog-private-locations` update helm provider @Benbentwo (#549) ### what -* Updates Helm Provider to the latest - -### why -* New API Version +- Updates Helm Provider to the latest +### why +- New API Version
- ## 1.113.0 (2023-02-06T02:26:22Z)
@@ -4405,18 +4517,16 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I ### what -* Stack example has an old variable defined +- Stack example has an old variable defined ### why -* `The root module does not declare a variable named "eks_tags_enabled" but a value was found in file "uw2-automation-vpc.terraform.tfvars.json".` +- `The root module does not declare a variable named "eks_tags_enabled" but a value was found in file "uw2-automation-vpc.terraform.tfvars.json".` ### references -
- ## 1.112.1 (2023-02-03T20:00:09Z) ### 🚀 Enhancements @@ -4425,42 +4535,40 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I Fixed non-html tags that fails rendering on docusaurus @zdmytriv (#546) ### what -* Fixed non-html tags -### why -* Rendering has been failing on docusaurus mdx/jsx engine +- Fixed non-html tags +### why +- Rendering has been failing on docusaurus mdx/jsx engine - ## 1.112.0 (2023-02-03T19:02:57Z)
`datadog-agent` allow values var merged @Benbentwo (#548) ### what -* Allows values to be passed in and merged to values file -### why -* Need to be able to easily override values files +- Allows values to be passed in and merged to values file +### why +- Need to be able to easily override values files
- ## 1.111.0 (2023-01-31T23:02:57Z)
Update echo and alb-controller-ingress-group @Benbentwo (#547) ### what -* Allows target group to be targeted by echo server -
+- Allows target group to be targeted by echo server + ## 1.110.0 (2023-01-26T00:25:13Z) @@ -4468,172 +4576,167 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I Chore/acme/bootcamp core tenant @dudymas (#543) ### what -* upgrade the vpn module in the ec2-client-vpn component -* and protect outputs on ec2-client-vpn + +- upgrade the vpn module in the ec2-client-vpn component +- and protect outputs on ec2-client-vpn ### why -* saml docs were broken in refarch-scaffold. module was trying to alter the cert provider +- saml docs were broken in refarch-scaffold. module was trying to alter the cert provider - ## 1.109.0 (2023-01-24T20:01:56Z)
Chore/acme/bootcamp spacelift @dudymas (#545) ### what -* adjust the type of context_filters in spacelift - -### why -* was getting errors trying to apply spacelift component +- adjust the type of context_filters in spacelift +### why +- was getting errors trying to apply spacelift component
- ## 1.108.0 (2023-01-20T22:36:54Z)
EC2 Client VPN Version Bump @Benbentwo (#544) ### what -* Bump Versin of EC2 Client VPN + +- Bump Versin of EC2 Client VPN ### why -* Bugfixes issue with TLS provider + +- Bugfixes issue with TLS provider ### references -* https://github.com/cloudposse/terraform-aws-ec2-client-vpn/pull/58 -* https://github.com/cloudposse/terraform-aws-ssm-tls-self-signed-cert/pull/20 +- https://github.com/cloudposse/terraform-aws-ec2-client-vpn/pull/58 +- https://github.com/cloudposse/terraform-aws-ssm-tls-self-signed-cert/pull/20
- ## 1.107.0 (2023-01-19T17:34:33Z)
Update pod security context schema in cert-manager @max-lobur (#538) ### what -Pod security context `enabled` field has been deprecated. Now you just specify the options and that's it. -Update the options per recent schema. See references + +Pod security context `enabled` field has been deprecated. Now you just specify the options and that's it. Update the +options per recent schema. See references Tested on k8s 1.24 ### why -* Otherwise it does not pass Deployment validation on newer clusters. -### references -https://github.com/cert-manager/cert-manager/commit/c17b11fa01455eb1b83dce0c2c06be555e4d53eb +- Otherwise it does not pass Deployment validation on newer clusters. +### references +https://github.com/cert-manager/cert-manager/commit/c17b11fa01455eb1b83dce0c2c06be555e4d53eb
- ## 1.106.0 (2023-01-18T15:36:52Z)
Fix github actions runner controller default variables @max-lobur (#542) ### what + Default value for string is null, not false ### why -* Otherwise this does not pass schema when you deploy it without storage requests - - +- Otherwise this does not pass schema when you deploy it without storage requests
- ## 1.105.0 (2023-01-18T15:24:11Z)
Update k8s metrics-server to latest @max-lobur (#537) - - ### what -Upgrade metrics-server -Tested on k8s 1.24 via `kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes"` - -### why -* The previous one was so old that bitnami has even removed the chart. +Upgrade metrics-server Tested on k8s 1.24 via `kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes"` +### why +- The previous one was so old that bitnami has even removed the chart.
- ## 1.104.0 (2023-01-18T14:52:58Z)
Pin kubernetes provider in metrics-server @max-lobur (#541) ### what -* Pin the k8s provider version -* Update versions + +- Pin the k8s provider version +- Update versions ### why -* Fix CI -### references -* https://github.com/cloudposse/terraform-aws-components/pull/537 +- Fix CI +### references +- https://github.com/cloudposse/terraform-aws-components/pull/537
- ## 1.103.0 (2023-01-17T21:09:56Z)
fix(dns-primary/acm): include zone_name arg @dudymas (#540) ### what -* in dns-primary, revert version of acm module 0.17.0 -> 0.16.2 (17 is a preview) + +- in dns-primary, revert version of acm module 0.17.0 -> 0.16.2 (17 is a preview) ### why -* primary zones must be specified now that names are trimmed before the dot (.) +- primary zones must be specified now that names are trimmed before the dot (.)
- ## 1.102.0 (2023-01-17T16:09:59Z)
Fix typo in karpenter-provisioner @max-lobur (#539) ### what -I formatted it last moment and did not notice that actually changed the object. -Fixing that and reformatting all of it so it's more obvious for future maintainers. + +I formatted it last moment and did not notice that actually changed the object. Fixing that and reformatting all of it +so it's more obvious for future maintainers. ### why -* Fixing bug + +- Fixing bug ### references -https://github.com/cloudposse/terraform-aws-components/pull/536 +https://github.com/cloudposse/terraform-aws-components/pull/536
- ## 1.101.0 (2023-01-17T07:47:30Z)
Support setting consolidation in karpenter-provisioner @max-lobur (#536) ### what + This is an alternative way of deprovisioning - proactive one. + ``` There is another way to configure Karpenter to deprovision nodes called Consolidation. This mode is preferred for workloads such as microservices and is imcompatible with setting @@ -4644,137 +4747,141 @@ to a change in the workloads ``` ### why -* To let users set a more aggressive deprovisioning strategy + +- To let users set a more aggressive deprovisioning strategy ### references -* https://ec2spotworkshops.com/karpenter/050_karpenter/consolidation.html +- https://ec2spotworkshops.com/karpenter/050_karpenter/consolidation.html
- ## 1.100.0 (2023-01-17T07:41:58Z)
Sync karpenter chart values with the schema @max-lobur (#535) ### what -Based on https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml#L129-L142 all these should go under settings.aws + +Based on +https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml#L129-L142 +all these should go under settings.aws ### why + Ensure compatibility with the new charts ### references -Based on https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml - +Based on https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml
- ## 1.99.0 (2023-01-13T14:59:16Z)
fix(aws-sso): dont hardcode account name for root @dudymas (#534) ### what -* remove hardcoding for root account moniker -* change default tenant from `gov` to `core` (now convention) + +- remove hardcoding for root account moniker +- change default tenant from `gov` to `core` (now convention) ### why -* tenant is not included in the account prefix. In this case, changed to be 'core' -* most accounts do not use `gov` as the root tenant +- tenant is not included in the account prefix. In this case, changed to be 'core' +- most accounts do not use `gov` as the root tenant
- ## 1.98.0 (2023-01-12T00:12:36Z)
Bump spacelift to latest @nitrocode (#532) ### what + - Bump spacelift to latest ### why + - Latest ### references -N/A +N/A
- ## 1.97.0 (2023-01-11T01:16:33Z)
Upstream EKS Action Runner Controller @milldr (#528) ### what + - Upstreaming the latest additions for the EKS actions runner controller component ### why -- We've added additional features for the ARC runners, primarily adding options for ephemeral storage and persistent storage. Persistent storage can be used to add image caching with EFS -- Allow for setting a `webhook_startup_timeout` value different than `scale_down_delay_seconds`. Defaults to `scale_down_delay_seconds` -### references -- N/A +- We've added additional features for the ARC runners, primarily adding options for ephemeral storage and persistent + storage. Persistent storage can be used to add image caching with EFS +- Allow for setting a `webhook_startup_timeout` value different than `scale_down_delay_seconds`. Defaults to + `scale_down_delay_seconds` +### references +- N/A
- ## 1.96.0 (2023-01-05T21:19:22Z)
Datadog Upstreams and Account Settings @Benbentwo (#533) ### what -* Datadog Upgrades (Bugfixes for Configuration on default datadog URL) -* Account Settings Fixes for emoji support and updated budgets - -### why -* Upstreams +- Datadog Upgrades (Bugfixes for Configuration on default datadog URL) +- Account Settings Fixes for emoji support and updated budgets +### why +- Upstreams
- ## 1.95.0 (2023-01-04T23:44:35Z)
fix(aws-sso): add missing tf update perms @dudymas (#530) ### what -* Changes for supporting [Refarch Scaffold](github.com/cloudposse/refarch-scaffold) -* TerraformUpdateAccess permission set added + +- Changes for supporting [Refarch Scaffold](github.com/cloudposse/refarch-scaffold) +- TerraformUpdateAccess permission set added ### why -* Allow SSO users to update dynamodb/s3 for terraform backend +- Allow SSO users to update dynamodb/s3 for terraform backend
- ## 1.94.0 (2022-12-21T18:38:15Z)
upstream `spacelift` @Benbentwo (#526) ### what -* Updated Spacelift Component to latest -* Updated README with new example + +- Updated Spacelift Component to latest +- Updated README with new example ### why -* Upstreams -
+- Upstreams + ## 1.93.0 (2022-12-21T18:37:37Z) @@ -4782,51 +4889,50 @@ N/A upstream `ecs` & `ecs-service` @Benbentwo (#529) ### what -* upstream - * `ecs` - * `ecs-service` -### why -* `enabled` flag correctly destroys resources -* bugfixes and improvements -* datadog support for ecs services +- upstream + - `ecs` + - `ecs-service` +### why +- `enabled` flag correctly destroys resources +- bugfixes and improvements +- datadog support for ecs services - ## 1.92.0 (2022-12-21T18:36:35Z)
Upstream Datadog @Benbentwo (#525) ### what -* Datadog updates -* New `datadog-configuration` component for setting up share functions and making codebase more dry +- Datadog updates +- New `datadog-configuration` component for setting up share functions and making codebase more dry
- ## 1.91.0 (2022-11-29T17:17:58Z)
CPLIVE-320: Set VPC to use region-less AZs @nitrocode (#524) ### what -* Set VPC to use region-less AZs + +- Set VPC to use region-less AZs ### why -* Prevent having to set VPC AZs within global region defaults + +- Prevent having to set VPC AZs within global region defaults ### references -* CPLIVE-320 +- CPLIVE-320
- ## 1.90.2 (2022-11-20T05:41:14Z) ### 🚀 Enhancements @@ -4835,20 +4941,20 @@ N/A Use cloudposse/template for arm support @nitrocode (#510) ### what -* Use cloudposse/template for arm support + +- Use cloudposse/template for arm support ### why -* The new cloudposse/template provider has a darwin arm binary for M1 laptops -### references -* https://github.com/cloudposse/terraform-provider-template -* https://registry.terraform.io/providers/cloudposse/template/latest +- The new cloudposse/template provider has a darwin arm binary for M1 laptops +### references +- https://github.com/cloudposse/terraform-provider-template +- https://registry.terraform.io/providers/cloudposse/template/latest - ## 1.90.1 (2022-10-31T13:27:37Z) ### 🚀 Enhancements @@ -4857,38 +4963,38 @@ N/A Allow vpc-peering to peer v2 to v2 @nitrocode (#521) ### what -* Allow vpc-peering to peer v2 to v2 + +- Allow vpc-peering to peer v2 to v2 ### why -* Alternative to transit gateway -### references -N/A +- Alternative to transit gateway +### references +N/A - ## 1.90.0 (2022-10-31T13:24:38Z)
Upstream iam-role component @nitrocode (#520) ### what + - Upstream iam-role component ### why + - Create simple IAM roles ### references -- https://github.com/cloudposse/terraform-aws-iam-role - +- https://github.com/cloudposse/terraform-aws-iam-role
- ## 1.89.0 (2022-10-28T15:35:38Z)
@@ -4899,35 +5005,32 @@ N/A - Support and prefer authentication via GitHub app - Support and prefer webhook-based autoscaling - ### why - GitHub app is much more restricted, plus has higher API rate limits - Webhook-based autoscaling is proactive without being overly expensive - -
- ## 1.88.0 (2022-10-24T15:40:47Z)
Upstream iam-service-linked-roles @nitrocode (#516) ### what -* Upstream iam-service-linked-roles (thanks to @aknysh for writing it) + +- Upstream iam-service-linked-roles (thanks to @aknysh for writing it) ### why -* Centralized component to create IAM service linked roles + +- Centralized component to create IAM service linked roles ### references -- N/A +- N/A
- ## 1.87.0 (2022-10-22T19:12:36Z)
@@ -4943,11 +5046,13 @@ N/A ### notes -Cloud Posse has a [service quotas module](https://github.com/cloudposse/terraform-aws-service-quotas), but it has issues, such as not allowing the service to be specified by name, and not having well documented inputs. It also takes a list input, but Atmos does not merge lists, so a map input is more appropriate. Overall I like this component better, and if others do, too, I will replace the existing module (only at version 0.1.0) with this code. +Cloud Posse has a [service quotas module](https://github.com/cloudposse/terraform-aws-service-quotas), but it has +issues, such as not allowing the service to be specified by name, and not having well documented inputs. It also takes a +list input, but Atmos does not merge lists, so a map input is more appropriate. Overall I like this component better, +and if others do, too, I will replace the existing module (only at version 0.1.0) with this code.
- ## 1.86.0 (2022-10-19T07:28:11Z)
@@ -4957,19 +5062,29 @@ Cloud Posse has a [service quotas module](https://github.com/cloudposse/terrafor Update EKS cluster and basic Kubernetes components for better behavior on initial deployment and on `terraform destroy`. -- Update minimum Terraform version to 1.1.0 and use `one()` where applicable to manage resources that can be disabled with `count = 0` and for bug fixes regarding destroy behavior +- Update minimum Terraform version to 1.1.0 and use `one()` where applicable to manage resources that can be disabled + with `count = 0` and for bug fixes regarding destroy behavior - Update `terraform-aws-eks-cluster` to v2.5.0 for better destroy behavior -- Update all components' (plus `account-map/modules/`)`remote-state` to v1.2.0 for better destroy behavior -- Update all components' `helm-release` to v0.7.0 and move namespace creation via Kubernetes provider into it to avoid race conditions regarding creating IAM roles, Namespaces, and deployments, and to delete namespaces when destroyed -- Update `alb-controller` to deploy a default IngressClass for central, obvious configuration of shared default ingress for services that do not have special needs. -- Add `alb-controller-ingress-class` for the rare case when we want to deploy a non-default IngressClass outside of the component that will be using it -- Update `echo-server` to use the default IngressClass and not specify any configuration that affects other Ingresses, and remove dependence on `alb-controller-ingress-group` (which should be deprecated in favor of `alb-controller-ingress-class` and perhaps a specialized future `alb-controller-ingress`) -- Update `cert-manager` to remove `default.auto.tfvars` (which had a lot of settings) and add dependencies so that initial deployment succeeds in one `terraform apply` and destroy works in one `terraform destroy` +- Update all components' (plus `account-map/modules/`)`remote-state` to v1.2.0 for better destroy behavior +- Update all components' `helm-release` to v0.7.0 and move namespace creation via Kubernetes provider into it to avoid + race conditions regarding creating IAM roles, Namespaces, and deployments, and to delete namespaces when destroyed +- Update `alb-controller` to deploy a default IngressClass for central, obvious configuration of shared default ingress + for services that do not have special needs. +- Add `alb-controller-ingress-class` for the rare case when we want to deploy a non-default IngressClass outside of the + component that will be using it +- Update `echo-server` to use the default IngressClass and not specify any configuration that affects other Ingresses, + and remove dependence on `alb-controller-ingress-group` (which should be deprecated in favor of + `alb-controller-ingress-class` and perhaps a specialized future `alb-controller-ingress`) +- Update `cert-manager` to remove `default.auto.tfvars` (which had a lot of settings) and add dependencies so that + initial deployment succeeds in one `terraform apply` and destroy works in one `terraform destroy` - Update `external-dns` to remove `default.auto.tfvars` (which had a lot of settings) - Update `karpenter` to v0.18.0, fix/update IAM policy (README still needs work, but leaving that for another day) -- Update `karpenter-provisioner` to require Terraform 1.3 and make elements of the Provisioner configuration optional. Support block device mappings (previously broken). Avoid perpetual Terraform plan diff/drift caused by setting fields to `null`. +- Update `karpenter-provisioner` to require Terraform 1.3 and make elements of the Provisioner configuration optional. + Support block device mappings (previously broken). Avoid perpetual Terraform plan diff/drift caused by setting fields + to `null`. - Update `reloader` -- Update `mixins/provider-helm` to better support `terraform destroy` and to default the Kubernetes client authentication API version to `client.authentication.k8s.io/v1beta1` +- Update `mixins/provider-helm` to better support `terraform destroy` and to default the Kubernetes client + authentication API version to `client.authentication.k8s.io/v1beta1` ### references @@ -4978,33 +5093,30 @@ Update EKS cluster and basic Kubernetes components for better behavior on initia - https://github.com/cloudposse/terraform-yaml-stack-config/pull/56 - https://github.com/hashicorp/terraform/issues/32023 - -
- ## 1.85.0 (2022-10-18T00:05:19Z)
Upstream `github-runners` @milldr (#508) ### what + - Minor TLC updates for GitHub Runners ASG component ### why -- Maintaining up-to-date upstream - +- Maintaining up-to-date upstream
- ## 1.84.0 (2022-10-12T22:49:28Z)
Fix feature allowing IAM users to assume team roles @Nuru (#507) ### what + - Replace `deny_all_iam_users` input with `iam_users_enabled` - Fix implementation - Provide more context for `bats` test failures @@ -5012,16 +5124,20 @@ Update EKS cluster and basic Kubernetes components for better behavior on initia ### why - Cloud Posse style guide dictates that boolean feature flags have names ending with `_enabled` -- Previous implementation only removed 1 of 2 policy provisions that blocked IAM users from assuming a role, and therefore IAM users were still not allowed to assume a role. Since the previous implementation did not work, a breaking change (changing the variable name) does not need major warnings or a major version bump. -- Indication of what was being tested was too far removed from `bats` test failure message to be able to easily identify what module had failed +- Previous implementation only removed 1 of 2 policy provisions that blocked IAM users from assuming a role, and + therefore IAM users were still not allowed to assume a role. Since the previous implementation did not work, a + breaking change (changing the variable name) does not need major warnings or a major version bump. +- Indication of what was being tested was too far removed from `bats` test failure message to be able to easily identify + what module had failed ### notes -Currently, any component provisioned by SuperAdmin needs to have a special provider configuration that requires SuperAdmin to provision the component. This feature is part of what is needed to enable SuperAdmin (an IAM User) to work with "normal" provider configurations. +Currently, any component provisioned by SuperAdmin needs to have a special provider configuration that requires +SuperAdmin to provision the component. This feature is part of what is needed to enable SuperAdmin (an IAM User) to work +with "normal" provider configurations. ### references - Breaks change introduced in #495, but that didn't work anyway. -
diff --git a/README.md b/README.md index 6badcac97..f604c5195 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ Please use the [issue tracker](https://github.com/cloudposse/terraform-aws-compo ### 💻 Developing -If you are interested in being a contributor and want to get involved in developing this project or help out with Cloud Posse's other projects, we would love to hear from you! +If you are interested in being a contributor and want to get involved in developing this project or help out with Cloud Posse's other projects, we would love to hear from you! Hit us up in [Slack](https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=slack), in the `#cloudposse` channel. In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. diff --git a/docs/targets.md b/docs/targets.md index e44b8acf8..4a98e523f 100644 --- a/docs/targets.md +++ b/docs/targets.md @@ -1,5 +1,7 @@ + ## Makefile Targets + ```text Available targets: @@ -11,4 +13,5 @@ Available targets: upstream-component Upstream a given component ``` + diff --git a/mixins/README.md b/mixins/README.md index 04e20811d..8818bf35b 100644 --- a/mixins/README.md +++ b/mixins/README.md @@ -1,12 +1,14 @@ # Terraform Mixins -A Terraform mixin (inspired by the [concept of the same name in OOP languages such as Python and Ruby](https://en.wikipedia.org/wiki/Mixin)) -is a Terraform configuration file that can be dropped into a root-level module, i.e. a component, in order to add additional +A Terraform mixin (inspired by the +[concept of the same name in OOP languages such as Python and Ruby](https://en.wikipedia.org/wiki/Mixin)) is a Terraform +configuration file that can be dropped into a root-level module, i.e. a component, in order to add additional functionality. Mixins are meant to encourage code reuse, leading to more simple components with less code repetition between component to component. + ## Mixin: `infra-state.mixin.tf` @@ -52,3 +54,4 @@ etc. That is, that it has the following characteristics: 2. Does not already instantiate a Kubernetes provider (only the Helm provider is necessary, typically, for EKS components). + diff --git a/mixins/github-actions-iam-role/README-github-action-iam-role.md b/mixins/github-actions-iam-role/README-github-action-iam-role.md index 1c8e0b6bb..c46fbf286 100644 --- a/mixins/github-actions-iam-role/README-github-action-iam-role.md +++ b/mixins/github-actions-iam-role/README-github-action-iam-role.md @@ -1,33 +1,32 @@ # Mixin: `github-actions-iam-role.mixin.tf` -This mixin component is responsible for creating an IAM role that can be assumed by a GitHub action for a specific purpose. -It requires that the `github-oidc-provider` component be installed in the same account, that -`components/terraform/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf` -is present in the repository, and that the component using this mixin contains a file (by convention named -`github-actions-iam-policy.tf`) which defines a JSON policy document that will be attached to the IAM role, -contained in a local variable named `github_actions_iam_policy`. It is up to the component using this mixin -to define the policy to be associated with the role. The policy should be as restrictive as possible. - -At this time, only one role can be created per component (per account, per region). Generated role names -include all the `null-label` labels, so it is possible to create multiple roles in the same account, -but not multiple roles in the same component in the same region with different policies. -This limitation of the mixin is somewhat intentional, in that each role should be created for a specific -component, and component can create its own specific role. If this limitation turns -out to be truly burdensome, note that `aws-teams` also supports GitHub actions assuming its roles. - +This mixin component is responsible for creating an IAM role that can be assumed by a GitHub action for a specific +purpose. It requires that the `github-oidc-provider` component be installed in the same account, that +`components/terraform/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf` is present in the +repository, and that the component using this mixin contains a file (by convention named `github-actions-iam-policy.tf`) +which defines a JSON policy document that will be attached to the IAM role, contained in a local variable named +`github_actions_iam_policy`. It is up to the component using this mixin to define the policy to be associated with the +role. The policy should be as restrictive as possible. + +At this time, only one role can be created per component (per account, per region). Generated role names include all the +`null-label` labels, so it is possible to create multiple roles in the same account, but not multiple roles in the same +component in the same region with different policies. This limitation of the mixin is somewhat intentional, in that each +role should be created for a specific component, and component can create its own specific role. If this limitation +turns out to be truly burdensome, note that `aws-teams` also supports GitHub actions assuming its roles. ## Usage **Stack Level**: Global or Regional This mixin provisions a specific IAM role that can be assumed by a GitHub action for a specific purpose, analogous to -how [EKS IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) +how +[EKS IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) works for EKS. ### Define the role policy -Create a file named `github-actions-iam-policy.tf` that defines the desired policy for the role and saves it -as a JSON string in a local variable named `github_actions_iam_policy`. For example: +Create a file named `github-actions-iam-policy.tf` that defines the desired policy for the role and saves it as a JSON +string in a local variable named `github_actions_iam_policy`. For example: ```hcl locals { @@ -48,11 +47,10 @@ data "aws_iam_policy_document" "github_actions_iam_policy" { ### Create the role alongside the component -Define values for the variables defined in `github-actions-iam-role.mixin.tf` in the stack for the component. -Most importantly, set `github_actions_allowed_repos` to the list of GitHub repositories where installed -GitHub actions will be allowed to assume the role. Wildcards are allowed, so you can allow all repositories -in your organization by setting `github_actions_allowed_repos = ["/*"]`. - +Define values for the variables defined in `github-actions-iam-role.mixin.tf` in the stack for the component. Most +importantly, set `github_actions_allowed_repos` to the list of GitHub repositories where installed GitHub actions will +be allowed to assume the role. Wildcards are allowed, so you can allow all repositories in your organization by setting +`github_actions_allowed_repos = ["/*"]`. ```yaml components: @@ -71,26 +69,25 @@ components: #### Add required workflow permissions -In the GitHub action workflow, add required permissions at the top of the -workflow, or within the job. See the [GitHub documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings) +In the GitHub action workflow, add required permissions at the top of the workflow, or within the job. See the +[GitHub documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings) for more details. ```yaml permissions: id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout + contents: read # This is required for actions/checkout ``` #### Configure settings via environment variables -Although you can configure the settings in various ways, including using -GitHub Secrets and Environments, for a balance of simplicity and visibility -we recommend configuration by hard-coding settings in the following environment variables +Although you can configure the settings in various ways, including using GitHub Secrets and Environments, for a balance +of simplicity and visibility we recommend configuration by hard-coding settings in the following environment variables at the top the workflow: ```yaml env: - AWS_REGION: us-east-1 # The AWS region where the workflow should run + AWS_REGION: us-east-1 # The AWS region where the workflow should run ECR_REPOSITORY: infrastructure # The ECR repository where the workflow should push the image ECR_REGISTRY: 123456789012.dkr.ecr.us-east-1.amazonaws.com # The ECR registry where the workflow should push the image GHA_IAM_ROLE: arn:aws:iam::123456789012:role/eg-mgmt-use1-art-gha # The ARN of the IAM role to assume @@ -99,10 +96,10 @@ env: Then add the following step to assume the role: ```yaml - - name: Configure AWS credentials for ECR - uses: aws-actions/configure-aws-credentials@v1 - with: - role-to-assume: ${{ env.GHA_IAM_ROLE }} - role-session-name: infra-gha-docker-build-and-push # This can be any name. It shows up in audit logs. - aws-region: ${{ env.AWS_REGION }} +- name: Configure AWS credentials for ECR + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ env.GHA_IAM_ROLE }} + role-session-name: infra-gha-docker-build-and-push # This can be any name. It shows up in audit logs. + aws-region: ${{ env.AWS_REGION }} ``` diff --git a/modules/account-map/README.md b/modules/account-map/README.md index 7e207c9c7..893586de3 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -1,18 +1,21 @@ # Component: `account-map` -This component is responsible for provisioning information only: it simply populates Terraform state with data (account ids, groups, and roles) that other root modules need via outputs. +This component is responsible for provisioning information only: it simply populates Terraform state with data (account +ids, groups, and roles) that other root modules need via outputs. ## Pre-requisites -- [account](https://docs.cloudposse.com/components/library/aws/account) must be provisioned before [account-map](https://docs.cloudposse.com/components/library/aws/account-map) component +- [account](https://docs.cloudposse.com/components/library/aws/account) must be provisioned before + [account-map](https://docs.cloudposse.com/components/library/aws/account-map) component ## Usage **Stack Level**: Global -Here is an example snippet for how to use this component. Include this snippet in the stack configuration for the management account -(typically `root`) in the management tenant/OU (usually something like `mgmt` or `core`) in the global region (`gbl`). You can include -the content directly, or create a `stacks/catalog/account-map.yaml` file and import it from there. +Here is an example snippet for how to use this component. Include this snippet in the stack configuration for the +management account (typically `root`) in the management tenant/OU (usually something like `mgmt` or `core`) in the +global region (`gbl`). You can include the content directly, or create a `stacks/catalog/account-map.yaml` file and +import it from there. ```yaml components: @@ -44,9 +47,9 @@ components: iam_role_arn_template_template: "arn:%s:iam::%s:role/%s-%s-%s-%s-%%s" # `profile_template` is the template used to render AWS Profile names. profile_template: "%s-%s-%s-%s-%s" - ``` + ## Requirements @@ -149,9 +152,11 @@ components: | [terraform\_role\_name\_map](#output\_terraform\_role\_name\_map) | Mapping of Terraform action (plan or apply) to aws-team-role name to assume for that action | | [terraform\_roles](#output\_terraform\_roles) | A list of all IAM roles used to run terraform updates | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-map) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-map) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/account-map/modules/iam-roles/README.md b/modules/account-map/modules/iam-roles/README.md index c08ecf2a5..3fb46aa56 100644 --- a/modules/account-map/modules/iam-roles/README.md +++ b/modules/account-map/modules/iam-roles/README.md @@ -1,16 +1,14 @@ # Submodule `iam-roles` -This submodule is used by other modules to determine which IAM Roles -or AWS CLI Config Profiles to use for various tasks, most commonly -for applying Terraform plans. +This submodule is used by other modules to determine which IAM Roles or AWS CLI Config Profiles to use for various +tasks, most commonly for applying Terraform plans. ## Special Configuration Needed -In order to avoid having to pass customization information through every module -that uses this submodule, if the default configuration does not suit your needs, -you are expected to add `variables_override.tf` to override the variables with -the defaults you want to use in your project. For example, if you are not using -"core" as the `tenant` portion of your "root" account (your Organization Management Account), -then you should include the `variable "overridable_global_tenant_name"` declaration -in your `variables_override.tf` so that `overridable_global_tenant_name` defaults -to the value you are using (or the empty string if you are not using `tenant` at all). +In order to avoid having to pass customization information through every module that uses this submodule, if the default +configuration does not suit your needs, you are expected to add `variables_override.tf` to override the variables with +the defaults you want to use in your project. For example, if you are not using "core" as the `tenant` portion of your +"root" account (your Organization Management Account), then you should include the +`variable "overridable_global_tenant_name"` declaration in your `variables_override.tf` so that +`overridable_global_tenant_name` defaults to the value you are using (or the empty string if you are not using `tenant` +at all). diff --git a/modules/account-map/modules/roles-to-principals/README.md b/modules/account-map/modules/roles-to-principals/README.md index 64d9b1419..65e45e000 100644 --- a/modules/account-map/modules/roles-to-principals/README.md +++ b/modules/account-map/modules/roles-to-principals/README.md @@ -1,17 +1,14 @@ # Submodule `roles-to-principals` -This submodule is used by other modules to map short role names and AWS -SSO Permission Set names in accounts designated by short account names -(for example, `terraform` in the `dev` account) to full IAM Role ARNs and -other related tasks. +This submodule is used by other modules to map short role names and AWS SSO Permission Set names in accounts designated +by short account names (for example, `terraform` in the `dev` account) to full IAM Role ARNs and other related tasks. ## Special Configuration Needed -As with `iam-roles`, in order to avoid having to pass customization information through every module -that uses this submodule, if the default configuration does not suit your needs, -you are expected to add `variables_override.tf` to override the variables with -the defaults you want to use in your project. For example, if you are not using -"core" as the `tenant` portion of your "root" account (your Organization Management Account), -then you should include the `variable "overridable_global_tenant_name"` declaration -in your `variables_override.tf` so that `overridable_global_tenant_name` defaults -to the value you are using (or the empty string if you are not using `tenant` at all). +As with `iam-roles`, in order to avoid having to pass customization information through every module that uses this +submodule, if the default configuration does not suit your needs, you are expected to add `variables_override.tf` to +override the variables with the defaults you want to use in your project. For example, if you are not using "core" as +the `tenant` portion of your "root" account (your Organization Management Account), then you should include the +`variable "overridable_global_tenant_name"` declaration in your `variables_override.tf` so that +`overridable_global_tenant_name` defaults to the value you are using (or the empty string if you are not using `tenant` +at all). diff --git a/modules/account-map/modules/team-assume-role-policy/README.md b/modules/account-map/modules/team-assume-role-policy/README.md index 7a22501fc..f309bf33c 100644 --- a/modules/account-map/modules/team-assume-role-policy/README.md +++ b/modules/account-map/modules/team-assume-role-policy/README.md @@ -2,13 +2,14 @@ This submodule generates a JSON-encoded IAM Policy Document suitable for use as an "Assume Role Policy". -You can designate both who is allowed to assume a role and who is explicitly denied permission -to assume a role. The value of this submodule is that it allows for many ways -to specify the "who" while at the same time limiting the "who" to assumed IAM roles: +You can designate both who is allowed to assume a role and who is explicitly denied permission to assume a role. The +value of this submodule is that it allows for many ways to specify the "who" while at the same time limiting the "who" +to assumed IAM roles: - All assumed roles in the `dev` account: `allowed_roles = { dev = ["*"] }` - Only the `admin` role in the dev account: `allowed_roles = { dev = ["admin"] }` -- A specific principal in any account (though it must still be an assumed role): `allowed_principal_arns = arn:aws:iam::123456789012:role/trusted-role` +- A specific principal in any account (though it must still be an assumed role): + `allowed_principal_arns = arn:aws:iam::123456789012:role/trusted-role` - A user of a specific AWS SSO Permission Set: `allowed_permission_sets = { dev = ["DeveloperAccess"] }` ## Usage @@ -30,6 +31,7 @@ resource "aws_iam_role" "default" { } ``` + ## Requirements @@ -100,3 +102,4 @@ No requirements. | [github\_assume\_role\_policy](#output\_github\_assume\_role\_policy) | JSON encoded string representing the "Assume Role" policy configured by the inputs | | [policy\_document](#output\_policy\_document) | JSON encoded string representing the "Assume Role" policy configured by the inputs | + diff --git a/modules/account-quotas/README.md b/modules/account-quotas/README.md index 59a6e7831..a442dddac 100644 --- a/modules/account-quotas/README.md +++ b/modules/account-quotas/README.md @@ -1,19 +1,19 @@ # Component: `account-quotas` -This component is responsible for requesting service quota increases. We recommend -making requests here rather than in `account-settings` because `account-settings` -is a restricted component that can only be applied by SuperAdmin. - +This component is responsible for requesting service quota increases. We recommend making requests here rather than in +`account-settings` because `account-settings` is a restricted component that can only be applied by SuperAdmin. ## Usage **Stack Level**: Global and Regional (depending on quota) -Global resources must be provisioned in `us-east-1`. Put them in the `gbl` stack, but set `region: us-east-1` in the `vars` section. +Global resources must be provisioned in `us-east-1`. Put them in the `gbl` stack, but set `region: us-east-1` in the +`vars` section. -You can refer to services either by their exact full name (e.g. `service_name: "Amazon Elastic Compute Cloud (Amazon EC2)"`) or by the -service code (e.g. `service_code: "ec2"`). Similarly, you can refer to quota names either by their exact full name -(e.g. `quota_name: "EC2-VPC Elastic IPs"`) or by the quota code (e.g. `quota_code: "L-0263D0A3"`). +You can refer to services either by their exact full name (e.g. +`service_name: "Amazon Elastic Compute Cloud (Amazon EC2)"`) or by the service code (e.g. `service_code: "ec2"`). +Similarly, you can refer to quota names either by their exact full name (e.g. `quota_name: "EC2-VPC Elastic IPs"`) or by +the quota code (e.g. `quota_code: "L-0263D0A3"`). You can find service codes and full names via the AWS CLI (be sure to use the correct region): @@ -21,17 +21,18 @@ You can find service codes and full names via the AWS CLI (be sure to use the co aws --region us-east-1 service-quotas list-services ``` -You can find quota codes and full names, and also whether the quotas are adjustable or global, via the AWS CLI, -but you will need the service code from the previous step: +You can find quota codes and full names, and also whether the quotas are adjustable or global, via the AWS CLI, but you +will need the service code from the previous step: ```bash aws --region us-east-1 service-quotas list-service-quotas --service-code ec2 ``` -If you make a request to raise a quota, the output will show the requested value as `value` while the request is pending. +If you make a request to raise a quota, the output will show the requested value as `value` while the request is +pending. -Even though the Terraform will submit the support request, you may need to follow up with AWS support to get the request approved, -via the AWS console or email. +Even though the Terraform will submit the support request, you may need to follow up with AWS support to get the request +approved, via the AWS console or email. Here's an example snippet for how to use this component. @@ -51,6 +52,7 @@ components: value: 10 ``` + ## Requirements @@ -111,10 +113,13 @@ components: |------|-------------| | [quotas](#output\_quotas) | Full report on all service quotas managed by this component. | + ## References - [AWS Service Quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) -- AWS CLI [command to list service codes](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/service-quotas/list-services.html): `aws service-quotas list-services` +- AWS CLI + [command to list service codes](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/service-quotas/list-services.html): + `aws service-quotas list-services` [](https://cpco.io/component) diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index 7fcd021df..a7e6a0d5e 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -1,14 +1,15 @@ # Component: `account-settings` -This component is responsible for provisioning account level settings: IAM password policy, AWS Account Alias, EBS encryption, and Service Quotas. +This component is responsible for provisioning account level settings: IAM password policy, AWS Account Alias, EBS +encryption, and Service Quotas. ## Usage **Stack Level**: Global -Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts, -so create a file `stacks/catalog/account-settings.yaml` with the following content and then import -that file in each account's global stack (overriding any parameters as needed): +Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts, so +create a file `stacks/catalog/account-settings.yaml` with the following content and then import that file in each +account's global stack (overriding any parameters as needed): ```yaml components: @@ -68,6 +69,7 @@ components: value: null ``` + ## Requirements @@ -138,8 +140,11 @@ components: |------|-------------| | [account\_alias](#output\_account\_alias) | Account alias | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-settings) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-settings) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/account/README.md b/modules/account/README.md index c3c2d50cb..b9f6fa874 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -1,32 +1,45 @@ # Component: `account` -This component is responsible for provisioning the full account hierarchy along with Organizational Units (OUs). It includes the ability to associate Service Control Policies (SCPs) to the Organization, each Organizational Unit and account. +This component is responsible for provisioning the full account hierarchy along with Organizational Units (OUs). It +includes the ability to associate Service Control Policies (SCPs) to the Organization, each Organizational Unit and +account. -:::info -Part of a [cold start](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) so it has to be initially run with `SuperAdmin` role. +:::info Part of a +[cold start](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) +so it has to be initially run with `SuperAdmin` role. ::: -In addition, it enables [AWS IAM Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html), which helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. Access Analyzer identifies resources that are shared with external principals by using logic-based reasoning to analyze the resource-based policies in your AWS environment. For each instance of a resource that is shared outside of your account, Access Analyzer generates a finding. Findings include information about the access and the external principal that it is granted to. You can review findings to determine whether the access is intended and safe, or the access is unintended and a security risk. +In addition, it enables +[AWS IAM Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html), which helps +you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared +with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. +Access Analyzer identifies resources that are shared with external principals by using logic-based reasoning to analyze +the resource-based policies in your AWS environment. For each instance of a resource that is shared outside of your +account, Access Analyzer generates a finding. Findings include information about the access and the external principal +that it is granted to. You can review findings to determine whether the access is intended and safe, or the access is +unintended and a security risk. ## Usage **Stack Level**: Global -**IMPORTANT**: Account Name building blocks (such as tenant, stage, environment) must not contain dashes. Doing so will lead to unpredictable resource names as a `-` is the default delimiter. Additionally, account names must be lower case alpha-numeric with no special characters. -For example: +**IMPORTANT**: Account Name building blocks (such as tenant, stage, environment) must not contain dashes. Doing so will +lead to unpredictable resource names as a `-` is the default delimiter. Additionally, account names must be lower case +alpha-numeric with no special characters. For example: | Key | Value | Correctness | -|------------------|-----------------|-------------| -| **Tenant** | foo | ✅ | -| **Tenant** | foo-bar | ❌ | -| **Environment** | use1 | ✅ | -| **Environment** | us-east-1 | ❌ | -| **Account Name** | `core-identity` | ✅ | - -Here is an example snippet for how to use this component. Include this snippet in the stack configuration for the management account -(typically `root`) in the management tenant/OU (usually something like `mgmt` or `core`) in the global region (`gbl`). You can insert -the content directly, or create a `stacks/catalog/account.yaml` file and import it from there. +| ---------------- | --------------- | ----------- | +| **Tenant** | foo | ✅ | +| **Tenant** | foo-bar | ❌ | +| **Environment** | use1 | ✅ | +| **Environment** | us-east-1 | ❌ | +| **Account Name** | `core-identity` | ✅ | + +Here is an example snippet for how to use this component. Include this snippet in the stack configuration for the +management account (typically `root`) in the management tenant/OU (usually something like `mgmt` or `core`) in the +global region (`gbl`). You can insert the content directly, or create a `stacks/catalog/account.yaml` file and import it +from there. ```yaml components: @@ -152,16 +165,22 @@ components: Your AWS Organization is managed by the `account` component, along with accounts and organizational units. -However, because the AWS defaults for an Organization and its accounts are not exactly what we want, and there is no way to change them via Terraform, we have to first provision the AWS Organization, then take some steps on the AWS console, and then we can provision the rest. +However, because the AWS defaults for an Organization and its accounts are not exactly what we want, and there is no way +to change them via Terraform, we have to first provision the AWS Organization, then take some steps on the AWS console, +and then we can provision the rest. ### Use AWS Console to create and set up the Organization -Unfortunately, there are some tasks that need to be done via the console. Log into the AWS Console with the root (not SuperAdmin) credentials you have saved in 1Password. +Unfortunately, there are some tasks that need to be done via the console. Log into the AWS Console with the root (not +SuperAdmin) credentials you have saved in 1Password. #### Request an increase in the maximum number of accounts allowed -:::caution -Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is necessary to expedite the quota increase requests, which could take several days on a basic support plan. Without it, AWS support will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the requests. AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to your AWS TAM. See [AWS](https://docs.cloudposse.com/reference-architecture/reference/aws). +:::caution Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is +necessary to expedite the quota increase requests, which could take several days on a basic support plan. Without it, +AWS support will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the +requests. AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to your +AWS TAM. See [AWS](https://docs.cloudposse.com/reference-architecture/reference/aws). ::: @@ -179,13 +198,19 @@ Make sure your support plan for the _root_ account was upgraded to the "Business 7. Click on "Request quota increase" on the right side of the view, which should pop us a request form -8. At the bottom of the form, under "Change quota value", enter the number you decided on in the previous step (probably "20") and click "Request" +8. At the bottom of the form, under "Change quota value", enter the number you decided on in the previous step (probably + "20") and click "Request" #### (Optional) Create templates to request other quota increases -New accounts start with a low limit on the number of instances you can create. However, as you add accounts, and use more instances, the numbers automatically adjust up. So you may or may not want to create a template to generate automatic quota increase requests, depending on how many instances per account you expect to want to provision right away. +New accounts start with a low limit on the number of instances you can create. However, as you add accounts, and use +more instances, the numbers automatically adjust up. So you may or may not want to create a template to generate +automatic quota increase requests, depending on how many instances per account you expect to want to provision right +away. -Create a [Quota request template](https://docs.aws.amazon.com/servicequotas/latest/userguide/organization-templates.html) for the organization. From the Sidebar, click "Quota request template" +Create a +[Quota request template](https://docs.aws.amazon.com/servicequotas/latest/userguide/organization-templates.html) for the +organization. From the Sidebar, click "Quota request template" Add each EC2 quota increase request you want to make: @@ -213,19 +238,25 @@ After you have added all the templates, click "Enable" on the Quota request temp #### Enable resource sharing with AWS Organization -[AWS Resource Access Manager (RAM)](https://docs.aws.amazon.com/ram/latest/userguide/what-is.html) lets you share your resources with any AWS account or through AWS Organizations. +[AWS Resource Access Manager (RAM)](https://docs.aws.amazon.com/ram/latest/userguide/what-is.html) lets you share your +resources with any AWS account or through AWS Organizations.
-If you have multiple AWS accounts, you can create resources centrally and use AWS RAM to share those resources with other accounts. +If you have multiple AWS accounts, you can create resources centrally and use AWS RAM to share those resources with +other accounts. -Resource sharing through AWS Organization will be used to share the Transit Gateway deployed in the `network` account with other accounts to connect their VPCs to the shared Transit Gateway. +Resource sharing through AWS Organization will be used to share the Transit Gateway deployed in the `network` account +with other accounts to connect their VPCs to the shared Transit Gateway. -This is a one-time manual step in the AWS Resource Access Manager console. When you share resources within your organization, AWS RAM does not send invitations to principals. Principals in your organization get access to shared resources without exchanging invitations. +This is a one-time manual step in the AWS Resource Access Manager console. When you share resources within your +organization, AWS RAM does not send invitations to principals. Principals in your organization get access to shared +resources without exchanging invitations. To enable resource sharing with AWS Organization via AWS Management Console -- Open the Settings page of AWS Resource Access Manager console at [https://console.aws.amazon.com/ram/home#Settings](https://console.aws.amazon.com/ram/home#Settings) +- Open the Settings page of AWS Resource Access Manager console at + [https://console.aws.amazon.com/ram/home#Settings](https://console.aws.amazon.com/ram/home#Settings) - Choose "Enable sharing with AWS Organizations" @@ -248,9 +279,11 @@ For more information, see: ### Import the organization into Terraform using the `account` component -After we are done with the above ClickOps and the Service Quota Increase for maximum number of accounts has been granted, we can then do the rest via Terraform. +After we are done with the above ClickOps and the Service Quota Increase for maximum number of accounts has been +granted, we can then do the rest via Terraform. -In the Geodesic shell, as SuperAdmin, execute the following command to get the AWS Organization ID that will be used to import the organization: +In the Geodesic shell, as SuperAdmin, execute the following command to get the AWS Organization ID that will be used to +import the organization: ``` aws organizations describe-organization @@ -268,7 +301,9 @@ From the output, identify the _organization-id_: Using the example above, the _organization-id_ is o-7qcakq6zxw. -In the Geodesic shell, as SuperAdmin, execute the following command to import the AWS Organization, changing the stack name `core-gbl-root` if needed, to reflect the stack where the organization management account is defined, and changing the last argument to reflect the _organization-id_ from the output of the previous command. +In the Geodesic shell, as SuperAdmin, execute the following command to import the AWS Organization, changing the stack +name `core-gbl-root` if needed, to reflect the stack where the organization management account is defined, and changing +the last argument to reflect the _organization-id_ from the output of the previous command. ``` atmos terraform import account --stack core-gbl-root 'aws_organizations_organization.this[0]' 'o-7qcakq6zxw' @@ -276,16 +311,18 @@ atmos terraform import account --stack core-gbl-root 'aws_organizations_organiza ### Provision AWS OUs and Accounts using the `account` component -AWS accounts and organizational units are generated dynamically by the `terraform/account` component using the configuration in the `gbl-root` stack. +AWS accounts and organizational units are generated dynamically by the `terraform/account` component using the +configuration in the `gbl-root` stack. -:::info -_**Special note:**_ **** In the rare case where you will need to be enabling non-default AWS Regions, temporarily comment out the `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore it later, after enabling the optional Regions. -See related: [Decide on Opting Into Non-default Regions](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) +:::info _**Special note:**_ \*\*\*\* In the rare case where you will need to be enabling non-default AWS Regions, +temporarily comment out the `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore +it later, after enabling the optional Regions. See related: +[Decide on Opting Into Non-default Regions](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) ::: -:::caution -**You must wait until your quota increase request has been granted.** If you try to create the accounts before the quota increase is granted, you can expect to see failures like `ACCOUNT_NUMBER_LIMIT_EXCEEDED`. +:::caution **You must wait until your quota increase request has been granted.** If you try to create the accounts +before the quota increase is granted, you can expect to see failures like `ACCOUNT_NUMBER_LIMIT_EXCEEDED`. ::: @@ -295,40 +332,60 @@ In the Geodesic shell, execute the following commands to provision AWS Organizat atmos terraform apply account --stack gbl-root ``` -Review the Terraform plan, _**ensure that no new organization will be created**_ (look for `aws_organizations_organization.this[0]`), type "yes" to approve and apply. This creates the AWS organizational units and AWS accounts. +Review the Terraform plan, _**ensure that no new organization will be created**_ (look for +`aws_organizations_organization.this[0]`), type "yes" to approve and apply. This creates the AWS organizational units +and AWS accounts. ### Configure root account credentials for each account -Note: unless you need to enable non-default AWS regions (see next step), this step can be done later or in parallel with other steps, for example while waiting for Terraform to create resources. +Note: unless you need to enable non-default AWS regions (see next step), this step can be done later or in parallel with +other steps, for example while waiting for Terraform to create resources. **For** _**each**_ **new account:** -1. Perform a password reset by attempting to [log in to the AWS console](https://signin.aws.amazon.com/signin) as a "root user", using that account's email address, and then clicking the "Forgot password?" link. You will receive a password reset link via email, which should be forwarded to the shared Slack channel for automated messages. Click the link and enter a new password. (Use 1Password or [Random.org](https://www.random.org/passwords) to create a password 26-38 characters long, including at least 3 of each class of character: lower case, uppercase, digit, and symbol. You may need to manually combine or add to the generated password to ensure 3 symbols and digits are present.) Save the email address and generated password as web login credentials in 1Password. While you are at it, save the account number in a separate field. +1. Perform a password reset by attempting to [log in to the AWS console](https://signin.aws.amazon.com/signin) as a + "root user", using that account's email address, and then clicking the "Forgot password?" link. You will receive a + password reset link via email, which should be forwarded to the shared Slack channel for automated messages. Click + the link and enter a new password. (Use 1Password or [Random.org](https://www.random.org/passwords) to create a + password 26-38 characters long, including at least 3 of each class of character: lower case, uppercase, digit, and + symbol. You may need to manually combine or add to the generated password to ensure 3 symbols and digits are + present.) Save the email address and generated password as web login credentials in 1Password. While you are at it, + save the account number in a separate field. -2. Log in using the new password, choose "My Security Credentials" from the account dropdown menu and set up Multi-Factor Authentication (MFA) to use a Virutal MFA device. Save the MFA TOTP key in 1Password by using 1Password's TOTP field and built-in screen scanner. Also, save the Virutal MFA ARN (sometimes shown as "serial number"). +2. Log in using the new password, choose "My Security Credentials" from the account dropdown menu and set up + Multi-Factor Authentication (MFA) to use a Virutal MFA device. Save the MFA TOTP key in 1Password by using + 1Password's TOTP field and built-in screen scanner. Also, save the Virutal MFA ARN (sometimes shown as "serial + number"). 3. While logged in, enable optional regions as described in the next step, if needed. -4. (Optional, but highly recommended): [Unsubscribe](https://pages.awscloud.com/communication-preferences.html) the account's email address from all marketing emails. +4. (Optional, but highly recommended): [Unsubscribe](https://pages.awscloud.com/communication-preferences.html) the + account's email address from all marketing emails. ### (Optional) Enable regions -Most AWS regions are enabled by default. If you are using a region that is not enabled by default (such as Middle East/Bahrain), you need to take extra steps. +Most AWS regions are enabled by default. If you are using a region that is not enabled by default (such as Middle +East/Bahrain), you need to take extra steps. -1. While logged in using root credentials (see the previous step), in the account dropdown menu, select "My Account" to get to the [Billing home page](https://console.aws.amazon.com/billing/home?#/account). +1. While logged in using root credentials (see the previous step), in the account dropdown menu, select "My Account" to + get to the [Billing home page](https://console.aws.amazon.com/billing/home?#/account). 2. In the "AWS Regions" section, enable the regions you want to enable. -3. Go to the IAM [account settings page](https://console.aws.amazon.com/iam/home?#/account_settings) and edit the STS Global endpoint to create session tokens valid in all AWS regions. +3. Go to the IAM [account settings page](https://console.aws.amazon.com/iam/home?#/account_settings) and edit the STS + Global endpoint to create session tokens valid in all AWS regions. -You will need to wait a few minutes for the regions to be enabled before you can proceed to the next step. Until they are enabled, you may get what look like AWS authentication or permissions errors. +You will need to wait a few minutes for the regions to be enabled before you can proceed to the next step. Until they +are enabled, you may get what look like AWS authentication or permissions errors. -After enabling the regions in all accounts, re-enable the `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml` and rerun +After enabling the regions in all accounts, re-enable the `DenyRootAccountAccess` service control policy setting in +`gbl-root.yaml` and rerun ``` atmos terraform apply account --stack gbl-root ``` + ## Requirements @@ -422,8 +479,11 @@ atmos terraform apply account --stack gbl-root | [organizational\_unit\_names\_organizational\_unit\_scp\_arns](#output\_organizational\_unit\_names\_organizational\_unit\_scp\_arns) | Map of OU names to SCP ARNs | | [organizational\_unit\_names\_organizational\_unit\_scp\_ids](#output\_organizational\_unit\_names\_organizational\_unit\_scp\_ids) | Map of OU names to SCP IDs | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/acm/README.md b/modules/acm/README.md index 38f60328d..bdd7d25a0 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -1,10 +1,19 @@ # Component: `acm` -This component is responsible for requesting an ACM certificate for a domain and adding a CNAME record to the DNS zone to complete certificate validation. +This component is responsible for requesting an ACM certificate for a domain and adding a CNAME record to the DNS zone +to complete certificate validation. -The ACM component is to manage an unlimited number of certificates, predominantly for vanity domains. While the [dns-primary](https://docs.cloudposse.com/components/library/aws/dns-primary) component has the ability to generate ACM certificates, it is very opinionated and can only manage one zone. In reality, companies have many branded domains associated with a load balancer, so we need to be able to generate more complicated certificates. +The ACM component is to manage an unlimited number of certificates, predominantly for vanity domains. While the +[dns-primary](https://docs.cloudposse.com/components/library/aws/dns-primary) component has the ability to generate ACM +certificates, it is very opinionated and can only manage one zone. In reality, companies have many branded domains +associated with a load balancer, so we need to be able to generate more complicated certificates. -We have, as a convenience, the ability to create an ACM certificate as part of creating a DNS zone, whether primary or delegated. That convenience is limited to creating `example.com` and `*.example.com` when creating a zone for `example.com`. For example, Acme has delegated `acct.acme.com` and in addition to `*.acct.acme.com` needed an ACM certificate for `*.usw2.acct.acme.com`, so we use the ACM component to provision that, rather than extend the DNS primary or delegated components to take a list of additional certificates. Both are different views on the Single Responsibility Principle. +We have, as a convenience, the ability to create an ACM certificate as part of creating a DNS zone, whether primary or +delegated. That convenience is limited to creating `example.com` and `*.example.com` when creating a zone for +`example.com`. For example, Acme has delegated `acct.acme.com` and in addition to `*.acct.acme.com` needed an ACM +certificate for `*.usw2.acct.acme.com`, so we use the ACM component to provision that, rather than extend the DNS +primary or delegated components to take a list of additional certificates. Both are different views on the Single +Responsibility Principle. ## Usage @@ -50,6 +59,7 @@ components: certificate_authority_component_key: subordinate ``` + ## Requirements @@ -130,8 +140,11 @@ components: | [domain\_validation\_options](#output\_domain\_validation\_options) | CNAME records that are added to the DNS zone to complete certificate validation | | [id](#output\_id) | The ID of the certificate | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/acm) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/acm) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/alb/README.md b/modules/alb/README.md index 0905867c7..25e47e977 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -1,6 +1,7 @@ # Component: `alb` -This component is responsible for provisioning a generic Application Load Balancer. It depends on the `vpc` and `dns-delegated` components. +This component is responsible for provisioning a generic Application Load Balancer. It depends on the `vpc` and +`dns-delegated` components. ## Usage @@ -17,6 +18,7 @@ components: health_check_path: /api/healthz ``` + ## Requirements @@ -125,10 +127,11 @@ No resources. | [listener\_arns](#output\_listener\_arns) | A list of all the listener ARNs | | [security\_group\_id](#output\_security\_group\_id) | The security group ID of the ALB | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/alb) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/alb) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/amplify/README.md b/modules/amplify/README.md index 50e5b12f5..b64597941 100644 --- a/modules/amplify/README.md +++ b/modules/amplify/README.md @@ -1,7 +1,7 @@ # Component: `amplify` -This component is responsible for provisioning -AWS Amplify apps, backend environments, branches, domain associations, and webhooks. +This component is responsible for provisioning AWS Amplify apps, backend environments, branches, domain associations, +and webhooks. ## Usage @@ -101,24 +101,25 @@ components: certificate_verification_dns_record_enabled: false ``` -The `amplify/example` YAML configuration defines an Amplify app in AWS. -The app is set up to use the `Next.js` framework with SSR (server-side rendering) and is linked to the -GitHub repository "https://github.com/cloudposse/amplify-test2". +The `amplify/example` YAML configuration defines an Amplify app in AWS. The app is set up to use the `Next.js` framework +with SSR (server-side rendering) and is linked to the GitHub repository "https://github.com/cloudposse/amplify-test2". -The app is set up to have two environments: `main` and `develop`. -Each environment has different configuration settings, such as the branch name, framework, and stage. -The `main` environment is set up for production, while the `develop` environments is set up for development. +The app is set up to have two environments: `main` and `develop`. Each environment has different configuration settings, +such as the branch name, framework, and stage. The `main` environment is set up for production, while the `develop` +environments is set up for development. -The app is also configured to have custom subdomains for each environment, with prefixes such as `example-prod` and `example-dev`. -The subdomains are configured to use DNS records, which are enabled through the `subdomains_dns_records_enabled` variable. +The app is also configured to have custom subdomains for each environment, with prefixes such as `example-prod` and +`example-dev`. The subdomains are configured to use DNS records, which are enabled through the +`subdomains_dns_records_enabled` variable. -The app also has an IAM service role configured with specific IAM actions, and environment variables set up for each environment. -Additionally, the app is configured to use the Atmos Spacelift workspace, as indicated by the `workspace_enabled: true` setting. +The app also has an IAM service role configured with specific IAM actions, and environment variables set up for each +environment. Additionally, the app is configured to use the Atmos Spacelift workspace, as indicated by the +`workspace_enabled: true` setting. The `amplify/example` Atmos component extends the `amplify/defaults` component. -The `amplify/example` configuration is imported into the `stacks/mixins/stage/dev.yaml` stack config file to be provisioned -in the `dev` account. +The `amplify/example` configuration is imported into the `stacks/mixins/stage/dev.yaml` stack config file to be +provisioned in the `dev` account. ```yaml # stacks/mixins/stage/dev.yaml @@ -132,6 +133,7 @@ You can execute the following command to provision the Amplify app using Atmos: atmos terraform apply amplify/example -s ``` + ## Requirements @@ -225,5 +227,6 @@ atmos terraform apply amplify/example -s | [sub\_domains](#output\_sub\_domains) | DNS records and the verified status for the subdomains | | [webhooks](#output\_webhooks) | Created webhooks | + [](https://cpco.io/component) diff --git a/modules/api-gateway-account-settings/README.md b/modules/api-gateway-account-settings/README.md index 04571f694..70a4a008b 100644 --- a/modules/api-gateway-account-settings/README.md +++ b/modules/api-gateway-account-settings/README.md @@ -1,8 +1,13 @@ # Component: `api-gateway-account-settings` -This component is responsible for setting the global, regional settings required to allow API Gateway to write to CloudWatch logs. +This component is responsible for setting the global, regional settings required to allow API Gateway to write to +CloudWatch logs. -Every AWS region you want to deploy an API Gateway to must be configured with an IAM Role that gives API Gateway permissions to create and write to CloudWatch logs. Without this configuration, API Gateway will not be able to send logs to CloudWatch. This configuration is done once per region regardless of the number of API Gateways deployed in that region. This module creates an IAM role, assigns it the necessary permissions to write logs and sets it as the "CloudWatch log role ARN" in the API Gateway configuration. +Every AWS region you want to deploy an API Gateway to must be configured with an IAM Role that gives API Gateway +permissions to create and write to CloudWatch logs. Without this configuration, API Gateway will not be able to send +logs to CloudWatch. This configuration is done once per region regardless of the number of API Gateways deployed in that +region. This module creates an IAM role, assigns it the necessary permissions to write logs and sets it as the +"CloudWatch log role ARN" in the API Gateway configuration. ## Usage @@ -23,6 +28,7 @@ components: Service: api-gateway ``` + ## Requirements @@ -77,9 +83,11 @@ No resources. |------|-------------| | [role\_arn](#output\_role\_arn) | Role ARN of the API Gateway logging role | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/api-gateway-settings) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/api-gateway-settings) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/api-gateway-rest-api/README.md b/modules/api-gateway-rest-api/README.md index d18e1efd4..7f347f130 100644 --- a/modules/api-gateway-rest-api/README.md +++ b/modules/api-gateway-rest-api/README.md @@ -1,6 +1,7 @@ # Component: `api-gateway-rest-api` This component is responsible for deploying an API Gateway REST API. + ## Usage **Stack Level**: Regional @@ -36,6 +37,7 @@ components: uri: https://api.ipify.org ``` + ## Requirements @@ -118,9 +120,11 @@ components: | [invoke\_url](#output\_invoke\_url) | The URL to invoke the REST API | | [root\_resource\_id](#output\_root\_resource\_id) | The resource ID of the REST API's root | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/argocd-repo/CHANGELOG.md b/modules/argocd-repo/CHANGELOG.md index cb57e1d6e..dad0c1fda 100644 --- a/modules/argocd-repo/CHANGELOG.md +++ b/modules/argocd-repo/CHANGELOG.md @@ -1,11 +1,11 @@ ## Components PR [#851](https://github.com/cloudposse/terraform-aws-components/pull/851) -This is a bug fix and feature enhancement update. -There are few actions necessary to upgrade. +This is a bug fix and feature enhancement update. There are few actions necessary to upgrade. ## Upgrade actions 1. Enable `github_default_notifications_enabled` (set `true`) + ```yaml components: terraform: @@ -16,21 +16,23 @@ components: enabled: true github_default_notifications_enabled: true ``` -2. Apply changes with Atmos +2. Apply changes with Atmos ## Features -* Support predefined GitHub commit status notifications for CD sync mode: - * `on-deploy-started` - * `app-repo-github-commit-status` - * `argocd-repo-github-commit-status` - * `on-deploy-succeded` - * `app-repo-github-commit-status` - * `argocd-repo-github-commit-status` - * `on-deploy-failed` - * `app-repo-github-commit-status` - * `argocd-repo-github-commit-status` + +- Support predefined GitHub commit status notifications for CD sync mode: + - `on-deploy-started` + - `app-repo-github-commit-status` + - `argocd-repo-github-commit-status` + - `on-deploy-succeded` + - `app-repo-github-commit-status` + - `argocd-repo-github-commit-status` + - `on-deploy-failed` + - `app-repo-github-commit-status` + - `argocd-repo-github-commit-status` ### Bug Fixes -* Remove legacy unnecessary helm values used in old ArgoCD versions (ex. `workflow auth` configs) and dropped notifications services +- Remove legacy unnecessary helm values used in old ArgoCD versions (ex. `workflow auth` configs) and dropped + notifications services diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index ac1931c07..c5342a2de 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -57,9 +57,8 @@ components: ```yaml # stacks/mgmt-gbl-corp.yaml import: -... - - catalog/argocd/repo/non-prod -... +--- +- catalog/argocd/repo/non-prod ``` If the repository already exists, it will need to be imported (replace names of IAM profile var file accordingly): @@ -76,6 +75,7 @@ $ cd components/terraform/argocd-repo $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file="mgmt-gbl-corp-argocd-deploy-non-prod.terraform.tfvars.json" "github_branch_default.default[0]" argocd-deploy-non-prod ``` + ## Requirements @@ -179,10 +179,11 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | [repository\_ssh\_clone\_url](#output\_repository\_ssh\_clone\_url) | Repository SSH clone URL | | [repository\_url](#output\_repository\_url) | Repository URL | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/argocd-repo) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/argocd-repo) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/athena/README.md b/modules/athena/README.md index ce1e8cc69..3e6ee7ed9 100644 --- a/modules/athena/README.md +++ b/modules/athena/README.md @@ -34,12 +34,11 @@ components: s3_output_path: "" workgroup_state: "ENABLED" database: [] - ``` ```yaml import: -- catalog/athena/defaults + - catalog/athena/defaults components: terraform: @@ -59,15 +58,16 @@ components: ### CloudTrail Integration -Using Athena with CloudTrail logs is a powerful way to enhance your analysis of AWS service activity. This component supports creating -a CloudTrail table for each account and setting up queries to read CloudTrail logs from a centralized location. +Using Athena with CloudTrail logs is a powerful way to enhance your analysis of AWS service activity. This component +supports creating a CloudTrail table for each account and setting up queries to read CloudTrail logs from a centralized +location. -To set up the CloudTrail Integration, first create the `create` and `alter` queries in Athena with this component. When `var.cloudtrail_database` -is defined, this component will create these queries. +To set up the CloudTrail Integration, first create the `create` and `alter` queries in Athena with this component. When +`var.cloudtrail_database` is defined, this component will create these queries. ```yaml import: -- catalog/athena/defaults + - catalog/athena/defaults components: terraform: @@ -80,7 +80,7 @@ components: enabled: true name: athena-audit workgroup_description: "Athena Workgroup for Auditing" - cloudtrail_database : audit + cloudtrail_database: audit databases: audit: comment: "Auditor database for Athena" @@ -97,20 +97,20 @@ components: eventtime FROM %s.platform_dev_cloudtrail_logs LIMIT 100; - ``` -Once those are created, run the `create` and then the `alter` queries in the AWS Console to create and then fill the tables in Athena. +Once those are created, run the `create` and then the `alter` queries in the AWS Console to create and then fill the +tables in Athena. :::info Athena runs queries with the permissions of the user executing the query. In order to be able to query CloudTrail logs, -the `audit` account must have access to the KMS key used to encrypt CloudTrails logs. Set `var.audit_access_enabled` to `true` in the `cloudtrail` -component +the `audit` account must have access to the KMS key used to encrypt CloudTrails logs. Set `var.audit_access_enabled` to +`true` in the `cloudtrail` component ::: - + ## Requirements @@ -195,9 +195,12 @@ component | [s3\_bucket\_id](#output\_s3\_bucket\_id) | ID of S3 bucket used for Athena query results. | | [workgroup\_id](#output\_workgroup\_id) | ID of newly created Athena workgroup. | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/athena) - Cloud Posse's upstream component -* [Querying AWS CloudTrail logs with AWS Athena](https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html) + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/athena) - + Cloud Posse's upstream component +- [Querying AWS CloudTrail logs with AWS Athena](https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html) [](https://cpco.io/component) diff --git a/modules/aurora-mysql-resources/README.md b/modules/aurora-mysql-resources/README.md index e958d130c..c8e8c5ad1 100644 --- a/modules/aurora-mysql-resources/README.md +++ b/modules/aurora-mysql-resources/README.md @@ -1,8 +1,10 @@ # Component: `aurora-mysql-resources` -This component is responsible for provisioning Aurora MySQL resources: additional databases, users, permissions, grants, etc. +This component is responsible for provisioning Aurora MySQL resources: additional databases, users, permissions, grants, +etc. -NOTE: Creating additional users (including read-only users) and databases requires Spacelift, since that action to be done via the mysql provider, and by default only the automation account is whitelisted by the Aurora cluster. +NOTE: Creating additional users (including read-only users) and databases requires Spacelift, since that action to be +done via the mysql provider, and by default only the automation account is whitelisted by the Aurora cluster. ## Usage @@ -10,7 +12,8 @@ NOTE: Creating additional users (including read-only users) and databases requir Here's an example snippet for how to use this component. -`stacks/catalog/aurora-mysql/resources/defaults.yaml` file (base component for Aurora MySQL Resources with default settings): +`stacks/catalog/aurora-mysql/resources/defaults.yaml` file (base component for Aurora MySQL Resources with default +settings): ```yaml components: @@ -22,8 +25,10 @@ components: enabled: true ``` -Example (not actual) -`stacks/uw2-dev.yaml` file (override the default settings for the cluster resources in the `dev` account, create an additional database and user): +Example (not actual): + +`stacks/uw2-dev.yaml` file (override the default settings for the cluster resources in the `dev` account, create an +additional database and user): ```yaml import: @@ -43,12 +48,13 @@ components: db_user: example db_password: "" grants: - - grant: [ "ALL" ] + - grant: ["ALL"] db: example object_type: database schema: null ``` + ## Requirements @@ -124,10 +130,11 @@ components: | [additional\_grants](#output\_additional\_grants) | Additional DB users created | | [additional\_users](#output\_additional\_users) | Additional DB users created | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-mysql-resources) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-mysql-resources) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index 2662da0de..ff8ad6570 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -1,7 +1,7 @@ # Component: `aurora-mysql` -This component is responsible for provisioning Aurora MySQL RDS clusters. -It seeds relevant database information (hostnames, username, password, etc.) into AWS SSM Parameter Store. +This component is responsible for provisioning Aurora MySQL RDS clusters. It seeds relevant database information +(hostnames, username, password, etc.) into AWS SSM Parameter Store. ## Usage @@ -78,17 +78,22 @@ components: mysql_db_name: main ``` -Example deployment with primary cluster deployed to us-east-1 in a `platform-dev` account: `atmos terraform apply aurora-mysql/dev -s platform-use1-dev` +Example deployment with primary cluster deployed to us-east-1 in a `platform-dev` account: +`atmos terraform apply aurora-mysql/dev -s platform-use1-dev` ## Disaster Recovery with Cross-Region Replication -This component is designed to support cross-region replication with continuous replication. If enabled and deployed, a secondary cluster will be deployed in a different region than the primary cluster. This approach is highly aggresive and costly, but in a disaster scenario where the primary cluster fails, the secondary cluster can be promoted to take its place. Follow these steps to handle a Disaster Recovery. +This component is designed to support cross-region replication with continuous replication. If enabled and deployed, a +secondary cluster will be deployed in a different region than the primary cluster. This approach is highly aggresive and +costly, but in a disaster scenario where the primary cluster fails, the secondary cluster can be promoted to take its +place. Follow these steps to handle a Disaster Recovery. ### Usage To deploy a secondary cluster for cross-region replication, add the following catalog entries to an alternative region: -Default settings for a secondary, replica cluster. For this example, this file is saved as `stacks/catalog/aurora-mysql/replica/defaults.yaml` +Default settings for a secondary, replica cluster. For this example, this file is saved as +`stacks/catalog/aurora-mysql/replica/defaults.yaml` ```yaml import: @@ -136,19 +141,23 @@ components: ### Promoting the Read Replica -Promoting an existing RDS Replicate cluster to a fully standalone cluster is not currently supported by Terraform: https://github.com/hashicorp/terraform-provider-aws/issues/6749 +Promoting an existing RDS Replicate cluster to a fully standalone cluster is not currently supported by Terraform: +https://github.com/hashicorp/terraform-provider-aws/issues/6749 -Instead, promote the Replicate cluster with the AWS CLI command: `aws rds promote-read-replica-db-cluster --db-cluster-identifier ` +Instead, promote the Replicate cluster with the AWS CLI command: +`aws rds promote-read-replica-db-cluster --db-cluster-identifier ` -After promoting the replica, update the stack configuration to prevent future Terrafrom runs from re-enabling replication. In this example, modify `stacks/catalog/aurora-mysql/replica/defaults.yaml` +After promoting the replica, update the stack configuration to prevent future Terrafrom runs from re-enabling +replication. In this example, modify `stacks/catalog/aurora-mysql/replica/defaults.yaml` ```yaml is_promoted_read_replica: true ``` -Reploying the component should show no changes. For example, `atmos terraform apply aurora-mysql/dev -s platform-use2-dev` - +Reploying the component should show no changes. For example, +`atmos terraform apply aurora-mysql/dev -s platform-use2-dev` + ## Requirements @@ -266,10 +275,11 @@ Reploying the component should show no changes. For example, `atmos terraform ap | [cluster\_domain](#output\_cluster\_domain) | Cluster DNS name | | [kms\_key\_arn](#output\_kms\_key\_arn) | KMS key ARN for Aurora MySQL | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-mysql) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-mysql) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aurora-postgres-resources/README.md b/modules/aurora-postgres-resources/README.md index ff041f3ba..1c6bc4a95 100644 --- a/modules/aurora-postgres-resources/README.md +++ b/modules/aurora-postgres-resources/README.md @@ -1,6 +1,7 @@ # Component: `aurora-postgres-resources` -This component is responsible for provisioning Aurora Postgres resources: additional databases, users, permissions, grants, etc. +This component is responsible for provisioning Aurora Postgres resources: additional databases, users, permissions, +grants, etc. ## Usage @@ -19,7 +20,7 @@ components: db_user: example db_password: "" grants: - - grant: [ "ALL" ] + - grant: ["ALL"] db: example object_type: database schema: "" @@ -27,21 +28,45 @@ components: ## PostgreSQL Quick Reference on Grants -GRANTS can be on database, schema, role, table, and other database objects (e.g. columns in a table for fine control). Database and schema do not have much to grant. The `object_type` field in the input determines which kind of object the grant is being applied to. The `db` field is always required. The `schema` field is required unless the `object_type` is `db`, in which case it should be set to the empty string (`""`). - -The keyword PUBLIC indicates that the privileges are to be granted to all roles, including those that might be created later. PUBLIC can be thought of as an implicitly defined group that always includes all roles. Any particular role will have the sum of privileges granted directly to it, privileges granted to any role it is presently a member of, and privileges granted to PUBLIC. - -When an object is created, it is assigned an owner. The owner is normally the role that executed the creation statement. For most kinds of objects, the initial state is that only the owner (or a superuser) can do anything with the object. To allow other roles to use it, privileges must be granted. (When using AWS managed RDS, you cannot have access to any superuser roles; superuser is reserved for AWS to use to manage the cluster.) - -PostgreSQL grants privileges on some types of objects to PUBLIC by default when the objects are created. No privileges are granted to PUBLIC by default on tables, table columns, sequences, foreign data wrappers, foreign servers, large objects, schemas, or tablespaces. For other types of objects, the default privileges granted to PUBLIC are as follows: CONNECT and TEMPORARY (create temporary tables) privileges for databases; EXECUTE privilege for functions and procedures; and USAGE privilege for languages and data types (including domains). The object owner can, of course, REVOKE both default and expressly granted privileges. (For maximum security, issue the REVOKE in the same transaction that creates the object; then there is no window in which another user can use the object.) Also, these default privilege settings can be overridden using the ALTER DEFAULT PRIVILEGES command. +GRANTS can be on database, schema, role, table, and other database objects (e.g. columns in a table for fine control). +Database and schema do not have much to grant. The `object_type` field in the input determines which kind of object the +grant is being applied to. The `db` field is always required. The `schema` field is required unless the `object_type` is +`db`, in which case it should be set to the empty string (`""`). + +The keyword PUBLIC indicates that the privileges are to be granted to all roles, including those that might be created +later. PUBLIC can be thought of as an implicitly defined group that always includes all roles. Any particular role will +have the sum of privileges granted directly to it, privileges granted to any role it is presently a member of, and +privileges granted to PUBLIC. + +When an object is created, it is assigned an owner. The owner is normally the role that executed the creation statement. +For most kinds of objects, the initial state is that only the owner (or a superuser) can do anything with the object. To +allow other roles to use it, privileges must be granted. (When using AWS managed RDS, you cannot have access to any +superuser roles; superuser is reserved for AWS to use to manage the cluster.) + +PostgreSQL grants privileges on some types of objects to PUBLIC by default when the objects are created. No privileges +are granted to PUBLIC by default on tables, table columns, sequences, foreign data wrappers, foreign servers, large +objects, schemas, or tablespaces. For other types of objects, the default privileges granted to PUBLIC are as follows: +CONNECT and TEMPORARY (create temporary tables) privileges for databases; EXECUTE privilege for functions and +procedures; and USAGE privilege for languages and data types (including domains). The object owner can, of course, +REVOKE both default and expressly granted privileges. (For maximum security, issue the REVOKE in the same transaction +that creates the object; then there is no window in which another user can use the object.) Also, these default +privilege settings can be overridden using the ALTER DEFAULT PRIVILEGES command. The CREATE privilege: -- For databases, allows new schemas and publications to be created within the database, and allows trusted extensions to be installed within the database. -- For schemas, allows new objects to be created within the schema. To rename an existing object, you must own the object and have this privilege for the containing schema. -For databases and schemas, there are not a lot of other privileges to grant, and all but CREATE are granted by default, so you might as well grant "ALL". For tables etc., the creator has full control. You grant access to other users via explicit grants. This component does not allow fine-grained grants. You have to specify the database, and unless the grant is on the database, you have to specify the schema. For any other object type (table, sequence, function, procedure, routine, foreign_data_wrapper, foreign_server, column), the component applies the grants to all objects of that type in the specified schema. +- For databases, allows new schemas and publications to be created within the database, and allows trusted extensions to + be installed within the database. +- For schemas, allows new objects to be created within the schema. To rename an existing object, you must own the object + and have this privilege for the containing schema. +For databases and schemas, there are not a lot of other privileges to grant, and all but CREATE are granted by default, +so you might as well grant "ALL". For tables etc., the creator has full control. You grant access to other users via +explicit grants. This component does not allow fine-grained grants. You have to specify the database, and unless the +grant is on the database, you have to specify the schema. For any other object type (table, sequence, function, +procedure, routine, foreign_data_wrapper, foreign_server, column), the component applies the grants to all objects of +that type in the specified schema. + ## Requirements @@ -121,13 +146,15 @@ For databases and schemas, there are not a lot of other privileges to grant, and | [additional\_schemas](#output\_additional\_schemas) | Additional schemas | | [additional\_users](#output\_additional\_users) | Additional users | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-postgres-resources) - Cloud Posse's upstream component -* PostgreSQL references (select the correct version of PostgreSQL at the top of the page): - * [GRANT command](https://www.postgresql.org/docs/14/sql-grant.html) - * [Privileges that can be GRANTed](https://www.postgresql.org/docs/14/ddl-priv.html) +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-postgres-resources) - + Cloud Posse's upstream component + +- PostgreSQL references (select the correct version of PostgreSQL at the top of the page): + - [GRANT command](https://www.postgresql.org/docs/14/sql-grant.html) + - [Privileges that can be GRANTed](https://www.postgresql.org/docs/14/ddl-priv.html) [](https://cpco.io/component) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 1d7c22f6e..56a9e59d2 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -1,7 +1,7 @@ # Component: `aurora-postgres` -This component is responsible for provisioning Aurora Postgres RDS clusters. -It seeds relevant database information (hostnames, username, password, etc.) into AWS SSM Parameter Store. +This component is responsible for provisioning Aurora Postgres RDS clusters. It seeds relevant database information +(hostnames, username, password, etc.) into AWS SSM Parameter Store. ## Usage @@ -9,7 +9,8 @@ It seeds relevant database information (hostnames, username, password, etc.) int Here's an example for how to use this component. -`stacks/catalog/aurora-postgres/defaults.yaml` file (base component for all Aurora Postgres clusters with default settings): +`stacks/catalog/aurora-postgres/defaults.yaml` file (base component for all Aurora Postgres clusters with default +settings): ```yaml components: @@ -54,10 +55,12 @@ components: allow_ingress_from_vpc_accounts: - tenant: core stage: auto - ``` -Example (not actual) -`stacks/uw2-dev.yaml` file (override the default settings for the cluster in the `dev` account, create an additional database and user): + +Example (not actual): + +`stacks/uw2-dev.yaml` file (override the default settings for the cluster in the `dev` account, create an additional +database and user): ```yaml import: @@ -72,12 +75,12 @@ components: - aurora-postgres/defaults vars: enabled: true - ``` ### Finding Aurora Engine Version -Use the following to query the AWS API by `engine-mode`. Both provisioned and Serverless v2 use the `privisoned` engine mode, whereas only Serverless v1 uses the `serverless` engine mode. +Use the following to query the AWS API by `engine-mode`. Both provisioned and Serverless v2 use the `privisoned` engine +mode, whereas only Serverless v1 uses the `serverless` engine mode. ```bash aws rds describe-db-engine-versions \ @@ -86,7 +89,8 @@ aws rds describe-db-engine-versions \ --filters 'Name=engine-mode,Values=serverless' ``` -Use the following to query AWS API by `db-instance-class`. Use this query to find supported versions for a specific instance class, such as `db.serverless` with Serverless v2. +Use the following to query AWS API by `db-instance-class`. Use this query to find supported versions for a specific +instance class, such as `db.serverless` with Serverless v2. ```bash aws rds describe-orderable-db-instance-options \ @@ -115,7 +119,8 @@ Generally there are three different engine configurations for Aurora: provisione Serverless v1 requires `engine-mode` set to `serverless` uses `scaling_configuration` to configure scaling options. -For valid values, see [ModifyCurrentDBClusterCapacity](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyCurrentDBClusterCapacity.html). +For valid values, see +[ModifyCurrentDBClusterCapacity](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_ModifyCurrentDBClusterCapacity.html). ```yaml components: @@ -174,9 +179,11 @@ components: ### Serverless v2 Aurora Postgres -Aurora Postgres Serverless v2 uses the `provisioned` engine mode with `db.serverless` instances. In order to configure scaling with Serverless v2, use `var.serverlessv2_scaling_configuration`. +Aurora Postgres Serverless v2 uses the `provisioned` engine mode with `db.serverless` instances. In order to configure +scaling with Serverless v2, use `var.serverlessv2_scaling_configuration`. -For more on valid scaling configurations, see [Performance and scaling for Aurora Serverless v2](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.setting-capacity.html). +For more on valid scaling configurations, see +[Performance and scaling for Aurora Serverless v2](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.setting-capacity.html). ```yaml components: @@ -230,6 +237,7 @@ components: additional_users: {} ``` + ## Requirements @@ -355,10 +363,11 @@ components: | [replicas\_hostname](#output\_replicas\_hostname) | Postgres replicas hostname | | [ssm\_key\_paths](#output\_ssm\_key\_paths) | Names (key paths) of all SSM parameters stored for this cluster | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-postgres) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-postgres) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index 66cdc0748..cf6c29e3e 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -10,7 +10,8 @@ Here's an example snippet for how to use this component. ### Component Abstraction and Separation -By separating the "common" settings from the component, we can first provision the IAM Role and AWS Backup Vault to prepare resources for future use without incuring cost. +By separating the "common" settings from the component, we can first provision the IAM Role and AWS Backup Vault to +prepare resources for future use without incuring cost. For example, `stacks/catalog/aws-backup/common`: @@ -37,8 +38,7 @@ components: iam_role_enabled: true # this will be reused vault_enabled: true # this will be reused plan_enabled: false - -## Please be careful when enabling backup_vault_lock_configuration, +## Please be careful when enabling backup_vault_lock_configuration, # backup_vault_lock_configuration: ## `changeable_for_days` enables compliance mode and once the lock is set, the retention policy cannot be changed unless through account deletion! # changeable_for_days: 36500 @@ -46,9 +46,11 @@ components: # min_retention_days: 1 ``` -Then if we would like to deploy the component into a given stacks we can import the following to deploy our backup plans. +Then if we would like to deploy the component into a given stacks we can import the following to deploy our backup +plans. -Since most of these values are shared and common, we can put them in a `catalog/aws-backup/` yaml file and share them across environments. +Since most of these values are shared and common, we can put them in a `catalog/aws-backup/` yaml file and share them +across environments. This makes deploying the same configuration to multiple environments easy. @@ -160,7 +162,8 @@ The above configuration can be used to deploy a new backup to a new region. ### Adding Resources to the Backup - Adding Tags -Once an `aws-backup` with a plan and `selection_tags` has been established we can begin adding resources for it to backup by using the tagging method. +Once an `aws-backup` with a plan and `selection_tags` has been established we can begin adding resources for it to +backup by using the tagging method. This only requires that we add tags to the resources we wish to backup, which can be done with the following snippet: @@ -175,11 +178,13 @@ components: Just ensure the tag key-value pair matches what was added to your backup plan and aws will take care of the rest. - ### Copying across regions -If we want to create a backup vault in another region that we can copy to, then we need to create another vault, and then specify that we want to copy to it. + +If we want to create a backup vault in another region that we can copy to, then we need to create another vault, and +then specify that we want to copy to it. To create a vault in a region simply: + ```yaml components: terraform: @@ -188,7 +193,9 @@ components: plan_enabled: false # disables the plan (which schedules resource backups) ``` -This will output an ARN - which you can then use as the destination in the rule object's `copy_action` (it will be specific to that particular plan), as seen in the following snippet: +This will output an ARN - which you can then use as the destination in the rule object's `copy_action` (it will be +specific to that particular plan), as seen in the following snippet: + ```yaml components: terraform: @@ -217,31 +224,38 @@ components: To enable backup lock configuration, you can use the following snippet: -* [AWS Backup Vault Lock](https://docs.aws.amazon.com/aws-backup/latest/devguide/vault-lock.html) +- [AWS Backup Vault Lock](https://docs.aws.amazon.com/aws-backup/latest/devguide/vault-lock.html) #### Compliance Mode -Vaults locked in compliance mode cannot be deleted once the cooling-off period ("grace time") expires. During grace time, you can still remove the vault lock and change the lock configuration. -To enable **Compliance Mode**, set `changeable_for_days` to a value greater than 0. Once the lock is set, the retention policy cannot be changed unless through account deletion! +Vaults locked in compliance mode cannot be deleted once the cooling-off period ("grace time") expires. During grace +time, you can still remove the vault lock and change the lock configuration. + +To enable **Compliance Mode**, set `changeable_for_days` to a value greater than 0. Once the lock is set, the retention +policy cannot be changed unless through account deletion! + ```yaml -# Please be careful when enabling backup_vault_lock_configuration, - backup_vault_lock_configuration: -# `changeable_for_days` enables compliance mode and once the lock is set, the retention policy cannot be changed unless through account deletion! - changeable_for_days: 36500 - max_retention_days: 365 - min_retention_days: 1 +# Please be careful when enabling backup_vault_lock_configuration, +backup_vault_lock_configuration: + # `changeable_for_days` enables compliance mode and once the lock is set, the retention policy cannot be changed unless through account deletion! + changeable_for_days: 36500 + max_retention_days: 365 + min_retention_days: 1 ``` #### Governance Mode + Vaults locked in governance mode can have the lock removed by users with sufficient IAM permissions. To enable **governance mode** + ```yaml - backup_vault_lock_configuration: - max_retention_days: 365 - min_retention_days: 1 +backup_vault_lock_configuration: + max_retention_days: 365 + min_retention_days: 1 ``` + ## Requirements @@ -259,7 +273,6 @@ No providers. | Name | Source | Version | |------|--------|---------| | [backup](#module\_backup) | cloudposse/backup/aws | 1.0.0 | -| [copy\_destination\_vault](#module\_copy\_destination\_vault) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -311,11 +324,12 @@ No resources. | [backup\_vault\_arn](#output\_backup\_vault\_arn) | Backup Vault ARN | | [backup\_vault\_id](#output\_backup\_vault\_id) | Backup Vault ID | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-backup) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-backup) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index c63e3b6d7..481850a1c 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -2,40 +2,51 @@ This component is responsible for configuring AWS Config. -AWS Config service enables you to track changes to your AWS resources over time. It continuously monitors and records configuration changes to your AWS resources and provides you with a detailed view of the relationships between those resources. With AWS Config, you can assess, audit, and evaluate the configurations of your AWS resources for compliance, security, and governance purposes. +AWS Config service enables you to track changes to your AWS resources over time. It continuously monitors and records +configuration changes to your AWS resources and provides you with a detailed view of the relationships between those +resources. With AWS Config, you can assess, audit, and evaluate the configurations of your AWS resources for compliance, +security, and governance purposes. Some of the key features of AWS Config include: -- Configuration history: AWS Config maintains a detailed history of changes to your AWS resources, allowing you to see when changes were made, who made them, and what the changes were. -- Configuration snapshots: AWS Config can take periodic snapshots of your AWS resources configurations, giving you a point-in-time view of their configuration. -- Compliance monitoring: AWS Config provides a range of pre-built rules and checks to monitor your resources for compliance with best practices and industry standards. -- Relationship mapping: AWS Config can map the relationships between your AWS resources, enabling you to see how changes to one resource can impact others. -- Notifications and alerts: AWS Config can send notifications and alerts when changes are made to your AWS resources that could impact their compliance or security posture. -Overall, AWS Config provides you with a powerful toolset to help you monitor and manage the configurations of your AWS resources, ensuring that they remain compliant, secure, and properly configured over time. +- Configuration history: AWS Config maintains a detailed history of changes to your AWS resources, allowing you to see + when changes were made, who made them, and what the changes were. +- Configuration snapshots: AWS Config can take periodic snapshots of your AWS resources configurations, giving you a + point-in-time view of their configuration. +- Compliance monitoring: AWS Config provides a range of pre-built rules and checks to monitor your resources for + compliance with best practices and industry standards. +- Relationship mapping: AWS Config can map the relationships between your AWS resources, enabling you to see how changes + to one resource can impact others. +- Notifications and alerts: AWS Config can send notifications and alerts when changes are made to your AWS resources + that could impact their compliance or security posture. + +Overall, AWS Config provides you with a powerful toolset to help you monitor and manage the configurations of your AWS +resources, ensuring that they remain compliant, secure, and properly configured over time. ## Prerequisites -As part of [CIS AWS Foundations 1.20](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.20), this component assumes that a designated support IAM role with the following permissions has been deployed to every account in the organization: +As part of +[CIS AWS Foundations 1.20](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.20), +this component assumes that a designated support IAM role with the following permissions has been deployed to every +account in the organization: ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowSupport", - "Effect": "Allow", - "Action": [ - "support:*" - ], - "Resource": "*" - }, - { - "Sid": "AllowTrustedAdvisor", - "Effect": "Allow", - "Action": "trustedadvisor:Describe*", - "Resource": "*" - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowSupport", + "Effect": "Allow", + "Action": ["support:*"], + "Resource": "*" + }, + { + "Sid": "AllowTrustedAdvisor", + "Effect": "Allow", + "Action": "trustedadvisor:Describe*", + "Resource": "*" + } + ] } ``` @@ -47,7 +58,8 @@ Before deploying this AWS Config component `config-bucket` and `cloudtrail-bucke _**NOTE**: Since AWS Config is regional AWS service, this component needs to be deployed to all regions._ -At the AWS Organizational level, the Components designate an account to be the `central collection account` and a single region to be the `central collection region` so that compliance information can be aggregated into a central location. +At the AWS Organizational level, the Components designate an account to be the `central collection account` and a single +region to be the `central collection region` so that compliance information can be aggregated into a central location. Logs are typically written to the `audit` account and AWS Config deployed into to the `security` account. @@ -58,7 +70,7 @@ components: terraform: aws-config: vars: - enabled: true + enabled: true account_map_tenant: core az_abbreviation_type: fixed # In each AWS account, an IAM role should be created in the main region. @@ -110,6 +122,7 @@ Apply aws-config to all stacks in all stages. atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} ``` + ## Requirements @@ -197,12 +210,13 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} | [storage\_bucket\_arn](#output\_storage\_bucket\_arn) | Storage Config bucket ARN | | [storage\_bucket\_id](#output\_storage\_bucket\_id) | Storage Config bucket ID | - + ## References -* [AWS Config Documentation](https://docs.aws.amazon.com/config/index.html) -* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-config) -* [Conformance Packs documentation](https://docs.aws.amazon.com/config/latest/developerguide/conformance-packs.html) -* [AWS Managed Sample Conformance Packs](https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs) + +- [AWS Config Documentation](https://docs.aws.amazon.com/config/index.html) +- [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-config) +- [Conformance Packs documentation](https://docs.aws.amazon.com/config/latest/developerguide/conformance-packs.html) +- [AWS Managed Sample Conformance Packs](https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs) [](https://cpco.io/component) diff --git a/modules/aws-inspector/README.md b/modules/aws-inspector/README.md index 5f75652c4..679d122d5 100644 --- a/modules/aws-inspector/README.md +++ b/modules/aws-inspector/README.md @@ -1,21 +1,36 @@ # Component: `aws-inspector` -This component is responsible for provisioning an [AWS Inspector](https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html) by installing the [Inspector agent](https://repost.aws/knowledge-center/set-up-amazon-inspector) across all EC2 instances and applying the Inspector rules. +This component is responsible for provisioning an +[AWS Inspector](https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html) by installing the +[Inspector agent](https://repost.aws/knowledge-center/set-up-amazon-inspector) across all EC2 instances and applying the +Inspector rules. -AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security vulnerabilities and deviations from security best practices. +AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate +the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically +assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security vulnerabilities +and deviations from security best practices. Here are some key features and functionalities of AWS Inspector: -- **Security Assessments:** AWS Inspector performs security assessments by analyzing the behavior of your resources and identifying potential security vulnerabilities. It examines the network configuration, operating system settings, and installed software to detect common security issues. -- **Vulnerability Detection:** AWS Inspector uses a predefined set of rules to identify common vulnerabilities, misconfigurations, and security exposures. It leverages industry-standard security best practices and continuously updates its knowledge base to stay current with emerging threats. +- **Security Assessments:** AWS Inspector performs security assessments by analyzing the behavior of your resources and + identifying potential security vulnerabilities. It examines the network configuration, operating system settings, and + installed software to detect common security issues. -- **Agent-Based Architecture:** AWS Inspector utilizes an agent-based approach, where you install an Inspector agent on your EC2 instances. The agent collects data about the system and its configuration, securely sends it to AWS Inspector, and allows for more accurate and detailed assessments. +- **Vulnerability Detection:** AWS Inspector uses a predefined set of rules to identify common vulnerabilities, + misconfigurations, and security exposures. It leverages industry-standard security best practices and continuously + updates its knowledge base to stay current with emerging threats. -- **Security Findings:** After performing an assessment, AWS Inspector generates detailed findings that highlight security vulnerabilities, including their severity level, impact, and remediation steps. These findings can help you prioritize and address security issues within your AWS environment. - -- **Integration with AWS Services:** AWS Inspector seamlessly integrates with other AWS services, such as AWS CloudFormation, AWS Systems Manager, and AWS Security Hub. This allows you to automate security assessments, manage findings, and centralize security information across your AWS infrastructure. +- **Agent-Based Architecture:** AWS Inspector utilizes an agent-based approach, where you install an Inspector agent on + your EC2 instances. The agent collects data about the system and its configuration, securely sends it to AWS + Inspector, and allows for more accurate and detailed assessments. +- **Security Findings:** After performing an assessment, AWS Inspector generates detailed findings that highlight + security vulnerabilities, including their severity level, impact, and remediation steps. These findings can help you + prioritize and address security issues within your AWS environment. +- **Integration with AWS Services:** AWS Inspector seamlessly integrates with other AWS services, such as AWS + CloudFormation, AWS Systems Manager, and AWS Security Hub. This allows you to automate security assessments, manage + findings, and centralize security information across your AWS infrastructure. ## Usage @@ -32,13 +47,23 @@ components: enabled_rules: - cis ``` -The `aws-inspector` component can be included in your Terraform stack configuration. In the provided example, it is enabled with the `enabled` variable set to `true`. The `enabled_rules` variable specifies a list of rules to enable, and in this case, it includes the `cis` rule. -To simplify rule selection, the short forms of the rules are used for the `enabled_rules` key. These short forms automatically retrieve the appropriate ARN for the rule package based on the region being used. You can find a list of available short forms and their corresponding rule packages in the [var.enabled_rules](https://github.com/cloudposse/terraform-aws-inspector#input_enabled_rules) input documentation. -For a comprehensive list of rules and their corresponding ARNs, you can refer to the [Amazon Inspector ARNs for rules packages](https://docs.aws.amazon.com/inspector/latest/userguide/inspector_rules-arns.html) documentation. This resource provides detailed information on various rules that can be used with AWS Inspector and their unique identifiers (ARNs). +The `aws-inspector` component can be included in your Terraform stack configuration. In the provided example, it is +enabled with the `enabled` variable set to `true`. The `enabled_rules` variable specifies a list of rules to enable, and +in this case, it includes the `cis` rule. To simplify rule selection, the short forms of the rules are used for the +`enabled_rules` key. These short forms automatically retrieve the appropriate ARN for the rule package based on the +region being used. You can find a list of available short forms and their corresponding rule packages in the +[var.enabled_rules](https://github.com/cloudposse/terraform-aws-inspector#input_enabled_rules) input documentation. + +For a comprehensive list of rules and their corresponding ARNs, you can refer to the +[Amazon Inspector ARNs for rules packages](https://docs.aws.amazon.com/inspector/latest/userguide/inspector_rules-arns.html) +documentation. This resource provides detailed information on various rules that can be used with AWS Inspector and +their unique identifiers (ARNs). -By customizing the configuration with the appropriate rules, you can tailor the security assessments performed by AWS Inspector to meet the specific requirements and compliance standards of your applications and infrastructure. +By customizing the configuration with the appropriate rules, you can tailor the security assessments performed by AWS +Inspector to meet the specific requirements and compliance standards of your applications and infrastructure. + ## Requirements @@ -98,6 +123,10 @@ By customizing the configuration with the appropriate rules, you can tailor the |------|-------------| | [inspector](#output\_inspector) | The AWS Inspector module outputs | + + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component -[](https://cpco.io/component) + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - + Cloud Posse's upstream component + [](https://cpco.io/component) diff --git a/modules/aws-inspector2/README.md b/modules/aws-inspector2/README.md index ba6324d68..e40a45838 100644 --- a/modules/aws-inspector2/README.md +++ b/modules/aws-inspector2/README.md @@ -8,13 +8,19 @@ This component is responsible for configuring Inspector V2 within an AWS Organiz ## Deployment Overview -The deployment of this component requires multiple runs with different variable settings to properly configure the AWS Organization. First, you delegate Inspector V2 central management to the Administrator account (usually `security` account). After the Adminstrator account is delegated, we configure the it to manage Inspector V2 across all the Organization accounts and send all their findings to that account. +The deployment of this component requires multiple runs with different variable settings to properly configure the AWS +Organization. First, you delegate Inspector V2 central management to the Administrator account (usually `security` +account). After the Adminstrator account is delegated, we configure the it to manage Inspector V2 across all the +Organization accounts and send all their findings to that account. -In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization Delegated Administrator account is `security`. +In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization +Delegated Administrator account is `security`. ### Deploy to Organization Management Account -First, the component is deployed to the AWS Organization Management account `root` in each region in order to configure the [AWS Delegated Administrator account](https://docs.aws.amazon.com/inspector/latest/user/designating-admin.html) that operates Amazon Inspector V2. +First, the component is deployed to the AWS Organization Management account `root` in each region in order to configure +the [AWS Delegated Administrator account](https://docs.aws.amazon.com/inspector/latest/user/designating-admin.html) that +operates Amazon Inspector V2. ```yaml # ue1-root @@ -30,7 +36,10 @@ components: ### Deploy Organization Settings in Delegated Administrator Account -Now the component can be deployed to the Delegated Administrator Account `security` to create the organization-wide configuration for all the Organization accounts. Note that `var.admin_delegated` set to `true` indicates that the delegation has already been performed from the Organization Management account, and only the resources required for organization-wide configuration will be created. +Now the component can be deployed to the Delegated Administrator Account `security` to create the organization-wide +configuration for all the Organization accounts. Note that `var.admin_delegated` set to `true` indicates that the +delegation has already been performed from the Organization Management account, and only the resources required for +organization-wide configuration will be created. ```yaml # ue1-security @@ -45,6 +54,7 @@ components: admin_delegated: true ``` + ## Requirements @@ -120,6 +130,7 @@ components: |------|-------------| | [aws\_inspector2\_member\_association](#output\_aws\_inspector2\_member\_association) | The Inspector2 member association resource. | + ## References diff --git a/modules/aws-saml/README.md b/modules/aws-saml/README.md index 67e14c855..94f2ccece 100644 --- a/modules/aws-saml/README.md +++ b/modules/aws-saml/README.md @@ -1,6 +1,8 @@ # Component: `aws-saml` -This component is responsible for provisioning SAML metadata into AWS IAM as new SAML providers. Additionally, for an Okta integration (`okta` must be mentioned in the key given to the `saml_providers` input) it creates an Okta API user and corresponding Access Key pair which is pushed into AWS SSM. +This component is responsible for provisioning SAML metadata into AWS IAM as new SAML providers. Additionally, for an +Okta integration (`okta` must be mentioned in the key given to the `saml_providers` input) it creates an Okta API user +and corresponding Access Key pair which is pushed into AWS SSM. ## Usage @@ -22,6 +24,7 @@ components: example-gsuite: GoogleIDPMetadata-example.com.xml ``` + ## Requirements @@ -85,10 +88,11 @@ components: | [saml\_provider\_arns](#output\_saml\_provider\_arns) | Map of SAML provider names to provider ARNs | | [saml\_provider\_assume\_role\_policy](#output\_saml\_provider\_assume\_role\_policy) | JSON "assume role" policy document to use for roles allowed to log in via SAML | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sso) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sso) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-shield/README.md b/modules/aws-shield/README.md index 78fd384a0..500f9785b 100644 --- a/modules/aws-shield/README.md +++ b/modules/aws-shield/README.md @@ -2,16 +2,16 @@ This component is responsible for enabling AWS Shield Advanced Protection for the following resources: -* Application Load Balancers (ALBs) -* CloudFront Distributions -* Elastic IPs -* Route53 Hosted Zones +- Application Load Balancers (ALBs) +- CloudFront Distributions +- Elastic IPs +- Route53 Hosted Zones -This component assumes that resources it is configured to protect are not already protected by other components -that have their `xxx_aws_shield_protection_enabled` variable set to `true`. +This component assumes that resources it is configured to protect are not already protected by other components that +have their `xxx_aws_shield_protection_enabled` variable set to `true`. -This component also requires that the account where the component is being provisioned to has -been [subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/enable-ddos-prem.html). +This component also requires that the account where the component is being provisioned to has been +[subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/enable-ddos-prem.html). ## Usage @@ -80,10 +80,12 @@ components: - 35.171.70.50 ``` -Stack configurations which rely on components with a `xxx_aws_shield_protection_enabled` variable should set that variable to `true` -and leave the corresponding variable for this component as empty, relying on that component's AWS Shield Advanced functionality instead. -This leads to more simplified inter-component dependencies and minimizes the need for maintaining the provisioning order during a cold-start. +Stack configurations which rely on components with a `xxx_aws_shield_protection_enabled` variable should set that +variable to `true` and leave the corresponding variable for this component as empty, relying on that component's AWS +Shield Advanced functionality instead. This leads to more simplified inter-component dependencies and minimizes the need +for maintaining the provisioning order during a cold-start. + ## Requirements @@ -159,9 +161,11 @@ This leads to more simplified inter-component dependencies and minimizes the nee | [elastic\_ip\_protections](#output\_elastic\_ip\_protections) | AWS Shield Advanced Protections for Elastic IPs | | [route53\_hosted\_zone\_protections](#output\_route53\_hosted\_zone\_protections) | AWS Shield Advanced Protections for Route53 Hosted Zones | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-shield) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-shield) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-sso/CHANGELOG.md b/modules/aws-sso/CHANGELOG.md index 5173ebc9e..ff01dbd53 100644 --- a/modules/aws-sso/CHANGELOG.md +++ b/modules/aws-sso/CHANGELOG.md @@ -1,33 +1,30 @@ # Change log for aws-sso component -***NOTE***: This file is manually generated and is a work-in-progress. +**_NOTE_**: This file is manually generated and is a work-in-progress. ### PR 830 - Fix `providers.tf` to properly assign roles for `root` account when deploying to `identity` account. -- Restore the `sts:SetSourceIdentity` permission for Identity-role-TeamAccess -permission sets added in PR 738 and inadvertently removed in PR 740. -- Update comments and documentation to reflect Cloud Posse's current - recommendation that SSO ***not*** be delegated to the `identity` account. +- Restore the `sts:SetSourceIdentity` permission for Identity-role-TeamAccess permission sets added in PR 738 and + inadvertently removed in PR 740. +- Update comments and documentation to reflect Cloud Posse's current recommendation that SSO **_not_** be delegated to + the `identity` account. ### Version 1.240.1, PR 740 -This PR restores compatibility with `account-map` prior to version 1.227.0 -and fixes bugs that made versions 1.227.0 up to this release unusable. +This PR restores compatibility with `account-map` prior to version 1.227.0 and fixes bugs that made versions 1.227.0 up +to this release unusable. -Access control configuration (`aws-teams`, `iam-primary-roles`, `aws-sso`, etc.) -has undergone several transformations over the evolution of Cloud Posse's reference -architecture. This update resolves a number of compatibility issues with some of them. +Access control configuration (`aws-teams`, `iam-primary-roles`, `aws-sso`, etc.) has undergone several transformations +over the evolution of Cloud Posse's reference architecture. This update resolves a number of compatibility issues with +some of them. -If the roles you are using to deploy this component are allowed to assume -the `tfstate-backend` access roles (typically `...-gbl-root-tfstate`, possibly -`...-gbl-root-tfstate-ro` or `...-gbl-root-terraform`), then you can use the -defaults. This configuration was introduced in `terraform-aws-components` v1.227.0 -and is the default for all new deployments. +If the roles you are using to deploy this component are allowed to assume the `tfstate-backend` access roles (typically +`...-gbl-root-tfstate`, possibly `...-gbl-root-tfstate-ro` or `...-gbl-root-terraform`), then you can use the defaults. +This configuration was introduced in `terraform-aws-components` v1.227.0 and is the default for all new deployments. -If the roles you are using to deploy this component are not allowed to assume -the `tfstate-backend` access roles, then you will need to configure this component -to include the following: +If the roles you are using to deploy this component are not allowed to assume the `tfstate-backend` access roles, then +you will need to configure this component to include the following: ```yaml components: @@ -40,14 +37,11 @@ components: privileged: true ``` -If you are deploying this component to the `identity` account, then this -restriction will require you to deploy it via the SuperAdmin user. If you are -deploying this component to the `root` account, then any user or role -in the `root` account with the `AdministratorAccess` policy attached will be -able to deploy this component. - +If you are deploying this component to the `identity` account, then this restriction will require you to deploy it via +the SuperAdmin user. If you are deploying this component to the `root` account, then any user or role in the `root` +account with the `AdministratorAccess` policy attached will be able to deploy this component. ## v1.227.0 -This component was broken by changes made in v1.227.0. Either use a version -before v1.227.0 or use the version released by PR 740 or later. +This component was broken by changes made in v1.227.0. Either use a version before v1.227.0 or use the version released +by PR 740 or later. diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index bcdb80a90..e351537be 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -1,8 +1,10 @@ # Component: `aws-sso` -This component is responsible for creating [AWS SSO Permission Sets][1] and creating AWS SSO Account Assignments, that is, assigning IdP (Okta) groups and/or users to AWS SSO permission sets in specific AWS Accounts. +This component is responsible for creating [AWS SSO Permission Sets][1] and creating AWS SSO Account Assignments, that +is, assigning IdP (Okta) groups and/or users to AWS SSO permission sets in specific AWS Accounts. -This component assumes that AWS SSO has already been enabled via the AWS Console (there isn't terraform or AWS CLI support for this currently) and that the IdP has been configured to sync users and groups to AWS SSO. +This component assumes that AWS SSO has already been enabled via the AWS Console (there isn't terraform or AWS CLI +support for this currently) and that the IdP has been configured to sync users and groups to AWS SSO. ## Usage @@ -16,27 +18,34 @@ This component assumes that AWS SSO has already been enabled via the AWS Console #### Delegation no longer recommended Previously, Cloud Posse recommended delegating SSO to the identity account by following the next 2 steps: + 1. Click Settings > Management 1. Delegate Identity as an administrator. This can take up to 30 minutes to take effect. -However, this is no longer recommended. Because the delegated SSO administrator cannot make changes in the `root` account -and this component needs to be able to make changes in the `root` account, any purported security advantage achieved by -delegating SSO to the `identity` account is lost. +However, this is no longer recommended. Because the delegated SSO administrator cannot make changes in the `root` +account and this component needs to be able to make changes in the `root` account, any purported security advantage +achieved by delegating SSO to the `identity` account is lost. -Nevertheless, it is also not worth the effort to remove the delegation. If you have already delegated SSO to the `identity`, -continue on, leaving the stack configuration in the `gbl-identity` stack rather than the currently recommended `gbl-root` stack. +Nevertheless, it is also not worth the effort to remove the delegation. If you have already delegated SSO to the +`identity`, continue on, leaving the stack configuration in the `gbl-identity` stack rather than the currently +recommended `gbl-root` stack. ### Google Workspace :::important -> Your identity source is currently configured as 'External identity provider'. To add new groups or edit their memberships, you must do this using your external identity provider. +> Your identity source is currently configured as 'External identity provider'. To add new groups or edit their +> memberships, you must do this using your external identity provider. Groups _cannot_ be created with ClickOps in the AWS console and instead must be created with AWS API. ::: -Google Workspace is now supported by AWS Identity Center, but Group creation is not automatically handled. After [configuring SAML and SCIM with Google Workspace and IAM Identity Center following the AWS documentation](https://docs.aws.amazon.com/singlesignon/latest/userguide/gs-gwp.html), add any Group name to `var.groups` to create the Group with Terraform. Once the setup steps as described in the AWS documentation have been completed and the Groups are created with Terraform, Users should automatically populate each created Group. +Google Workspace is now supported by AWS Identity Center, but Group creation is not automatically handled. After +[configuring SAML and SCIM with Google Workspace and IAM Identity Center following the AWS documentation](https://docs.aws.amazon.com/singlesignon/latest/userguide/gs-gwp.html), +add any Group name to `var.groups` to create the Group with Terraform. Once the setup steps as described in the AWS +documentation have been completed and the Groups are created with Terraform, Users should automatically populate each +created Group. ```yaml components: @@ -50,13 +59,14 @@ components: ### Atmos -**Stack Level**: Global -**Deployment**: Must be deployed by root-admin using `atmos` CLI +**Stack Level**: Global **Deployment**: Must be deployed by root-admin using `atmos` CLI Add catalog to `gbl-root` root stack. #### `account_assignments` -The `account_assignments` setting configures access to permission sets for users and groups in accounts, in the following structure: + +The `account_assignments` setting configures access to permission sets for users and groups in accounts, in the +following structure: ```yaml : @@ -71,11 +81,17 @@ The `account_assignments` setting configures access to permission sets for users ``` - The account names (a.k.a. "stages") must already be configured via the `accounts` component. -- The user and group names must already exist in AWS SSO. Usually this is accomplished by configuring them in Okta and syncing Okta with AWS SSO. -- The permission sets are defined (by convention) in files names `policy-.tf` in the `aws-sso` component. The definition includes the name of the permission set. See `components/terraform/aws-sso/policy-AdminstratorAccess.tf` for an example. +- The user and group names must already exist in AWS SSO. Usually this is accomplished by configuring them in Okta and + syncing Okta with AWS SSO. +- The permission sets are defined (by convention) in files names `policy-.tf` in the `aws-sso` + component. The definition includes the name of the permission set. See + `components/terraform/aws-sso/policy-AdminstratorAccess.tf` for an example. #### `identity_roles_accessible` -The `identity_roles_accessible` element provides a list of role names corresponding to roles created in the `iam-primary-roles` component. For each named role, a corresponding permission set will be created which allows the user to assume that role. The permission set name is generated in Terraform from the role name using this statement: + +The `identity_roles_accessible` element provides a list of role names corresponding to roles created in the +`iam-primary-roles` component. For each named role, a corresponding permission set will be created which allows the user +to assume that role. The permission set name is generated in Terraform from the role name using this statement: ``` format("Identity%sTeamAccess", replace(title(role), "-", "")) @@ -83,53 +99,57 @@ format("Identity%sTeamAccess", replace(title(role), "-", "")) ### Defining a new permission set -1. Give the permission set a name, capitalized, in CamelCase, e.g. `AuditManager`. We will use `NAME` as a - placeholder for the name in the instructions below. In Terraform, convert the name to lowercase snake case, e.g. `audit_manager`. +1. Give the permission set a name, capitalized, in CamelCase, e.g. `AuditManager`. We will use `NAME` as a placeholder + for the name in the instructions below. In Terraform, convert the name to lowercase snake case, e.g. `audit_manager`. 2. Create a file in the `aws-sso` directory with the name `policy-NAME.tf`. 3. In that file, create a policy as follows: - ```hcl - data "aws_iam_policy_document" "TerraformUpdateAccess" { - # Define the custom policy here - } - - locals { - NAME_permission_set = { # e.g. audit_manager_permission_set - name = "NAME", # e.g. AuditManager - description = "", - relay_state = "", - session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles - tags = {}, - inline_policy = data.aws_iam_policy_document.NAME.json, - policy_attachments = [] # ARNs of AWS managed IAM policies to attach, e.g. arn:aws:iam::aws:policy/ReadOnlyAccess - customer_managed_policy_attachments = [] # ARNs of customer managed IAM policies to attach - } - } - ``` -4. Create a file named `additional-permission-sets-list_override.tf` in the `aws-sso` directory (if it does not already exist). - This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), meaning its - contents will be merged with the main terraform file, and any locals defined in it will override locals defined in other files. - Having your code in this separate override file makes it possible for the component to provide a placeholder local variable - so that it works without customization, while allowing you to customize the component and still update it without losing your customizations. + ```hcl + data "aws_iam_policy_document" "TerraformUpdateAccess" { + # Define the custom policy here + } + + locals { + NAME_permission_set = { # e.g. audit_manager_permission_set + name = "NAME", # e.g. AuditManager + description = "", + relay_state = "", + session_duration = "PT1H", # One hour, maximum allowed for chained assumed roles + tags = {}, + inline_policy = data.aws_iam_policy_document.NAME.json, + policy_attachments = [] # ARNs of AWS managed IAM policies to attach, e.g. arn:aws:iam::aws:policy/ReadOnlyAccess + customer_managed_policy_attachments = [] # ARNs of customer managed IAM policies to attach + } + } + ``` + +4. Create a file named `additional-permission-sets-list_override.tf` in the `aws-sso` directory (if it does not already + exist). This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), + meaning its contents will be merged with the main terraform file, and any locals defined in it will override locals + defined in other files. Having your code in this separate override file makes it possible for the component to + provide a placeholder local variable so that it works without customization, while allowing you to customize the + component and still update it without losing your customizations. 5. In that file, redefine the local variable `overridable_additional_permission_sets` as follows: - ```hcl - locals { - overridable_additional_permission_sets = [ - local.NAME_permission_set, - ] - } - ``` + ```hcl + locals { + overridable_additional_permission_sets = [ + local.NAME_permission_set, + ] + } + ``` If you have multiple custom policies, add each one to the list. -6. With that done, the new permission set will be created when the changes are applied. - You can then use it just like the others. -7. If you want the permission set to be able to use Terraform, enable access to the - Terraform state read/write (default) role in `tfstate-backend`. +6. With that done, the new permission set will be created when the changes are applied. You can then use it just like + the others. +7. If you want the permission set to be able to use Terraform, enable access to the Terraform state read/write (default) + role in `tfstate-backend`. #### Example -The example snippet below shows how to use this module with various combinations (plain YAML, YAML Anchors and a combination of the two): + +The example snippet below shows how to use this module with various combinations (plain YAML, YAML Anchors and a +combination of the two): ```yaml prod-cloud-engineers: &prod-cloud-engineers @@ -171,12 +191,13 @@ components: - AdministratorAccess - ReadOnlyAccess aws_teams_accessible: - - "developers" - - "devops" - - "managers" - - "support" + - "developers" + - "devops" + - "managers" + - "support" ``` + ## Requirements @@ -254,6 +275,7 @@ components: | [permission\_sets](#output\_permission\_sets) | Permission sets | | [sso\_account\_assignments](#output\_sso\_account\_assignments) | SSO account assignments | + ## References @@ -261,43 +283,43 @@ components: [][40] -[1]: https://docs.aws.amazon.com/singlesignon/latest/userguide/permissionsetsconcept.html -[2]: #requirement%5C_terraform -[3]: #requirement%5C_aws -[4]: #requirement%5C_external -[5]: #requirement%5C_local -[6]: #requirement%5C_template -[7]: #requirement%5C_utils -[8]: #provider%5C_aws -[9]: #module%5C_account%5C_map -[10]: #module%5C_permission%5C_sets -[11]: #module%5C_role%5C_prefix -[12]: #module%5C_sso%5C_account%5C_assignments -[13]: #module%5C_this -[14]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document -[15]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document -[16]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document -[17]: #input%5C_account%5C_assignments -[18]: #input%5C_additional%5C_tag%5C_map -[19]: #input%5C_attributes -[20]: #input%5C_context -[21]: #input%5C_delimiter -[22]: #input%5C_enabled -[23]: #input%5C_environment -[24]: #input%5C_global%5C_environment%5C_name -[25]: #input%5C_iam%5C_primary%5C_roles%5C_stage%5C_name -[26]: #input%5C_id%5C_length%5C_limit -[27]: #input%5C_identity%5C_roles%5C_accessible -[28]: #input%5C_label%5C_key%5C_case -[29]: #input%5C_label%5C_order -[30]: #input%5C_label%5C_value%5C_case -[31]: #input%5C_name -[32]: #input%5C_namespace -[33]: #input%5C_privileged -[34]: #input%5C_regex%5C_replace%5C_chars -[35]: #input%5C_region -[36]: #input%5C_root%5C_account%5C_stage%5C_name -[37]: #input%5C_stage -[38]: #input%5C_tags -[39]: https://github.com/cloudposse/terraform-aws-sso -[40]: https://cpco.io/component +[1]: https://docs.aws.amazon.com/singlesignon/latest/userguide/permissionsetsconcept.html +[2]: #requirement%5C_terraform +[3]: #requirement%5C_aws +[4]: #requirement%5C_external +[5]: #requirement%5C_local +[6]: #requirement%5C_template +[7]: #requirement%5C_utils +[8]: #provider%5C_aws +[9]: #module%5C_account%5C_map +[10]: #module%5C_permission%5C_sets +[11]: #module%5C_role%5C_prefix +[12]: #module%5C_sso%5C_account%5C_assignments +[13]: #module%5C_this +[14]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document +[15]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document +[16]: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document +[17]: #input%5C_account%5C_assignments +[18]: #input%5C_additional%5C_tag%5C_map +[19]: #input%5C_attributes +[20]: #input%5C_context +[21]: #input%5C_delimiter +[22]: #input%5C_enabled +[23]: #input%5C_environment +[24]: #input%5C_global%5C_environment%5C_name +[25]: #input%5C_iam%5C_primary%5C_roles%5C_stage%5C_name +[26]: #input%5C_id%5C_length%5C_limit +[27]: #input%5C_identity%5C_roles%5C_accessible +[28]: #input%5C_label%5C_key%5C_case +[29]: #input%5C_label%5C_order +[30]: #input%5C_label%5C_value%5C_case +[31]: #input%5C_name +[32]: #input%5C_namespace +[33]: #input%5C_privileged +[34]: #input%5C_regex%5C_replace%5C_chars +[35]: #input%5C_region +[36]: #input%5C_root%5C_account%5C_stage%5C_name +[37]: #input%5C_stage +[38]: #input%5C_tags +[39]: https://github.com/cloudposse/terraform-aws-sso +[40]: https://cpco.io/component diff --git a/modules/aws-ssosync/README.md b/modules/aws-ssosync/README.md index f02653193..d4bc7384a 100644 --- a/modules/aws-ssosync/README.md +++ b/modules/aws-ssosync/README.md @@ -4,17 +4,20 @@ Deploys [AWS ssosync](https://github.com/awslabs/ssosync) to sync Google Groups AWS `ssosync` is a Lambda application that regularly manages Identity Store users. -This component requires manual deployment by a privileged user because it deploys a role in the root or identity management account. +This component requires manual deployment by a privileged user because it deploys a role in the root or identity +management account. ## Usage -You should be able to deploy the `aws-ssosync` component to the same account as `aws-sso`. Typically that is the `core-gbl-root` or `gbl-root` stack. -**Stack Level**: Global -**Deployment**: Must be deployed by `managers` or SuperAdmin using `atmos` CLI +You should be able to deploy the `aws-ssosync` component to the same account as `aws-sso`. Typically that is the +`core-gbl-root` or `gbl-root` stack. + +**Stack Level**: Global **Deployment**: Must be deployed by `managers` or SuperAdmin using `atmos` CLI The following is an example snippet for how to use this component: (`stacks/catalog/aws-ssosync.yaml`) + ```yaml components: terraform: @@ -31,8 +34,8 @@ components: schedule_expression: "rate(15 minutes)" ``` -We recommend following a similar process to what the [AWS ssosync](https://github.com/awslabs/ssosync) -documentation recommends. +We recommend following a similar process to what the [AWS ssosync](https://github.com/awslabs/ssosync) documentation +recommends. ### Deployment @@ -44,24 +47,22 @@ Overview of steps: 1. Deploy the `aws-ssosync` component 1. Deploy the `aws-sso` component - #### 1. Configure AWS IAM Identity Center (AWS SSO) -Follow [AWS documentation to configure SAML and SCIM with Google Workspace and IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/gs-gwp.html). +Follow +[AWS documentation to configure SAML and SCIM with Google Workspace and IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/gs-gwp.html). -As part of this process, save the SCIM endpoint token and URL. Then in AWS SSM Parameter Store, -create two `SecureString` parameters in the same account used for AWS SSO. -This is usually the root account in the primary region. +As part of this process, save the SCIM endpoint token and URL. Then in AWS SSM Parameter Store, create two +`SecureString` parameters in the same account used for AWS SSO. This is usually the root account in the primary region. ``` /ssosync/scim_endpoint_access_token /ssosync/scim_endpoint_url ``` -One more parameter you'll need is your Identity Store ID. -To obtain your Identity Store ID, go to the AWS Identity Center console and -select `Settings`. Under the `Identity Source` section, copy the Identity Store ID. -In the same account used for AWS SSO, create the following parameter: +One more parameter you'll need is your Identity Store ID. To obtain your Identity Store ID, go to the AWS Identity +Center console and select `Settings`. Under the `Identity Source` section, copy the Identity Store ID. In the same +account used for AWS SSO, create the following parameter: ``` /ssosync/identity_store_id @@ -69,24 +70,30 @@ In the same account used for AWS SSO, create the following parameter: #### 2. Configure Google Cloud console -Within the Google Cloud console, we need to create a new Google Project and Service Account and enable the Admin SDK API. -Follow these steps: +Within the Google Cloud console, we need to create a new Google Project and Service Account and enable the Admin SDK +API. Follow these steps: 1. Open the Google Cloud
console: https://console.cloud.google.com 2. Create a new project. Give the project a descriptive name such as `AWS SSO Sync` 3. Enable Admin SDK in APIs: `APIs & Services > Enabled APIs & Services > + ENABLE APIS AND SERVICES` -![Enable Admin SDK](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/admin_sdk.png) # use raw URL so that this works in both GitHub and docusaurus +![Enable Admin SDK](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/admin_sdk.png) # +use raw URL so that this works in both GitHub and docusaurus -4. Create Service Account: `IAM & Admin > Service Accounts > Create Service Account` [(ref)](https://cloud.google.com/iam/docs/service-accounts-create). +4. Create Service Account: `IAM & Admin > Service Accounts > Create Service Account` + [(ref)](https://cloud.google.com/iam/docs/service-accounts-create). -![Create Service Account](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/create_service_account.png) # use raw URL so that this works in both GitHub and docusaurus +![Create Service Account](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/create_service_account.png) # +use raw URL so that this works in both GitHub and docusaurus -5. Download credentials for the new Service Account: `IAM & Admin > Service Accounts > select Service Account > Keys > ADD KEY > Create new key > JSON` +5. Download credentials for the new Service Account: + `IAM & Admin > Service Accounts > select Service Account > Keys > ADD KEY > Create new key > JSON` -![Download Credentials](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/dl_service_account_creds.png) # use raw URL so that this works in both GitHub and docusaurus +![Download Credentials](https://raw.githubusercontent.com/cloudposse/terraform-aws-components/main/modules/aws-ssosync/docs/img/dl_service_account_creds.png) # +use raw URL so that this works in both GitHub and docusaurus -6. Save the JSON credentials as a new `SecureString` AWS SSM parameter in the same account used for AWS SSO. Use the full JSON string as the value for the parameter. +6. Save the JSON credentials as a new `SecureString` AWS SSM parameter in the same account used for AWS SSO. Use the + full JSON string as the value for the parameter. ``` /ssosync/google_credentials @@ -95,11 +102,13 @@ Follow these steps: #### 3. Configure Google Admin console - Open the Google Admin console -- From your domain’s Admin console, go to `Main menu menu > Security > Access and data control > API controls` [(ref)](https://developers.google.com/cloud-search/docs/guides/delegation) +- From your domain’s Admin console, go to `Main menu menu > Security > Access and data control > API controls` + [(ref)](https://developers.google.com/cloud-search/docs/guides/delegation) - In the Domain wide delegation pane, select `Manage Domain Wide Delegation`. - Click `Add new`. - In the Client ID field, enter the client ID obtained from the service account creation steps above. -- In the OAuth Scopes field, enter a comma-delimited list of the scopes required for your application. Use the scope `https://www.googleapis.com/auth/cloud_search.query` for search applications using the Query API. +- In the OAuth Scopes field, enter a comma-delimited list of the scopes required for your application. Use the scope + `https://www.googleapis.com/auth/cloud_search.query` for search applications using the Query API. - Add the following permission: [(ref)](https://github.com/awslabs/ssosync?tab=readme-ov-file#google) ```console @@ -108,37 +117,43 @@ https://www.googleapis.com/auth/admin.directory.group.member.readonly https://www.googleapis.com/auth/admin.directory.user.readonly ``` - #### 4. Deploy the `aws-ssosync` component Make sure that all four of the following SSM parameters exist in the target account and region: -* `/ssosync/scim_endpoint_url` -* `/ssosync/scim_endpoint_access_token` -* `/ssosync/identity_store_id` -* `/ssosync/google_credentials` +- `/ssosync/scim_endpoint_url` +- `/ssosync/scim_endpoint_access_token` +- `/ssosync/identity_store_id` +- `/ssosync/google_credentials` -If deployed successfully, Groups and Users should be programmatically copied from the Google Workspace into AWS IAM Identity Center on the given schedule. +If deployed successfully, Groups and Users should be programmatically copied from the Google Workspace into AWS IAM +Identity Center on the given schedule. -If these Groups are not showing up, check the CloudWatch logs for the new Lambda function and refer the [FAQs](#FAQ) included below. +If these Groups are not showing up, check the CloudWatch logs for the new Lambda function and refer the [FAQs](#FAQ) +included below. #### 5. Deploy the `aws-sso` component -Use the names of the Groups now provisioned programmatically in the `aws-sso` component catalog. Follow the [aws-sso](../aws-sso/) component documentation to deploy the `aws-sso` component. +Use the names of the Groups now provisioned programmatically in the `aws-sso` component catalog. Follow the +[aws-sso](../aws-sso/) component documentation to deploy the `aws-sso` component. ### FAQ #### Why is the tool forked by `Benbentwo`? -The `awslabs` tool requires AWS Secrets Managers for the Google Credentials. However, we would prefer to use AWS SSM to store all credentials consistency and not require AWS Secrets Manager. Therefore we've created a Pull Request and will point to a fork until the PR is merged. +The `awslabs` tool requires AWS Secrets Managers for the Google Credentials. However, we would prefer to use AWS SSM to +store all credentials consistency and not require AWS Secrets Manager. Therefore we've created a Pull Request and will +point to a fork until the PR is merged. Ref: + - https://github.com/awslabs/ssosync/pull/133 - https://github.com/awslabs/ssosync/issues/93 #### What should I use for the Google Admin Email Address? -The Service Account created will assume the User given by `--google-admin` / `SSOSYNC_GOOGLE_ADMIN` / `var.google_admin_email`. Therefore, this user email must be a valid Google admin user in your organization. +The Service Account created will assume the User given by `--google-admin` / `SSOSYNC_GOOGLE_ADMIN` / +`var.google_admin_email`. Therefore, this user email must be a valid Google admin user in your organization. This is not the same email as the Service Account. @@ -150,7 +165,8 @@ Notifying Lambda and mark this execution as Failure: googleapi: Error 404: Domai #### Common Group Name Query Error -If filtering group names using query strings, make sure the provided string is valid. For example, `google_group_match: "name:aws*"` is incorrect. Instead use `google_group_match: "Name:aws*"` +If filtering group names using query strings, make sure the provided string is valid. For example, +`google_group_match: "name:aws*"` is incorrect. Instead use `google_group_match: "Name:aws*"` If not, you may again see the same error message: @@ -160,11 +176,12 @@ Notifying Lambda and mark this execution as Failure: googleapi: Error 404: Domai Ref: -> The specific error you are seeing is because the google api doesn't like the query string you provided for the -g parameter. try -g "Name:Fuel*" +> The specific error you are seeing is because the google api doesn't like the query string you provided for the -g +> parameter. try -g "Name:Fuel\*" https://github.com/awslabs/ssosync/issues/91 - + ## Requirements @@ -254,9 +271,11 @@ https://github.com/awslabs/ssosync/issues/91 | [invoke\_arn](#output\_invoke\_arn) | Invoke ARN of the lambda function | | [qualified\_arn](#output\_qualified\_arn) | ARN identifying your Lambda Function Version (if versioning is enabled via publish = true) | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-ssosync) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-ssosync) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index 67d6908cc..3ae6961c6 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -1,59 +1,58 @@ # Component: `aws-team-roles` -This component is responsible for provisioning user and system IAM roles outside the `identity` account. -It sets them up to be assumed from the "team" roles defined in the `identity` account by -[the `aws-teams` component](../aws-teams) and/or the AWS SSO permission sets -defined in [the `aws-sso` component](../aws-sso), and/or be directly accessible via SAML logins. - +This component is responsible for provisioning user and system IAM roles outside the `identity` account. It sets them up +to be assumed from the "team" roles defined in the `identity` account by [the `aws-teams` component](../aws-teams) +and/or the AWS SSO permission sets defined in [the `aws-sso` component](../aws-sso), and/or be directly accessible via +SAML logins. ### Privileges are Granted to Users via IAM Policies -Each role is granted permissions by attaching a list of IAM policies to the IAM role -via its `role_policy_arns` list. You can configure AWS managed policies by entering the ARNs of the policies -directly into the list, or you can create a custom policy as follows: +Each role is granted permissions by attaching a list of IAM policies to the IAM role via its `role_policy_arns` list. +You can configure AWS managed policies by entering the ARNs of the policies directly into the list, or you can create a +custom policy as follows: 1. Give the policy a name, e.g. `eks-admin`. We will use `NAME` as a placeholder for the name in the instructions below. 2. Create a file in the `aws-teams` directory with the name `policy-NAME.tf`. 3. In that file, create a policy as follows: - ```hcl - data "aws_iam_policy_document" "NAME" { - # Define the policy here - } - - resource "aws_iam_policy" "NAME" { - name = format("%s-NAME", module.this.id) - policy = data.aws_iam_policy_document.NAME.json - - tags = module.this.tags - } - ``` - -4. Create a file named `additional-policy-map_override.tf` in the `aws-team-roles` directory (if it does not already exist). - This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), meaning its - contents will be merged with the main terraform file, and any locals defined in it will override locals defined in other files. - Having your code in this separate override file makes it possible for the component to provide a placeholder local variable - so that it works without customization, while allowing you to customize the component and still update it without losing your customizations. + ```hcl + data "aws_iam_policy_document" "NAME" { + # Define the policy here + } + + resource "aws_iam_policy" "NAME" { + name = format("%s-NAME", module.this.id) + policy = data.aws_iam_policy_document.NAME.json + + tags = module.this.tags + } + ``` + +4. Create a file named `additional-policy-map_override.tf` in the `aws-team-roles` directory (if it does not already + exist). This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), + meaning its contents will be merged with the main terraform file, and any locals defined in it will override locals + defined in other files. Having your code in this separate override file makes it possible for the component to + provide a placeholder local variable so that it works without customization, while allowing you to customize the + component and still update it without losing your customizations. 5. In that file, redefine the local variable `overridable_additional_custom_policy_map` map as follows: - ```hcl - locals { - overridable_additional_custom_policy_map = { - NAME = aws_iam_policy.NAME.arn - } - } - ``` + ```hcl + locals { + overridable_additional_custom_policy_map = { + NAME = aws_iam_policy.NAME.arn + } + } + ``` If you have multiple custom policies, add each one to the map in the form `NAME = aws_iam_policy.NAME.arn`. -6. With that done, you can now attach that policy by adding the name to the `role_policy_arns` list. For example: - - ```yaml - role_policy_arns: - - "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" - - "NAME" - ``` +6. With that done, you can now attach that policy by adding the name to the `role_policy_arns` list. For example: + ```yaml + role_policy_arns: + - "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" + - "NAME" + ``` ## Usage @@ -61,14 +60,19 @@ directly into the list, or you can create a custom policy as follows: **Deployment**: Must be deployed by _SuperAdmin_ using `atmos` CLI -Here's an example snippet for how to use this component. This specific usage is an example only, and not intended for production use. -You set the defaults in one YAML file, and import that file into each account's Global stack (except for the `identity` account itself). -If desired, you can make account-specific changes by overriding settings, for example +Here's an example snippet for how to use this component. This specific usage is an example only, and not intended for +production use. You set the defaults in one YAML file, and import that file into each account's Global stack (except for +the `identity` account itself). If desired, you can make account-specific changes by overriding settings, for example + - Disable entire roles in the account by setting `enabled: false` - Limit who can access the role by setting a different value for `trusted_teams` -- Change the permissions available to that role by overriding the `role_policy_arns` (not recommended, limit access to the role or create a different role with the desired set of permissions instead). +- Change the permissions available to that role by overriding the `role_policy_arns` (not recommended, limit access to + the role or create a different role with the desired set of permissions instead). -Note that when overriding, **maps are deep merged, but lists are replaced**. This means, for example, that your setting of `trusted_primary_roles` in an override completely replaces the default, it does not add to it, so if you want to allow an extra "primary" role to have access to the role, you have to include all the default "primary" roles in the list, too, or they will lose access. +Note that when overriding, **maps are deep merged, but lists are replaced**. This means, for example, that your setting +of `trusted_primary_roles` in an override completely replaces the default, it does not add to it, so if you want to +allow an extra "primary" role to have access to the role, you have to include all the default "primary" roles in the +list, too, or they will lose access. ```yaml components: @@ -84,8 +88,7 @@ components: # `template` serves as the default configuration for other roles via the YAML anchor. # However, `atmos` does not support "import" of YAML anchors, so if you define a new role # in another file, you will not be able to reference this anchor. - template: &user-template - # If `enabled: false`, the role will not be created in this account + template: &user-template # If `enabled: false`, the role will not be created in this account enabled: false # `max_session_duration` set the maximum session duration (in seconds) for the IAM roles. @@ -137,7 +140,7 @@ components: <<: *user-template enabled: true role_policy_arns: - - "arn:aws:iam::aws:policy/AdministratorAccess" + - "arn:aws:iam::aws:policy/AdministratorAccess" role_description: "Full administration of this account" trusted_teams: ["admin"] @@ -150,12 +153,12 @@ components: # administrative permissions and use a more restrictive role # for Terraform, such as PowerUser (further restricted to deny AWS SSO changes). role_policy_arns: - - "arn:aws:iam::aws:policy/AdministratorAccess" + - "arn:aws:iam::aws:policy/AdministratorAccess" role_description: "Role for Terraform administration of this account" trusted_teams: ["admin", "spacelift"] - ``` + ## Requirements @@ -235,6 +238,9 @@ components: |------|-------------| | [role\_name\_role\_arn\_map](#output\_role\_name\_role\_arn\_map) | Map of role names to role ARNs | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components) - Cloud Posse's upstream components + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components) - Cloud Posse's upstream + components diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index edf7929f4..38b71abf1 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -1,53 +1,54 @@ # Component: `aws-teams` This component is responsible for provisioning all primary user and system roles into the centralized identity account. -This is expected to be used alongside [the `aws-team-roles` component](../aws-team-roles) to provide -fine-grained role delegation across the account hierarchy. +This is expected to be used alongside [the `aws-team-roles` component](../aws-team-roles) to provide fine-grained role +delegation across the account hierarchy. ### Teams Function Like Groups and are Implemented as Roles -The "teams" created in the `identity` account by this module can be thought of as access control "groups": -a user who is allowed access one of these teams gets access to a set of roles (and corresponding permissions) -across a set of accounts. Generally, there is nothing else provisioned in the `identity` account, -so the teams have limited access to resources in the `identity` account by design. -Teams are implemented as IAM Roles in each account. Access to the "teams" in the `identity` -account is controlled by the `aws-saml` and `aws-sso` components. Access to the roles in all the -other accounts is controlled by the "assume role" policies of those roles, which allow the "team" -or AWS SSO Permission set to assume the role (or not). +The "teams" created in the `identity` account by this module can be thought of as access control "groups": a user who is +allowed access one of these teams gets access to a set of roles (and corresponding permissions) across a set of +accounts. Generally, there is nothing else provisioned in the `identity` account, so the teams have limited access to +resources in the `identity` account by design. + +Teams are implemented as IAM Roles in each account. Access to the "teams" in the `identity` account is controlled by the +`aws-saml` and `aws-sso` components. Access to the roles in all the other accounts is controlled by the "assume role" +policies of those roles, which allow the "team" or AWS SSO Permission set to assume the role (or not). ### Privileges are Defined for Each Role in Each Account by `aws-team-roles` -Every account besides the `identity` account has a set of IAM roles created by the -`aws-team-roles` component. In that component, the account's roles are assigned privileges, -and those privileges ultimately determine what a user can do in that account. +Every account besides the `identity` account has a set of IAM roles created by the `aws-team-roles` component. In that +component, the account's roles are assigned privileges, and those privileges ultimately determine what a user can do in +that account. -Access to the roles can be granted in a number of ways. -One way is by listing "teams" created by this component as "trusted" (`trusted_teams`), -meaning that users who have access to the team role in the `identity` account are -allowed (trusted) to assume the role configured in the target account. -Another is by listing an AWS SSO Permission Set in the account (`trusted_permission_sets`). +Access to the roles can be granted in a number of ways. One way is by listing "teams" created by this component as +"trusted" (`trusted_teams`), meaning that users who have access to the team role in the `identity` account are allowed +(trusted) to assume the role configured in the target account. Another is by listing an AWS SSO Permission Set in the +account (`trusted_permission_sets`). ### Role Access is Enabled by SAML and/or AWS SSO configuration + Users can again access to a role in the `identity` account through either (or both) of 2 mechanisms: #### SAML Access -- SAML access is globally configured via the `aws-saml` component, enabling an external -SAML Identity Provider (IdP) to control access to roles in the `identity` account. -(SAML access can be separately configured for other accounts, see the `aws-saml` and `aws-team-roles` components for more on that.) + +- SAML access is globally configured via the `aws-saml` component, enabling an external SAML Identity Provider (IdP) to + control access to roles in the `identity` account. (SAML access can be separately configured for other accounts, see + the `aws-saml` and `aws-team-roles` components for more on that.) - Individual roles are enabled for SAML access by setting `aws_saml_login_enabled: true` in the role configuration. - Individual users are granted access to these roles by configuration in the SAML IdP. #### AWS SSO Access -The `aws-sso` component can create AWS Permission Sets that allow users to assume specific roles -in the `identity` account. See the `aws-sso` component for details. + +The `aws-sso` component can create AWS Permission Sets that allow users to assume specific roles in the `identity` +account. See the `aws-sso` component for details. ## Usage -**Stack Level**: Global -**Deployment**: Must be deployed by SuperAdmin using `atmos` CLI +**Stack Level**: Global **Deployment**: Must be deployed by SuperAdmin using `atmos` CLI -Here's an example snippet for how to use this component. The component should only be applied once, -which is typically done via the identity stack (e.g. `gbl-identity.yaml`). +Here's an example snippet for how to use this component. The component should only be applied once, which is typically +done via the identity stack (e.g. `gbl-identity.yaml`). ```yaml components: @@ -87,47 +88,48 @@ components: # If a role is both trusted and denied, it will not be able to access this role. # Permission sets specify users operating from the given AWS SSO permission set in this account. - trusted_permission_sets: [ ] - denied_permission_sets: [ ] + trusted_permission_sets: [] + denied_permission_sets: [] # Primary roles specify the short role names of roles in the primary (identity) # account that are allowed to assume this role. - trusted_teams: [ ] - denied_teams: [ "viewer" ] + trusted_teams: [] + denied_teams: ["viewer"] # Role ARNs specify Role ARNs in any account that are allowed to assume this role. # BE CAREFUL: there is nothing limiting these Role ARNs to roles within our organization. - trusted_role_arns: [ ] - denied_role_arns: [ ] + trusted_role_arns: [] + denied_role_arns: [] admin: <<: *user-template - role_description: "Team with PowerUserAccess permissions in `identity` and AdministratorAccess to all other accounts except `root`" + role_description: + "Team with PowerUserAccess permissions in `identity` and AdministratorAccess to all other accounts except + `root`" # Limit `admin` to Power User to prevent accidentally destroying the admin role itself # Use SuperAdmin to administer IAM access - role_policy_arns: [ "arn:aws:iam::aws:policy/PowerUserAccess" ] + role_policy_arns: ["arn:aws:iam::aws:policy/PowerUserAccess"] # TODO Create a "security" team with AdministratorAccess to audit and security, remove "admin" write access to those accounts aws_saml_login_enabled: true # list of roles in primary that can assume into this role in delegated accounts # primary admin can assume delegated admin - trusted_teams: [ "admin" ] + trusted_teams: ["admin"] # GH runner should be moved to its own `ghrunner` role - trusted_permission_sets: [ "IdentityAdminTeamAccess" ] - + trusted_permission_sets: ["IdentityAdminTeamAccess"] spacelift: <<: *user-template role_description: Team for our privileged Spacelift server role_policy_arns: - - team_role_access + - team_role_access aws_saml_login_enabled: false trusted_teams: - - admin + - admin trusted_role_arns: ["arn:aws:iam::123456789012:role/eg-ue2-auto-spacelift-worker-pool-admin"] - ``` + ## Requirements @@ -205,29 +207,34 @@ components: | [team\_names](#output\_team\_names) | List of team names | | [teams\_config](#output\_teams\_config) | Map of team config with name, target arn, and description | - + ## Known Problems ### Error: `assume role policy: LimitExceeded: Cannot exceed quota for ACLSizePerRole: 2048` -The `aws-teams` architecture, when enabling access to a role via lots of AWS SSO Profiles, can create large "assume role" policies, large enough to exceed the default quota of 2048 characters. If you run into this limitation, you will get an error like this: +The `aws-teams` architecture, when enabling access to a role via lots of AWS SSO Profiles, can create large "assume +role" policies, large enough to exceed the default quota of 2048 characters. If you run into this limitation, you will +get an error like this: ``` Error: error updating IAM Role (acme-gbl-root-tfstate-backend-analytics-ro) assume role policy: LimitExceeded: Cannot exceed quota for ACLSizePerRole: 2048 ``` -This can happen in either/both the `identity` and `root` accounts (for Terraform state access). So far, we have always been able to resolve this by requesting a quota increase, which is automatically granted a few minutes after making the request. To request the quota increase: +This can happen in either/both the `identity` and `root` accounts (for Terraform state access). So far, we have always +been able to resolve this by requesting a quota increase, which is automatically granted a few minutes after making the +request. To request the quota increase: - Log in to the AWS Web console as admin in the affected account -- Set your region to N. Virginia `us-east-1` +- Set your region to N. Virginia `us-east-1` - Navigate to the Service Quotas page via the account dropdown menu - Click on AWS Services in the left sidebar -- Search for "IAM" and select "AWS Identity and Access Management (IAM)". (If you don't find that option, make sure you have selected the `us-east-1` region. +- Search for "IAM" and select "AWS Identity and Access Management (IAM)". (If you don't find that option, make sure you + have selected the `us-east-1` region. - Find and select "Role trust policy length" @@ -235,6 +242,7 @@ This can happen in either/both the `identity` and `root` accounts (for Terraform - Wait for the request to be approved, usually less than a few minutes - ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components)- Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components)- Cloud Posse's upstream + component diff --git a/modules/bastion/README.md b/modules/bastion/README.md index 2c99962bb..4af775ebf 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -1,10 +1,15 @@ # Component: `bastion` -This component is responsible for provisioning a generic Bastion host within an ASG with parameterized `user_data` and support for AWS SSM Session Manager for remote access with IAM authentication. +This component is responsible for provisioning a generic Bastion host within an ASG with parameterized `user_data` and +support for AWS SSM Session Manager for remote access with IAM authentication. -If a special `container.sh` script is desired to run, set `container_enabled` to `true`, and set the `image_repository` and `image_container` variables. +If a special `container.sh` script is desired to run, set `container_enabled` to `true`, and set the `image_repository` +and `image_container` variables. -By default, this component acts as an "SSM Bastion", which is deployed to a private subnet and has SSM Enabled, allowing access via the AWS Console, AWS CLI, or SSM Session tools such as [aws-gate](https://github.com/xen0l/aws-gate). Alternatively, this component can be used as a regular SSH Bastion, deployed to a public subnet with Security Group Rules allowing inbound traffic over port 22. +By default, this component acts as an "SSM Bastion", which is deployed to a private subnet and has SSM Enabled, allowing +access via the AWS Console, AWS CLI, or SSM Session tools such as [aws-gate](https://github.com/xen0l/aws-gate). +Alternatively, this component can be used as a regular SSH Bastion, deployed to a public subnet with Security Group +Rules allowing inbound traffic over port 22. ## Usage @@ -41,18 +46,19 @@ components: custom_bastion_hostname: bastion vanity_domain: example.com security_group_rules: - - type : "ingress" - from_port : 22 - to_port : 22 - protocol : tcp - cidr_blocks : ["1.2.3.4/32"] - - type : "egress" - from_port : 0 - to_port : 0 - protocol : -1 - cidr_blocks : ["0.0.0.0/0"] + - type: "ingress" + from_port: 22 + to_port: 22 + protocol: tcp + cidr_blocks: ["1.2.3.4/32"] + - type: "egress" + from_port: 0 + to_port: 0 + protocol: -1 + cidr_blocks: ["0.0.0.0/0"] ``` + ## Requirements @@ -132,8 +138,11 @@ components: | [iam\_instance\_profile](#output\_iam\_instance\_profile) | Name of AWS IAM Instance Profile | | [security\_group\_id](#output\_security\_group\_id) | ID on the AWS Security Group associated with the ASG | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/bastion) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/bastion) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/cloudtrail-bucket/README.md b/modules/cloudtrail-bucket/README.md index 633101aed..a38604932 100644 --- a/modules/cloudtrail-bucket/README.md +++ b/modules/cloudtrail-bucket/README.md @@ -1,12 +1,15 @@ # Component: `cloudtrail-bucket` -This component is responsible for provisioning a bucket for storing cloudtrail logs for auditing purposes. It's expected to be used alongside [the `cloudtrail` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail). +This component is responsible for provisioning a bucket for storing cloudtrail logs for auditing purposes. It's expected +to be used alongside +[the `cloudtrail` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail). ## Usage **Stack Level**: Regional -Here's an example snippet for how to use this component. It's suggested to apply this component to only the centralized `audit` account. +Here's an example snippet for how to use this component. It's suggested to apply this component to only the centralized +`audit` account. ```yaml components: @@ -22,6 +25,7 @@ components: expiration_days: 365 ``` + ## Requirements @@ -87,9 +91,11 @@ No resources. | [cloudtrail\_bucket\_domain\_name](#output\_cloudtrail\_bucket\_domain\_name) | CloudTrail S3 bucket domain name | | [cloudtrail\_bucket\_id](#output\_cloudtrail\_bucket\_id) | CloudTrail S3 bucket ID | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail-bucket) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail-bucket) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/cloudtrail/README.md b/modules/cloudtrail/README.md index 47175bc07..715cdc696 100644 --- a/modules/cloudtrail/README.md +++ b/modules/cloudtrail/README.md @@ -1,11 +1,12 @@ # Component: `cloudtrail` -This component is responsible for provisioning cloudtrail auditing in an individual account. It's expected to be used alongside +This component is responsible for provisioning cloudtrail auditing in an individual account. It's expected to be used +alongside [the `cloudtrail-bucket` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail-bucket) as it utilizes that bucket via remote state. -This component can either be deployed selectively to various accounts with `is_organization_trail=false`, or alternatively -created in all accounts if deployed to the management account `is_organization_trail=true`. +This component can either be deployed selectively to various accounts with `is_organization_trail=false`, or +alternatively created in all accounts if deployed to the management account `is_organization_trail=true`. ## Usage @@ -27,6 +28,7 @@ components: is_organization_trail: true ``` + ## Requirements @@ -113,9 +115,11 @@ components: | [cloudtrail\_logs\_role\_arn](#output\_cloudtrail\_logs\_role\_arn) | CloudTrail Logs role ARN | | [cloudtrail\_logs\_role\_name](#output\_cloudtrail\_logs\_role\_name) | CloudTrail Logs role name | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/cloudwatch-logs/README.md b/modules/cloudwatch-logs/README.md index 128a10cba..9a78855af 100644 --- a/modules/cloudwatch-logs/README.md +++ b/modules/cloudwatch-logs/README.md @@ -21,6 +21,7 @@ components: - app-2 ``` + ## Requirements @@ -90,8 +91,11 @@ components: | [role\_name](#output\_role\_name) | Name of role to assume | | [stream\_arns](#output\_stream\_arns) | ARN of the log stream | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudwatch-logs) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudwatch-logs) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/cognito/README.md b/modules/cognito/README.md index b87c3ae04..a9219970b 100644 --- a/modules/cognito/README.md +++ b/modules/cognito/README.md @@ -4,13 +4,12 @@ This component is responsible for provisioning and managing AWS Cognito resource This component can provision the following resources: - - [Cognito User Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) - - [Cognito User Pool Clients](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html) - - [Cognito User Pool Domains](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html) - - [Cognito User Pool Identity Providers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-provider.html) - - [Cognito User Pool Resource Servers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html) - - [Cognito User Pool User Groups](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html) - +- [Cognito User Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) +- [Cognito User Pool Clients](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html) +- [Cognito User Pool Domains](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html) +- [Cognito User Pool Identity Providers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-provider.html) +- [Cognito User Pool Resource Servers](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html) +- [Cognito User Pool User Groups](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html) ## Usage @@ -37,6 +36,7 @@ components: required: true ``` + ## Requirements @@ -204,9 +204,11 @@ components: | [last\_modified\_date](#output\_last\_modified\_date) | The date the User Pool was last modified | | [resource\_servers\_scope\_identifiers](#output\_resource\_servers\_scope\_identifiers) | A list of all scopes configured in the format identifier/scope\_name | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cognito) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cognito) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/config-bucket/README.md b/modules/config-bucket/README.md index 9d9151732..72f36c015 100644 --- a/modules/config-bucket/README.md +++ b/modules/config-bucket/README.md @@ -2,8 +2,8 @@ This module creates an S3 bucket suitable for storing `AWS Config` data. -It implements a configurable log retention policy, which allows you to efficiently manage logs across different -storage classes (_e.g._ `Glacier`) and ultimately expire the data altogether. +It implements a configurable log retention policy, which allows you to efficiently manage logs across different storage +classes (_e.g._ `Glacier`) and ultimately expire the data altogether. It enables server-side encryption by default. https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html @@ -15,7 +15,8 @@ It blocks public access to the bucket by default. **Stack Level**: Regional -Here's an example snippet for how to use this component. It's suggested to apply this component to only the centralized `audit` account. +Here's an example snippet for how to use this component. It's suggested to apply this component to only the centralized +`audit` account. ```yaml components: @@ -31,6 +32,7 @@ components: expiration_days: 365 ``` + ## Requirements @@ -96,9 +98,11 @@ No resources. | [config\_bucket\_domain\_name](#output\_config\_bucket\_domain\_name) | Config bucket FQDN | | [config\_bucket\_id](#output\_config\_bucket\_id) | Config bucket ID | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/config-bucket) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/config-bucket) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index de47b95ee..90736c474 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -2,10 +2,12 @@ This component is responsible for provisioning SSM or ASM entries for Datadog API keys. -It's required that the DataDog API and APP secret keys are available in the `var.datadog_secrets_source_store_account` account -in AWS SSM Parameter Store at the `/datadog/%v/datadog_app_key` paths (where `%v` are the corresponding account names). +It's required that the DataDog API and APP secret keys are available in the `var.datadog_secrets_source_store_account` +account in AWS SSM Parameter Store at the `/datadog/%v/datadog_app_key` paths (where `%v` are the corresponding account +names). -This component copies keys from the source account (e.g. `auto`) to the destination account where this is being deployed. The purpose of using this formatted copying of keys handles a couple of problems. +This component copies keys from the source account (e.g. `auto`) to the destination account where this is being +deployed. The purpose of using this formatted copying of keys handles a couple of problems. 1. The keys are needed in each account where datadog resources will be deployed. 1. The keys might need to be different per account or tenant, or any subset of accounts. @@ -13,17 +15,21 @@ This component copies keys from the source account (e.g. `auto`) to the destinat This module also has a submodule which allows other resources to quickly use it to create a datadog provider. -See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. +See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for +more information. ## Usage **Stack Level**: Global -This component should be deployed to every account where you want to provision datadog resources. This is usually every account except `root` and `identity` +This component should be deployed to every account where you want to provision datadog resources. This is usually every +account except `root` and `identity` -Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts which you want to track AWS metrics with DataDog. -In this example we use the key paths `/datadog/%v/datadog_api_key` and `/datadog/%v/datadog_app_key` where `%v` is `default`, this can be changed through `datadog_app_secret_key` & `datadog_api_secret_key` variables. -The output Keys in the deployed account will be `/datadog/datadog_api_key` and `/datadog/datadog_app_key`. +Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts which +you want to track AWS metrics with DataDog. In this example we use the key paths `/datadog/%v/datadog_api_key` and +`/datadog/%v/datadog_app_key` where `%v` is `default`, this can be changed through `datadog_app_secret_key` & +`datadog_api_secret_key` variables. The output Keys in the deployed account will be `/datadog/datadog_api_key` and +`/datadog/datadog_app_key`. ```yaml components: @@ -57,6 +63,7 @@ provider "datadog" { } ``` + ## Requirements @@ -137,11 +144,12 @@ provider "datadog" { | [datadog\_site](#output\_datadog\_site) | The Datadog site to use | | [region](#output\_region) | The region where the keys will be created | - + ## References -* Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-configuration) - Cloud Posse's upstream component +- Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-configuration) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index cc930cb2f..56325b4e0 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -19,6 +19,7 @@ provider "datadog" { } ``` + ## Requirements @@ -88,3 +89,4 @@ provider "datadog" { | [datadog\_site](#output\_datadog\_site) | Datadog Site | | [datadog\_tags](#output\_datadog\_tags) | The Context Tags in datadog tag format (list of strings formated as 'key:value') | + diff --git a/modules/datadog-integration/CHANGELOG.md b/modules/datadog-integration/CHANGELOG.md index bafd98f33..a42d26323 100644 --- a/modules/datadog-integration/CHANGELOG.md +++ b/modules/datadog-integration/CHANGELOG.md @@ -2,21 +2,20 @@ ### Possible Breaking Change -The `module "datadog_integration"` and `module "store_write"` had been changed -in an earlier PR from a module without a `count` -to a module with a `count` of zero or one. This PR changes it back to a module -without a count. If you were using the module with a `count` of zero or one, -applying this new version will cause it be destroyed and recreated. This should only -cause a very brief outage in your Datadog monitoring. +The `module "datadog_integration"` and `module "store_write"` had been changed in an earlier PR from a module without a +`count` to a module with a `count` of zero or one. This PR changes it back to a module without a count. If you were +using the module with a `count` of zero or one, applying this new version will cause it be destroyed and recreated. This +should only cause a very brief outage in your Datadog monitoring. ### New Integration Options This PR adds the following new integration options: -- `cspm_resource_collection_enabled` - Enable Datadog Cloud Security Posture Management scanning of your AWS account. See [announcement](https://www.datadoghq.com/product/cloud-security-management/cloud-security-posture-management/) for details. -- `metrics_collection_enabled` - When enabled, a metric-by-metric crawl of the CloudWatch API pulls data and sends it -to Datadog. New metrics are pulled every ten minutes, on average. -- `resource_collection_enabled` - Some Datadog products leverage information about how your AWS resources ( -such as S3 Buckets, RDS snapshots, and CloudFront distributions) are configured. -When `resource_collection_enabled` is `true`, Datadog collects this information -by making read-only API calls into your AWS account. +- `cspm_resource_collection_enabled` - Enable Datadog Cloud Security Posture Management scanning of your AWS account. + See [announcement](https://www.datadoghq.com/product/cloud-security-management/cloud-security-posture-management/) for + details. +- `metrics_collection_enabled` - When enabled, a metric-by-metric crawl of the CloudWatch API pulls data and sends it to + Datadog. New metrics are pulled every ten minutes, on average. +- `resource_collection_enabled` - Some Datadog products leverage information about how your AWS resources ( such as S3 + Buckets, RDS snapshots, and CloudFront distributions) are configured. When `resource_collection_enabled` is `true`, + Datadog collects this information by making read-only API calls into your AWS account. diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index 123027d84..d27d078ae 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -1,15 +1,17 @@ # Component: `datadog-integration` -This component is responsible for provisioning Datadog AWS integrations. It depends on -the `datadog-configuration` component to get the Datadog API keys. +This component is responsible for provisioning Datadog AWS integrations. It depends on the `datadog-configuration` +component to get the Datadog API keys. -See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for more information. +See Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) for +more information. ## Usage **Stack Level**: Global -Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts which you want to track AWS metrics with DataDog. +Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts which +you want to track AWS metrics with DataDog. ```yaml components: @@ -22,6 +24,7 @@ components: enabled: true ``` + ## Requirements @@ -96,11 +99,12 @@ components: | [aws\_role\_name](#output\_aws\_role\_name) | Name of the AWS IAM Role for the Datadog integration | | [datadog\_external\_id](#output\_datadog\_external\_id) | Datadog integration external ID | - + ## References -* Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-integration) - Cloud Posse's upstream component +- Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-integration) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-lambda-forwarder/CHANGELOG.md b/modules/datadog-lambda-forwarder/CHANGELOG.md index 9a1593e45..478db1e47 100644 --- a/modules/datadog-lambda-forwarder/CHANGELOG.md +++ b/modules/datadog-lambda-forwarder/CHANGELOG.md @@ -2,12 +2,10 @@ ### Fix for `enabled = false` or Destroy and Recreate -Previously, when `enabled = false` was set, the component would not necessarily -function as desired (deleting any existing resources and not creating any new ones). -Also, previously, when deleting the component, there was a race condition where -the log group could be deleted before the lambda function was deleted, causing -the lambda function to trigger automatic recreation of the log group. This -would result in re-creation failing because Terraform would try to create the -log group but it already existed. +Previously, when `enabled = false` was set, the component would not necessarily function as desired (deleting any +existing resources and not creating any new ones). Also, previously, when deleting the component, there was a race +condition where the log group could be deleted before the lambda function was deleted, causing the lambda function to +trigger automatic recreation of the log group. This would result in re-creation failing because Terraform would try to +create the log group but it already existed. These issues have been fixed in this PR. diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 7d856d77d..6de150851 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -1,9 +1,8 @@ # Component: `datadog-lambda-forwarder` -This component is responsible for provision all the necessary infrastructure to -deploy [Datadog Lambda forwarders](https://github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring). It depends on -the `datadog-configuration` component to get the Datadog API keys. - +This component is responsible for provision all the necessary infrastructure to deploy +[Datadog Lambda forwarders](https://github.com/DataDog/datadog-serverless-functions/tree/master/aws/logs_monitoring). It +depends on the `datadog-configuration` component to get the Datadog API keys. ## Usage @@ -44,6 +43,7 @@ components: filter_pattern: "" ``` + ## Requirements @@ -150,11 +150,12 @@ components: | [lambda\_forwarder\_vpc\_log\_function\_arn](#output\_lambda\_forwarder\_vpc\_log\_function\_arn) | Datadog Lambda forwarder VPC Flow Logs function ARN | | [lambda\_forwarder\_vpc\_log\_function\_name](#output\_lambda\_forwarder\_vpc\_log\_function\_name) | Datadog Lambda forwarder VPC Flow Logs function name | - + ## References -* Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-lambda-forwarder) - Cloud Posse's upstream component +- Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-lambda-forwarder) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-logs-archive/README.md b/modules/datadog-logs-archive/README.md index 6c4e26a5d..8eb8ffcdb 100644 --- a/modules/datadog-logs-archive/README.md +++ b/modules/datadog-logs-archive/README.md @@ -1,34 +1,42 @@ # Component: `datadog-logs-archive` -This component is responsible for provisioning Datadog Log Archives. It creates a single log archive pipeline for each AWS account. If the `catchall` flag is set, it creates a catchall archive within the same S3 bucket. +This component is responsible for provisioning Datadog Log Archives. It creates a single log archive pipeline for each +AWS account. If the `catchall` flag is set, it creates a catchall archive within the same S3 bucket. -Each log archive filters for the tag `env:$env` where $env is the environment/account name (ie sbx, prd, tools, etc), as well as any tags identified in the additional_tags key. The `catchall` archive, as the name implies, filters for '*'. +Each log archive filters for the tag `env:$env` where $env is the environment/account name (ie sbx, prd, tools, etc), as +well as any tags identified in the additional_tags key. The `catchall` archive, as the name implies, filters for '\*'. -A second bucket is created for cloudtrail, and a cloudtrail is configured to monitor the log archive bucket and log activity to the cloudtrail bucket. To forward these cloudtrail logs to datadog, the cloudtrail bucket's id must be added to the s3_buckets key for our datadog-lambda-forwarder component. +A second bucket is created for cloudtrail, and a cloudtrail is configured to monitor the log archive bucket and log +activity to the cloudtrail bucket. To forward these cloudtrail logs to datadog, the cloudtrail bucket's id must be added +to the s3_buckets key for our datadog-lambda-forwarder component. Both buckets support object lock, with overrideable defaults of COMPLIANCE mode with a duration of 7 days. ## Prerequisites -* Datadog integration set up in target environment - * We rely on the datadog api and app keys added by our datadog integration component +- Datadog integration set up in target environment + - We rely on the datadog api and app keys added by our datadog integration component ## Issues, Gotchas, Good-to-Knows ### Destroy/reprovision process -Because of the protections for S3 buckets, if we want to destroy/replace our bucket, we need to do so in two passes or destroy the bucket manually and then use terraform to clean up the rest. If reprovisioning a recently provisioned bucket, the two-pass process works well. If the bucket has a full day or more of logs, though, deleting it manually first will avoid terraform timeouts, and then the terraform process can be used to clean up everything else. +Because of the protections for S3 buckets, if we want to destroy/replace our bucket, we need to do so in two passes or +destroy the bucket manually and then use terraform to clean up the rest. If reprovisioning a recently provisioned +bucket, the two-pass process works well. If the bucket has a full day or more of logs, though, deleting it manually +first will avoid terraform timeouts, and then the terraform process can be used to clean up everything else. #### Two step process to destroy via terraform -* first set `s3_force_destroy` var to true and apply -* next set `enabled` to false and apply or use tf destroy +- first set `s3_force_destroy` var to true and apply +- next set `enabled` to false and apply or use tf destroy ## Usage **Stack Level**: Global -Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts from which Datadog receives logs. +Here's an example snippet for how to use this component. It's suggested to apply this component to all accounts from +which Datadog receives logs. ```yaml components: @@ -39,87 +47,88 @@ components: workspace_enabled: true vars: enabled: true - # additional_query_tags: - # - "forwardername:*-dev-datadog-lambda-forwarder-logs" - # - "account:123456789012" - + # additional_query_tags: + # - "forwardername:*-dev-datadog-lambda-forwarder-logs" + # - "account:123456789012" ``` ## Requirements -| Name | Version | -|------|---------| +| Name | Version | +| --------- | --------- | | terraform | >= 0.13.0 | -| aws | >= 2.0 | -| datadog | >= 3.3.0 | -| local | >= 1.3 | +| aws | >= 2.0 | +| datadog | >= 3.3.0 | +| local | >= 1.3 | ## Providers -| Name | Version | -|------|---------| -| aws | >= 2.0 | +| Name | Version | +| ------- | -------- | +| aws | >= 2.0 | | datadog | >= 3.7.0 | -| http | >= 2.1.0 | +| http | >= 2.1.0 | ## Modules -| Name | Source | Version | -|------|--------|---------| -| cloudtrail | cloudposse/cloudtrail/aws | 0.21.0 | -| cloudtrail_s3_bucket | cloudposse/cloudtrail-s3-bucket/aws | 0.23.1 | -| iam_roles | ../account-map/modules/iam-roles | n/a | -| s3_bucket | cloudposse/s3-bucket/aws | 0.46.0 | -| this | cloudposse/label/null | 0.25.0 | +| Name | Source | Version | +| -------------------- | ----------------------------------- | ------- | +| cloudtrail | cloudposse/cloudtrail/aws | 0.21.0 | +| cloudtrail_s3_bucket | cloudposse/cloudtrail-s3-bucket/aws | 0.23.1 | +| iam_roles | ../account-map/modules/iam-roles | n/a | +| s3_bucket | cloudposse/s3-bucket/aws | 0.46.0 | +| this | cloudposse/label/null | 0.25.0 | ## Resources -| Name | Type | -|------|------| -| aws_caller_identity.current | data source | -| aws_partition.current | data source | -| aws_ssm_parameter.datadog_api_key | data source | -| aws_ssm_parameter.datadog_app_key | data source | +| Name | Type | +| --------------------------------------- | ----------- | +| aws_caller_identity.current | data source | +| aws_partition.current | data source | +| aws_ssm_parameter.datadog_api_key | data source | +| aws_ssm_parameter.datadog_app_key | data source | | aws_ssm_parameter.datadog_aws_role_name | data source | -| aws_ssm_parameter.datadog_external_id | data source | -| datadog_logs_archive.catchall_archive | resource | -| datadog_logs_archive.logs_archive | resource | -| http.current_order | data source | +| aws_ssm_parameter.datadog_external_id | data source | +| datadog_logs_archive.catchall_archive | resource | +| datadog_logs_archive.logs_archive | resource | +| http.current_order | data source | ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|----------| -| additional_query_tags | Additional tags to include in query for logs for this archive | `list` | [] | no | -| catchall | Set to true to enable a catchall for logs unmatched by any queries. This should only be used in one environment/account | `bool` | false | no | -| datadog_aws_account_id | The AWS account ID Datadog's integration servers use for all integrations | `string` | 464622532012 | no | -| enable_glacier_transition | Enable/disable transition to glacier. Has no effect unless `lifecycle_rules_enabled` set to true | `bool` | true | no | -| glacier_transition_days | Number of days after which to transition objects to glacier storage | `number` | 365 | no | -| lifecycle_rules_enabled | Enable/disable lifecycle management rules for s3 objects | `bool` | true | no | -| object_lock_days_archive | Set duration of archive bucket object lock | `number` | 7 | yes | -| object_lock_days_cloudtrail | Set duration of cloudtrail bucket object lock | `number` | 7 | yes | -| object_lock_mode_archive | Set mode of archive bucket object lock | `string` | COMPLIANCE | yes | -| object_lock_mode_cloudtrail | Set mode of cloudtrail bucket object lock | `string` | COMPLIANCE | yes | -| s3_force_destroy | Set to true to delete non-empty buckets when `enabled` is set to false | `bool` | false | for destroy only | - +| Name | Description | Type | Default | Required | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ------------ | ---------------- | +| additional_query_tags | Additional tags to include in query for logs for this archive | `list` | [] | no | +| catchall | Set to true to enable a catchall for logs unmatched by any queries. This should only be used in one environment/account | `bool` | false | no | +| datadog_aws_account_id | The AWS account ID Datadog's integration servers use for all integrations | `string` | 464622532012 | no | +| enable_glacier_transition | Enable/disable transition to glacier. Has no effect unless `lifecycle_rules_enabled` set to true | `bool` | true | no | +| glacier_transition_days | Number of days after which to transition objects to glacier storage | `number` | 365 | no | +| lifecycle_rules_enabled | Enable/disable lifecycle management rules for s3 objects | `bool` | true | no | +| object_lock_days_archive | Set duration of archive bucket object lock | `number` | 7 | yes | +| object_lock_days_cloudtrail | Set duration of cloudtrail bucket object lock | `number` | 7 | yes | +| object_lock_mode_archive | Set mode of archive bucket object lock | `string` | COMPLIANCE | yes | +| object_lock_mode_cloudtrail | Set mode of cloudtrail bucket object lock | `string` | COMPLIANCE | yes | +| s3_force_destroy | Set to true to delete non-empty buckets when `enabled` is set to false | `bool` | false | for destroy only | ## Outputs -| Name | Description | -|------|-------------| -| archive_id | The ID of the environment-specific log archive | -| bucket_arn | The ARN of the bucket used for log archive storage | -| bucket_domain_name | The FQDN of the bucket used for log archive storage | -| bucket_id | The ID (name) of the bucket used for log archive storage | -| bucket_region | The region of the bucket used for log archive storage | -| cloudtrail_bucket_arn | The ARN of the bucket used for cloudtrail log storage | -| cloudtrail_bucket_domain_name | The FQDN of the bucket used for cloudtrail log storage | -| cloudtrail_bucket_id | The ID (name) of the bucket used for cloudtrail log storage | -| catchall_id | The ID of the catchall log archive | +| Name | Description | +| ----------------------------- | ----------------------------------------------------------- | +| archive_id | The ID of the environment-specific log archive | +| bucket_arn | The ARN of the bucket used for log archive storage | +| bucket_domain_name | The FQDN of the bucket used for log archive storage | +| bucket_id | The ID (name) of the bucket used for log archive storage | +| bucket_region | The region of the bucket used for log archive storage | +| cloudtrail_bucket_arn | The ARN of the bucket used for cloudtrail log storage | +| cloudtrail_bucket_domain_name | The FQDN of the bucket used for cloudtrail log storage | +| cloudtrail_bucket_id | The ID (name) of the bucket used for cloudtrail log storage | +| catchall_id | The ID of the catchall log archive | ## References -* [cloudposse/s3-bucket/aws](https://registry.terraform.io/modules/cloudposse/s3-bucket/aws/latest) - Cloud Posse's S3 component -* [datadog_logs_archive resource] (https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/logs_archive) - Datadog's provider documentation for the datadog_logs_archive resource +- [cloudposse/s3-bucket/aws](https://registry.terraform.io/modules/cloudposse/s3-bucket/aws/latest) - Cloud Posse's S3 + component +- [datadog_logs_archive resource] + (https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/logs_archive) - Datadog's provider + documentation for the datadog_logs_archive resource [](https://cpco.io/component) diff --git a/modules/datadog-monitor/CHANGELOG.md b/modules/datadog-monitor/CHANGELOG.md index ca3260084..7ca47e3d6 100644 --- a/modules/datadog-monitor/CHANGELOG.md +++ b/modules/datadog-monitor/CHANGELOG.md @@ -11,7 +11,7 @@ The following inputs were removed because they no longer have any effect: - role_paths - secrets_store_type -Except for `monitors_roles_map` and `role_paths`, these inputs were deprecated -in an earlier PR, and replaced with outputs from `datadog-configuration`. +Except for `monitors_roles_map` and `role_paths`, these inputs were deprecated in an earlier PR, and replaced with +outputs from `datadog-configuration`. The implementation of `monitors_roles_map` and `role_paths` has been lost. diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 968cf5e09..95774e6c3 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -24,14 +24,20 @@ components: ``` ## Conventions -- Treat datadog like a separate cloud provider with integrations ([datadog-integration](https://docs.cloudposse.com/components/library/aws/datadog-integration)) into your accounts. + +- Treat datadog like a separate cloud provider with integrations + ([datadog-integration](https://docs.cloudposse.com/components/library/aws/datadog-integration)) into your accounts. - Use the `catalog` convention to define a step of alerts. You can use ours or define your own. [https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors](https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors) ## Adjust Thresholds per Stack -Since there are so many parameters that may be adjusted for a given monitor, we define all monitors through YAML. By convention, we define the **default monitors** that should apply to all environments, and then adjust the thresholds per environment. This is accomplished using the `datadog-monitor` components variable `datadog_monitors_config_paths` which defines the path to the YAML configuration files. By passing a path for `dev` and `prod`, we can define configurations that are different per environment. +Since there are so many parameters that may be adjusted for a given monitor, we define all monitors through YAML. By +convention, we define the **default monitors** that should apply to all environments, and then adjust the thresholds per +environment. This is accomplished using the `datadog-monitor` components variable `datadog_monitors_config_paths` which +defines the path to the YAML configuration files. By passing a path for `dev` and `prod`, we can define configurations +that are different per environment. For example, you might have the following settings defined for `prod` and `dev` stacks that override the defaults. @@ -47,6 +53,7 @@ components: - catalog/monitors/*.yaml - catalog/monitors/dev/*.yaml # note this line ``` + For `prod` stack: ``` @@ -60,7 +67,8 @@ components: - catalog/monitors/prod/*.yaml # note this line ``` -Behind the scenes (with `atmos`) we fetch all files from these glob patterns, template them, and merge them by key. If we peek into the `*.yaml` and `dev/*.yaml` files above you could see an example like this: +Behind the scenes (with `atmos`) we fetch all files from these glob patterns, template them, and merge them by key. If +we peek into the `*.yaml` and `dev/*.yaml` files above you could see an example like this: **components/terraform/datadog-monitor/catalog/monitors/elb.yaml** @@ -105,6 +113,7 @@ elb-lb-httpcode-5xx-notify: critical: 50 warning: 20 ``` + **components/terraform/datadog-monitor/catalog/monitors/dev/elb.yaml** ``` @@ -120,10 +129,16 @@ elb-lb-httpcode-5xx-notify: ## Key Notes ### Inheritance -The important thing to note here is that the default yaml is applied to every stage that it's deployed to. For dev specifically however, we want to override the thresholds and priority for this monitor. This merging is done by key of the monitor, in this case `elb-lb-httpcode-5xx-notify`. + +The important thing to note here is that the default yaml is applied to every stage that it's deployed to. For dev +specifically however, we want to override the thresholds and priority for this monitor. This merging is done by key of +the monitor, in this case `elb-lb-httpcode-5xx-notify`. ### Templating -The second thing to note is `${ dd_env }`. This is **terraform** templating in action. While double braces (`{{ env }}`) refers to datadog templating, `${ dd_env }` is a template variable we pass into our monitors. in this example we use it to specify a grouping int he message. This value is passed in and can be overridden via stacks. + +The second thing to note is `${ dd_env }`. This is **terraform** templating in action. While double braces (`{{ env }}`) +refers to datadog templating, `${ dd_env }` is a template variable we pass into our monitors. in this example we use it +to specify a grouping int he message. This value is passed in and can be overridden via stacks. We pass a value via: @@ -140,6 +155,7 @@ components: datadog_monitors_config_parameters: dd_env: "dev" ``` + This allows us to further use inheritance from stack configuration to keep our monitors dry, but configurable. Another available option is to use our catalog as base monitors and then override them with your specific fine tuning. @@ -156,10 +172,14 @@ components: ## Other Gotchas -Our integration action that checks for `'source_type_name' equals 'Monitor Alert'` will also be true for synthetics. Whereas if we check for `'event_type' equals 'query_alert_monitor'`, that's only true for monitors, because synthetics will only be picked up by an integration action when `event_type` is `synthetics_alert`. +Our integration action that checks for `'source_type_name' equals 'Monitor Alert'` will also be true for synthetics. +Whereas if we check for `'event_type' equals 'query_alert_monitor'`, that's only true for monitors, because synthetics +will only be picked up by an integration action when `event_type` is `synthetics_alert`. -This is important if we need to distinguish between monitors and synthetics in OpsGenie, which is the case when we want to ensure clean messaging on OpsGenie incidents in Statuspage. +This is important if we need to distinguish between monitors and synthetics in OpsGenie, which is the case when we want +to ensure clean messaging on OpsGenie incidents in Statuspage. + ## Requirements @@ -230,7 +250,7 @@ No resources. |------|-------------| | [datadog\_monitor\_names](#output\_datadog\_monitor\_names) | Names of the created Datadog monitors | - + ## Related How-to Guides @@ -240,10 +260,12 @@ No resources. - [How to Implement SRE with Datadog](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-sre-with-datadog) ## Component Dependencies + - [datadog-integration](https://docs.cloudposse.com/components/library/aws/datadog-integration/) ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-monitor) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-monitor) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index 06b594330..3d75f2286 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -55,9 +55,9 @@ components: logDriver: awslogs options: {} port_mappings: [] - ``` + ## Requirements @@ -132,8 +132,11 @@ components: | [vpc\_id](#output\_vpc\_id) | Selected VPC ID | | [vpc\_sg\_id](#output\_vpc\_sg\_id) | Selected VPC SG ID | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs-service) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs-service) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-synthetics-private-location/CHANGELOG.md b/modules/datadog-synthetics-private-location/CHANGELOG.md index aa19dd6d1..ee538026d 100644 --- a/modules/datadog-synthetics-private-location/CHANGELOG.md +++ b/modules/datadog-synthetics-private-location/CHANGELOG.md @@ -2,16 +2,12 @@ ### Possible Breaking Change -Previously this component directly created the Kubernetes namespace for -the agent when `create_namespace` was set to `true`. Now this component -delegates that responsibility to the `helm-release` module, which -better coordinates the destruction of resources at destruction time -(for example, ensuring that the Helm release is completely destroyed -and finalizers run before deleting the namespace). +Previously this component directly created the Kubernetes namespace for the agent when `create_namespace` was set to +`true`. Now this component delegates that responsibility to the `helm-release` module, which better coordinates the +destruction of resources at destruction time (for example, ensuring that the Helm release is completely destroyed and +finalizers run before deleting the namespace). -Generally the simplest upgrade path is to destroy the Helm release, -then destroy the namespace, then apply the new configuration. Alternatively, -you can use `terraform state mv` to move the existing namespace to the new -Terraform "address", which will preserve the existing deployment and reduce -the possibility of the destroy failing and leaving the Kubernetes cluster -in a bad state. +Generally the simplest upgrade path is to destroy the Helm release, then destroy the namespace, then apply the new +configuration. Alternatively, you can use `terraform state mv` to move the existing namespace to the new Terraform +"address", which will preserve the existing deployment and reduce the possibility of the destroy failing and leaving the +Kubernetes cluster in a bad state. diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index 8567d102f..3e79812f7 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -2,7 +2,8 @@ This component provisions a Datadog synthetics private location on Datadog and a private location agent on EKS cluster. -Private locations allow you to monitor internal-facing applications or any private URLs that are not accessible from the public internet. +Private locations allow you to monitor internal-facing applications or any private URLs that are not accessible from the +public internet. ## Usage @@ -116,11 +117,12 @@ Environment variables: ## References -* https://docs.datadoghq.com/synthetics/private_locations -* https://docs.datadoghq.com/synthetics/private_locations/configuration/ -* https://github.com/DataDog/helm-charts/tree/main/charts/synthetics-private-location -* https://github.com/DataDog/helm-charts/blob/main/charts/synthetics-private-location/values.yaml +- https://docs.datadoghq.com/synthetics/private_locations +- https://docs.datadoghq.com/synthetics/private_locations/configuration/ +- https://github.com/DataDog/helm-charts/tree/main/charts/synthetics-private-location +- https://github.com/DataDog/helm-charts/blob/main/charts/synthetics-private-location/values.yaml + ## Requirements @@ -213,10 +215,11 @@ Environment variables: | [metadata](#output\_metadata) | Block status of the deployed release | | [synthetics\_private\_location\_id](#output\_synthetics\_private\_location\_id) | Synthetics private location ID | + ## References -* https://docs.datadoghq.com/getting_started/synthetics/private_location -* https://docs.datadoghq.com/synthetics/private_locations/configuration -* https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_private_location -* https://github.com/DataDog/helm-charts/tree/main/charts/synthetics-private-location +- https://docs.datadoghq.com/getting_started/synthetics/private_location +- https://docs.datadoghq.com/synthetics/private_locations/configuration +- https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_private_location +- https://github.com/DataDog/helm-charts/tree/main/charts/synthetics-private-location diff --git a/modules/datadog-synthetics/CHANGELOG.md b/modules/datadog-synthetics/CHANGELOG.md index 16bb69d8c..f9ccb06db 100644 --- a/modules/datadog-synthetics/CHANGELOG.md +++ b/modules/datadog-synthetics/CHANGELOG.md @@ -2,19 +2,18 @@ ### API Schema accepted -Test can now be defined using the Datadog API schema, meaning that the test definition -returned by +Test can now be defined using the Datadog API schema, meaning that the test definition returned by + - `https://api.datadoghq.com/api/v1/synthetics/tests/api/{public_id}` - `https://api.datadoghq.com/api/v1/synthetics/tests/browser/{public_id}` can be directly used a map value (you still need to supply a key, though). -You can mix tests using the API schema with tests using the old Terraform schema. -You could probably get away with mixing them in the same test, but it is not recommended. +You can mix tests using the API schema with tests using the old Terraform schema. You could probably get away with +mixing them in the same test, but it is not recommended. ### Default locations -Previously, the default locations for Synthetics tests were "all" public locations. -Now the default is no locations, in favor of locations being specified in each test configuration, -which is more flexible. Also, since the tests are expensive, it is better to err on the side of -too few test locations than too many. +Previously, the default locations for Synthetics tests were "all" public locations. Now the default is no locations, in +favor of locations being specified in each test configuration, which is more flexible. Also, since the tests are +expensive, it is better to err on the side of too few test locations than too many. diff --git a/modules/datadog-synthetics/README.md b/modules/datadog-synthetics/README.md index 26eccb1db..a18461c1e 100644 --- a/modules/datadog-synthetics/README.md +++ b/modules/datadog-synthetics/README.md @@ -1,10 +1,11 @@ # Component: `datadog-synthetics` -This component provides the ability to implement [Datadog synthetic tests](https://docs.datadoghq.com/synthetics/guide/). +This component provides the ability to implement +[Datadog synthetic tests](https://docs.datadoghq.com/synthetics/guide/). -Synthetic tests allow you to observe how your systems and applications are performing using simulated requests and actions -from the AWS managed locations around the globe, and to monitor internal endpoints -from [Private Locations](https://docs.datadoghq.com/synthetics/private_locations). +Synthetic tests allow you to observe how your systems and applications are performing using simulated requests and +actions from the AWS managed locations around the globe, and to monitor internal endpoints from +[Private Locations](https://docs.datadoghq.com/synthetics/private_locations). ## Usage @@ -39,13 +40,14 @@ components: Below are examples of Datadog browser and API synthetic tests. -The synthetic tests are defined in YAML using either the [Datadog Terraform provider](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) schema -or the [Datadog Synthetics API](https://docs.datadoghq.com/api/latest/synthetics) schema. -See the `terraform-datadog-platform` Terraform module [README](https://github.com/cloudposse/terraform-datadog-platform/blob/main/modules/synthetics/README.md) for more details. -We recommend using the API schema so you can more create and edit tests using the Datadog -web API and then import them into this module by downloading the test using -the Datadog REST API. (See the Datadog API documentation for the appropriate -`curl` commands to use.) +The synthetic tests are defined in YAML using either the +[Datadog Terraform provider](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) +schema or the [Datadog Synthetics API](https://docs.datadoghq.com/api/latest/synthetics) schema. See the +`terraform-datadog-platform` Terraform module +[README](https://github.com/cloudposse/terraform-datadog-platform/blob/main/modules/synthetics/README.md) for more +details. We recommend using the API schema so you can more create and edit tests using the Datadog web API and then +import them into this module by downloading the test using the Datadog REST API. (See the Datadog API documentation for +the appropriate `curl` commands to use.) ```yaml # API schema @@ -124,24 +126,31 @@ my-api-test: jsonpath: foo.bar ``` -These configuration examples are defined in the YAML files in the [catalog/synthetics/examples](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-synthetics/catalog/synthetics/examples) folder. +These configuration examples are defined in the YAML files in the +[catalog/synthetics/examples](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-synthetics/catalog/synthetics/examples) +folder. -You can use different subfolders for your use-case. -For example, you can have `dev` and `prod` subfolders to define different synthetic tests for the `dev` and `prod` environments. +You can use different subfolders for your use-case. For example, you can have `dev` and `prod` subfolders to define +different synthetic tests for the `dev` and `prod` environments. Then use the `synthetic_paths` variable to point the component to the synthetic test configuration files. The configuration files are processed and transformed in the following order: -- The `datadog-synthetics` component loads the YAML configuration files from the filesystem paths specified by the `synthetics_paths` variable +- The `datadog-synthetics` component loads the YAML configuration files from the filesystem paths specified by the + `synthetics_paths` variable -- Then, in the [synthetics](https://github.com/cloudposse/terraform-datadog-platform/blob/master/modules/synthetics/main.tf) module, - the YAML configuration files are merged and transformed from YAML into - the [Datadog Terraform provider](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) schema +- Then, in the + [synthetics](https://github.com/cloudposse/terraform-datadog-platform/blob/master/modules/synthetics/main.tf) module, + the YAML configuration files are merged and transformed from YAML into the + [Datadog Terraform provider](https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test) + schema - And finally, the Datadog Terraform provider uses the - [Datadog Synthetics API](https://docs.datadoghq.com/api/latest/synthetics) specifications to call the Datadog API and provision the synthetic tests + [Datadog Synthetics API](https://docs.datadoghq.com/api/latest/synthetics) specifications to call the Datadog API and + provision the synthetic tests + ## Requirements @@ -214,6 +223,7 @@ No resources. | [datadog\_synthetics\_test\_monitor\_ids](#output\_datadog\_synthetics\_test\_monitor\_ids) | IDs of the monitors associated with the Datadog synthetics tests | | [datadog\_synthetics\_test\_names](#output\_datadog\_synthetics\_test\_names) | Names of the created Datadog synthetic tests | + ## References diff --git a/modules/dms/endpoint/README.md b/modules/dms/endpoint/README.md index 264580caa..b65dfc542 100644 --- a/modules/dms/endpoint/README.md +++ b/modules/dms/endpoint/README.md @@ -69,6 +69,7 @@ components: - target ``` + ## Requirements @@ -151,10 +152,11 @@ components: | [dms\_endpoint\_arn](#output\_dms\_endpoint\_arn) | DMS endpoint ARN | | [dms\_endpoint\_id](#output\_dms\_endpoint\_id) | DMS endpoint ID | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-endpoint) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-endpoint) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dms/iam/README.md b/modules/dms/iam/README.md index 21eb4e22b..021da144d 100644 --- a/modules/dms/iam/README.md +++ b/modules/dms/iam/README.md @@ -23,6 +23,7 @@ components: name: dms ``` + ## Requirements @@ -79,10 +80,11 @@ No resources. | [dms\_redshift\_s3\_role\_arn](#output\_dms\_redshift\_s3\_role\_arn) | DMS Redshift S3 role ARN | | [dms\_vpc\_management\_role\_arn](#output\_dms\_vpc\_management\_role\_arn) | DMS VPC management role ARN | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-iam) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-iam) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dms/replication-instance/README.md b/modules/dms/replication-instance/README.md index 42b1b31b0..fade7e38c 100644 --- a/modules/dms/replication-instance/README.md +++ b/modules/dms/replication-instance/README.md @@ -42,6 +42,7 @@ components: allocated_storage: 50 ``` + ## Requirements @@ -114,10 +115,11 @@ No resources. | [dms\_replication\_instance\_arn](#output\_dms\_replication\_instance\_arn) | DMS replication instance ARN | | [dms\_replication\_instance\_id](#output\_dms\_replication\_instance\_id) | DMS replication instance ID | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-replication-instance) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-replication-instance) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dms/replication-task/README.md b/modules/dms/replication-task/README.md index f7968c246..4732e9072 100644 --- a/modules/dms/replication-task/README.md +++ b/modules/dms/replication-task/README.md @@ -37,6 +37,7 @@ components: table_mappings_file: "config/replication-task-table-mappings-example.json" ``` + ## Requirements @@ -104,10 +105,11 @@ No resources. | [dms\_replication\_task\_arn](#output\_dms\_replication\_task\_arn) | DMS replication task ARN | | [dms\_replication\_task\_id](#output\_dms\_replication\_task\_id) | DMS replication task ID | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-replication-task) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-replication-task) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index c608b8a3c..ed73c60a6 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -1,6 +1,8 @@ # Component: `dns-delegated` -This component is responsible for provisioning a DNS zone which delegates nameservers to the DNS zone in the primary DNS account. The primary DNS zone is expected to already be provisioned via [the `dns-primary` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-primary). +This component is responsible for provisioning a DNS zone which delegates nameservers to the DNS zone in the primary DNS +account. The primary DNS zone is expected to already be provisioned via +[the `dns-primary` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-primary). This component also provisions a wildcard ACM certificate for the given subdomain. @@ -8,9 +10,12 @@ This component also provisions a wildcard ACM certificate for the given subdomai **Stack Level**: Global or Regional -Here's an example snippet for how to use this component. Use this component in global or regional stacks for any accounts where you host services that need DNS records on a given subdomain (e.g. delegated zone) of the root domain (e.g. primary zone). +Here's an example snippet for how to use this component. Use this component in global or regional stacks for any +accounts where you host services that need DNS records on a given subdomain (e.g. delegated zone) of the root domain +(e.g. primary zone). -Public Hosted Zone `devplatform.example.net` will be created and `example.net` HZ in the dns primary account will contain a record delegating DNS to the new HZ +Public Hosted Zone `devplatform.example.net` will be created and `example.net` HZ in the dns primary account will +contain a record delegating DNS to the new HZ This will create an ACM record @@ -20,8 +25,8 @@ components: dns-delegated: vars: zone_config: - - subdomain: devplatform - zone_name: example.net + - subdomain: devplatform + zone_name: example.net request_acm_certificate: true dns_private_zone_enabled: false # dns_soa_config configures the SOA record for the zone:: @@ -33,10 +38,10 @@ components: # - 60 ; nxdomain TTL, or time in seconds for secondary DNS servers to cache negative responses # See [SOA Record Documentation](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/SOA-NSrecords.html) for more information. dns_soa_config: "awsdns-hostmaster.amazon.com. 1 7200 900 1209600 60" - ``` -Private Hosted Zone `devplatform.example.net` will be created and `example.net` HZ in the dns primary account will contain a record delegating DNS to the new HZ +Private Hosted Zone `devplatform.example.net` will be created and `example.net` HZ in the dns primary account will +contain a record delegating DNS to the new HZ This will create an ACM record using a Private CA @@ -46,8 +51,8 @@ components: dns-delegated: vars: zone_config: - - subdomain: devplatform - zone_name: example.net + - subdomain: devplatform + zone_name: example.net request_acm_certificate: true dns_private_zone_enabled: true vpc_region_abbreviation_type: short @@ -60,13 +65,19 @@ components: ### Limitations -Switching a hosted zone from public to private can cause issues because the provider will try to do an update instead of a ForceNew. +Switching a hosted zone from public to private can cause issues because the provider will try to do an update instead of +a ForceNew. See: https://github.com/hashicorp/terraform-provider-aws/issues/7614 -It's not possible to toggle between public and private so if switching from public to private and downtime is acceptable, delete the records of the hosted zone, delete the hosted zone, destroy the terraform component, and deploy with the new settings. +It's not possible to toggle between public and private so if switching from public to private and downtime is +acceptable, delete the records of the hosted zone, delete the hosted zone, destroy the terraform component, and deploy +with the new settings. -NOTE: With each of these workarounds, you may have an issue connecting to the service specific provider e.g. for `auroro-postgres` you may get an error of the host set to `localhost` on the `postgresql` provider resulting in an error. To get around this, dump the endpoint using `atmos terraform show`, hardcode the `host` input on the provider, and re-run the apply. +NOTE: With each of these workarounds, you may have an issue connecting to the service specific provider e.g. for +`auroro-postgres` you may get an error of the host set to `localhost` on the `postgresql` provider resulting in an +error. To get around this, dump the endpoint using `atmos terraform show`, hardcode the `host` input on the provider, +and re-run the apply. #### Workaround if downtime is fine @@ -84,12 +95,15 @@ NOTE: With each of these workarounds, you may have an issue connecting to the se 1. Deploy the new dns-delegated-private component 1. Move aurora-postgres, msk, external-dns, echo-server, etc to the new hosted zone by re-deploying - ## Caveats -- Do not create a delegation for subdomain of a domain in a zone for which that zone is not authoritative for the subdomain (usually because you already delegated a parent subdomain). Though Amazon Route 53 will allow you to, you should not do it. For historic reasons, Route 53 Public DNS allows customers to create two NS delegations within a hosted zone which creates a conflict (and can return either set to resolvers depending on the query). +- Do not create a delegation for subdomain of a domain in a zone for which that zone is not authoritative for the + subdomain (usually because you already delegated a parent subdomain). Though Amazon Route 53 will allow you to, you + should not do it. For historic reasons, Route 53 Public DNS allows customers to create two NS delegations within a + hosted zone which creates a conflict (and can return either set to resolvers depending on the query). -For example, in a single hosted zone with the domain name `example.com`, it is possible to create two NS delegations which are parent and child of each other as follows: +For example, in a single hosted zone with the domain name `example.com`, it is possible to create two NS delegations +which are parent and child of each other as follows: ``` a.example.com. 172800 IN NS ns-1084.awsdns-07.org. @@ -105,21 +119,29 @@ b.a.example.com. 172800 IN NS ns-338.awsdns-42.com. This configuration creates two discrete possible resolution paths. -1. If a resolver directly queries the `example.com` nameservers for `c.b.a.example.com`, it will receive the second set of nameservers. +1. If a resolver directly queries the `example.com` nameservers for `c.b.a.example.com`, it will receive the second set + of nameservers. 2. If a resolver queries `example.com` for `a.example.com`, it will receive the first set of nameservers. -If the resolver then proceeds to query the `a.example.com` nameservers for `c.b.a.example.com`, the response is driven by the contents of the `a.example.com` zone, which may be different than the results returned by the `b.a.example.com` nameservers. `c.b.a.example.com` may not have an entry in the `a.example.com` nameservers, resulting in an error (`NXDOMAIN`) being returned. +If the resolver then proceeds to query the `a.example.com` nameservers for `c.b.a.example.com`, the response is driven +by the contents of the `a.example.com` zone, which may be different than the results returned by the `b.a.example.com` +nameservers. `c.b.a.example.com` may not have an entry in the `a.example.com` nameservers, resulting in an error +(`NXDOMAIN`) being returned. -From 15th May 2020, Route 53 Resolver has been enabling a modern DNS resolver standard called "QName Minimization"[*]. This change causes the resolver to more strictly use recursion path [2] described above where path [1] was common before. [*] [https://tools.ietf.org/html/rfc7816](https://tools.ietf.org/html/rfc7816) +From 15th May 2020, Route 53 Resolver has been enabling a modern DNS resolver standard called "QName Minimization"[*]. +This change causes the resolver to more strictly use recursion path [2] described above where path [1] was common +before. [*] [https://tools.ietf.org/html/rfc7816](https://tools.ietf.org/html/rfc7816) -As of January 2022, you can observe the different query strategies in use by Google DNS at `8.8.8.8` (strategy 1) and Cloudflare DNS at `1.1.1.1` (strategy 2). You should verify that both DNS servers resolve your host records properly. +As of January 2022, you can observe the different query strategies in use by Google DNS at `8.8.8.8` (strategy 1) and +Cloudflare DNS at `1.1.1.1` (strategy 2). You should verify that both DNS servers resolve your host records properly. Takeaway -1. In order to ensure DNS resolution is consistent no matter the resolver, it is important to always create NS delegations only authoritative zones. - +1. In order to ensure DNS resolution is consistent no matter the resolver, it is important to always create NS + delegations only authoritative zones. + ## Requirements @@ -208,10 +230,11 @@ Takeaway | [route53\_hosted\_zone\_protections](#output\_route53\_hosted\_zone\_protections) | List of AWS Shield Advanced Protections for Route53 Hosted Zones. | | [zones](#output\_zones) | Subdomain and zone config | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-delegated) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-delegated) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index 32e53470b..668dd0bf0 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -1,37 +1,59 @@ # Component: `dns-primary` -This component is responsible for provisioning the primary DNS zones into an AWS account. By convention, we typically provision the primary DNS zones in the `dns` account. The primary account for branded zones (e.g. `example.com`), however, would be in the `prod` account, while staging zone (e.g. `example.qa`) might be in the `staging` account. +This component is responsible for provisioning the primary DNS zones into an AWS account. By convention, we typically +provision the primary DNS zones in the `dns` account. The primary account for branded zones (e.g. `example.com`), +however, would be in the `prod` account, while staging zone (e.g. `example.qa`) might be in the `staging` account. -The zones from the primary DNS zone are then expected to be delegated to other accounts via [the `dns-delegated` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-delegated). Additionally, external records can be created on the primary DNS zones via the `record_config` variable. +The zones from the primary DNS zone are then expected to be delegated to other accounts via +[the `dns-delegated` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-delegated). +Additionally, external records can be created on the primary DNS zones via the `record_config` variable. ## Architecture ### Summary -The `dns` account gets a single `dns-primary` component deployed. Every other account that needs DNS entries gets a single `dns-delegated` component, chaining off the domains in the `dns` account. Optionally, accounts can have a single `dns-primary` component of their own, to have apex domains (which Cloud Posse calls "vanity domains"). Typically, these domains are configured with CNAME (or apex alias) records to point to service domain entries. +The `dns` account gets a single `dns-primary` component deployed. Every other account that needs DNS entries gets a +single `dns-delegated` component, chaining off the domains in the `dns` account. Optionally, accounts can have a single +`dns-primary` component of their own, to have apex domains (which Cloud Posse calls "vanity domains"). Typically, these +domains are configured with CNAME (or apex alias) records to point to service domain entries. ### Details -The purpose of the `dns` account is to host root domains shared by several accounts (with each account being delegated its own subdomain) and to be the owner of domain registrations purchased from Amazon. +The purpose of the `dns` account is to host root domains shared by several accounts (with each account being delegated +its own subdomain) and to be the owner of domain registrations purchased from Amazon. -The purpose of the `dns-primary` component is to provision AWS Route53 zones for the root domains. These zones, once provisioned, must be manually configured into the Domain Name Registrar's records as name servers. A single component can provision multiple domains and, optionally, associated ACM (SSL) certificates in a single account. +The purpose of the `dns-primary` component is to provision AWS Route53 zones for the root domains. These zones, once +provisioned, must be manually configured into the Domain Name Registrar's records as name servers. A single component +can provision multiple domains and, optionally, associated ACM (SSL) certificates in a single account. -Cloud Posse's architecture expects root domains shared by several accounts to be provisioned in the `dns` account with `dns-primary` and delegated to other accounts using the `dns-delegated` component, with each account getting its own subdomain corresponding to a Route 53 zone in the delegated account. Cloud Posse's architecture requires at least one such domain, called "the service domain", be provisioned. The service domain is not customer facing, and is provisioned to allow fully automated construction of host names without any concerns about how they look. Although they are not secret, the public will never see them. +Cloud Posse's architecture expects root domains shared by several accounts to be provisioned in the `dns` account with +`dns-primary` and delegated to other accounts using the `dns-delegated` component, with each account getting its own +subdomain corresponding to a Route 53 zone in the delegated account. Cloud Posse's architecture requires at least one +such domain, called "the service domain", be provisioned. The service domain is not customer facing, and is provisioned +to allow fully automated construction of host names without any concerns about how they look. Although they are not +secret, the public will never see them. -Root domains used by a single account are provisioned with the `dns-primary` component directly in that account. Cloud Posse calls these "vanity domains". These can be whatever the marketing or PR or other stakeholders want to be. +Root domains used by a single account are provisioned with the `dns-primary` component directly in that account. Cloud +Posse calls these "vanity domains". These can be whatever the marketing or PR or other stakeholders want to be. -After a domain is provisioned in the `dns` account, the `dns-delegated` component can provision one or more subdomains for each account, and, optionally, associated ACM certificates. For the service domain, Cloud Posse recommends using the account name as the delegated subdomain (either directly, e.g. "plat-dev", or as multiple subdomains, e.g. "dev.plat") because that allows `dns-delegated` to automatically provision any required host name in that zone. +After a domain is provisioned in the `dns` account, the `dns-delegated` component can provision one or more subdomains +for each account, and, optionally, associated ACM certificates. For the service domain, Cloud Posse recommends using the +account name as the delegated subdomain (either directly, e.g. "plat-dev", or as multiple subdomains, e.g. "dev.plat") +because that allows `dns-delegated` to automatically provision any required host name in that zone. -There is no automated support for `dns-primary` to provision root domains outside of the `dns` account that are to be shared by multiple accounts, and such usage is not recommended. If you must, `dns-primary` can provision a subdomain of a root domain that is provisioned in another account (not `dns`). In this case, the delegation of the subdomain must be done manually by entering the name servers into the parent domain's records (instead of in the Registrar's records). +There is no automated support for `dns-primary` to provision root domains outside of the `dns` account that are to be +shared by multiple accounts, and such usage is not recommended. If you must, `dns-primary` can provision a subdomain of +a root domain that is provisioned in another account (not `dns`). In this case, the delegation of the subdomain must be +done manually by entering the name servers into the parent domain's records (instead of in the Registrar's records). The architecture does not support other configurations, or non-standard component names. - ## Usage **Stack Level**: Global -Here's an example snippet for how to use this component. This component should only be applied once as the DNS zones it creates are global. This is typically done via the DNS stack (e.g. `gbl-dns.yaml`). +Here's an example snippet for how to use this component. This component should only be applied once as the DNS zones it +creates are global. This is typically done via the DNS stack (e.g. `gbl-dns.yaml`). ```yaml components: @@ -71,11 +93,12 @@ components: YourVeryLongStringGoesHere ``` -:::info -Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component for more advanced certificate requirements. +:::info Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component for more advanced certificate +requirements. ::: + ## Requirements @@ -143,8 +166,11 @@ Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component | [acms](#output\_acms) | ACM certificates for domains | | [zones](#output\_zones) | DNS zones | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-primary) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-primary) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/documentdb/README.md b/modules/documentdb/README.md index fb43cab53..cdea391ee 100644 --- a/modules/documentdb/README.md +++ b/modules/documentdb/README.md @@ -24,6 +24,7 @@ components: retention_period: 35 ``` + ## Requirements @@ -118,10 +119,11 @@ components: | [security\_group\_id](#output\_security\_group\_id) | ID of the DocumentDB cluster Security Group | | [security\_group\_name](#output\_security\_group\_name) | Name of the DocumentDB cluster Security Group | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/documentdb) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/documentdb) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dynamodb/README.md b/modules/dynamodb/README.md index eec46b957..48c5c4d0b 100644 --- a/modules/dynamodb/README.md +++ b/modules/dynamodb/README.md @@ -25,9 +25,9 @@ components: point_in_time_recovery_enabled: true streams_enabled: false ttl_enabled: false - ``` + ## Requirements @@ -44,7 +44,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [dynamodb\_table](#module\_dynamodb\_table) | cloudposse/dynamodb/aws | 0.31.0 | +| [dynamodb\_table](#module\_dynamodb\_table) | cloudposse/dynamodb/aws | 0.35.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -79,6 +79,7 @@ No resources. | [hash\_key](#input\_hash\_key) | DynamoDB table Hash Key | `string` | n/a | yes | | [hash\_key\_type](#input\_hash\_key\_type) | Hash Key type, which must be a scalar type: `S`, `N`, or `B` for String, Number or Binary data, respectively. | `string` | `"S"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_table](#input\_import\_table) | Import Amazon S3 data into a new table. |
object({
# Valid values are GZIP, ZSTD and NONE
input_compression_type = optional(string, null)
# Valid values are CSV, DYNAMODB_JSON, and ION.
input_format = string
input_format_options = optional(object({
csv = object({
delimiter = string
header_list = list(string)
})
}), null)
s3_bucket_source = object({
bucket = string
bucket_owner = optional(string)
key_prefix = optional(string)
})
})
| `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | @@ -113,10 +114,11 @@ No resources. | [table\_stream\_arn](#output\_table\_stream\_arn) | DynamoDB table stream ARN | | [table\_stream\_label](#output\_table\_stream\_label) | DynamoDB table stream label | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dynamodb) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dynamodb) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index e71633a2c..41db52018 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -6,7 +6,9 @@ This component is responsible for provisioning VPN Client Endpoints. **Stack Level**: Regional -Here's an example snippet for how to use this component. This component should only be applied once as the resources it creates are regional. This is typically done via the corp stack (e.g. `uw2-corp.yaml`). This is because a vpc endpoint requires a vpc and the network stack does not have a vpc. +Here's an example snippet for how to use this component. This component should only be applied once as the resources it +creates are regional. This is typically done via the corp stack (e.g. `uw2-corp.yaml`). This is because a vpc endpoint +requires a vpc and the network stack does not have a vpc. ```yaml components: @@ -24,23 +26,24 @@ components: organization_name: acme split_tunnel: true availability_zones: - - us-west-2a - - us-west-2b - - us-west-2c + - us-west-2a + - us-west-2b + - us-west-2c associated_security_group_ids: [] additional_routes: - - destination_cidr_block: 0.0.0.0/0 - description: Internet Route + - destination_cidr_block: 0.0.0.0/0 + description: Internet Route authorization_rules: - - name: Internet Rule - authorize_all_groups: true - description: Allows routing to the internet" - target_network_cidr: 0.0.0.0/0 + - name: Internet Rule + authorize_all_groups: true + description: Allows routing to the internet" + target_network_cidr: 0.0.0.0/0 ``` ## Deploying -NOTE: This module uses the `aws_ec2_client_vpn_route` resource which throws an error if too many API calls come from a single host. Ignore this error and repeat the terraform command. It usually takes 3 deploys (or destroys) to complete. +NOTE: This module uses the `aws_ec2_client_vpn_route` resource which throws an error if too many API calls come from a +single host. Ignore this error and repeat the terraform command. It usually takes 3 deploys (or destroys) to complete. Error on create (See issue https://github.com/hashicorp/terraform-provider-aws/issues/19750) @@ -56,9 +59,12 @@ timeout while waiting for resource to be gone (last state: 'deleting', timeout: ## Testing -NOTE: The `GoogleIDPMetadata-cloudposse.com.xml` in this repo is equivalent to the one in the `sso` component and is used for testing. This component can only specify a single SAML document. The customer SAML xml should be placed in this directory side-by-side the CloudPosse SAML xml. +NOTE: The `GoogleIDPMetadata-cloudposse.com.xml` in this repo is equivalent to the one in the `sso` component and is +used for testing. This component can only specify a single SAML document. The customer SAML xml should be placed in this +directory side-by-side the CloudPosse SAML xml. -Prior to testing, the component needs to be deployed and the AWS client app needs to be setup by the IdP admin otherwise the following steps will result in an error similar to `app_not_configured_for_user`. +Prior to testing, the component needs to be deployed and the AWS client app needs to be setup by the IdP admin otherwise +the following steps will result in an error similar to `app_not_configured_for_user`. 1. Deploy the component in a regional account with a VPC like `ue2-corp`. 1. Copy the contents of `client_configuration` into a file called `client_configuration.ovpn` @@ -74,15 +80,17 @@ Prior to testing, the component needs to be deployed and the AWS client app need A browser will launch and allow you to connect to the VPN. -1. Make a note of where this component is deployed -1. Ensure that the resource to connect to is in a VPC that is connected by the transit gateway -1. Ensure that the resource to connect to contains a security group with a rule that allows ingress from where the client vpn is deployed (e.g. `ue2-corp`) -1. Use `nmap` to test if the port is `open`. If the port is `filtered` then it's not open. +1. Make a note of where this component is deployed +1. Ensure that the resource to connect to is in a VPC that is connected by the transit gateway +1. Ensure that the resource to connect to contains a security group with a rule that allows ingress from where the + client vpn is deployed (e.g. `ue2-corp`) +1. Use `nmap` to test if the port is `open`. If the port is `filtered` then it's not open. nmap -p Successful tests have been seen with MSK and RDS. + ## Requirements @@ -159,10 +167,12 @@ No resources. | [vpn\_endpoint\_dns\_name](#output\_vpn\_endpoint\_dns\_name) | The DNS Name of the Client VPN Endpoint Connection. | | [vpn\_endpoint\_id](#output\_vpn\_endpoint\_id) | The ID of the Client VPN Endpoint Connection. | + ## References -* [cloudposse/terraform-aws-ec2-client-vpn](https://github.com/cloudposse/terraform-aws-ec2-client-vpn) - Cloud Posse's upstream component -* [cloudposse/awsutils](https://github.com/cloudposse/terraform-provider-awsutils) - Cloud Posse's awsutils provider +- [cloudposse/terraform-aws-ec2-client-vpn](https://github.com/cloudposse/terraform-aws-ec2-client-vpn) - Cloud Posse's + upstream component +- [cloudposse/awsutils](https://github.com/cloudposse/terraform-provider-awsutils) - Cloud Posse's awsutils provider [](https://cpco.io/component) diff --git a/modules/ecr/README.md b/modules/ecr/README.md index 46c209cd1..413b40394 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -1,13 +1,13 @@ # Component: `ecr` This component is responsible for provisioning repositories, lifecycle rules, and permissions for streamlined ECR usage. -This utilizes [the roles-to-principals submodule](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-map/modules/roles-to-principals) +This utilizes +[the roles-to-principals submodule](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-map/modules/roles-to-principals) to assign accounts to various roles. It is also compatible with the [GitHub Actions IAM Role mixin](https://github.com/cloudposse/terraform-aws-components/blob/master/mixins/github-actions-iam-role/README-github-action-iam-role.md). - -:::caution -Older versions of our reference architecture have an`eks-iam` component that needs to be updated to provide sufficient IAM roles to allow pods to pull from ECR repos +:::caution Older versions of our reference architecture have an`eks-iam` component that needs to be updated to provide +sufficient IAM roles to allow pods to pull from ECR repos ::: @@ -16,9 +16,9 @@ Older versions of our reference architecture have an`eks-iam` component that nee **Stack Level**: Regional Here's an example snippet for how to use this component. This component is normally only applied once as the resources -it creates are globally accessible, but you may want to create ECRs in multiple regions for redundancy. -This is typically provisioned via the stack for the "artifact" account (typically `auto`, `artifact`, or `corp`) -in the primary region. +it creates are globally accessible, but you may want to create ECRs in multiple regions for redundancy. This is +typically provisioned via the stack for the "artifact" account (typically `auto`, `artifact`, or `corp`) in the primary +region. ```yaml components: @@ -40,10 +40,10 @@ components: - microservice-c read_write_account_role_map: identity: - - admin - - cicd + - admin + - cicd automation: - - admin + - admin read_only_account_role_map: corp: ["*"] dev: ["*"] @@ -51,6 +51,7 @@ components: stage: ["*"] ``` + ## Requirements @@ -129,6 +130,7 @@ components: | [ecr\_user\_unique\_id](#output\_ecr\_user\_unique\_id) | ECR user unique ID assigned by AWS | | [repository\_host](#output\_repository\_host) | ECR repository name | + ## Related @@ -137,7 +139,8 @@ components: - [Decide on ECR Strategy](https://docs.cloudposse.com/reference-architecture/design-decisions/foundational-platform/decide-on-ecr-strategy) ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecr) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecr) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index d96f3ca89..567f832bd 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -101,7 +101,10 @@ components: hostPort: 80 protocol: tcp command: - - '/bin/sh -c "echo '' Amazon ECS Sample App

Amazon ECS Sample App

Congratulations!

Your application is now running on a container in Amazon ECS.

'' > /usr/local/apache2/htdocs/index.html && httpd-foreground"' + - '/bin/sh -c "echo '' Amazon ECS Sample App

Amazon ECS + Sample App

Congratulations!

Your application is now running on a container in Amazon + ECS.

'' > /usr/local/apache2/htdocs/index.html && httpd-foreground"' entrypoint: ["sh", "-c"] task: desired_count: 1 @@ -147,19 +150,23 @@ components: #### Other Domains This component supports alternate service names for your ECS Service through a couple of variables: - - `vanity_domain` & `vanity_alias` - This will create a route to the service in the listener rules of the ALB. This will also create a Route 53 alias record in the hosted zone in this account. The hosted zone is looked up by the `vanity_domain` input. - - `additional_targets` - This will create a route to the service in the listener rules of the ALB. This will not create a Route 53 alias record. + +- `vanity_domain` & `vanity_alias` - This will create a route to the service in the listener rules of the ALB. This will + also create a Route 53 alias record in the hosted zone in this account. The hosted zone is looked up by the + `vanity_domain` input. +- `additional_targets` - This will create a route to the service in the listener rules of the ALB. This will not create + a Route 53 alias record. Examples: ```yaml - ecs/platform/service/echo-server: - vars: - vanity_domain: "dev-acme.com" - vanity_alias: - - "echo-server.dev-acme.com" - additional_targets: - - "echo.acme.com" +ecs/platform/service/echo-server: + vars: + vanity_domain: "dev-acme.com" + vanity_alias: + - "echo-server.dev-acme.com" + additional_targets: + - "echo.acme.com" ``` This then creates the following listener rules: @@ -171,33 +178,35 @@ echo-server.public-platform.use2.dev.plat.service-discovery.com OR echo.acme.com ``` -It will also create the record in Route53 to point `"echo-server.dev-acme.com"` to the ALB. Thus `"echo-server.dev-acme.com"` should resolve. +It will also create the record in Route53 to point `"echo-server.dev-acme.com"` to the ALB. Thus +`"echo-server.dev-acme.com"` should resolve. We can then create a pointer to this service in the `acme.come` hosted zone. ```yaml - dns-primary: - vars: - domain_names: - - acme.com - record_config: - - root_zone: acme.com - name: echo. - type: CNAME - ttl: 60 - records: - - echo-server.dev-acme.com +dns-primary: + vars: + domain_names: + - acme.com + record_config: + - root_zone: acme.com + name: echo. + type: CNAME + ttl: 60 + records: + - echo-server.dev-acme.com ``` This will create a CNAME record in the `acme.com` hosted zone that points `echo.acme.com` to `echo-server.dev-acme.com`. ### EFS -EFS is supported by this ecs service, you can use either `efs_volumes` or `efs_component_volumes` in your task definition. +EFS is supported by this ecs service, you can use either `efs_volumes` or `efs_component_volumes` in your task +definition. +This example shows how to use `efs_component_volumes` which remote looks up efs component and uses the `efs_id` to mount +the volume. And how to use `efs_volumes` -This example shows how to use `efs_component_volumes` which remote looks up efs component and uses the `efs_id` to mount the volume. -And how to use `efs_volumes` ```yaml components: terraform: @@ -243,7 +252,7 @@ components: authorization_config: [] ``` - + ## Requirements @@ -450,9 +459,11 @@ components: | [vpc\_id](#output\_vpc\_id) | Selected VPC ID | | [vpc\_sg\_id](#output\_vpc\_sg\_id) | Selected VPC SG ID | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs-service) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs-service) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ecs/README.md b/modules/ecs/README.md index b7ce3168c..aeb446def 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -12,7 +12,7 @@ The following will create - ecs cluster - load balancer with an ACM cert placed on example.com -- r53 record on all *.example.com which will point to the load balancer +- r53 record on all \*.example.com which will point to the load balancer ```yaml components: @@ -50,6 +50,7 @@ components: - "my-vanity-domain.com" ``` + ## Requirements @@ -144,8 +145,11 @@ components: | [security\_group\_id](#output\_security\_group\_id) | Security group id | | [vpc\_id](#output\_vpc\_id) | VPC ID | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/efs/README.md b/modules/efs/README.md index 722714068..34981e144 100644 --- a/modules/efs/README.md +++ b/modules/efs/README.md @@ -1,6 +1,8 @@ # Component: `efs` -This component is responsible for provisioning an [EFS](https://aws.amazon.com/efs/) Network File System with KMS encryption-at-rest. EFS is an excellent choice as the default block storage for EKS clusters so that volumes are not zone-locked. +This component is responsible for provisioning an [EFS](https://aws.amazon.com/efs/) Network File System with KMS +encryption-at-rest. EFS is an excellent choice as the default block storage for EKS clusters so that volumes are not +zone-locked. ## Usage @@ -27,6 +29,7 @@ components: # cidr_blocks: ["0.0.0.0/0"] ``` + ## Requirements @@ -109,10 +112,11 @@ components: | [security\_group\_id](#output\_security\_group\_id) | EFS Security Group ID | | [security\_group\_name](#output\_security\_group\_name) | EFS Security Group name | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/efs) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/efs) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 6940f7c5b..bedb6ddd9 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -1,6 +1,7 @@ # Component: `actions-runner-controller` -This component creates a Helm release for [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) on an EKS cluster. +This component creates a Helm release for +[actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) on an EKS cluster. ## Usage @@ -165,7 +166,6 @@ components: # - "amd64" # - "AMD64" # - "core-auto" - ``` ### Generating Required Secrets @@ -174,25 +174,27 @@ AWS SSM is used to store and retrieve secrets. Decide on the SSM path for the GitHub secret (PAT or Application private key) and GitHub webhook secret. -Since the secret is automatically scoped by AWS to the account and region where the secret is stored, -we recommend the secret be stored at `/github_runners/controller_github_app_secret` unless you -plan on running multiple instances of the controller. If you plan on running multiple instances of the controller, -and want to give them different access (otherwise they could share the same secret), then you can add -a path component to the SSM path. For example `/github_runners/cicd/controller_github_app_secret`. +Since the secret is automatically scoped by AWS to the account and region where the secret is stored, we recommend the +secret be stored at `/github_runners/controller_github_app_secret` unless you plan on running multiple instances of the +controller. If you plan on running multiple instances of the controller, and want to give them different access +(otherwise they could share the same secret), then you can add a path component to the SSM path. For example +`/github_runners/cicd/controller_github_app_secret`. ``` ssm_github_secret_path: "/github_runners/controller_github_app_secret" ``` -The preferred way to authenticate is by _creating_ and _installing_ a GitHub App. -This is the recommended approach as it allows for more much more restricted access than using a personal access token, -at least until [fine-grained personal access token permissions](https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/) are generally available. -Follow the instructions [here](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/docs/detailed-docs.md#deploying-using-github-app-authentication) to create and install the GitHub App. +The preferred way to authenticate is by _creating_ and _installing_ a GitHub App. This is the recommended approach as it +allows for more much more restricted access than using a personal access token, at least until +[fine-grained personal access token permissions](https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/) +are generally available. Follow the instructions +[here](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/docs/detailed-docs.md#deploying-using-github-app-authentication) +to create and install the GitHub App. -At the creation stage, you will be asked to generate a private key. This is the private key that will be used to authenticate -the Action Runner Controller. Download the file and store the contents in SSM using the following command, adjusting the profile -and file name. The profile should be the `admin` role in the account to which you are deploying the runner controller. -The file name should be the name of the private key file you downloaded. +At the creation stage, you will be asked to generate a private key. This is the private key that will be used to +authenticate the Action Runner Controller. Download the file and store the contents in SSM using the following command, +adjusting the profile and file name. The profile should be the `admin` role in the account to which you are deploying +the runner controller. The file name should be the name of the private key file you downloaded. ``` AWS_PROFILE=acme-mgmt-use2-auto-admin chamber write github_runners controller_github_app_secret -- "$(cat APP_NAME.DATE.private-key.pem)" @@ -204,15 +206,15 @@ You can verify the file was correctly written to SSM by matching the private key AWS_PROFILE=acme-mgmt-use2-auto-admin chamber read -q github_runners controller_github_app_secret | openssl rsa -in - -pubout -outform DER | openssl sha256 -binary | openssl base64 ``` -At this stage, record the Application ID and the private key fingerprint in your secrets manager (e.g. 1Password). -You will need the Application ID to configure the runner controller, and want the fingerprint to verify the private key. +At this stage, record the Application ID and the private key fingerprint in your secrets manager (e.g. 1Password). You +will need the Application ID to configure the runner controller, and want the fingerprint to verify the private key. -Proceed to install the GitHub App in the organization or repository you want to use the runner controller for, -and record the Installation ID (the final numeric part of the URL, as explained in the instructions -linked above) in your secrets manager. You will need the Installation ID to configure the runner controller. +Proceed to install the GitHub App in the organization or repository you want to use the runner controller for, and +record the Installation ID (the final numeric part of the URL, as explained in the instructions linked above) in your +secrets manager. You will need the Installation ID to configure the runner controller. -In your stack configuration, set the following variables, making sure to quote the values so they are -treated as strings, not numbers. +In your stack configuration, set the following variables, making sure to quote the values so they are treated as +strings, not numbers. ``` github_app_id: "12345" @@ -220,44 +222,52 @@ github_app_installation_id: "12345" ``` OR (obsolete) -- A PAT with the scope outlined in [this document](https://github.com/actions-runner-controller/actions-runner-controller#deploying-using-pat-authentication). - Save this to the value specified by `ssm_github_token_path` using the following command, adjusting the - AWS\_PROFILE to refer to the `admin` role in the account to which you are deploying the runner controller: + +- A PAT with the scope outlined in + [this document](https://github.com/actions-runner-controller/actions-runner-controller#deploying-using-pat-authentication). + Save this to the value specified by `ssm_github_token_path` using the following command, adjusting the AWS_PROFILE to + refer to the `admin` role in the account to which you are deploying the runner controller: ``` AWS_PROFILE=acme-mgmt-use2-auto-admin chamber write github_runners controller_github_app_secret -- "" ``` -2. If using the Webhook Driven autoscaling (recommended), generate a random string to use as the Secret when creating the webhook in GitHub. +2. If using the Webhook Driven autoscaling (recommended), generate a random string to use as the Secret when creating + the webhook in GitHub. Generate the string using 1Password (no special characters, length 45) or by running + ```bash dd if=/dev/random bs=1 count=33 2>/dev/null | base64 ``` Store this key in AWS SSM under the same path specified by `ssm_github_webhook_secret_token_path` + ``` ssm_github_webhook_secret_token_path: "/github_runners/github_webhook_secret" ``` ### Using Runner Groups -GitHub supports grouping runners into distinct [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups), which allow you to have different access controls -for different runners. Read the linked documentation about creating and configuring Runner Groups, which you must do -through the GitHub Web UI. If you choose to create Runner Groups, you can assign one or more Runner pools (from the -`runners` map) to groups (only one group per runner pool) by including `group: ` in the runner -configuration. We recommend including it immediately after `scope`. +GitHub supports grouping runners into distinct +[Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups), +which allow you to have different access controls for different runners. Read the linked documentation about creating +and configuring Runner Groups, which you must do through the GitHub Web UI. If you choose to create Runner Groups, you +can assign one or more Runner pools (from the `runners` map) to groups (only one group per runner pool) by including +`group: ` in the runner configuration. We recommend including it immediately after `scope`. ### Using Webhook Driven Autoscaling (recommended) -We recommend using Webhook Driven Autoscaling until GitHub releases their own autoscaling solution (said to be "in the works" as of April 2023). +We recommend using Webhook Driven Autoscaling until GitHub releases their own autoscaling solution (said to be "in the +works" as of April 2023). -To use the Webhook Driven Autoscaling, in addition to setting `webhook_driven_scaling_enabled` to `true`, you must -also install the GitHub organization-level webhook after deploying the component (specifically, the webhook server). -The URL for the webhook is determined by the `webhook.hostname_template` and where -it is deployed. Recommended URL is `https://gha-webhook.[environment].[stage].[tenant].[service-discovery-domain]`. +To use the Webhook Driven Autoscaling, in addition to setting `webhook_driven_scaling_enabled` to `true`, you must also +install the GitHub organization-level webhook after deploying the component (specifically, the webhook server). The URL +for the webhook is determined by the `webhook.hostname_template` and where it is deployed. Recommended URL is +`https://gha-webhook.[environment].[stage].[tenant].[service-discovery-domain]`. As a GitHub organization admin, go to `https://github.com/organizations/[organization]/settings/hooks`, and then: + - Click"Add webhook" and create a new webhook with the following settings: - Payload URL: copy from Terraform output `webhook_payload_url` - Content type: `application/json` @@ -269,62 +279,62 @@ As a GitHub organization admin, go to `https://github.com/organizations/[organiz - Ensure that "Active" is checked (should be checked by default) - Click "Add webhook" at the bottom of the settings page -After the webhook is created, select "edit" for the webhook and go to the "Recent Deliveries" tab and verify that there is a delivery -(of a "ping" event) with a green check mark. If not, verify all the settings and consult -the logs of the `actions-runner-controller-github-webhook-server` pod. +After the webhook is created, select "edit" for the webhook and go to the "Recent Deliveries" tab and verify that there +is a delivery (of a "ping" event) with a green check mark. If not, verify all the settings and consult the logs of the +`actions-runner-controller-github-webhook-server` pod. ### Configuring Webhook Driven Autoscaling -The `HorizontalRunnerAutoscaler scaleUpTriggers.duration` (see [Webhook Driven Scaling documentation](https://github. com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#webhook-driven-scaling)) is -controlled by the `webhook_startup_timeout` setting for each Runner. The purpose of this timeout is to ensure, in -case a job cancellation or termination event gets missed, that the resulting idle runner eventually gets terminated. +The `HorizontalRunnerAutoscaler scaleUpTriggers.duration` (see [Webhook Driven Scaling documentation](https://github. +com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#webhook-driven-scaling)) is +controlled by the `webhook_startup_timeout` setting for each Runner. The purpose of this timeout is to ensure, in case a +job cancellation or termination event gets missed, that the resulting idle runner eventually gets terminated. #### How the Autoscaler Determines the Desired Runner Pool Size -When a job is queued, a `capacityReservation` is created for it. The HRA (Horizontal Runner Autoscaler) sums up all -the capacity reservations to calculate the desired size of the runner pool, subject to the limits of `minReplicas` -and `maxReplicas`. The idea is that a `capacityReservation` is deleted when a job is completed or canceled, and the -pool size will be equal to `jobsStarted - jobsFinished`. However, it can happen that a job will finish without the -HRA being successfully notified about it, so as a safety measure, the `capacityReservation` will expire after a -configurable amount of time, at which point it will be deleted without regard to the job being finished. This -ensures that eventually an idle runner pool will scale down to `minReplicas`. - -If it happens that the capacity reservation expires before the job is finished, the Horizontal Runner Autoscaler (HRA) will scale down the pool -by 2 instead of 1: once because the capacity reservation expired, and once because the job finished. This will -also cause starvation of waiting jobs, because the next in line will have its timeout timer started but will not -actually start running because no runner is available. And if `minReplicas` is set to zero, the pool will scale down -to zero before finishing all the jobs, leaving some waiting indefinitely. This is why it is important to set the -`webhook_startup_timeout` to a time long enough to cover the full time a job may have to wait between the time it is +When a job is queued, a `capacityReservation` is created for it. The HRA (Horizontal Runner Autoscaler) sums up all the +capacity reservations to calculate the desired size of the runner pool, subject to the limits of `minReplicas` and +`maxReplicas`. The idea is that a `capacityReservation` is deleted when a job is completed or canceled, and the pool +size will be equal to `jobsStarted - jobsFinished`. However, it can happen that a job will finish without the HRA being +successfully notified about it, so as a safety measure, the `capacityReservation` will expire after a configurable +amount of time, at which point it will be deleted without regard to the job being finished. This ensures that eventually +an idle runner pool will scale down to `minReplicas`. + +If it happens that the capacity reservation expires before the job is finished, the Horizontal Runner Autoscaler (HRA) +will scale down the pool by 2 instead of 1: once because the capacity reservation expired, and once because the job +finished. This will also cause starvation of waiting jobs, because the next in line will have its timeout timer started +but will not actually start running because no runner is available. And if `minReplicas` is set to zero, the pool will +scale down to zero before finishing all the jobs, leaving some waiting indefinitely. This is why it is important to set +the `webhook_startup_timeout` to a time long enough to cover the full time a job may have to wait between the time it is queued and the time it finishes, assuming that the HRA scales up the pool by 1 and runs the job on the new runner. -:::info -If there are more jobs queued than there are runners allowed by `maxReplicas`, the timeout timer does not start on the -capacity reservation until enough reservations ahead of it are removed for it to be considered as representing +:::info If there are more jobs queued than there are runners allowed by `maxReplicas`, the timeout timer does not start +on the capacity reservation until enough reservations ahead of it are removed for it to be considered as representing and active job. Although there are some edge cases regarding `webhook_startup_timeout` that seem not to be covered -properly (see [actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)), -they only merit adding a few extra minutes to the timeout. -::: - +properly (see +[actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)), they only +merit adding a few extra minutes to the timeout. ::: ### Recommended `webhook_startup_timeout` Duration #### Consequences of Too Short of a `webhook_startup_timeout` Duration If you set `webhook_startup_timeout` to too short a duration, the Horizontal Runner Autoscaler will cancel capacity -reservations for jobs that have not yet finished, and the pool will become too small. This will be most serious if you have -set `minReplicas = 0` because in this case, jobs will be left in the queue indefinitely. With a higher value of -`minReplicas`, the pool will eventually make it through all the queued jobs, but not as quickly as intended due to -the incorrectly reduced capacity. +reservations for jobs that have not yet finished, and the pool will become too small. This will be most serious if you +have set `minReplicas = 0` because in this case, jobs will be left in the queue indefinitely. With a higher value of +`minReplicas`, the pool will eventually make it through all the queued jobs, but not as quickly as intended due to the +incorrectly reduced capacity. #### Consequences of Too Long of a `webhook_startup_timeout` Duration If the Horizontal Runner Autoscaler misses a scale-down event (which can happen because events do not have delivery -guarantees), a runner may be left running idly for as long as the `webhook_startup_timeout` duration. The only -problem with this is the added expense of leaving the idle runner running. +guarantees), a runner may be left running idly for as long as the `webhook_startup_timeout` duration. The only problem +with this is the added expense of leaving the idle runner running. #### Recommendation As a result, we recommend setting `webhook_startup_timeout` to a period long enough to cover: + - The time it takes for the HRA to scale up the pool and make a new runner available - The time it takes for the runner to pick up the job from GitHub - The time it takes for the job to start running on the new runner @@ -332,21 +342,21 @@ As a result, we recommend setting `webhook_startup_timeout` to a period long eno Because the consequences of expiring a capacity reservation before the job is finished are so severe, we recommend setting `webhook_startup_timeout` to a period at least 30 minutes longer than you expect the longest job to take. -Remember, when everything works properly, the HRA will scale down the pool as jobs finish, so there is little cost -to setting a long duration, and the cost looks even smaller by comparison to the cost of having too short a duration. +Remember, when everything works properly, the HRA will scale down the pool as jobs finish, so there is little cost to +setting a long duration, and the cost looks even smaller by comparison to the cost of having too short a duration. -For lightly used runner pools expecting only short jobs, you can set `webhook_startup_timeout` to `"30m"`. -As a rule of thumb, we recommend setting `maxReplicas` high enough that jobs never wait on the queue more than an hour. +For lightly used runner pools expecting only short jobs, you can set `webhook_startup_timeout` to `"30m"`. As a rule of +thumb, we recommend setting `maxReplicas` high enough that jobs never wait on the queue more than an hour. ### Interaction with Karpenter or other EKS autoscaling solutions -Kubernetes cluster autoscaling solutions generally expect that a Pod runs a service that can be terminated on one -Node and restarted on another with only a short duration needed to finish processing any in-flight requests. When -the cluster is resized, the cluster autoscaler will do just that. However, GitHub Action Runner Jobs do not fit this -model. If a Pod is terminated in the middle of a job, the job is lost. The likelihood of this happening is increased -by the fact that the Action Runner Controller Autoscaler is expanding and contracting the size of the Runner Pool on -a regular basis, causing the cluster autoscaler to more frequently want to scale up or scale down the EKS cluster, -and, consequently, to move Pods around. +Kubernetes cluster autoscaling solutions generally expect that a Pod runs a service that can be terminated on one Node +and restarted on another with only a short duration needed to finish processing any in-flight requests. When the cluster +is resized, the cluster autoscaler will do just that. However, GitHub Action Runner Jobs do not fit this model. If a Pod +is terminated in the middle of a job, the job is lost. The likelihood of this happening is increased by the fact that +the Action Runner Controller Autoscaler is expanding and contracting the size of the Runner Pool on a regular basis, +causing the cluster autoscaler to more frequently want to scale up or scale down the EKS cluster, and, consequently, to +move Pods around. To handle these kinds of situations, Karpenter respects an annotation on the Pod: @@ -358,42 +368,44 @@ spec: karpenter.sh/do-not-evict: "true" ``` -When you set this annotation on the Pod, Karpenter will not evict it. This means that the Pod will stay on the Node -it is on, and the Node it is on will not be considered for eviction. This is good because it means that the Pod -will not be terminated in the middle of a job. However, it also means that the Node the Pod is on will not be considered -for termination, which means that the Node will not be removed from the cluster, which means that the cluster will -not shrink in size when you would like it to. +When you set this annotation on the Pod, Karpenter will not evict it. This means that the Pod will stay on the Node it +is on, and the Node it is on will not be considered for eviction. This is good because it means that the Pod will not be +terminated in the middle of a job. However, it also means that the Node the Pod is on will not be considered for +termination, which means that the Node will not be removed from the cluster, which means that the cluster will not +shrink in size when you would like it to. Since the Runner Pods terminate at the end of the job, this is not a problem for the Pods actually running jobs. However, if you have set `minReplicas > 0`, then you have some Pods that are just idling, waiting for jobs to be -assigned to them. These Pods are exactly the kind of Pods you want terminated and moved when the cluster is underutilized. -Therefore, when you set `minReplicas > 0`, you should **NOT** set `karpenter.sh/do-not-evict: "true"` on the Pod. +assigned to them. These Pods are exactly the kind of Pods you want terminated and moved when the cluster is +underutilized. Therefore, when you set `minReplicas > 0`, you should **NOT** set `karpenter.sh/do-not-evict: "true"` on +the Pod. -We have [requested a feature](https://github.com/actions/actions-runner-controller/issues/2562) -that will allow you to set `karpenter.sh/do-not-evict: "true"` and `minReplicas > 0` at the same time by only -annotating Pods running jobs. Meanwhile, another option is to set `minReplicas = 0` on a schedule using an ARC -Autoscaler [scheduled override](https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides). -At present, this component does not support that option, but it could be added in the future if our preferred -solution is not implemented. +We have [requested a feature](https://github.com/actions/actions-runner-controller/issues/2562) that will allow you to +set `karpenter.sh/do-not-evict: "true"` and `minReplicas > 0` at the same time by only annotating Pods running jobs. +Meanwhile, another option is to set `minReplicas = 0` on a schedule using an ARC Autoscaler +[scheduled override](https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides). +At present, this component does not support that option, but it could be added in the future if our preferred solution +is not implemented. ### Updating CRDs When updating the chart or application version of `actions-runner-controller`, it is possible you will need to install -new CRDs. Such a requirement should be indicated in the `actions-runner-controller` release notes and may require some adjustment to our -custom chart or configuration. +new CRDs. Such a requirement should be indicated in the `actions-runner-controller` release notes and may require some +adjustment to our custom chart or configuration. -This component uses `helm` to manage the deployment, and `helm` will not auto-update CRDs. -If new CRDs are needed, install them manually via a command like +This component uses `helm` to manage the deployment, and `helm` will not auto-update CRDs. If new CRDs are needed, +install them manually via a command like ``` kubectl create -f https://raw.githubusercontent.com/actions-runner-controller/actions-runner-controller/master/charts/actions-runner-controller/crds/actions.summerwind.dev_horizontalrunnerautoscalers.yaml ``` - ### Useful Reference -Consult [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) documentation for further details. +Consult [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) +documentation for further details. + ## Requirements @@ -497,10 +509,12 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller | [metadata\_action\_runner\_releases](#output\_metadata\_action\_runner\_releases) | Block statuses of the deployed actions-runner chart releases | | [webhook\_payload\_url](#output\_webhook\_payload\_url) | Payload URL for GitHub webhook | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) - + Cloud Posse's upstream component - [alb-controller](https://artifacthub.io/packages/helm/aws/aws-load-balancer-controller) - Helm Chart - [alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) - AWS Load Balancer Controller - [actions-runner-controller Webhook Driven Scaling](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/docs/detailed-docs.md#webhook-driven-scaling) diff --git a/modules/eks/alb-controller-ingress-class/README.md b/modules/eks/alb-controller-ingress-class/README.md index 8df9c1549..e0fa8c847 100644 --- a/modules/eks/alb-controller-ingress-class/README.md +++ b/modules/eks/alb-controller-ingress-class/README.md @@ -1,10 +1,10 @@ # Component: `eks/alb-controller-ingress-class` -This component deploys a Kubernetes `IngressClass` resource for the AWS Load Balancer Controller. -This is not often needed, as the default IngressClass deployed by the `eks/alb-controller` component -is sufficient for most use cases, and when it is not, a service can deploy its own IngressClass. -This is for the rare case where you want to deploy an additional IngressClass deploying an additional -ALB that you nevertheless want to be shared by some services, with none of them explicitly owning it. +This component deploys a Kubernetes `IngressClass` resource for the AWS Load Balancer Controller. This is not often +needed, as the default IngressClass deployed by the `eks/alb-controller` component is sufficient for most use cases, and +when it is not, a service can deploy its own IngressClass. This is for the rare case where you want to deploy an +additional IngressClass deploying an additional ALB that you nevertheless want to be shared by some services, with none +of them explicitly owning it. ## Usage @@ -21,6 +21,7 @@ components: scheme: internet-facing ``` + ## Requirements @@ -101,6 +102,7 @@ components: No outputs. + ## References diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index f9c20227e..80066889c 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -2,7 +2,8 @@ This component provisions a Kubernetes Service that creates an ALB for a specific [IngressGroup]. -An [IngressGroup] is a feature of the [alb-controller] which allows multiple Kubernetes Ingresses to share the same Application Load Balancer. +An [IngressGroup] is a feature of the [alb-controller] which allows multiple Kubernetes Ingresses to share the same +Application Load Balancer. ## Usage @@ -15,8 +16,8 @@ import: - catalog/eks/alb-controller-ingress-group ``` -The default catalog values `e.g. stacks/catalog/eks/alb-controller-ingress-group.yaml` -will create a Kubernetes Service in the `default` namespace with an [IngressGroup] named `alb-controller-ingress-group`. +The default catalog values `e.g. stacks/catalog/eks/alb-controller-ingress-group.yaml` will create a Kubernetes Service +in the `default` namespace with an [IngressGroup] named `alb-controller-ingress-group`. ```yaml components: @@ -33,6 +34,7 @@ components: name: alb-controller-ingress-group ``` + ## Requirements @@ -139,12 +141,15 @@ components: | [load\_balancer\_scheme](#output\_load\_balancer\_scheme) | The value of the `alb.ingress.kubernetes.io/scheme` annotation of the Kubernetes Ingress | | [message\_body\_length](#output\_message\_body\_length) | The length of the message body to ensure it's lower than the maximum limit | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/alb-controller-ingress-group) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/alb-controller-ingress-group) - + Cloud Posse's upstream component [](https://cpco.io/component) -[IngressGroup]: -[alb-controller]: +[ingressgroup]: + https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/#ingressgroup +[alb-controller]: https://github.com/kubernetes-sigs/aws-load-balancer-controller diff --git a/modules/eks/alb-controller/CHANGELOG.md b/modules/eks/alb-controller/CHANGELOG.md index a33945647..56d5ba294 100644 --- a/modules/eks/alb-controller/CHANGELOG.md +++ b/modules/eks/alb-controller/CHANGELOG.md @@ -2,23 +2,19 @@ ### Update IAM Policy and Change How it is Managed -The ALB controller needs a lot of permissions and has a complex IAM policy. -For this reason, the project releases a complete JSON policy document that is -updated as needed. +The ALB controller needs a lot of permissions and has a complex IAM policy. For this reason, the project releases a +complete JSON policy document that is updated as needed. In this release: -1. We have updated the policy to the one distributed with version 2.6.0 of the ALB controller. This fixes an issue - where the controller was not able to create the service-linked role for the Elastic Load Balancing service. -2. To ease maintenance, we have moved the policy document to a separate file, - `distributed-iam-policy.tf` and made it easy to update or override. - +1. We have updated the policy to the one distributed with version 2.6.0 of the ALB controller. This fixes an issue where + the controller was not able to create the service-linked role for the Elastic Load Balancing service. +2. To ease maintenance, we have moved the policy document to a separate file, `distributed-iam-policy.tf` and made it + easy to update or override. #### Gov Cloud and China Regions -Actually, the project releases 3 policy documents, one for each of the -three AWS partitions: `aws`, `aws-cn`, and `aws-us-gov`. For simplicity, -this module only uses the `aws` partition policy. If you are in another -partition, you can create a `distributed-iam-policy_override.tf` file in your -directory and override the `overridable_distributed_iam_policy` local -variable with the policy document for your partition. +Actually, the project releases 3 policy documents, one for each of the three AWS partitions: `aws`, `aws-cn`, and +`aws-us-gov`. For simplicity, this module only uses the `aws` partition policy. If you are in another partition, you can +create a `distributed-iam-policy_override.tf` file in your directory and override the +`overridable_distributed_iam_policy` local variable with the policy document for your partition. diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 3b6ca1e98..64d6f6c86 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -1,18 +1,19 @@ # Component: `eks/alb-controller` -This component creates a Helm release for [alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) on an EKS cluster. +This component creates a Helm release for +[alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) on an EKS cluster. -[alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) is a Kubernetes addon that, -in the context of AWS, provisions and manages ALBs and NLBs based on Service and Ingress annotations. -This module also can (and is recommended to) provision a default IngressClass. +[alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) is a Kubernetes addon that, in the +context of AWS, provisions and manages ALBs and NLBs based on Service and Ingress annotations. This module also can (and +is recommended to) provision a default IngressClass. ### Special note about upgrading -When upgrading the chart version, check to see if the IAM policy for the service account needs to be updated. -If it does, update the policy in the `distributed-iam-policy.tf` file. -Probably the easiest way to check if it needs updating is to simply download the policy from -https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json -and compare it to the policy in `distributed-iam-policy.tf`. +When upgrading the chart version, check to see if the IAM policy for the service account needs to be updated. If it +does, update the policy in the `distributed-iam-policy.tf` file. Probably the easiest way to check if it needs updating +is to simply download the policy from +https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json and +compare it to the policy in `distributed-iam-policy.tf`. ## Usage @@ -57,6 +58,7 @@ components: chart_values: {} ``` + ## Requirements @@ -149,6 +151,7 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References diff --git a/modules/eks/argocd/CHANGELOG.md b/modules/eks/argocd/CHANGELOG.md index df97d2e81..f88fcb32f 100644 --- a/modules/eks/argocd/CHANGELOG.md +++ b/modules/eks/argocd/CHANGELOG.md @@ -1,11 +1,11 @@ ## Components PR [#905](https://github.com/cloudposse/terraform-aws-components/pull/905) -The `notifictations.tf` file has been renamed to `notifications.tf`. Delete `notifictations.tf` after vendoring these changes. +The `notifictations.tf` file has been renamed to `notifications.tf`. Delete `notifictations.tf` after vendoring these +changes. ## Components PR [#851](https://github.com/cloudposse/terraform-aws-components/pull/851) -This is a bug fix and feature enhancement update. -There are few actions necessary to upgrade. +This is a bug fix and feature enhancement update. There are few actions necessary to upgrade. ## Upgrade actions @@ -15,6 +15,7 @@ There are few actions necessary to upgrade. 3. Remove `notifications_triggers` 4. Remove `notifications_templates` 5. Remove `notifications_notifiers` + ```diff components: terraform: @@ -46,43 +47,58 @@ There are few actions necessary to upgrade. - appID: xxxxxxx - installationID: xxxxxxx ``` -2. Move secrets from `/argocd/notifications/notifiers/service_webhook_github-commit-status/github-token` to `argocd/notifications/notifiers/common/github-token` + +2. Move secrets from `/argocd/notifications/notifiers/service_webhook_github-commit-status/github-token` to + `argocd/notifications/notifiers/common/github-token` + ```bash chamber read -q argocd/notifications/notifiers/service_webhook_github-commit-status github-token | chamber write argocd/notifications/notifiers/common github-token chamber delete argocd/notifications/notifiers/service_webhook_github-commit-status github-token ``` -3. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) with scope `admin:repo_hook` + +3. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) + with scope `admin:repo_hook` 4. Save the PAT to SSM `/argocd/github/api_key` + ```bash chamber write argocd/github api_key ${PAT} ``` + 5. Apply changes with atmos ## Features -* [Git Webhook Configuration](https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/) - makes GitHub trigger ArgoCD sync on each commit into argocd repo -* Replace [GitHub notification service](https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/services/github/) with predefined [Webhook notification service](https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/services/webhook/) -* Added predefined GitHub commit status notifications for CD sync mode: - * `on-deploy-started` - * `app-repo-github-commit-status` - * `argocd-repo-github-commit-status` - * `on-deploy-succeded` - * `app-repo-github-commit-status` - * `argocd-repo-github-commit-status` - * `on-deploy-failed` - * `app-repo-github-commit-status` - * `argocd-repo-github-commit-status` -* Support SSM secrets (`/argocd/notifications/notifiers/common/*`) common for all notification services. (Can be referenced with `$common_{secret-name}` ) + +- [Git Webhook Configuration](https://argo-cd.readthedocs.io/en/stable/operator-manual/webhook/) - makes GitHub trigger + ArgoCD sync on each commit into argocd repo +- Replace + [GitHub notification service](https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/services/github/) + with predefined + [Webhook notification service](https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/services/webhook/) +- Added predefined GitHub commit status notifications for CD sync mode: + - `on-deploy-started` + - `app-repo-github-commit-status` + - `argocd-repo-github-commit-status` + - `on-deploy-succeded` + - `app-repo-github-commit-status` + - `argocd-repo-github-commit-status` + - `on-deploy-failed` + - `app-repo-github-commit-status` + - `argocd-repo-github-commit-status` +- Support SSM secrets (`/argocd/notifications/notifiers/common/*`) common for all notification services. (Can be + referenced with `$common_{secret-name}` ) ### Bug Fixes -* ArgoCD notifications pods recreated on deployment that change notifications related configs and secrets -* Remove `metadata` output that expose helm values configs (used in debug purpose) -* Remove legacy unnecessary helm values used in old ArgoCD versions (ex. `workflow auth` configs) and dropped notifications services +- ArgoCD notifications pods recreated on deployment that change notifications related configs and secrets +- Remove `metadata` output that expose helm values configs (used in debug purpose) +- Remove legacy unnecessary helm values used in old ArgoCD versions (ex. `workflow auth` configs) and dropped + notifications services ## Breaking changes -* Removed `service_github` from `notifications_notifiers` variable structure -* Renamed `service_webhook` to `webhook` in `notifications_notifiers` variable structure +- Removed `service_github` from `notifications_notifiers` variable structure +- Renamed `service_webhook` to `webhook` in `notifications_notifiers` variable structure + ```diff variable "notifications_notifiers" { type = object({ @@ -102,7 +118,9 @@ variable "notifications_notifiers" { )) }) ``` -* Removed `github` from `notifications_templates` variable structure + +- Removed `github` from `notifications_templates` variable structure + ```diff variable "notifications_templates" { type = map(object({ diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 1c0be446a..47bc24a74 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -4,7 +4,9 @@ This component is responsible for provisioning [Argo CD](https://argoproj.github Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. -> :warning::warning::warning: ArgoCD CRDs must be installed separately from this component/helm release. :warning::warning::warning: +> :warning::warning::warning: ArgoCD CRDs must be installed separately from this component/helm release. +> :warning::warning::warning: + ```shell kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=" @@ -16,11 +18,10 @@ kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=v2.4.9" ### Preparing AppProject repos: -First, make sure you have a GitHub repo ready to go. We have a component for this -called the `argocd-repo` component. It will create a GitHub repo and adds -some secrets and code owners. Most importantly, it configures an `applicationset.yaml` -that includes all the details for helm to create ArgoCD CRDs. These CRDs -let ArgoCD know how to fulfill changes to its repo. +First, make sure you have a GitHub repo ready to go. We have a component for this called the `argocd-repo` component. It +will create a GitHub repo and adds some secrets and code owners. Most importantly, it configures an +`applicationset.yaml` that includes all the details for helm to create ArgoCD CRDs. These CRDs let ArgoCD know how to +fulfill changes to its repo. ```yaml components: @@ -34,9 +35,9 @@ components: github_user_email: infra@acme.com github_organization: ACME github_codeowner_teams: - - "@ACME/acme-admins" - - "@ACME/CloudPosse" - - "@ACME/developers" + - "@ACME/acme-admins" + - "@ACME/CloudPosse" + - "@ACME/developers" gitignore_entries: - "**/.DS_Store" - ".DS_Store" @@ -54,11 +55,10 @@ components: ``` ### Injecting infrastructure details into applications -Second, your application repos could use values to best configure their -helm releases. We have an `eks/platform` component for exposing various -infra outputs. It takes remote state lookups and stores them into SSM. -We demonstrate how to pull the platform SSM parameters later. Here's an -example `eks/platform` config: + +Second, your application repos could use values to best configure their helm releases. We have an `eks/platform` +component for exposing various infra outputs. It takes remote state lookups and stores them into SSM. We demonstrate how +to pull the platform SSM parameters later. Here's an example `eks/platform` config: ```yaml components: @@ -127,13 +127,13 @@ components: certificate_authority_enabled: false ``` -In the previous sample we create platform settings for a `dev` platform and a -`qa2` platform. Understand that these are arbitrary titles that are used to separate -the SSM parameters so that if, say, a particular hostname is needed, we can safely -select the right hostname using a moniker such as `qa2`. These otherwise are meaningless -and do not need to align with any particular stage or tenant. +In the previous sample we create platform settings for a `dev` platform and a `qa2` platform. Understand that these are +arbitrary titles that are used to separate the SSM parameters so that if, say, a particular hostname is needed, we can +safely select the right hostname using a moniker such as `qa2`. These otherwise are meaningless and do not need to align +with any particular stage or tenant. ### ArgoCD on SAML / AWS Identity Center (formerly aws-sso) + Here's an example snippet for how to use this component: ```yaml @@ -191,50 +191,48 @@ components: groupsAttr: groups ``` -Note, if you set up `sso-saml-provider`, you will need to restart DEX on your EKS cluster -manually: +Note, if you set up `sso-saml-provider`, you will need to restart DEX on your EKS cluster manually: + ```bash kubectl delete pod -n argocd ``` -The configuration above will work for AWS Identity Center if you have -the following attributes in a +The configuration above will work for AWS Identity Center if you have the following attributes in a [Custom SAML 2.0 application](https://docs.aws.amazon.com/singlesignon/latest/userguide/samlapps.html): | attribute name | value | type | -|:---------------|:----------------|:------------| +| :------------- | :-------------- | :---------- | | Subject | ${user:subject} | persistent | | email | ${user:email} | unspecified | | groups | ${user:groups} | unspecified | -You will also need to assign AWS Identity Center groups to your Custom SAML 2.0 -application. Make a note of each group and replace the IDs in the `argocd_rbac_groups` -var accordingly. +You will also need to assign AWS Identity Center groups to your Custom SAML 2.0 application. Make a note of each group +and replace the IDs in the `argocd_rbac_groups` var accordingly. ### Google Workspace OIDC To use Google OIDC: ```yaml - oidc_enabled: true - saml_enabled: false - oidc_providers: - google: - uses_dex: true - type: google - id: google - name: Google - serviceAccountAccess: - enabled: true - key: googleAuth.json - value: /sso/oidc/google/serviceaccount - admin_email: an_actual_user@acme.com - config: - # This filters emails when signing in with Google to only this domain. helpful for picking the right one. - hostedDomains: - - acme.com - clientID: /sso/saml/google/clientid - clientSecret: /sso/saml/google/clientsecret +oidc_enabled: true +saml_enabled: false +oidc_providers: + google: + uses_dex: true + type: google + id: google + name: Google + serviceAccountAccess: + enabled: true + key: googleAuth.json + value: /sso/oidc/google/serviceaccount + admin_email: an_actual_user@acme.com + config: + # This filters emails when signing in with Google to only this domain. helpful for picking the right one. + hostedDomains: + - acme.com + clientID: /sso/saml/google/clientid + clientSecret: /sso/saml/google/clientsecret ``` ### Working with ArgoCD and GitHub @@ -289,8 +287,8 @@ jobs: ``` In the above example, we make a few assumptions: -- You've already made the app in ArgoCD by creating a YAML file - in your non-prod ArgoCD repo at the path + +- You've already made the app in ArgoCD by creating a YAML file in your non-prod ArgoCD repo at the path `plat/use2-dev/apps/my-preview-acme-app/config.yaml` with contents: ```yaml @@ -303,20 +301,19 @@ manifests: plat/use2-dev/apps/my-preview-acme-app/manifests ``` - you have set up `ecr` with permissions for github to push docker images to it -- you already have your `ApplicationSet` and `AppProject` crd's in - `plat/use2-dev/argocd/applicationset.yaml`, which should be generated by our `argocd-repo` - component. -- your app has a [helmfile template](https://helmfile.readthedocs.io/en/latest/#templating) - in `deploy/app/release.yaml` -- that helmfile template can accept both the `eks/platform` config which is pulled from - ssm at the path configured in `eks/platform/defaults` +- you already have your `ApplicationSet` and `AppProject` crd's in `plat/use2-dev/argocd/applicationset.yaml`, which + should be generated by our `argocd-repo` component. +- your app has a [helmfile template](https://helmfile.readthedocs.io/en/latest/#templating) in `deploy/app/release.yaml` +- that helmfile template can accept both the `eks/platform` config which is pulled from ssm at the path configured in + `eks/platform/defaults` - the helmfile template can update container resources using the output of `docker image inspect` ### Notifications Here's a configuration for letting argocd send notifications back to GitHub: -1. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) with scope `repo:status` +1. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) + with scope `repo:status` 2. Save the PAT to SSM `/argocd/notifications/notifiers/common/github-token` 3. Use this atmos stack configuration @@ -334,7 +331,8 @@ components: Here's a configuration Github notify ArgoCD on commit: -1. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) with scope `admin:repo_hook` +1. [Create GitHub PAT](https://docs.github.com/en/enterprise-server@3.6/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token) + with scope `admin:repo_hook` 2. Save the PAT to SSM `/argocd/github/api_key` 3. Use this atmos stack configuration @@ -350,7 +348,9 @@ components: #### Creating Webhooks with `github-webhook` -If you are creating webhooks for ArgoCD deployment repos in multiple GitHub Organizations, you cannot use the same Terraform GitHub provider. Instead, we can use Atmos to deploy multiple component. To do this, disable the webhook creation in this component and deploy the webhook with the `github-webhook` component as such: +If you are creating webhooks for ArgoCD deployment repos in multiple GitHub Organizations, you cannot use the same +Terraform GitHub provider. Instead, we can use Atmos to deploy multiple component. To do this, disable the webhook +creation in this component and deploy the webhook with the `github-webhook` component as such: ```yaml components: @@ -396,37 +396,43 @@ components: ArgoCD supports Slack notifications on application deployments. -1. In order to enable Slack notifications, first create a Slack Application following the [ArgoCD documentation](https://argocd-notifications.readthedocs.io/en/stable/services/slack/). +1. In order to enable Slack notifications, first create a Slack Application following the + [ArgoCD documentation](https://argocd-notifications.readthedocs.io/en/stable/services/slack/). 1. Create an OAuth token for the new Slack App -1. Save the OAuth token to AWS SSM Parameter Store in the same account and region as Github tokens. For example, `core-use2-auto` +1. Save the OAuth token to AWS SSM Parameter Store in the same account and region as Github tokens. For example, + `core-use2-auto` 1. Add the app to the chosen Slack channel. _If not added, notifications will not work_ -1. For this component, enable Slack integrations for each Application with `var.slack_notifications_enabled` and `var.slack_notifications`: +1. For this component, enable Slack integrations for each Application with `var.slack_notifications_enabled` and + `var.slack_notifications`: ```yaml - slack_notifications_enabled: true - slack_notifications: - channel: argocd-updates +slack_notifications_enabled: true +slack_notifications: + channel: argocd-updates ``` -6. In the `argocd-repo` component, set `var.slack_notifications_channel` to the name of the Slack notification channel to add the relevant ApplicationSet annotations +6. In the `argocd-repo` component, set `var.slack_notifications_channel` to the name of the Slack notification channel + to add the relevant ApplicationSet annotations ## Troubleshooting ## Login to ArgoCD admin UI -For ArgoCD v1.9 and later, the initial admin password is available from a Kubernetes secret named `argocd-initial-admin-secret`. -To get the initial password, execute the following command: +For ArgoCD v1.9 and later, the initial admin password is available from a Kubernetes secret named +`argocd-initial-admin-secret`. To get the initial password, execute the following command: ```shell kubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 --decode ``` -Then open the ArgoCD admin UI and use the username `admin` and the password obtained in the previous step to log in to the ArgoCD admin. +Then open the ArgoCD admin UI and use the username `admin` and the password obtained in the previous step to log in to +the ArgoCD admin. ## Error "server.secretkey is missing" -If you provision a new version of the `eks/argocd` component, and some Helm Chart values get updated, you might encounter the error -"server.secretkey is missing" in the ArgoCD admin UI. To fix the error, execute the following commands: +If you provision a new version of the `eks/argocd` component, and some Helm Chart values get updated, you might +encounter the error "server.secretkey is missing" in the ArgoCD admin UI. To fix the error, execute the following +commands: ```shell # Download `kubeconfig` and set EKS cluster @@ -438,8 +444,10 @@ kubectl rollout restart deploy/argocd-server -n argocd # Get the new admin password from the Kubernetes secret kubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 --decode ``` + Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-secretkey-is-missing + ## Requirements @@ -592,6 +600,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec |------|-------------| | [github\_webhook\_value](#output\_github\_webhook\_value) | The value of the GitHub webhook secret used for ArgoCD | + ## References diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 59155bbf8..15a838604 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -1,7 +1,11 @@ # Component: `aws-node-termination-handler` -This component creates a Helm release for [aws-node-termination-handler](https://github.com/aws/aws-node-termination-handler) on a Kubernetes cluster. [aws-node-termination-handler](https://github.com/aws/aws-node-termination-handler) is a Kubernetes addon that (by default) monitors the EC2 IMDS endpoint for scheduled maintenance events, spot instance termination events, and rebalance recommendation events, and drains and/or cordons nodes upon such events. -This ensures that workloads on Kubernetes are evicted gracefully when a node needs to be terminated. +This component creates a Helm release for +[aws-node-termination-handler](https://github.com/aws/aws-node-termination-handler) on a Kubernetes cluster. +[aws-node-termination-handler](https://github.com/aws/aws-node-termination-handler) is a Kubernetes addon that (by +default) monitors the EC2 IMDS endpoint for scheduled maintenance events, spot instance termination events, and +rebalance recommendation events, and drains and/or cordons nodes upon such events. This ensures that workloads on +Kubernetes are evicted gracefully when a node needs to be terminated. ## Usage @@ -38,6 +42,7 @@ components: chart_values: {} ``` + ## Requirements @@ -126,6 +131,7 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index 354008796..d7302cc6b 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -1,6 +1,8 @@ # Component: `eks/cert-manager` -This component creates a Helm release for [cert-manager](https://github.com/jetstack/cert-manager) on a Kubernetes cluster. [cert-manager](https://github.com/jetstack/cert-manager) is a Kubernetes addon that provisions X.509 certificates. +This component creates a Helm release for [cert-manager](https://github.com/jetstack/cert-manager) on a Kubernetes +cluster. [cert-manager](https://github.com/jetstack/cert-manager) is a Kubernetes addon that provisions X.509 +certificates. ## Usage @@ -17,38 +19,39 @@ import: The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` ```yaml - enabled: true - name: cert-manager - kubernetes_namespace: cert-manager - # `helm_manifest_experiment_enabled` does not work with cert-manager or any Helm chart that uses CRDs - helm_manifest_experiment_enabled: false - # Use the cert-manager as a private CA (Certificate Authority) - # to issue certificates for use within the Kubernetes cluster. - # Something like this is required for the ALB Ingress Controller. - cert_manager_issuer_selfsigned_enabled: true - # Use Let's Encrypt to issue certificates for use outside the Kubernetes cluster, - # ones that will be trusted by browsers. - # These do not (yet) work with the ALB Ingress Controller, - # which require ACM certificates, so we have no use for them. - letsencrypt_enabled: true - # cert_manager_issuer_support_email_template is only used if letsencrypt_enabled is true. - # If it were true, we would want to set it at the organization level. - cert_manager_issuer_support_email_template: "aws+%s@acme.com" - cert_manager_repository: https://charts.jetstack.io - cert_manager_chart: cert-manager - cert_manager_chart_version: v1.5.4 - - # use a local chart to provision Certificate Issuers - cert_manager_issuer_chart: ./cert-manager-issuer/ - cert_manager_resources: - limits: - cpu: 200m - memory: 256Mi - requests: - cpu: 100m - memory: 128Mi +enabled: true +name: cert-manager +kubernetes_namespace: cert-manager +# `helm_manifest_experiment_enabled` does not work with cert-manager or any Helm chart that uses CRDs +helm_manifest_experiment_enabled: false +# Use the cert-manager as a private CA (Certificate Authority) +# to issue certificates for use within the Kubernetes cluster. +# Something like this is required for the ALB Ingress Controller. +cert_manager_issuer_selfsigned_enabled: true +# Use Let's Encrypt to issue certificates for use outside the Kubernetes cluster, +# ones that will be trusted by browsers. +# These do not (yet) work with the ALB Ingress Controller, +# which require ACM certificates, so we have no use for them. +letsencrypt_enabled: true +# cert_manager_issuer_support_email_template is only used if letsencrypt_enabled is true. +# If it were true, we would want to set it at the organization level. +cert_manager_issuer_support_email_template: "aws+%s@acme.com" +cert_manager_repository: https://charts.jetstack.io +cert_manager_chart: cert-manager +cert_manager_chart_version: v1.5.4 + +# use a local chart to provision Certificate Issuers +cert_manager_issuer_chart: ./cert-manager-issuer/ +cert_manager_resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi ``` + ## Requirements @@ -148,9 +151,10 @@ The default catalog values `e.g. stacks/catalog/eks/cert-manager.yaml` | [cert\_manager\_issuer\_metadata](#output\_cert\_manager\_issuer\_metadata) | Block status of the deployed release | | [cert\_manager\_metadata](#output\_cert\_manager\_metadata) | Block status of the deployed release | + ## References -* [cert-manager](https://github.com/jetstack/cert-manager) +- [cert-manager](https://github.com/jetstack/cert-manager) [](https://cpco.io/component) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 50beb2380..cafcdb448 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -2,36 +2,31 @@ Bug fix and updates to Changelog, no action required. -Fixed: Error about managed node group ARNs list being null, which could happen -when adding a managed node group to an existing cluster that never had one. +Fixed: Error about managed node group ARNs list being null, which could happen when adding a managed node group to an +existing cluster that never had one. ## Upgrading to `v1.303.0` Components PR [#852](https://github.com/cloudposse/terraform-aws-components/pull/852) -This is a bug fix and feature enhancement update. No action is necessary to upgrade. -However, with the new features and new recommendations, you may want to change -your configuration. +This is a bug fix and feature enhancement update. No action is necessary to upgrade. However, with the new features and +new recommendations, you may want to change your configuration. ## Recommended (optional) changes -Previously, we recommended deploying Karpenter to Fargate and not provisioning -any nodes. However, this causes issues with add-ons that require compute power -to fully initialize, such as `coredns`, and it can reduce the cluster to a -single node, removing the high availability that comes from having a node -per Availability Zone and replicas of pods spread across those nodes. +Previously, we recommended deploying Karpenter to Fargate and not provisioning any nodes. However, this causes issues +with add-ons that require compute power to fully initialize, such as `coredns`, and it can reduce the cluster to a +single node, removing the high availability that comes from having a node per Availability Zone and replicas of pods +spread across those nodes. -As a result, we now recommend deploying a minimal node group with a single -instance (currently recommended to be a `c6a.large`) in each of 3 Availability -Zones. This will provide the compute power needed to initialize add-ons, and -will provide high availability for the cluster. As a bonus, it will also -remove the need to deploy Karpenter to Fargate. +As a result, we now recommend deploying a minimal node group with a single instance (currently recommended to be a +`c6a.large`) in each of 3 Availability Zones. This will provide the compute power needed to initialize add-ons, and will +provide high availability for the cluster. As a bonus, it will also remove the need to deploy Karpenter to Fargate. -**NOTE about instance type**: The `c6a.large` instance type is relatively -new. If you have deployed an old version of our ServiceControlPolicy -`DenyEC2NonNitroInstances`, `DenyNonNitroInstances` (obsolete, replaced by -`DenyEC2NonNitroInstances`), and/or `DenyEC2InstancesWithoutEncryptionInTransit`, -you will want to update them to v0.12.0 or choose a difference instance type. +**NOTE about instance type**: The `c6a.large` instance type is relatively new. If you have deployed an old version of +our ServiceControlPolicy `DenyEC2NonNitroInstances`, `DenyNonNitroInstances` (obsolete, replaced by +`DenyEC2NonNitroInstances`), and/or `DenyEC2InstancesWithoutEncryptionInTransit`, you will want to update them to +v0.12.0 or choose a difference instance type. ### Migration procedure @@ -41,88 +36,81 @@ To perform the recommended migration, follow these steps: Change your `eks/cluster` configuration to set `deploy_addons_to_fargate: false`. -Add the following to your `eks/cluster` configuration, but -copy the block device name, volume size, and volume type from your existing -Karpenter provisioner configuration. Also select the correct `ami_type` -according to the `ami_family` in your Karpenter provisioner configuration. +Add the following to your `eks/cluster` configuration, but copy the block device name, volume size, and volume type from +your existing Karpenter provisioner configuration. Also select the correct `ami_type` according to the `ami_family` in +your Karpenter provisioner configuration. ```yaml - node_groups: - # will create 1 node group for each item in map - # Provision a minimal static node group for add-ons and redundant replicas - main: - # EKS AMI version to use, e.g. "1.16.13-20200821" (no "v"). - ami_release_version: null - # Type of Amazon Machine Image (AMI) associated with the EKS Node Group - # Typically AL2_x86_64 or BOTTLEROCKET_x86_64 - ami_type: BOTTLEROCKET_x86_64 - # Additional name attributes (e.g. `1`) for the node group - attributes: [] - # will create 1 auto scaling group in each specified availability zone - # or all AZs with subnets if none are specified anywhere - availability_zones: null - # Whether to enable Node Group to scale its AutoScaling Group - cluster_autoscaler_enabled: false - # True (recommended) to create new node_groups before deleting old ones, avoiding a temporary outage - create_before_destroy: true - # Configure storage for the root block device for instances in the Auto Scaling Group - # For Bottlerocket, use /dev/xvdb. For all others, use /dev/xvda. - block_device_map: - "/dev/xvdb": - ebs: - volume_size: 125 # in GiB - volume_type: gp3 - encrypted: true - delete_on_termination: true - # Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided. - instance_types: - - c6a.large - # Desired number of worker nodes when initially provisioned - desired_group_size: 3 - max_group_size: 3 - min_group_size: 3 - resources_to_tag: - - instance - - volume - tags: null +node_groups: + # will create 1 node group for each item in map + # Provision a minimal static node group for add-ons and redundant replicas + main: + # EKS AMI version to use, e.g. "1.16.13-20200821" (no "v"). + ami_release_version: null + # Type of Amazon Machine Image (AMI) associated with the EKS Node Group + # Typically AL2_x86_64 or BOTTLEROCKET_x86_64 + ami_type: BOTTLEROCKET_x86_64 + # Additional name attributes (e.g. `1`) for the node group + attributes: [] + # will create 1 auto scaling group in each specified availability zone + # or all AZs with subnets if none are specified anywhere + availability_zones: null + # Whether to enable Node Group to scale its AutoScaling Group + cluster_autoscaler_enabled: false + # True (recommended) to create new node_groups before deleting old ones, avoiding a temporary outage + create_before_destroy: true + # Configure storage for the root block device for instances in the Auto Scaling Group + # For Bottlerocket, use /dev/xvdb. For all others, use /dev/xvda. + block_device_map: + "/dev/xvdb": + ebs: + volume_size: 125 # in GiB + volume_type: gp3 + encrypted: true + delete_on_termination: true + # Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided. + instance_types: + - c6a.large + # Desired number of worker nodes when initially provisioned + desired_group_size: 3 + max_group_size: 3 + min_group_size: 3 + resources_to_tag: + - instance + - volume + tags: null ``` -You do not need to apply the above changes yet, although you can if you -want to. To reduce overhead, you can apply the changes in the next step. +You do not need to apply the above changes yet, although you can if you want to. To reduce overhead, you can apply the +changes in the next step. #### 2. Move Karpenter to the node group, remove legacy support -Delete the `fargate_profiles` section from your `eks/cluster` configuration, -or at least remove the `karpenter` profile from it. Disable legacy support -by adding: +Delete the `fargate_profiles` section from your `eks/cluster` configuration, or at least remove the `karpenter` profile +from it. Disable legacy support by adding: ```yaml - legacy_fargate_1_role_per_profile_enabled: false +legacy_fargate_1_role_per_profile_enabled: false ``` #### 2.a Optional: Move Karpenter instance profile to `eks/cluster` component -If you have the patience to manually import and remove a Terraform -resource, you should move the Karpenter instance profile to the `eks/cluster` -component. This fixes an issue where the Karpenter instance profile -could be broken by certain sequences of Terraform operations. -However, if you have multiple clusters to migrate, this can be tedious, -and the issue is not a serious one, so you may want to skip this step. +If you have the patience to manually import and remove a Terraform resource, you should move the Karpenter instance +profile to the `eks/cluster` component. This fixes an issue where the Karpenter instance profile could be broken by +certain sequences of Terraform operations. However, if you have multiple clusters to migrate, this can be tedious, and +the issue is not a serious one, so you may want to skip this step. To do this, add the following to your `eks/cluster` configuration: ```yaml - legacy_do_not_create_karpenter_instance_profile: false +legacy_do_not_create_karpenter_instance_profile: false ``` - -**BEFORE APPLYING CHANGES**: -Run `atmos terraform plan` (with the appropriate arguments) to see the changes -that will be made. Among the resources to be created will be -`aws_iam_instance_profile.default[0]`. Using the same arguments as before, run -`atmos`, but replace `plan` with `import 'aws_iam_instance_profile.default[0]' `, -where `` is the name of the profile the plan indicated it would create. -It will be something like `-karpenter`. +**BEFORE APPLYING CHANGES**: Run `atmos terraform plan` (with the appropriate arguments) to see the changes that will be +made. Among the resources to be created will be `aws_iam_instance_profile.default[0]`. Using the same arguments as +before, run `atmos`, but replace `plan` with `import 'aws_iam_instance_profile.default[0]' `, where +`` is the name of the profile the plan indicated it would create. It will be something like +`-karpenter`. **NOTE**: If you perform this step, you must also perform 3.a below. @@ -132,27 +120,24 @@ Apply the changes with `atmos terraform apply`. #### 3. Upgrade Karpenter -Upgrade the `eks/karpenter` component to the latest version. Follow the upgrade -instructions to enable the new `karpenter-crd` chart by setting `crd_chart_enabled: true`. +Upgrade the `eks/karpenter` component to the latest version. Follow the upgrade instructions to enable the new +`karpenter-crd` chart by setting `crd_chart_enabled: true`. -Upgrade to at least Karpenter v0.30.0, which is the first version to support -factoring in the existing node group when determining the number of nodes to -provision. This will prevent Karpenter from provisioning nodes when they are not -needed because the existing node group already has enough capacity. Be -careful about upgrading to v0.32.0 or later, as that version introduces -significant breaking changes. We recommend updating to v0.31.2 or later -versions of v0.31.x, but not v0.32.0 or later, as a first step. This -provides a safe (revertible) upgrade path to v0.32.0 or later. +Upgrade to at least Karpenter v0.30.0, which is the first version to support factoring in the existing node group when +determining the number of nodes to provision. This will prevent Karpenter from provisioning nodes when they are not +needed because the existing node group already has enough capacity. Be careful about upgrading to v0.32.0 or later, as +that version introduces significant breaking changes. We recommend updating to v0.31.2 or later versions of v0.31.x, but +not v0.32.0 or later, as a first step. This provides a safe (revertible) upgrade path to v0.32.0 or later. #### 3.a Finish Move of Karpenter instance profile to `eks/cluster` component -If you performed step 2.a above, you must also perform this step. If you did -not perform step 2.a, you must NOT perform this step. +If you performed step 2.a above, you must also perform this step. If you did not perform step 2.a, you must NOT perform +this step. In the `eks/karpenter` stack, set `legacy_create_karpenter_instance_profile: false`. -**BEFORE APPLYING CHANGES**: Remove the Karpenter instance profile from the Terraform state, since -it is now managed by the `eks/cluster` component, or else Terraform will delete it. +**BEFORE APPLYING CHANGES**: Remove the Karpenter instance profile from the Terraform state, since it is now managed by +the `eks/cluster` component, or else Terraform will delete it. ```shell atmos terraform state eks/karpenter rm 'aws_iam_instance_profile.default[0]' -s= @@ -169,21 +154,19 @@ This is a bug fix and feature enhancement update. No action is necessary to upgr ### Bug Fixes - Timeouts for Add-Ons are now honored (they were being ignored) -- If you supply a service account role ARN for an Add-On, it will be used, and - no new role will be created. Previously it was used, but the component created - a new role anyway. -- The EKS EFS controller add-on cannot be deployed to Fargate, and enabling it - along with `deploy_addons_to_fargate` will no longer attempt to deploy EFS - to Fargate. Note that this means to use the EFS Add-On, you must create - a managed node group. Track the status of this feature with [this issue](https://github.com/kubernetes-sigs/aws-efs-csi-driver/issues/1100). -- If you are using an old VPC component that does not supply `az_private_subnets_map`, - this module will now use the older the `private_subnet_ids` output. +- If you supply a service account role ARN for an Add-On, it will be used, and no new role will be created. Previously + it was used, but the component created a new role anyway. +- The EKS EFS controller add-on cannot be deployed to Fargate, and enabling it along with `deploy_addons_to_fargate` + will no longer attempt to deploy EFS to Fargate. Note that this means to use the EFS Add-On, you must create a managed + node group. Track the status of this feature with + [this issue](https://github.com/kubernetes-sigs/aws-efs-csi-driver/issues/1100). +- If you are using an old VPC component that does not supply `az_private_subnets_map`, this module will now use the + older the `private_subnet_ids` output. ### Add-Ons have `enabled` option -The EKS Add-Ons now have an optional "enabled" flag (defaults to `true`) so -that you can selectively disable them in a stack where the inherited configuration -has them enabled. +The EKS Add-Ons now have an optional "enabled" flag (defaults to `true`) so that you can selectively disable them in a +stack where the inherited configuration has them enabled. ## Upgrading to `v1.270.0` @@ -191,89 +174,80 @@ Components PR [#795](https://github.com/cloudposse/terraform-aws-components/pull ### Removed `identity` roles from cluster RBAC (`aws-auth` ConfigMap) -Previously, this module added `identity` roles configured by the `aws_teams_rbac` -input to the `aws-auth` ConfigMap. This never worked, and so now `aws_teams_rbac` -is ignored. When upgrading, you may see these roles being removed from the `aws-auth`: -this is expected and harmless. +Previously, this module added `identity` roles configured by the `aws_teams_rbac` input to the `aws-auth` ConfigMap. +This never worked, and so now `aws_teams_rbac` is ignored. When upgrading, you may see these roles being removed from +the `aws-auth`: this is expected and harmless. ### Better support for Manged Node Group Block Device Specifications -Previously, this module only supported specifying the disk size and encryption state -for the root volume of Managed Node Groups. Now, the full set of block device -specifications is supported, including the ability to specify the device name. -This is particularly important when using BottleRocket, which uses a very small -root volume for storing the OS and configuration, and exposes a second volume -(`/dev/xvdb`) for storing data. +Previously, this module only supported specifying the disk size and encryption state for the root volume of Managed Node +Groups. Now, the full set of block device specifications is supported, including the ability to specify the device name. +This is particularly important when using BottleRocket, which uses a very small root volume for storing the OS and +configuration, and exposes a second volume (`/dev/xvdb`) for storing data. #### Block Device Migration -Almost all of the attributes of `node_groups` and `node_group_defaults` are now -optional. This means you can remove from your configuration any attributes that -previously you were setting to `null`. +Almost all of the attributes of `node_groups` and `node_group_defaults` are now optional. This means you can remove from +your configuration any attributes that previously you were setting to `null`. -The `disk_size` and `disk_encryption_enabled` attributes are deprecated. They -only apply to `/dev/xvda`, and only provision a `gp2` volume. In order to -provide backwards compatibility, they are still supported, and, when specified, +The `disk_size` and `disk_encryption_enabled` attributes are deprecated. They only apply to `/dev/xvda`, and only +provision a `gp2` volume. In order to provide backwards compatibility, they are still supported, and, when specified, cause the new `block_device_map` attribute to be ignored. -The new `block_device_map` attribute is a map of objects. The keys are the names -of block devices, and the values are objects with the attributes from the Terraform -[launch_template.block-devices](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template#block-devices) resource. +The new `block_device_map` attribute is a map of objects. The keys are the names of block devices, and the values are +objects with the attributes from the Terraform +[launch_template.block-devices](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template#block-devices) +resource. -Note that the new default, when none of `block_device_map`, `disk_size`, or -`disk_encryption_enabled` are specified, is to provision a 20GB `gp3` volume -for `/dev/xvda`, with encryption enabled. This is a change from the previous -default, which provisioned a `gp2` volume instead. +Note that the new default, when none of `block_device_map`, `disk_size`, or `disk_encryption_enabled` are specified, is +to provision a 20GB `gp3` volume for `/dev/xvda`, with encryption enabled. This is a change from the previous default, +which provisioned a `gp2` volume instead. ### Support for EFS add-on -This module now supports the EFS CSI driver add-on, in very much the same way -as it supports the EBS CSI driver add-on. The only difference is that the -EFS CSI driver add-on requires that you first provision an EFS file system. +This module now supports the EFS CSI driver add-on, in very much the same way as it supports the EBS CSI driver add-on. +The only difference is that the EFS CSI driver add-on requires that you first provision an EFS file system. #### Migration from `eks/efs-controller` to EFS CSI Driver Add-On -If you are currently using the `eks/efs-controller` module, you can migrate -to the EFS CSI Driver Add-On by following these steps: +If you are currently using the `eks/efs-controller` module, you can migrate to the EFS CSI Driver Add-On by following +these steps: 1. Remove or scale to zero Pods any Deployments using the EFS file system. -2. Remove (`terraform destroy`) the `eks/efs-controller` module from your - cluster. This will also remove the `efs-sc` StorageClass. -3. Use the [eks/storage-class](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/storage-class) - module to create a replacement EFS StorageClass `efs-sc`. This component is new and you may need to add it to your cluster. +2. Remove (`terraform destroy`) the `eks/efs-controller` module from your cluster. This will also remove the `efs-sc` + StorageClass. +3. Use the + [eks/storage-class](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/storage-class) + module to create a replacement EFS StorageClass `efs-sc`. This component is new and you may need to add it to your + cluster. 4. Deploy the EFS CSI Driver Add-On by adding `aws-efs-csi-driver` to the `addons` map (see [README](./README.md)). 5. Restore the Deployments you modified in step 1. ### More options for specifying Availability Zones -Previously, this module required you to specify the Availability Zones for the -cluster in one of two ways: +Previously, this module required you to specify the Availability Zones for the cluster in one of two ways: 1. Explicitly, by providing the full AZ names via the `availability_zones` input 2. Implicitly, via private subnets in the VPC Option 2 is still usually the best way, but now you have additional options: -- You can specify the Availability Zones via the `availability_zones` input - without specifying the full AZ names. You can just specify the suffixes of - the AZ names, and the module will find the full names for you, using the - current region. This is useful for using the same configuration in multiple regions. -- You can specify Availability Zone IDs via the `availability_zone_ids` input. - This is useful to ensure that clusters in different accounts are nevertheless - deployed to the same Availability Zones. As with the `availability_zones` input, - you can specify the suffixes of the AZ IDs, and the module will find the full - IDs for you, using the current region. +- You can specify the Availability Zones via the `availability_zones` input without specifying the full AZ names. You + can just specify the suffixes of the AZ names, and the module will find the full names for you, using the current + region. This is useful for using the same configuration in multiple regions. +- You can specify Availability Zone IDs via the `availability_zone_ids` input. This is useful to ensure that clusters in + different accounts are nevertheless deployed to the same Availability Zones. As with the `availability_zones` input, + you can specify the suffixes of the AZ IDs, and the module will find the full IDs for you, using the current region. ### Support for Karpenter Instance Profile -Previously, this module created an IAM Role for instances launched by Karpenter, -but did not create the corresponding Instance Profile, which was instead created by -the `eks/karpenter` component. This can cause problems if you delete and recreate the cluster, -so for new clusters, this module can now create the Instance Profile as well. +Previously, this module created an IAM Role for instances launched by Karpenter, but did not create the corresponding +Instance Profile, which was instead created by the `eks/karpenter` component. This can cause problems if you delete and +recreate the cluster, so for new clusters, this module can now create the Instance Profile as well. -Because this is disruptive to existing clusters, this is not enabled by default. -To enable it, set the `legacy_do_not_create_karpenter_instance_profile` input to `false`, -and also set the `eks/karpenter` input `legacy_create_karpenter_instance_profile` to `false`. +Because this is disruptive to existing clusters, this is not enabled by default. To enable it, set the +`legacy_do_not_create_karpenter_instance_profile` input to `false`, and also set the `eks/karpenter` input +`legacy_create_karpenter_instance_profile` to `false`. ## Upgrading to `v1.250.0` @@ -285,57 +259,44 @@ This has improved support for EKS Add-Ons. ##### Configuration and Timeouts -The `addons` input now accepts a `configuration_values` input to allow you -to configure the add-ons, and various timeout inputs to allow you to fine-tune -the timeouts for the add-ons. +The `addons` input now accepts a `configuration_values` input to allow you to configure the add-ons, and various timeout +inputs to allow you to fine-tune the timeouts for the add-ons. ##### Automatic IAM Role Creation -If you enable `aws-ebs-csi-driver` or `vpc-cni` add-ons, the module will -automatically create the required Service Account IAM Role and attach it to -the add-on. +If you enable `aws-ebs-csi-driver` or `vpc-cni` add-ons, the module will automatically create the required Service +Account IAM Role and attach it to the add-on. ##### Add-Ons can be deployed to Fargate -If you are using Karpenter and not provisioning any nodes with this module, -the `coredns` and `aws-ebs-csi-driver` add-ons can be deployed to Fargate. -(They must be able to run somewhere in the cluster or else the deployment -will fail.) +If you are using Karpenter and not provisioning any nodes with this module, the `coredns` and `aws-ebs-csi-driver` +add-ons can be deployed to Fargate. (They must be able to run somewhere in the cluster or else the deployment will +fail.) -To cause the add-ons to be deployed to Fargate, set the `deploy_addons_to_fargate` -input to `true`. +To cause the add-ons to be deployed to Fargate, set the `deploy_addons_to_fargate` input to `true`. -**Note about CoreDNS**: If you want to deploy CoreDNS to Fargate, as of this -writing you must set the `configuration_values` input for CoreDNS to -`'{"computeType": "Fargate"}'`. If you want to deploy CoreDNS to EC2 instances, -you must NOT include the `computeType` configuration value. +**Note about CoreDNS**: If you want to deploy CoreDNS to Fargate, as of this writing you must set the +`configuration_values` input for CoreDNS to `'{"computeType": "Fargate"}'`. If you want to deploy CoreDNS to EC2 +instances, you must NOT include the `computeType` configuration value. ### Availability Zones implied by Private Subnets -You can now avoid specifying Availability Zones for the cluster anywhere. -If all of the possible Availability Zones inputs are empty, the module will -use the Availability Zones implied by the private subnets. That is, it will -deploy the cluster to all of the Availability Zones in which the VPC has -private subnets. +You can now avoid specifying Availability Zones for the cluster anywhere. If all of the possible Availability Zones +inputs are empty, the module will use the Availability Zones implied by the private subnets. That is, it will deploy the +cluster to all of the Availability Zones in which the VPC has private subnets. ### Optional support for 1 Fargate Pod Execution Role per Cluster -Previously, this module created a separate Fargate Pod Execution Role for each -Fargate Profile it created. This is unnecessary, excessive, and can cause -problems due to name collisions, but is otherwise merely inefficient, so it is -not important to fix this on existiong, working clusters. -This update brings a feature that causes the module to create at +Previously, this module created a separate Fargate Pod Execution Role for each Fargate Profile it created. This is +unnecessary, excessive, and can cause problems due to name collisions, but is otherwise merely inefficient, so it is not +important to fix this on existiong, working clusters. This update brings a feature that causes the module to create at most 1 Fargate Pod Execution Role per cluster. -**This change is recommended for all NEW clusters, but only NEW clusters**. -Because it is a breaking change, it is not enabled by default. To enable it, set the -`legacy_fargate_1_role_per_profile_enabled` variable to `false`. - -**WARNING**: If you enable this feature on an existing cluster, and that -cluster is using Karpenter, the update could destroy all of your existing -Karpenter-provisioned nodes. Depending on your Karpenter version, this -could leave you with stranded EC2 instances (still running, but not managed by -Karpenter or visible to the cluster) and an interruption of service, and -possibly other problems. If you are using Karpenter and want to enable this -feature, the safest way is to destroy the existing cluster and create a new -one with this feature enabled. +**This change is recommended for all NEW clusters, but only NEW clusters**. Because it is a breaking change, it is not +enabled by default. To enable it, set the `legacy_fargate_1_role_per_profile_enabled` variable to `false`. + +**WARNING**: If you enable this feature on an existing cluster, and that cluster is using Karpenter, the update could +destroy all of your existing Karpenter-provisioned nodes. Depending on your Karpenter version, this could leave you with +stranded EC2 instances (still running, but not managed by Karpenter or visible to the cluster) and an interruption of +service, and possibly other problems. If you are using Karpenter and want to enable this feature, the safest way is to +destroy the existing cluster and create a new one with this feature enabled. diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index a880aca45..d000e4bd5 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -1,14 +1,15 @@ # Component: `eks/cluster` -This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups and Fargate profiles. +This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups and Fargate +profiles. :::warning -This component should only be deployed after logging into AWS via Federated login with SAML (e.g. GSuite) or -assuming an IAM role (e.g. from a CI/CD system). It should not be deployed if you log into AWS via AWS SSO, the -reason being that on initial deployment, the EKS cluster will be owned by the assumed role that provisioned it, -and AWS SSO roles are ephemeral (replaced on every configuration change). If this were to be the AWS SSO Role, then -we risk losing access to the EKS cluster once the ARN of the AWS SSO Role eventually changes. +This component should only be deployed after logging into AWS via Federated login with SAML (e.g. GSuite) or assuming an +IAM role (e.g. from a CI/CD system). It should not be deployed if you log into AWS via AWS SSO, the reason being that on +initial deployment, the EKS cluster will be owned by the assumed role that provisioned it, and AWS SSO roles are +ephemeral (replaced on every configuration change). If this were to be the AWS SSO Role, then we risk losing access to +the EKS cluster once the ARN of the AWS SSO Role eventually changes. ::: @@ -20,13 +21,14 @@ Here's an example snippet for how to use this component. This example expects the [Cloud Posse Reference Architecture](https://docs.cloudposse.com/reference-architecture/) Identity and Network designs deployed for mapping users to EKS service roles and granting access in a private network. -In addition, this example has the GitHub OIDC integration added and makes use of Karpenter to dynamically scale cluster nodes. +In addition, this example has the GitHub OIDC integration added and makes use of Karpenter to dynamically scale cluster +nodes. For more on these requirements, see [Identity Reference Architecture](https://docs.cloudposse.com/reference-architecture/quickstart/iam-identity/), -[Network Reference Architecture](https://docs.cloudposse.com/reference-architecture/scaffolding/setup/network/), -the [GitHub OIDC component](https://docs.cloudposse.com/components/catalog/aws/github-oidc-provider/), -and the [Karpenter component](https://docs.cloudposse.com/components/catalog/aws/eks/karpenter/). +[Network Reference Architecture](https://docs.cloudposse.com/reference-architecture/scaffolding/setup/network/), the +[GitHub OIDC component](https://docs.cloudposse.com/components/catalog/aws/github-oidc-provider/), and the +[Karpenter component](https://docs.cloudposse.com/components/catalog/aws/eks/karpenter/). ```yaml components: @@ -67,7 +69,7 @@ components: # Allows GitHub OIDC role github_actions_iam_role_enabled: true - github_actions_iam_role_attributes: [ "eks" ] + github_actions_iam_role_attributes: ["eks"] github_actions_allowed_repos: - acme/infra @@ -114,8 +116,8 @@ components: aws_sso_permission_sets_rbac: - aws_sso_permission_set: PowerUserAccess groups: - - idp:poweruser - - system:authenticated + - idp:poweruser + - system:authenticated # Fargate Profiles for Karpenter fargate_profiles: @@ -139,18 +141,18 @@ components: addons: # https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html vpc-cni: - addon_version: v1.13.4-eksbuild.1 # set `addon_version` to `null` to use the latest version + addon_version: v1.13.4-eksbuild.1 # set `addon_version` to `null` to use the latest version # https://docs.aws.amazon.com/eks/latest/userguide/managing-kube-proxy.html kube-proxy: - addon_version: "v1.27.1-eksbuild.1" # set `addon_version` to `null` to use the latest version + addon_version: "v1.27.1-eksbuild.1" # set `addon_version` to `null` to use the latest version # https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html coredns: - addon_version: "v1.10.1-eksbuild.1" # set `addon_version` to `null` to use the latest version + addon_version: "v1.10.1-eksbuild.1" # set `addon_version` to `null` to use the latest version # https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons # https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html # https://github.com/kubernetes-sigs/aws-ebs-csi-driver aws-ebs-csi-driver: - addon_version: "v1.20.0-eksbuild.1" # set `addon_version` to `null` to use the latest version + addon_version: "v1.20.0-eksbuild.1" # set `addon_version` to `null` to use the latest version # If you are not using [volume snapshots](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/#how-to-use-volume-snapshots) # (and you probably are not), disable the EBS Snapshotter with: configuration_values: '{"sidecars":{"snapshotter":{"forceEnable":false}}}' @@ -166,10 +168,11 @@ components: ### Amazon EKS End-of-Life Dates -When picking a Kubernetes version, be sure to review the [end-of-life dates for Amazon EKS](https://endoflife.date/amazon-eks). Refer to the chart below: +When picking a Kubernetes version, be sure to review the +[end-of-life dates for Amazon EKS](https://endoflife.date/amazon-eks). Refer to the chart below: | cycle | release | latest | latest release | eol | -|:------|:----------:|:------------|:--------------:|:----------:| +| :---- | :--------: | :---------- | :------------: | :--------: | | 1.28 | 2023-09-26 | 1.28-eks-1 | 2023-09-26 | 2024-11-01 | | 1.27 | 2023-05-24 | 1.27-eks-5 | 2023-08-30 | 2024-07-01 | | 1.26 | 2023-04-11 | 1.26-eks-6 | 2023-08-30 | 2024-06-01 | @@ -182,21 +185,23 @@ When picking a Kubernetes version, be sure to review the [end-of-life dates for | 1.19 | 2021-02-16 | 1.19-eks-11 | 2022-08-15 | 2022-08-01 | | 1.18 | 2020-10-13 | 1.18-eks-13 | 2022-08-15 | 2022-08-15 | -*This Chart was updated as of 10/16/2023 and is generated with [the `eol` tool](https://github.com/hugovk/norwegianblue). Check the latest updates by running `eol amazon-eks` locally or [on the website directly](https://endoflife.date/amazon-eks). +\*This Chart was updated as of 10/16/2023 and is generated with +[the `eol` tool](https://github.com/hugovk/norwegianblue). Check the latest updates by running `eol amazon-eks` locally +or [on the website directly](https://endoflife.date/amazon-eks). -You can also view the release and support timeline for [the Kubernetes project itself](https://endoflife.date/kubernetes). +You can also view the release and support timeline for +[the Kubernetes project itself](https://endoflife.date/kubernetes). ### Usage with Node Groups -The `eks/cluster` component also supports managed Node Groups. In order to add a set of nodes to -provision with the cluster, provide values for `var.managed_node_groups_enabled` and `var.node_groups`. +The `eks/cluster` component also supports managed Node Groups. In order to add a set of nodes to provision with the +cluster, provide values for `var.managed_node_groups_enabled` and `var.node_groups`. :::info -You can use managed Node Groups in conjunction with Karpenter. We recommend provisioning a -managed node group with as many nodes as Availability Zones used by your cluster (typically 3), to ensure a -minimum support for a high-availability set of daemons, and then using Karpenter to provision additional nodes -as needed. +You can use managed Node Groups in conjunction with Karpenter. We recommend provisioning a managed node group with as +many nodes as Availability Zones used by your cluster (typically 3), to ensure a minimum support for a high-availability +set of daemons, and then using Karpenter to provision additional nodes as needed. ::: @@ -243,14 +248,15 @@ node_groups: # for most attributes, setting null here means use setting from nod ### Using Addons -EKS clusters support “Addons” that can be automatically installed on a cluster. -Install these addons with the [`var.addons` input](https://docs.cloudposse.com/components/library/aws/eks/cluster/#input_addons). +EKS clusters support “Addons” that can be automatically installed on a cluster. Install these addons with the +[`var.addons` input](https://docs.cloudposse.com/components/library/aws/eks/cluster/#input_addons). :::info -Run the following command to see all available addons, their type, and their publisher. -You can also see the URL for addons that are available through the AWS Marketplace. Replace 1.27 with the version of your cluster. -See [Creating an addon](https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on) for more details. +Run the following command to see all available addons, their type, and their publisher. You can also see the URL for +addons that are available through the AWS Marketplace. Replace 1.27 with the version of your cluster. See +[Creating an addon](https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on) for more +details. ::: @@ -262,8 +268,8 @@ aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION \ :::info -You can see which versions are available for each addon by executing the following commands. -Replace 1.27 with the version of your cluster. +You can see which versions are available for each addon by executing the following commands. Replace 1.27 with the +version of your cluster. ::: @@ -286,7 +292,8 @@ echo "aws-efs-csi-driver:" && aws eks describe-addon-versions --kubernetes-versi ``` Some add-ons accept additional configuration. For example, the `vpc-cni` addon accepts a `disableNetworking` parameter. -View the available configuration options (as JSON Schema) via the `aws eks describe-addon-configuration` command. For example: +View the available configuration options (as JSON Schema) via the `aws eks describe-addon-configuration` command. For +example: ```shell aws eks describe-addon-configuration \ @@ -313,13 +320,13 @@ addons: # https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role # https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/#deploy-vpc-cni-managed-add-on vpc-cni: - addon_version: "v1.12.2-eksbuild.1" # set `addon_version` to `null` to use the latest version + addon_version: "v1.12.2-eksbuild.1" # set `addon_version` to `null` to use the latest version # https://docs.aws.amazon.com/eks/latest/userguide/managing-kube-proxy.html kube-proxy: - addon_version: "v1.25.6-eksbuild.1" # set `addon_version` to `null` to use the latest version + addon_version: "v1.25.6-eksbuild.1" # set `addon_version` to `null` to use the latest version # https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html coredns: - addon_version: "v1.9.3-eksbuild.2" # set `addon_version` to `null` to use the latest version + addon_version: "v1.9.3-eksbuild.2" # set `addon_version` to `null` to use the latest version # Uncomment to override default replica count of 2 # configuration_values: '{"replicaCount": 3}' # https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html @@ -327,15 +334,15 @@ addons: # https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role # https://github.com/kubernetes-sigs/aws-ebs-csi-driver aws-ebs-csi-driver: - addon_version: "v1.19.0-eksbuild.2" # set `addon_version` to `null` to use the latest version + addon_version: "v1.19.0-eksbuild.2" # set `addon_version` to `null` to use the latest version # If you are not using [volume snapshots](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/#how-to-use-volume-snapshots) # (and you probably are not), disable the EBS Snapshotter with: configuration_values: '{"sidecars":{"snapshotter":{"forceEnable":false}}}' ``` -Some addons, such as CoreDNS, require at least one node to be fully provisioned first. -See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. -Set `var.addons_depends_on` to `true` to require the Node Groups to be provisioned before addons. +Some addons, such as CoreDNS, require at least one node to be fully provisioned first. See +[issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. Set +`var.addons_depends_on` to `true` to require the Node Groups to be provisioned before addons. ```yaml addons_depends_on: true @@ -346,15 +353,14 @@ addons: :::warning -Addons may not be suitable for all use-cases! For example, if you are using Karpenter to provision nodes, -these nodes will never be available before the cluster component is deployed. +Addons may not be suitable for all use-cases! For example, if you are using Karpenter to provision nodes, these nodes +will never be available before the cluster component is deployed. ::: For more information on upgrading EKS Addons, see ["How to Upgrade EKS Cluster Addons"](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons/) - ### Adding and Configuring a new EKS Addon Add a new EKS addon to the `addons` map (`addons` variable): @@ -369,8 +375,8 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor - Add a file `addons-custom.tf` to the `eks/cluster` folder -- In the file, add an IAM policy document with the permissions required for the addon, - and use the `eks-iam-role` module to provision an IAM Role for Kubernetes Service Account for the addon: +- In the file, add an IAM policy document with the permissions required for the addon, and use the `eks-iam-role` module + to provision an IAM Role for Kubernetes Service Account for the addon: ```hcl data "aws_iam_policy_document" "my_addon" { @@ -405,7 +411,8 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor - Add a file `additional-addon-support_override.tf` to the `eks/cluster` folder -- In the file, add the IAM Role for Kubernetes Service Account for the addon to the `overridable_additional_addon_service_account_role_arn_map` map: +- In the file, add the IAM Role for Kubernetes Service Account for the addon to the + `overridable_additional_addon_service_account_role_arn_map` map: ```hcl locals { @@ -415,13 +422,14 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor } ``` -- This map will override the default map in the [additional-addon-support.tf](additional-addon-support.tf) file, - and will be merged into the final map together with the default EKS addons `vpc-cni` and `aws-ebs-csi-driver` - (which this component configures and creates IAM Roles for Kubernetes Service Accounts) +- This map will override the default map in the [additional-addon-support.tf](additional-addon-support.tf) file, and + will be merged into the final map together with the default EKS addons `vpc-cni` and `aws-ebs-csi-driver` (which this + component configures and creates IAM Roles for Kubernetes Service Accounts) -- Follow the instructions in the [additional-addon-support.tf](additional-addon-support.tf) file - if the addon may need to be deployed to Fargate, or has dependencies that Terraform cannot detect automatically. +- Follow the instructions in the [additional-addon-support.tf](additional-addon-support.tf) file if the addon may need + to be deployed to Fargate, or has dependencies that Terraform cannot detect automatically. + ## Requirements @@ -580,6 +588,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [karpenter\_iam\_role\_name](#output\_karpenter\_iam\_role\_name) | Karpenter IAM Role name | | [vpc\_cidr](#output\_vpc\_cidr) | The CIDR of the VPC where this cluster is deployed. | + ## Related How-to Guides @@ -593,6 +602,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/cluster) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/cluster) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/eks/datadog-agent/CHANGELOG.md b/modules/eks/datadog-agent/CHANGELOG.md index 1b1c8e8d3..7c45a6350 100644 --- a/modules/eks/datadog-agent/CHANGELOG.md +++ b/modules/eks/datadog-agent/CHANGELOG.md @@ -2,42 +2,34 @@ ### Possible Breaking Change -Removed inputs `iam_role_enabled` and `iam_policy_statements` because -the Datadog agent does not need an IAM (IRSA) role or any special AWS -permissions because it works solely within the Kubernetes environment. -(Datadog has AWS integrations to handle monitoring that requires AWS permissions.) +Removed inputs `iam_role_enabled` and `iam_policy_statements` because the Datadog agent does not need an IAM (IRSA) role +or any special AWS permissions because it works solely within the Kubernetes environment. (Datadog has AWS integrations +to handle monitoring that requires AWS permissions.) -This only a breaking change if you were setting these inputs. If you were, -simply remove them from your configuration. +This only a breaking change if you were setting these inputs. If you were, simply remove them from your configuration. ### Possible Breaking Change -Previously this component directly created the Kubernetes namespace for -the agent when `create_namespace` was set to `true`. Now this component -delegates that responsibility to the `helm-release` module, which -better coordinates the destruction of resources at destruction time -(for example, ensuring that the Helm release is completely destroyed -and finalizers run before deleting the namespace). +Previously this component directly created the Kubernetes namespace for the agent when `create_namespace` was set to +`true`. Now this component delegates that responsibility to the `helm-release` module, which better coordinates the +destruction of resources at destruction time (for example, ensuring that the Helm release is completely destroyed and +finalizers run before deleting the namespace). -Generally the simplest upgrade path is to destroy the Helm release, -then destroy the namespace, then apply the new configuration. Alternatively, -you can use `terraform state mv` to move the existing namespace to the new -Terraform "address", which will preserve the existing deployment and reduce -the possibility of the destroy failing and leaving the Kubernetes cluster -in a bad state. +Generally the simplest upgrade path is to destroy the Helm release, then destroy the namespace, then apply the new +configuration. Alternatively, you can use `terraform state mv` to move the existing namespace to the new Terraform +"address", which will preserve the existing deployment and reduce the possibility of the destroy failing and leaving the +Kubernetes cluster in a bad state. ### Cluster Agent Redundancy -In this PR we have defaulted the number of Cluster Agents to 2. This is -because when there are no Cluster Agents, all cluster metrics are lost. -Having 2 agents makes it possible to keep 1 agent running at all times, even -when the other is on a node being drained. +In this PR we have defaulted the number of Cluster Agents to 2. This is because when there are no Cluster Agents, all +cluster metrics are lost. Having 2 agents makes it possible to keep 1 agent running at all times, even when the other is +on a node being drained. ### DNS Resolution Enhancement -If Datadog processes are looking for where to send data and are configured -to look up `datadog.monitoring.svc.cluster.local`, by default the cluster -will make a DNS query for each of the following: +If Datadog processes are looking for where to send data and are configured to look up +`datadog.monitoring.svc.cluster.local`, by default the cluster will make a DNS query for each of the following: 1. `datadog.monitoring.svc.cluster.local.monitoring.svc.cluster.local` 2. `datadog.monitoring.svc.cluster.local.svc.cluster.local` @@ -45,31 +37,30 @@ will make a DNS query for each of the following: 4. `datadog.monitoring.svc.cluster.local.ec2.internal` 5. `datadog.monitoring.svc.cluster.local` -due to the DNS resolver's [search path](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#namespaces-of-services). -Because this lookup happens so frequently -(several times a second in a production environment), it can cause a lot of +due to the DNS resolver's +[search path](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#namespaces-of-services). Because +this lookup happens so frequently (several times a second in a production environment), it can cause a lot of unnecessary work, even if the DNS query is cached. -In this PR we have set `ndots: 2` in the agent and cluster agent configuration -so that only the 5th query is made. (In Kubernetes, the default value for -`ndots` is 5. DNS queries having fewer than `ndots` dots in them will be attempted -using each component of the search path in turn until a match is -found, while those with more dots, or with a final dot, are looked up as is.) - -Alternately, where you are setting the host name to be resolved, you can add a final dot at the end so that the -search path is not used, e.g. `datadog.monitoring.svc.cluster.local.` +In this PR we have set `ndots: 2` in the agent and cluster agent configuration so that only the 5th query is made. (In +Kubernetes, the default value for `ndots` is 5. DNS queries having fewer than `ndots` dots in them will be attempted +using each component of the search path in turn until a match is found, while those with more dots, or with a final dot, +are looked up as is.) +Alternately, where you are setting the host name to be resolved, you can add a final dot at the end so that the search +path is not used, e.g. `datadog.monitoring.svc.cluster.local.` ### Note for Bottlerocket users -If you are using Bottlerocket, you will want to uncomment the following from -`vaules.yaml` or add it to your `values` input: +If you are using Bottlerocket, you will want to uncomment the following from `vaules.yaml` or add it to your `values` +input: ```yaml -criSocketPath: /run/dockershim.sock # Bottlerocket Only -env: # Bottlerocket Only - - name: DD_AUTOCONFIG_INCLUDE_FEATURES # Bottlerocket Only - value: "containerd" # Bottlerocket Only +criSocketPath: /run/dockershim.sock # Bottlerocket Only +env: # Bottlerocket Only + - name: DD_AUTOCONFIG_INCLUDE_FEATURES # Bottlerocket Only + value: "containerd" # Bottlerocket Only ``` -See the [Datadog documentation](https://docs.datadoghq.com/containers/kubernetes/distributions/?tab=helm#EKS) for details. +See the [Datadog documentation](https://docs.datadoghq.com/containers/kubernetes/distributions/?tab=helm#EKS) for +details. diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index 136f8848d..d212a078c 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -52,7 +52,6 @@ components: env: - name: DD_EC2_PREFER_IMDSV2 # this merges ec2 instances and the node in the hostmap section value: "true" - ``` Deploy this to a particular environment such as dev, prod, etc. @@ -78,30 +77,39 @@ components: ## Cluster Checks -Cluster Checks are configurations that allow us to setup external URLs to be monitored. They can be configured through the datadog agent or annotations on kubernetes services. +Cluster Checks are configurations that allow us to setup external URLs to be monitored. They can be configured through +the datadog agent or annotations on kubernetes services. -Cluster Checks are similar to synthetics checks, they are not as indepth, but significantly cheaper. Use Cluster Checks when you need a simple health check beyond the kubernetes pod health check. +Cluster Checks are similar to synthetics checks, they are not as indepth, but significantly cheaper. Use Cluster Checks +when you need a simple health check beyond the kubernetes pod health check. -Public addresses that test endpoints must use the agent configuration, whereas service addresses internal to the cluster can be tested by annotations. +Public addresses that test endpoints must use the agent configuration, whereas service addresses internal to the cluster +can be tested by annotations. ### Adding Cluster Checks Cluster Checks can be enabled or disabled via the `cluster_checks_enabled` variable. We recommend this be set to true. -New Cluster Checks can be added to defaults to be applied in every account. Alternatively they can be placed in an individual stage folder which will be applied to individual stages. This is controlled by the `datadog_cluster_check_config_parameters` variable, which determines the paths of yaml files to look for cluster checks per stage. +New Cluster Checks can be added to defaults to be applied in every account. Alternatively they can be placed in an +individual stage folder which will be applied to individual stages. This is controlled by the +`datadog_cluster_check_config_parameters` variable, which determines the paths of yaml files to look for cluster checks +per stage. -Once they are added, and properly configured, the new checks show up in the network monitor creation under `ssl` and `Http` +Once they are added, and properly configured, the new checks show up in the network monitor creation under `ssl` and +`Http` -**Please note:** the yaml file name doesn't matter, but the root key inside which is `something.yaml` does matter. this is following [datadogs docs](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) for `.yaml`. +**Please note:** the yaml file name doesn't matter, but the root key inside which is `something.yaml` does matter. this +is following +[datadogs docs](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) +for `.yaml`. #### Sample Yaml -:::caution -The key of a filename must match datadog docs, which is `.yaml` +:::caution The key of a filename must match datadog docs, which is `.yaml` [Datadog Cluster Checks](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) -::: -Cluster Checks **can** be used for external URL testing (loadbalancer endpoints), whereas annotations **must** be used for kubernetes services. +::: Cluster Checks **can** be used for external URL testing (loadbalancer endpoints), whereas annotations **must** be +used for kubernetes services. ``` http_check.yaml: @@ -119,7 +127,8 @@ http_check.yaml: ### Monitoring Cluster Checks -Using Cloudposse's `datadog-monitor` component. The following yaml snippet will monitor all HTTP Cluster Checks, this can be added to each stage (usually via a defaults folder). +Using Cloudposse's `datadog-monitor` component. The following yaml snippet will monitor all HTTP Cluster Checks, this +can be added to each stage (usually via a defaults folder). ```yaml https-checks: @@ -146,7 +155,7 @@ https-checks: new_host_delay: 0 new_group_delay: 0 no_data_timeframe: 2 - threshold_windows: { } + threshold_windows: {} thresholds: critical: 1 warning: 1 @@ -155,12 +164,13 @@ https-checks: ## References -* https://github.com/DataDog/helm-charts/tree/main/charts/datadog -* https://github.com/DataDog/helm-charts/blob/main/charts/datadog/values.yaml -* https://github.com/DataDog/helm-charts/blob/main/examples/datadog/agent_basic_values.yaml -* https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release -* https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm +- https://github.com/DataDog/helm-charts/tree/main/charts/datadog +- https://github.com/DataDog/helm-charts/blob/main/charts/datadog/values.yaml +- https://github.com/DataDog/helm-charts/blob/main/examples/datadog/agent_basic_values.yaml +- https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release +- https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm + ## Requirements @@ -256,7 +266,10 @@ https-checks: | [cluster\_checks](#output\_cluster\_checks) | Cluster Checks for the cluster | | [metadata](#output\_metadata) | Block status of the deployed release | + ## References -* Datadog's [Kubernetes Agent documentation](https://docs.datadoghq.com/containers/kubernetes/) -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-agent) - Cloud Posse's upstream component + +- Datadog's [Kubernetes Agent documentation](https://docs.datadoghq.com/containers/kubernetes/) +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-agent) - + Cloud Posse's upstream component diff --git a/modules/eks/echo-server/CHANGELOG.md b/modules/eks/echo-server/CHANGELOG.md index a2a187cae..5dc0fb54a 100644 --- a/modules/eks/echo-server/CHANGELOG.md +++ b/modules/eks/echo-server/CHANGELOG.md @@ -1,22 +1,17 @@ ## Changes in PR #893, components version ~v1.337.0 -- Moved `eks/echo-server` v1.147.0 to `/deprecated/eks/echo-server` for those -who still need it and do not want to switch. It may later become the basis -for an example app or something similar. +- Moved `eks/echo-server` v1.147.0 to `/deprecated/eks/echo-server` for those who still need it and do not want to + switch. It may later become the basis for an example app or something similar. - Removed dependency on and connection to the `eks/alb-controller-ingress-group` component -- Added liveness probe, and disabled logging of probe requests. Probe request -logging can be restored by setting `livenessProbeLogging: true` in `chart_values` -- This component no longer configures automatic redirects from HTTP to HTTPS. This -is because for ALB controller, setting that on one ingress sets it for all -ingresses in the same IngressGroup, and it is a design goal that deploying -this component does not affect other Ingresses (with the obvious exception -of possibly being the first to create the Application Load Balancer). -- Removed from `chart_values`:`ingress.nginx.class` (was set to "nginx") and -`ingress.alb.class` (was set to "alb"). IngressClass should usually not be set, -as this component is intended to be used to test the defaults, including the -default IngressClass. However, if you do want to set it, you can do so by -setting `ingress.class` in `chart_values`. -- Removed the deprecated `kubernetes.io/ingress.class` annotation by default. -It can be restored by setting `ingress.use_ingress_class_annotation: true` in `chart_values`. -IngressClass is now set using the preferred `ingressClassName` field of the -Ingress resource. +- Added liveness probe, and disabled logging of probe requests. Probe request logging can be restored by setting + `livenessProbeLogging: true` in `chart_values` +- This component no longer configures automatic redirects from HTTP to HTTPS. This is because for ALB controller, + setting that on one ingress sets it for all ingresses in the same IngressGroup, and it is a design goal that deploying + this component does not affect other Ingresses (with the obvious exception of possibly being the first to create the + Application Load Balancer). +- Removed from `chart_values`:`ingress.nginx.class` (was set to "nginx") and `ingress.alb.class` (was set to "alb"). + IngressClass should usually not be set, as this component is intended to be used to test the defaults, including the + default IngressClass. However, if you do want to set it, you can do so by setting `ingress.class` in `chart_values`. +- Removed the deprecated `kubernetes.io/ingress.class` annotation by default. It can be restored by setting + `ingress.use_ingress_class_annotation: true` in `chart_values`. IngressClass is now set using the preferred + `ingressClassName` field of the Ingress resource. diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index 6e065874d..7add6693e 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -1,38 +1,42 @@ # Component: `eks/echo-server` -This is copied from [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/echo-server). +This is copied from +[cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/echo-server). -This component installs the [Ealenn/Echo-Server](https://github.com/Ealenn/Echo-Server) to EKS clusters. -The echo server is a server that sends it back to the client a JSON representation of all the data -the server received, which is a combination of information sent by the client and information sent -by the web server infrastructure. For further details, please consult the [Echo-Server documentation](https://ealenn.github.io/Echo-Server/). +This component installs the [Ealenn/Echo-Server](https://github.com/Ealenn/Echo-Server) to EKS clusters. The echo server +is a server that sends it back to the client a JSON representation of all the data the server received, which is a +combination of information sent by the client and information sent by the web server infrastructure. For further +details, please consult the [Echo-Server documentation](https://ealenn.github.io/Echo-Server/). ## Prerequisites -Echo server is intended to provide end-to-end testing of everything needed -to deploy an application or service with a public HTTPS endpoint. It uses -defaults where possible, such as using the default IngressClass, in order -to verify that the defaults are sufficient for a typical application. +Echo server is intended to provide end-to-end testing of everything needed to deploy an application or service with a +public HTTPS endpoint. It uses defaults where possible, such as using the default IngressClass, in order to verify that +the defaults are sufficient for a typical application. -In order to minimize the impact of the echo server on the rest of the cluster, -it does not set any configuration that would affect other ingresses, such -as WAF rules, logging, or redirecting HTTP to HTTPS. Those settings should -be configured in the IngressClass where possible. +In order to minimize the impact of the echo server on the rest of the cluster, it does not set any configuration that +would affect other ingresses, such as WAF rules, logging, or redirecting HTTP to HTTPS. Those settings should be +configured in the IngressClass where possible. Therefore, it requires several other components. At the moment, it supports 2 configurations: 1. ALB with ACM Certificate - - AWS Load Balancer Controller (ALB) version 2.2.0 or later, with ACM certificate auto-discovery enabled - - A default IngressClass, which can be provisioned by the `alb-controller` component as part of deploying - the controller, or can be provisioned separately, for example by the `alb-controller-ingress-class` component. - - Pre-provisioned ACM TLS certificate covering the provisioned host name (typically a wildcard certificate covering all hosts in the domain) + +- AWS Load Balancer Controller (ALB) version 2.2.0 or later, with ACM certificate auto-discovery enabled +- A default IngressClass, which can be provisioned by the `alb-controller` component as part of deploying the + controller, or can be provisioned separately, for example by the `alb-controller-ingress-class` component. +- Pre-provisioned ACM TLS certificate covering the provisioned host name (typically a wildcard certificate covering all + hosts in the domain) + 2. Nginx with Cert Manager Certificate - - Nginx (via `kubernetes/ingress-nginx` controller). We recommend `ingress-nginx` v1.1.0 or later, but `echo-server` - should work with any version that supports Ingress API version `networking.k8s.io/v1`. - - `jetstack/cert-manager` configured to automatically (via Ingress Shim, installed by default) generate TLS certificates via a Cluster Issuer - (by default, named `letsEncrypt-prod`). + +- Nginx (via `kubernetes/ingress-nginx` controller). We recommend `ingress-nginx` v1.1.0 or later, but `echo-server` + should work with any version that supports Ingress API version `networking.k8s.io/v1`. +- `jetstack/cert-manager` configured to automatically (via Ingress Shim, installed by default) generate TLS certificates + via a Cluster Issuer (by default, named `letsEncrypt-prod`). In both configurations, it has these common requirements: + - EKS component deployed, with component name specified in `eks_component_name` (defaults to "eks/cluster") - Kubernetes version 1.19 or later - Ingress API version `networking.k8s.io/v1` @@ -42,10 +46,9 @@ In both configurations, it has these common requirements: ## Warnings A Terraform plan may fail to apply, giving a Kubernetes authentication failure. This is due to a known issue with -Terraform and the Kubernetes provider. During the "plan" phase Terraform gets a short-lived Kubernetes -authentication token and caches it, and then tries to use it during "apply". If the token has expired by -the time you try to run "apply", the "apply" will fail. The workaround is to run `terraform apply -auto-approve` without -a "plan" file. +Terraform and the Kubernetes provider. During the "plan" phase Terraform gets a short-lived Kubernetes authentication +token and caches it, and then tries to use it during "apply". If the token has expired by the time you try to run +"apply", the "apply" will fail. The workaround is to run `terraform apply -auto-approve` without a "plan" file. ## Usage @@ -57,6 +60,7 @@ Set `ingress_type` to "alb" if using `alb-controller` or "nginx" if using `ingre Normally, you should not set the IngressClass or IngressGroup, as this component is intended to test the defaults. However, if you need to, set them in `chart_values`: + ```yaml chart_values: ingress: @@ -66,13 +70,11 @@ chart_values: group_name: "other-ingress-group" ``` -Note that if you follow recommendations and do not set the ingress class name, -the deployed Ingress will have the ingressClassName setting injected by the -Ingress controller, set to the then-current default. This means that if later -you change the default IngressClass, the Ingress will be NOT be updated to use -the new default. Furthermore, because of limitations in the Helm provider, this -will not be detected as drift. You will need to destroy and re-deploy the -echo server to update the Ingress to the new default. +Note that if you follow recommendations and do not set the ingress class name, the deployed Ingress will have the +ingressClassName setting injected by the Ingress controller, set to the then-current default. This means that if later +you change the default IngressClass, the Ingress will be NOT be updated to use the new default. Furthermore, because of +limitations in the Helm provider, this will not be detected as drift. You will need to destroy and re-deploy the echo +server to update the Ingress to the new default. ```yaml components: @@ -97,10 +99,10 @@ components: hostname_template: "echo.%[3]v.%[2]v.%[1]v.sample-domain.net" ``` -In rare cases where some ingress controllers do not support the `ingressClassName` field, -you can restore the old `kubernetes.io/ingress.class` annotation by setting -`ingress.use_ingress_class_annotation: true` in `chart_values`. +In rare cases where some ingress controllers do not support the `ingressClassName` field, you can restore the old +`kubernetes.io/ingress.class` annotation by setting `ingress.use_ingress_class_annotation: true` in `chart_values`. + ## Requirements @@ -188,6 +190,8 @@ you can restore the old `kubernetes.io/ingress.class` annotation by setting | [hostname](#output\_hostname) | Hostname of the deployed echo server | | [metadata](#output\_metadata) | Block status of the deployed release | + ## References -* https://github.com/Ealenn/Echo-Server + +- https://github.com/Ealenn/Echo-Server diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index ff4ef476e..d4b630c06 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -1,6 +1,8 @@ # Component: `eks/external-dns` -This component creates a Helm deployment for [external-dns](https://github.com/bitnami/bitnami-docker-external-dns) on a Kubernetes cluster. [external-dns](https://github.com/bitnami/bitnami-docker-external-dns) is a Kubernetes addon that configures public DNS servers with information about exposed Kubernetes services to make them discoverable. +This component creates a Helm deployment for [external-dns](https://github.com/bitnami/bitnami-docker-external-dns) on a +Kubernetes cluster. [external-dns](https://github.com/bitnami/bitnami-docker-external-dns) is a Kubernetes addon that +configures public DNS servers with information about exposed Kubernetes services to make them discoverable. ## Usage @@ -45,6 +47,7 @@ components: chart_values: {} ``` + ## Requirements @@ -142,6 +145,7 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References diff --git a/modules/eks/external-secrets-operator/CHANGELOG.md b/modules/eks/external-secrets-operator/CHANGELOG.md index 5e1c3aa10..2a073f4d6 100644 --- a/modules/eks/external-secrets-operator/CHANGELOG.md +++ b/modules/eks/external-secrets-operator/CHANGELOG.md @@ -1,7 +1,7 @@ ## Components PR [[eks/external-secrets-operator] Set default chart](https://github.com/cloudposse/terraform-aws-components/pull/856) -This is a bug fix and feature enhancement update. -No actions necessary to upgrade. +This is a bug fix and feature enhancement update. No actions necessary to upgrade. ## Fixes -* Set default chart + +- Set default chart diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index d7381ec7c..e2a0d2332 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -1,8 +1,11 @@ # Component: `external-secrets-operator` -This component (ESO) is used to create an external `SecretStore` configured to synchronize secrets from AWS SSM Parameter store as Kubernetes Secrets within the cluster. Per the operator pattern, the `external-secret-operator` pods will watch for any `ExternalSecret` resources which reference the `SecretStore` to pull secrets from. +This component (ESO) is used to create an external `SecretStore` configured to synchronize secrets from AWS SSM +Parameter store as Kubernetes Secrets within the cluster. Per the operator pattern, the `external-secret-operator` pods +will watch for any `ExternalSecret` resources which reference the `SecretStore` to pull secrets from. -In practice, this means apps will define an `ExternalSecret` that pulls all env into a single secret as part of a helm chart; e.g.: +In practice, this means apps will define an `ExternalSecret` that pulls all env into a single secret as part of a helm +chart; e.g.: ``` # Part of the charts in `/releases @@ -29,15 +32,15 @@ spec: target: "$1" ``` -This component assumes secrets are prefixed by "service" in parameter store (e.g. `/app/my_secret`). The `SecretStore`. The component is designed to pull secrets from a `path` prefix (defaulting to `"app"`). This should work nicely along `chamber` which uses this same path (called a "service" in Chamber). For example, developers should store keys like so. - +This component assumes secrets are prefixed by "service" in parameter store (e.g. `/app/my_secret`). The `SecretStore`. +The component is designed to pull secrets from a `path` prefix (defaulting to `"app"`). This should work nicely along +`chamber` which uses this same path (called a "service" in Chamber). For example, developers should store keys like so. ```bash assume-role acme-platform-gbl-sandbox-admin chamber write app MY_KEY my-value ``` - See `docs/recipies.md` for more information on managing secrets. ## Usage @@ -88,6 +91,7 @@ components: chart_values: {} ``` + ## Requirements @@ -181,8 +185,10 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References -* [Secrets Management Strategy](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-secrets-management-strategy-for-terraform/) -* https://external-secrets.io/v0.5.9/ -* https://external-secrets.io/v0.5.9/provider-aws-parameter-store/ + +- [Secrets Management Strategy](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-secrets-management-strategy-for-terraform/) +- https://external-secrets.io/v0.5.9/ +- https://external-secrets.io/v0.5.9/provider-aws-parameter-store/ diff --git a/modules/eks/github-actions-runner/CHANGELOG.md b/modules/eks/github-actions-runner/CHANGELOG.md index bb7b5e33b..4a6f97722 100644 --- a/modules/eks/github-actions-runner/CHANGELOG.md +++ b/modules/eks/github-actions-runner/CHANGELOG.md @@ -1,74 +1,68 @@ ## Initial Release -This release has been tested and used in production, but testing has not covered -all available features. Please use with caution and report any issues you -encounter. +This release has been tested and used in production, but testing has not covered all available features. Please use with +caution and report any issues you encounter. ### Migration from `actions-runner-controller` -GitHub has released its own official self-hosted GitHub Actions Runner support, -replacing the `actions-runner-controller` implementation developed by Summerwind. -(See the [announcement from GitHub](https://github.com/actions/actions-runner-controller/discussions/2072).) -Accordingly, this component is a replacement for the [`actions-runner-controller`](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) -component. Although there are different defaults for some of the configuration options, if -you are already using `actions-runner-controller` you should be able to reuse -the GitHub app or PAT and image pull secret you are already using, making -migration relatively straightforward. +GitHub has released its own official self-hosted GitHub Actions Runner support, replacing the +`actions-runner-controller` implementation developed by Summerwind. (See the +[announcement from GitHub](https://github.com/actions/actions-runner-controller/discussions/2072).) Accordingly, this +component is a replacement for the +[`actions-runner-controller`](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) +component. Although there are different defaults for some of the configuration options, if you are already using +`actions-runner-controller` you should be able to reuse the GitHub app or PAT and image pull secret you are already +using, making migration relatively straightforward. -We recommend deploying this component into a separate namespace (or namespaces) -than `actions-runner-controller` and get the new runners sets running before -you remove the old ones. You can then migrate your workflows to use the new -runners sets and have zero downtime. +We recommend deploying this component into a separate namespace (or namespaces) than `actions-runner-controller` and get +the new runners sets running before you remove the old ones. You can then migrate your workflows to use the new runners +sets and have zero downtime. Major differences: -- The official GitHub runners deployed are different from the GitHub hosted - runners and the Summerwind self-hosted runners in that [they have very few tools installed](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image). You will need to - install any tools you need in your workflows, either as part of your workflow - (recommended) or by maintaining a [custom runner image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image), or by running - such steps in a [separate container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) - that has the tools pre-installed. Many tools have publicly available actions - to install them, such as `actions/setup-node` to install NodeJS or `dcarbone/install-jq-action` - to install `jq`. You can also install packages using `awalsh128/cache-apt-pkgs-action`, - which has the advantage of being able to skip the installation if the package - is already installed, so you can more efficiently run the same workflow on - GitHub hosted as well as self-hosted runners. -- Self-hosted runners, such as those deployed with the `actions-runner-controller` - component, are targeted by a set of labels indicated by a workflow's `runs-on` - array, of which the first must be "self-hosted". Runner Sets, such as are - deployed with this component, are targeted by a single label, which is the - name of the Runner Set. This means that you will need to update your workflows - to target the new Runner Set label. See [here](https://github.com/actions/actions-runner-controller/discussions/2921#discussioncomment-7501051) - for the reasoning behind GitHub's decision to use a single label instead of a set. -- The `actions-runner-controller` component uses the published Helm chart for the - controller, but there is none for the runners, so it includes a custom Helm chart - for them. However, for Runner Sets, GitHub has published 2 charts, one for the controller - and one for the runners (runner sets). This means that this component requires - configuration (e.g. version numbers) of 2 charts, although both should be - kept at the same version. -- The `actions-runner-controller` component has a `resources/values.yaml` file - that provided defaults for the controller Helm chart. This component does not have - files like that by default, but supports a `resources/values-controller.yaml` file - for the "gha-runner-scale-set-controller" chart and a `resources/values-runner.yaml` - file for the "gha-runner-scale-set" chart. -- The default values for the SSM paths for the GitHub auth secret and the imagePullSecret - have changed. Specify the old values explicitly to keep using the same secrets. -- The `actions-runner-controller` component creates an IAM Role (IRSA) for the runners - to use. This component does not create an IRSA, because the chart does not support - using one while in "dind" mode. Use GitHub OIDC authentication inside your workflows instead. -- The Runner Sets deployed by this component use a different autoscaling mechanism, - so most of the `actions-runner-controller` configuration options related to - autoscaling are not applicable. -- For the same reason, this component does not deploy a webhook listener or Ingress and - does not require configuration of a GitHub webhook. -- The `actions-runner-controller` component has an input named `existing_kubernetes_secret_name`. - The equivalent input for this component is `github_kubernetes_secret_name`, - in order to clearly distinguish it from the `image_pull_kubernetes_secret_name` input. + +- The official GitHub runners deployed are different from the GitHub hosted runners and the Summerwind self-hosted + runners in that + [they have very few tools installed](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image). + You will need to install any tools you need in your workflows, either as part of your workflow (recommended) or by + maintaining a + [custom runner image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image), + or by running such steps in a + [separate container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) that has the tools + pre-installed. Many tools have publicly available actions to install them, such as `actions/setup-node` to install + NodeJS or `dcarbone/install-jq-action` to install `jq`. You can also install packages using + `awalsh128/cache-apt-pkgs-action`, which has the advantage of being able to skip the installation if the package is + already installed, so you can more efficiently run the same workflow on GitHub hosted as well as self-hosted runners. +- Self-hosted runners, such as those deployed with the `actions-runner-controller` component, are targeted by a set of + labels indicated by a workflow's `runs-on` array, of which the first must be "self-hosted". Runner Sets, such as are + deployed with this component, are targeted by a single label, which is the name of the Runner Set. This means that you + will need to update your workflows to target the new Runner Set label. See + [here](https://github.com/actions/actions-runner-controller/discussions/2921#discussioncomment-7501051) for the + reasoning behind GitHub's decision to use a single label instead of a set. +- The `actions-runner-controller` component uses the published Helm chart for the controller, but there is none for the + runners, so it includes a custom Helm chart for them. However, for Runner Sets, GitHub has published 2 charts, one for + the controller and one for the runners (runner sets). This means that this component requires configuration (e.g. + version numbers) of 2 charts, although both should be kept at the same version. +- The `actions-runner-controller` component has a `resources/values.yaml` file that provided defaults for the controller + Helm chart. This component does not have files like that by default, but supports a `resources/values-controller.yaml` + file for the "gha-runner-scale-set-controller" chart and a `resources/values-runner.yaml` file for the + "gha-runner-scale-set" chart. +- The default values for the SSM paths for the GitHub auth secret and the imagePullSecret have changed. Specify the old + values explicitly to keep using the same secrets. +- The `actions-runner-controller` component creates an IAM Role (IRSA) for the runners to use. This component does not + create an IRSA, because the chart does not support using one while in "dind" mode. Use GitHub OIDC authentication + inside your workflows instead. +- The Runner Sets deployed by this component use a different autoscaling mechanism, so most of the + `actions-runner-controller` configuration options related to autoscaling are not applicable. +- For the same reason, this component does not deploy a webhook listener or Ingress and does not require configuration + of a GitHub webhook. +- The `actions-runner-controller` component has an input named `existing_kubernetes_secret_name`. The equivalent input + for this component is `github_kubernetes_secret_name`, in order to clearly distinguish it from the + `image_pull_kubernetes_secret_name` input. ### Translating configuration from `actions-runner-controller` -Here is an example configuration for the `github-actions-runner` controller, -with comments indicating where in the `actions-runner-controller` configuration -the corresponding configuration option can be copied from. +Here is an example configuration for the `github-actions-runner` controller, with comments indicating where in the +`actions-runner-controller` configuration the corresponding configuration option can be copied from. ```yaml components: @@ -100,14 +94,11 @@ components: replicas: 1 # From `actions-runner-controller` file `resources/values.yaml`, value `replicaCount` # resources from var.resources - - # These values can be copied directly from the `actions-runner-controller` configuration ssm_github_secret_path: "/github_runners/controller_github_app_secret" github_app_id: "250828" github_app_installation_id: "30395627" - # These values require some converstion from the `actions-runner-controller` configuration # Set `create_github_kubernetes_secret` to `true` if `existing_kubernetes_secret_name` was not set, `false` otherwise. create_github_kubernetes_secret: true diff --git a/modules/eks/github-actions-runner/README.md b/modules/eks/github-actions-runner/README.md index 36bf50d53..d6fde7712 100644 --- a/modules/eks/github-actions-runner/README.md +++ b/modules/eks/github-actions-runner/README.md @@ -1,97 +1,92 @@ # Component: `github-actions-runner` -This component deploys self-hosted GitHub Actions Runners and a [Controller](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller#introduction) -on an EKS cluster, using "[runner scale sets](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#runner-scale-set)". +This component deploys self-hosted GitHub Actions Runners and a +[Controller](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller#introduction) +on an EKS cluster, using +"[runner scale sets](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#runner-scale-set)". -This solution is supported by GitHub and supersedes the [actions-runner-controller](https://github.com/actions/actions-runner-controller/blob/master/docs/about-arc.md) developed -by Summerwind and deployed by Cloud Posse's [actions-runner-controller](https://docs.cloudposse.com/components/library/aws/eks/actions-runner-controller/) component. +This solution is supported by GitHub and supersedes the +[actions-runner-controller](https://github.com/actions/actions-runner-controller/blob/master/docs/about-arc.md) +developed by Summerwind and deployed by Cloud Posse's +[actions-runner-controller](https://docs.cloudposse.com/components/library/aws/eks/actions-runner-controller/) +component. ### Current limitations -The runner image used by Runner Sets contains [no more packages than are necessary](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image) -to run the runner. This is in contrast to the Summerwind implementation, which -contains some commonly needed packages like `build-essential`, `curl`, `wget`, -`git`, and `jq`, and the GitHub hosted images which contain a robust set of tools. -(This is a limitation of the official Runner Sets implementation, not this -component per se.) You will need to -install any tools you need in your workflows, either as part of your workflow -(recommended), by maintaining a [custom runner image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image), or by running -such steps in a [separate container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) -that has the tools pre-installed. Many tools have publicly available actions -to install them, such as `actions/setup-node` to install NodeJS or `dcarbone/install-jq-action` -to install `jq`. You can also install packages using `awalsh128/cache-apt-pkgs-action`, -which has the advantage of being able to skip the installation if the package -is already installed, so you can more efficiently run the same workflow on -GitHub hosted as well as self-hosted runners. - -:::info -There are (as of this writing) open feature requests to add some -commonly needed packages to the official Runner Sets runner image. You can -upvote these -requests [here](https://github.com/actions/actions-runner-controller/discussions/3168) -and [here](https://github.com/orgs/community/discussions/80868) to help get them -implemented. +The runner image used by Runner Sets contains +[no more packages than are necessary](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image) +to run the runner. This is in contrast to the Summerwind implementation, which contains some commonly needed packages +like `build-essential`, `curl`, `wget`, `git`, and `jq`, and the GitHub hosted images which contain a robust set of +tools. (This is a limitation of the official Runner Sets implementation, not this component per se.) You will need to +install any tools you need in your workflows, either as part of your workflow (recommended), by maintaining a +[custom runner image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image), +or by running such steps in a +[separate container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) that has the tools +pre-installed. Many tools have publicly available actions to install them, such as `actions/setup-node` to install +NodeJS or `dcarbone/install-jq-action` to install `jq`. You can also install packages using +`awalsh128/cache-apt-pkgs-action`, which has the advantage of being able to skip the installation if the package is +already installed, so you can more efficiently run the same workflow on GitHub hosted as well as self-hosted runners. + +:::info There are (as of this writing) open feature requests to add some commonly needed packages to the official Runner +Sets runner image. You can upvote these requests +[here](https://github.com/actions/actions-runner-controller/discussions/3168) and +[here](https://github.com/orgs/community/discussions/80868) to help get them implemented. ::: -In the current version of this component, only "dind" (Docker in Docker) mode has been tested. -Support for "kubernetes" mode is provided, but has not been validated. +In the current version of this component, only "dind" (Docker in Docker) mode has been tested. Support for "kubernetes" +mode is provided, but has not been validated. -Many elements in the Controller chart are not directly configurable by named inputs. -To configure them, you can use the `controller.chart_values` input or create a -`resources/values-controller.yaml` file in the component to supply values. +Many elements in the Controller chart are not directly configurable by named inputs. To configure them, you can use the +`controller.chart_values` input or create a `resources/values-controller.yaml` file in the component to supply values. + +Almost all the features of the Runner Scale Set chart are configurable by named inputs. The exceptions are: -Almost all the features of the Runner Scale Set chart are configurable by named inputs. -The exceptions are: - There is no specific input for specifying an outbound HTTP proxy. -- There is no specific input for supplying a [custom certificate authority (CA) certificate](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#custom-tls-certificates) +- There is no specific input for supplying a + [custom certificate authority (CA) certificate](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#custom-tls-certificates) to use when connecting to GitHub Enterprise Server. -You can specify these values by creating a `resources/values-runner.yaml` file -in the component and setting values as shown by the default Helm [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml), +You can specify these values by creating a `resources/values-runner.yaml` file in the component and setting values as +shown by the default Helm +[values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml), and they will be applied to all runners. Currently, this component has some additional limitations. In particular: -- The controller and all runners and listeners share the Image Pull Secrets. -You cannot use different ones for different runners. -- All the runners use the same GitHub secret (app or PAT). Using a GitHub app - is preferred anyway, and the single GitHub app serves the entire organization. + +- The controller and all runners and listeners share the Image Pull Secrets. You cannot use different ones for different + runners. +- All the runners use the same GitHub secret (app or PAT). Using a GitHub app is preferred anyway, and the single GitHub + app serves the entire organization. - Only one controller is supported per cluster, though it can have multiple replicas. -These limitations could be addressed if there is demand. Contact [Cloud Posse Professional Services](https://cloudposse.com/professional-services/) -if you would be interested in sponsoring the development of any of these features. +These limitations could be addressed if there is demand. Contact +[Cloud Posse Professional Services](https://cloudposse.com/professional-services/) if you would be interested in +sponsoring the development of any of these features. ### Ephemeral work storage -The runners are configured to use ephemeral storage for workspaces, but the -details and defaults can be a bit confusing. - -When running in "dind" ("Docker in Docker") mode, the default is to use `emptyDir`, which -means space on the `kubelet` base directory, which is usually the root disk. You -can manage the amount of storage allowed to be used with `ephemeral_storage` requests and limits, -or you can just let it use whatever free space there is on the root disk. +The runners are configured to use ephemeral storage for workspaces, but the details and defaults can be a bit confusing. -When running in `kubernetes` mode, the only supported local disk storage is an -ephemeral `PersistentVolumeClaim`, which causes a separate disk to be allocated -for the runner pod. This disk is ephemeral, and will be deleted when the runner -pod is deleted. When combined with the recommended ephemeral runner -configuration, this means that a new disk will be created for each job, and -deleted when the job is complete. That is a lot of overhead and will slow things -down somewhat. +When running in "dind" ("Docker in Docker") mode, the default is to use `emptyDir`, which means space on the `kubelet` +base directory, which is usually the root disk. You can manage the amount of storage allowed to be used with +`ephemeral_storage` requests and limits, or you can just let it use whatever free space there is on the root disk. +When running in `kubernetes` mode, the only supported local disk storage is an ephemeral `PersistentVolumeClaim`, which +causes a separate disk to be allocated for the runner pod. This disk is ephemeral, and will be deleted when the runner +pod is deleted. When combined with the recommended ephemeral runner configuration, this means that a new disk will be +created for each job, and deleted when the job is complete. That is a lot of overhead and will slow things down +somewhat. -The size of the attached PersistentVolume is controlled -by `ephemeral_pvc_storage` (a Kubernetes size string like "1G") and the kind of -storage is controlled by `ephemeral_pvc_storage_class` -(which can be omitted to use the cluster default storage class). +The size of the attached PersistentVolume is controlled by `ephemeral_pvc_storage` (a Kubernetes size string like "1G") +and the kind of storage is controlled by `ephemeral_pvc_storage_class` (which can be omitted to use the cluster default +storage class). -This mode is also optionally available when using `dind`. To enable it, set -`ephemeral_pvc_storage` to the desired size. Leave `ephemeral_pvc_storage` at -the default value of `null` to use `emptyDir` storage (recommended). +This mode is also optionally available when using `dind`. To enable it, set `ephemeral_pvc_storage` to the desired size. +Leave `ephemeral_pvc_storage` at the default value of `null` to use `emptyDir` storage (recommended). -Beware that using a PVC may significantly increase the startup of the runner. -If you are using a PVC, you may want to keep idle runners available so that -jobs can be started without waiting for a new runner to start. +Beware that using a PVC may significantly increase the startup of the runner. If you are using a PVC, you may want to +keep idle runners available so that jobs can be started without waiting for a new runner to start. ## Usage @@ -183,61 +178,51 @@ components: cpu: 4000m memory: 7680Mi ephemeral-storage: 40G - ``` ### Authentication and Secrets -The GitHub Action Runners need to authenticate to GitHub in order to do such -things as register runners and pickup jobs. You can authenticate using either -a [GitHub App](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-github-app) -or a [Personal Access Token (classic)](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-personal-access-token-classic). -The preferred way to authenticate is by _creating_ and _installing_ a GitHub -App. This is the recommended approach as it allows for much more restricted -access than using a Personal Access Token (classic), and the Action Runners do -not currently support using a fine-grained Personal Access Token. - +The GitHub Action Runners need to authenticate to GitHub in order to do such things as register runners and pickup jobs. +You can authenticate using either a +[GitHub App](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-github-app) +or a +[Personal Access Token (classic)](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-personal-access-token-classic). +The preferred way to authenticate is by _creating_ and _installing_ a GitHub App. This is the recommended approach as it +allows for much more restricted access than using a Personal Access Token (classic), and the Action Runners do not +currently support using a fine-grained Personal Access Token. #### Site note about SSM and Regions -This component supports using AWS SSM to store and retrieve secrets. SSM -parameters are regional, so if you want to deploy to multiple regions -you have 2 choices: - -1. Create the secrets in each region. This is the most robust approach, but - requires you to create the secrets in each region and keep them in sync. -2. Create the secrets in one region and use the `ssm_region` input to specify - the region where they are stored. This is the easiest approach, but does add - some obstacles to managing deployments during a region outage. If the region - where the secrets are stored goes down, there will be no impact on runners in - other regions, but you will not be able to deploy new runners or modify - existing runners until the SSM region is restored or until you set up SSM - parameters in a new region. - -Alternatively, you can create Kubernetes secrets outside of this component -(perhaps using [SOPS](https://github.com/getsops/sops)) and reference them by -name. We describe here how to save the secrets to SSM, but you can save the -secrets wherever and however you want to, as long as you deploy them as -Kubernetes secret the runners can reference. If you store them in SSM, this -component will take care of the rest, but the standard Terraform caveat applies: -any secrets referenced by Terraform will be stored unencrypted in the Terraform -state file. +This component supports using AWS SSM to store and retrieve secrets. SSM parameters are regional, so if you want to +deploy to multiple regions you have 2 choices: + +1. Create the secrets in each region. This is the most robust approach, but requires you to create the secrets in each + region and keep them in sync. +2. Create the secrets in one region and use the `ssm_region` input to specify the region where they are stored. This is + the easiest approach, but does add some obstacles to managing deployments during a region outage. If the region where + the secrets are stored goes down, there will be no impact on runners in other regions, but you will not be able to + deploy new runners or modify existing runners until the SSM region is restored or until you set up SSM parameters in + a new region. + +Alternatively, you can create Kubernetes secrets outside of this component (perhaps using +[SOPS](https://github.com/getsops/sops)) and reference them by name. We describe here how to save the secrets to SSM, +but you can save the secrets wherever and however you want to, as long as you deploy them as Kubernetes secret the +runners can reference. If you store them in SSM, this component will take care of the rest, but the standard Terraform +caveat applies: any secrets referenced by Terraform will be stored unencrypted in the Terraform state file. #### Creating and Using a GitHub App -Follow the instructions [here](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-github-app) to create and install a GitHub App -for the runners to use for authentication. +Follow the instructions +[here](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-github-app) +to create and install a GitHub App for the runners to use for authentication. -At the App creation stage, you will be asked to generate a private key. This is -the private key that will be used to authenticate the Action Runner. Download -the file and store the contents in SSM using the following command, adjusting -the profile, region, and file name. The profile should be the `terraform` role in the -account to which you are deploying the runner controller. The region should be -the region where you are deploying the primary runner controller. If you are -deploying runners to multiple regions, they can all reference the same SSM -parameter by using the `ssm_region` input to specify the region where they are -stored. The file name (argument to `cat`) should be the name of the private key -file you downloaded. +At the App creation stage, you will be asked to generate a private key. This is the private key that will be used to +authenticate the Action Runner. Download the file and store the contents in SSM using the following command, adjusting +the profile, region, and file name. The profile should be the `terraform` role in the account to which you are deploying +the runner controller. The region should be the region where you are deploying the primary runner controller. If you are +deploying runners to multiple regions, they can all reference the same SSM parameter by using the `ssm_region` input to +specify the region where they are stored. The file name (argument to `cat`) should be the name of the private key file +you downloaded. ``` # Adjust profile name and region to suit your environment, use file name you chose for key @@ -250,17 +235,17 @@ You can verify the file was correctly written to SSM by matching the private key AWS_PROFILE=acme-core-gbl-auto-terraform AWS_REGION=us-west-2 chamber read -q github-action-runners github-auth-secret | openssl rsa -in - -pubout -outform DER | openssl sha256 -binary | openssl base64 ``` -At this stage, record the Application ID and the private key fingerprint in your secrets manager (e.g. 1Password). -You may want to record the private key as well, or you may consider it sufficient to have it in SSM. -You will need the Application ID to configure the runner controller, and want the fingerprint to verify the private key. -(You can see the fingerprint in the GitHub App settings, under "Private keys".) +At this stage, record the Application ID and the private key fingerprint in your secrets manager (e.g. 1Password). You +may want to record the private key as well, or you may consider it sufficient to have it in SSM. You will need the +Application ID to configure the runner controller, and want the fingerprint to verify the private key. (You can see the +fingerprint in the GitHub App settings, under "Private keys".) -Proceed to install the GitHub App in the organization or repository you want to use the runner controller for, -and record the Installation ID (the final numeric part of the URL, as explained in the instructions -linked above) in your secrets manager. You will need the Installation ID to configure the runner controller. +Proceed to install the GitHub App in the organization or repository you want to use the runner controller for, and +record the Installation ID (the final numeric part of the URL, as explained in the instructions linked above) in your +secrets manager. You will need the Installation ID to configure the runner controller. -In your stack configuration, set the following variables, making sure to quote the values so they are -treated as strings, not numbers. +In your stack configuration, set the following variables, making sure to quote the values so they are treated as +strings, not numbers. ``` github_app_id: "12345" @@ -269,10 +254,11 @@ github_app_installation_id: "12345" #### OR (obsolete): Creating and Using a Personal Access Token (classic) -Though not recommended, you can use a Personal Access Token (classic) to -authenticate the runners. To do so, create a PAT (classic) as described in the [GitHub Documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-personal-access-token-classic). -Save this to the value specified by `ssm_github_token_path` using the following command, adjusting the - AWS profile and region as explained above: +Though not recommended, you can use a Personal Access Token (classic) to authenticate the runners. To do so, create a +PAT (classic) as described in the +[GitHub Documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/authenticating-to-the-github-api#authenticating-arc-with-a-personal-access-token-classic). +Save this to the value specified by `ssm_github_token_path` using the following command, adjusting the AWS profile and +region as explained above: ``` AWS_PROFILE=acme-core-gbl-auto-terraform AWS_REGION=us-west-2 chamber write github-action-runners github-auth-secret -- "" @@ -280,22 +266,23 @@ AWS_PROFILE=acme-core-gbl-auto-terraform AWS_REGION=us-west-2 chamber write gith ### Using Runner Groups -GitHub supports grouping runners into distinct [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups), which allow you to have different access controls -for different runners. Read the linked documentation about creating and configuring Runner Groups, which you must do -through the GitHub Web UI. If you choose to create Runner Groups, you can assign one or more Runner Sets (from the -`runners` map) to groups (only one group per runner set, but multiple sets can be in the same group) by including -`group: ` in the runner configuration. We recommend including it immediately after `github_url`. - +GitHub supports grouping runners into distinct +[Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups), +which allow you to have different access controls for different runners. Read the linked documentation about creating +and configuring Runner Groups, which you must do through the GitHub Web UI. If you choose to create Runner Groups, you +can assign one or more Runner Sets (from the `runners` map) to groups (only one group per runner set, but multiple sets +can be in the same group) by including `group: ` in the runner configuration. We recommend including +it immediately after `github_url`. ### Interaction with Karpenter or other EKS autoscaling solutions -Kubernetes cluster autoscaling solutions generally expect that a Pod runs a service that can be terminated on one -Node and restarted on another with only a short duration needed to finish processing any in-flight requests. When -the cluster is resized, the cluster autoscaler will do just that. However, GitHub Action Runner Jobs do not fit this -model. If a Pod is terminated in the middle of a job, the job is lost. The likelihood of this happening is increased -by the fact that the Action Runner Controller Autoscaler is expanding and contracting the size of the Runner Pool on -a regular basis, causing the cluster autoscaler to more frequently want to scale up or scale down the EKS cluster, -and, consequently, to move Pods around. +Kubernetes cluster autoscaling solutions generally expect that a Pod runs a service that can be terminated on one Node +and restarted on another with only a short duration needed to finish processing any in-flight requests. When the cluster +is resized, the cluster autoscaler will do just that. However, GitHub Action Runner Jobs do not fit this model. If a Pod +is terminated in the middle of a job, the job is lost. The likelihood of this happening is increased by the fact that +the Action Runner Controller Autoscaler is expanding and contracting the size of the Runner Pool on a regular basis, +causing the cluster autoscaler to more frequently want to scale up or scale down the EKS cluster, and, consequently, to +move Pods around. To handle these kinds of situations, Karpenter respects an annotation on the Pod: @@ -307,63 +294,61 @@ spec: karpenter.sh/do-not-evict: "true" ``` -When you set this annotation on the Pod, Karpenter will not voluntarily evict it. This means that the Pod will stay on the Node -it is on, and the Node it is on will not be considered for deprovisioning (scale down). This is good because it means that the Pod -will not be terminated in the middle of a job. However, it also means that the Node the Pod is on will remain running -until the Pod is terminated, even if the node is underutilized and Karpenter would like to get rid of it. +When you set this annotation on the Pod, Karpenter will not voluntarily evict it. This means that the Pod will stay on +the Node it is on, and the Node it is on will not be considered for deprovisioning (scale down). This is good because it +means that the Pod will not be terminated in the middle of a job. However, it also means that the Node the Pod is on +will remain running until the Pod is terminated, even if the node is underutilized and Karpenter would like to get rid +of it. Since the Runner Pods terminate at the end of the job, this is not a problem for the Pods actually running jobs. However, if you have set `minReplicas > 0`, then you have some Pods that are just idling, waiting for jobs to be -assigned to them. These Pods are exactly the kind of Pods you want terminated and moved when the cluster is underutilized. -Therefore, when you set `minReplicas > 0`, you should **NOT** set `karpenter.sh/do-not-evict: "true"` on the Pod. - +assigned to them. These Pods are exactly the kind of Pods you want terminated and moved when the cluster is +underutilized. Therefore, when you set `minReplicas > 0`, you should **NOT** set `karpenter.sh/do-not-evict: "true"` on +the Pod. ### Updating CRDs -When updating the chart or application version -of `gha-runner-scale-set-controller`, it is possible you will need to install -new CRDs. Such a requirement should be indicated in -the `gha-runner-scale-set-controller` release notes and may require some -adjustment to this component. - -This component uses `helm` to manage the deployment, and `helm` will not auto-update CRDs. -If new CRDs are needed, follow the instructions in the release notes for the Helm chart -or `gha-runner-scale-set-controller` itself. - +When updating the chart or application version of `gha-runner-scale-set-controller`, it is possible you will need to +install new CRDs. Such a requirement should be indicated in the `gha-runner-scale-set-controller` release notes and may +require some adjustment to this component. +This component uses `helm` to manage the deployment, and `helm` will not auto-update CRDs. If new CRDs are needed, +follow the instructions in the release notes for the Helm chart or `gha-runner-scale-set-controller` itself. ### Useful Reference -- Runner Scale Set Controller's Helm chart [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set-controller/values.yaml) -- Runner Scale Set's Helm chart [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml) -- Runner Scale Set's [Docker image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image) and [how to create your own](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image) - -When reviewing documentation, code, issues, etc. for self-hosted GitHub action runners -or the Actions Runner Controller (ARC), keep in mind that there are 2 implementations -going by that name. The original implementation, which is now deprecated, uses -the `actions.summerwind.dev` API group, and is at times called the Summerwind -or Legacy implementation. It is primarily described by documentation in the -[actions/actions-runner-controller](https://github.com/actions/actions-runner-controller) -GitHub repository itself. - -The new implementation, which is the one this component -uses, uses the `actions.github.com` API group, and is at times called the GitHub -implementation or "Runner Scale Sets" implementation. The new implementation is -described in the official [GitHub documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller). - -Feature requests about the new implementation are officially -directed to the [Actions category of GitHub community discussion](https://github.com/orgs/community/discussions/categories/actions). -However, Q&A and community support is directed to the `actions/actions-runner-controller` -repo's [Discussion section](https://github.com/actions/actions-runner-controller/discussions), -though beware that discussions about the old implementation are mixed in with -discussions about the new implementation. - -Bug reports for the new implementation are still filed under the `actions/actions-runner-controller` -repo's [Issues](https://github.com/actions/actions-runner-controller/issues) tab, -though again, these are mixed in with bug reports for the old implementation. -Look for the `gha-runner-scale-set` label to find issues specific to the new +- Runner Scale Set Controller's Helm chart + [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set-controller/values.yaml) +- Runner Scale Set's Helm chart + [values.yaml](https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml) +- Runner Scale Set's + [Docker image](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#about-the-runner-container-image) + and + [how to create your own](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller#creating-your-own-runner-image) + +When reviewing documentation, code, issues, etc. for self-hosted GitHub action runners or the Actions Runner Controller +(ARC), keep in mind that there are 2 implementations going by that name. The original implementation, which is now +deprecated, uses the `actions.summerwind.dev` API group, and is at times called the Summerwind or Legacy implementation. +It is primarily described by documentation in the +[actions/actions-runner-controller](https://github.com/actions/actions-runner-controller) GitHub repository itself. + +The new implementation, which is the one this component uses, uses the `actions.github.com` API group, and is at times +called the GitHub implementation or "Runner Scale Sets" implementation. The new implementation is described in the +official +[GitHub documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller). + +Feature requests about the new implementation are officially directed to the +[Actions category of GitHub community discussion](https://github.com/orgs/community/discussions/categories/actions). +However, Q&A and community support is directed to the `actions/actions-runner-controller` repo's +[Discussion section](https://github.com/actions/actions-runner-controller/discussions), though beware that discussions +about the old implementation are mixed in with discussions about the new implementation. + +Bug reports for the new implementation are still filed under the `actions/actions-runner-controller` repo's +[Issues](https://github.com/actions/actions-runner-controller/issues) tab, though again, these are mixed in with bug +reports for the old implementation. Look for the `gha-runner-scale-set` label to find issues specific to the new implementation. + ## Requirements @@ -462,10 +447,12 @@ implementation. | [metadata](#output\_metadata) | Block status of the deployed release | | [runners](#output\_runners) | Human-readable summary of the deployed runners | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) - + Cloud Posse's upstream component - [alb-controller](https://artifacthub.io/packages/helm/aws/aws-load-balancer-controller) - Helm Chart - [alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) - AWS Load Balancer Controller - [actions-runner-controller Webhook Driven Scaling](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/docs/detailed-docs.md#webhook-driven-scaling) diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index d173a768b..a1acb1f44 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -1,6 +1,7 @@ # Component: `eks/idp-roles` -This component installs the `idp-roles` for EKS clusters. These identity provider roles specify severl pre-determined permission levels for cluster users and come with bindings that make them easy to assign to Users and Groups. +This component installs the `idp-roles` for EKS clusters. These identity provider roles specify severl pre-determined +permission levels for cluster users and come with bindings that make them easy to assign to Users and Groups. ## Usage @@ -21,6 +22,7 @@ components: kubeconfig_exec_auth_api_version: "client.authentication.k8s.io/v1beta1" ``` + ## Requirements @@ -105,6 +107,8 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References -* https://kubernetes.io/docs/reference/access-authn-authz/authentication/ + +- https://kubernetes.io/docs/reference/access-authn-authz/authentication/ diff --git a/modules/eks/karpenter-provisioner/README.md b/modules/eks/karpenter-provisioner/README.md index eb2323119..88dbdc0ef 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/modules/eks/karpenter-provisioner/README.md @@ -6,7 +6,8 @@ This component deploys [Karpenter provisioners](https://karpenter.sh/v0.18.0/aws **Stack Level**: Regional -If provisioning more than one provisioner, it is [best practice](https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive-or-weighted) +If provisioning more than one provisioner, it is +[best practice](https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive-or-weighted) to create provisioners that are mutually exclusive or weighted. ```yaml @@ -42,63 +43,64 @@ components: # and capacity type (such as AWS spot or on-demand). # See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details requirements: - - key: "karpenter.k8s.aws/instance-category" - operator: "In" - values: ["c", "m", "r"] - - key: "karpenter.k8s.aws/instance-generation" - operator: "Gt" - values: ["2"] - - key: "karpenter.sh/capacity-type" - operator: "In" - values: - - "on-demand" - - "spot" - - key: "node.kubernetes.io/instance-type" - operator: "In" - # See https://aws.amazon.com/ec2/instance-explorer/ and https://aws.amazon.com/ec2/instance-types/ - # Values limited by DenyEC2InstancesWithoutEncryptionInTransit service control policy - # See https://github.com/cloudposse/terraform-aws-service-control-policies/blob/master/catalog/ec2-policies.yaml - # Karpenter recommends allowing at least 20 instance types to ensure availability. - values: - - "c5n.2xlarge" - - "c5n.xlarge" - - "c5n.large" - - "c6i.2xlarge" - - "c6i.xlarge" - - "c6i.large" - - "m5n.2xlarge" - - "m5n.xlarge" - - "m5n.large" - - "m5zn.2xlarge" - - "m5zn.xlarge" - - "m5zn.large" - - "m6i.2xlarge" - - "m6i.xlarge" - - "m6i.large" - - "r5n.2xlarge" - - "r5n.xlarge" - - "r5n.large" - - "r6i.2xlarge" - - "r6i.xlarge" - - "r6i.large" - - key: "kubernetes.io/arch" - operator: "In" - values: - - "amd64" + - key: "karpenter.k8s.aws/instance-category" + operator: "In" + values: ["c", "m", "r"] + - key: "karpenter.k8s.aws/instance-generation" + operator: "Gt" + values: ["2"] + - key: "karpenter.sh/capacity-type" + operator: "In" + values: + - "on-demand" + - "spot" + - key: "node.kubernetes.io/instance-type" + operator: "In" + # See https://aws.amazon.com/ec2/instance-explorer/ and https://aws.amazon.com/ec2/instance-types/ + # Values limited by DenyEC2InstancesWithoutEncryptionInTransit service control policy + # See https://github.com/cloudposse/terraform-aws-service-control-policies/blob/master/catalog/ec2-policies.yaml + # Karpenter recommends allowing at least 20 instance types to ensure availability. + values: + - "c5n.2xlarge" + - "c5n.xlarge" + - "c5n.large" + - "c6i.2xlarge" + - "c6i.xlarge" + - "c6i.large" + - "m5n.2xlarge" + - "m5n.xlarge" + - "m5n.large" + - "m5zn.2xlarge" + - "m5zn.xlarge" + - "m5zn.large" + - "m6i.2xlarge" + - "m6i.xlarge" + - "m6i.large" + - "r5n.2xlarge" + - "r5n.xlarge" + - "r5n.large" + - "r6i.2xlarge" + - "r6i.xlarge" + - "r6i.large" + - key: "kubernetes.io/arch" + operator: "In" + values: + - "amd64" # The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM) # Bottlerocket, AL2, Ubuntu # https://karpenter.sh/v0.18.0/aws/provisioning/#amazon-machine-image-ami-family ami_family: AL2 # Karpenter provisioner block device mappings. block_device_mappings: - - deviceName: /dev/xvda - ebs: - volumeSize: 200Gi - volumeType: gp3 - encrypted: true - deleteOnTermination: true + - deviceName: /dev/xvda + ebs: + volumeSize: 200Gi + volumeType: gp3 + encrypted: true + deleteOnTermination: true ``` + ## Requirements @@ -177,6 +179,7 @@ components: | [providers](#output\_providers) | Deployed Karpenter AWSNodeTemplates | | [provisioners](#output\_provisioners) | Deployed Karpenter provisioners | + ## References diff --git a/modules/eks/karpenter/CHANGELOG.md b/modules/eks/karpenter/CHANGELOG.md index 14dd2a56b..f3ff1cc20 100644 --- a/modules/eks/karpenter/CHANGELOG.md +++ b/modules/eks/karpenter/CHANGELOG.md @@ -2,23 +2,36 @@ Components PR [#868](https://github.com/cloudposse/terraform-aws-components/pull/868) -The `karpenter-crd` helm chart can now be installed alongside the `karpenter` helm chart to automatically manage the lifecycle of Karpenter CRDs. However since this chart must be installed before the `karpenter` helm chart, the Kubernetes namespace must be available before either chart is deployed. Furthermore, this namespace should persist whether or not the `karpenter-crd` chart is deployed, so it should not be installed with that given `helm-release` resource. Therefore, we've moved namespace creation to a separate resource that runs before both charts. Terraform will handle that namespace state migration with the `moved` block. +The `karpenter-crd` helm chart can now be installed alongside the `karpenter` helm chart to automatically manage the +lifecycle of Karpenter CRDs. However since this chart must be installed before the `karpenter` helm chart, the +Kubernetes namespace must be available before either chart is deployed. Furthermore, this namespace should persist +whether or not the `karpenter-crd` chart is deployed, so it should not be installed with that given `helm-release` +resource. Therefore, we've moved namespace creation to a separate resource that runs before both charts. Terraform will +handle that namespace state migration with the `moved` block. -There are several scenarios that may or may not require additional steps. Please review the following scenarios and follow the steps for your given requirements. +There are several scenarios that may or may not require additional steps. Please review the following scenarios and +follow the steps for your given requirements. ### Upgrading an existing `eks/karpenter` deployment without changes -If you currently have `eks/karpenter` deployed to an EKS cluster and have upgraded to this version of the component, no changes are required. `var.crd_chart_enabled` will default to `false`. +If you currently have `eks/karpenter` deployed to an EKS cluster and have upgraded to this version of the component, no +changes are required. `var.crd_chart_enabled` will default to `false`. ### Upgrading an existing `eks/karpenter` deployment and deploying the `karpenter-crd` chart -If you currently have `eks/karpenter` deployed to an EKS cluster, have upgraded to this version of the component, do not currently have the `karpenter-crd` chart installed, and want to now deploy the `karpenter-crd` helm chart, a few additional steps are required! +If you currently have `eks/karpenter` deployed to an EKS cluster, have upgraded to this version of the component, do not +currently have the `karpenter-crd` chart installed, and want to now deploy the `karpenter-crd` helm chart, a few +additional steps are required! First, set `var.crd_chart_enabled` to `true`. -Next, update the installed Karpenter CRDs in order for Helm to automatically take over their management when the `karpenter-crd` chart is deployed. We have included a script to run that upgrade. Run the `./karpenter-crd-upgrade` script or run the following commands on the given cluster before deploying the chart. Please note that this script or commands will only need to be run on first use of the CRD chart. +Next, update the installed Karpenter CRDs in order for Helm to automatically take over their management when the +`karpenter-crd` chart is deployed. We have included a script to run that upgrade. Run the `./karpenter-crd-upgrade` +script or run the following commands on the given cluster before deploying the chart. Please note that this script or +commands will only need to be run on first use of the CRD chart. -Before running the script, ensure that the `kubectl` context is set to the cluster where the `karpenter` helm chart is deployed. In Geodesic, you can usually do this with the `set-cluster` command, though your configuration may vary. +Before running the script, ensure that the `kubectl` context is set to the cluster where the `karpenter` helm chart is +deployed. In Geodesic, you can usually do this with the `set-cluster` command, though your configuration may vary. ```bash set-cluster -- terraform @@ -34,33 +47,39 @@ kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.s :::info -Previously the `karpenter-crd-upgrade` script included deploying the `karpenter-crd` chart. Now that this chart is moved to Terraform, that helm deployment is no longer necessary. +Previously the `karpenter-crd-upgrade` script included deploying the `karpenter-crd` chart. Now that this chart is moved +to Terraform, that helm deployment is no longer necessary. For reference, the `karpenter-crd` chart can be installed with helm with the following: + ```bash helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version "$VERSION" --namespace karpenter ``` ::: -Now that the CRDs are upgraded, the component is ready to be applied. Apply the `eks/karpenter` component and then apply `eks/karpenter-provisioner`. +Now that the CRDs are upgraded, the component is ready to be applied. Apply the `eks/karpenter` component and then apply +`eks/karpenter-provisioner`. #### Note for upgrading Karpenter from before v0.27.3 to v0.27.3 or later -If you are upgrading Karpenter from before v0.27.3 to v0.27.3 or later, -you may need to run the following command to remove an obsolete webhook: +If you are upgrading Karpenter from before v0.27.3 to v0.27.3 or later, you may need to run the following command to +remove an obsolete webhook: ```bash kubectl delete mutatingwebhookconfigurations defaulting.webhook.karpenter.sh ``` -See [the Karpenter upgrade guide](https://karpenter.sh/v0.32/upgrading/upgrade-guide/#upgrading-to-v0273) -for more details. +See [the Karpenter upgrade guide](https://karpenter.sh/v0.32/upgrading/upgrade-guide/#upgrading-to-v0273) for more +details. ### Upgrading an existing `eks/karpenter` deployment where the `karpenter-crd` chart is already deployed -If you currently have `eks/karpenter` deployed to an EKS cluster, have upgraded to this version of the component, and already have the `karpenter-crd` chart installed, simply set `var.crd_chart_enabled` to `true` and redeploy Terraform to have Terraform manage the helm release for `karpenter-crd`. +If you currently have `eks/karpenter` deployed to an EKS cluster, have upgraded to this version of the component, and +already have the `karpenter-crd` chart installed, simply set `var.crd_chart_enabled` to `true` and redeploy Terraform to +have Terraform manage the helm release for `karpenter-crd`. ### Net new deployments -If you are initially deploying `eks/karpenter`, no changes are required, but we recommend installing the CRD chart. Set `var.crd_chart_enabled` to `true` and continue with deployment. +If you are initially deploying `eks/karpenter`, no changes are required, but we recommend installing the CRD chart. Set +`var.crd_chart_enabled` to `true` and continue with deployment. diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index b4be40954..1437929e2 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -1,22 +1,19 @@ # Component: `eks/karpenter` -This component provisions [Karpenter](https://karpenter.sh) on an EKS cluster. -It requires at least version 0.19.0 of Karpenter, though you are encouraged to -use the latest version. +This component provisions [Karpenter](https://karpenter.sh) on an EKS cluster. It requires at least version 0.19.0 of +Karpenter, though you are encouraged to use the latest version. ## Usage **Stack Level**: Regional -These instructions assume you are provisioning 2 EKS clusters in the same account -and region, named "blue" and "green", and alternating between them. -If you are only using a single cluster, you can ignore the "blue" and "green" -references and remove the `metadata` block from the `karpenter` module. +These instructions assume you are provisioning 2 EKS clusters in the same account and region, named "blue" and "green", +and alternating between them. If you are only using a single cluster, you can ignore the "blue" and "green" references +and remove the `metadata` block from the `karpenter` module. ```yaml components: terraform: - # Base component of all `karpenter` components eks/karpenter: metadata: @@ -63,26 +60,24 @@ components: ## Provision Karpenter on EKS cluster -Here we describe how to provision Karpenter on an EKS cluster. -We will be using the `plat-ue2-dev` stack as an example. +Here we describe how to provision Karpenter on an EKS cluster. We will be using the `plat-ue2-dev` stack as an example. ### Provision Service-Linked Roles for EC2 Spot and EC2 Spot Fleet -__Note:__ If you want to use EC2 Spot for the instances launched by Karpenter, -you may need to provision the following Service-Linked Role for EC2 Spot: +**Note:** If you want to use EC2 Spot for the instances launched by Karpenter, you may need to provision the following +Service-Linked Role for EC2 Spot: - Service-Linked Role for EC2 Spot -This is only necessary if this is the first time you're using EC2 Spot in the account. -Since this is a one-time operation, we recommend you do this manually via -the AWS CLI: +This is only necessary if this is the first time you're using EC2 Spot in the account. Since this is a one-time +operation, we recommend you do this manually via the AWS CLI: ```bash aws --profile --gbl--admin iam create-service-linked-role --aws-service-name spot.amazonaws.com ``` -Note that if the Service-Linked Roles already exist in the AWS account (if you used EC2 Spot or Spot Fleet before), -and you try to provision them again, you will see the following errors: +Note that if the Service-Linked Roles already exist in the AWS account (if you used EC2 Spot or Spot Fleet before), and +you try to provision them again, you will see the following errors: ```text An error occurred (InvalidInput) when calling the CreateServiceLinkedRole operation: @@ -90,14 +85,16 @@ Service role name AWSServiceRoleForEC2Spot has been taken in this account, pleas ``` For more details, see: - - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html - - https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html + +- https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html +- https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html The process of provisioning Karpenter on an EKS cluster consists of 3 steps. ### 1. Provision EKS Fargate Profile for Karpenter and IAM Role for Nodes Launched by Karpenter -EKS Fargate Profile for Karpenter and IAM Role for Nodes launched by Karpenter are provisioned by the `eks/cluster` component: +EKS Fargate Profile for Karpenter and IAM Role for Nodes launched by Karpenter are provisioned by the `eks/cluster` +component: ```yaml components: @@ -124,9 +121,12 @@ components: karpenter_iam_role_enabled: true ``` -__Notes__: - - Fargate Profile role ARNs need to be added to the `aws-auth` ConfigMap to allow the Fargate Profile nodes to join the EKS cluster (this is done by EKS) - - Karpenter IAM role ARN needs to be added to the `aws-auth` ConfigMap to allow the nodes launched by Karpenter to join the EKS cluster (this is done by the `eks/cluster` component) +**Notes**: + +- Fargate Profile role ARNs need to be added to the `aws-auth` ConfigMap to allow the Fargate Profile nodes to join the + EKS cluster (this is done by EKS) +- Karpenter IAM role ARN needs to be added to the `aws-auth` ConfigMap to allow the nodes launched by Karpenter to join + the EKS cluster (this is done by the `eks/cluster` component) We use EKS Fargate Profile for Karpenter because It is recommended to run Karpenter on an EKS Fargate Profile. @@ -140,7 +140,8 @@ karpenter namespace. Doing so will cause all pods deployed into this namespace t Do not run Karpenter on a node that is managed by Karpenter. ``` -See [Run Karpenter Controller on EKS Fargate](https://aws.github.io/aws-eks-best-practices/karpenter/#run-the-karpenter-controller-on-eks-fargate-or-on-a-worker-node-that-belongs-to-a-node-group) +See +[Run Karpenter Controller on EKS Fargate](https://aws.github.io/aws-eks-best-practices/karpenter/#run-the-karpenter-controller-on-eks-fargate-or-on-a-worker-node-that-belongs-to-a-node-group) for more details. We provision IAM Role for Nodes launched by Karpenter because they must run with an Instance Profile that grants @@ -148,10 +149,11 @@ permissions necessary to run containers and configure networking. We define the IAM role for the Instance Profile in `components/terraform/eks/cluster/karpenter.tf`. -Note that we provision the EC2 Instance Profile for the Karpenter IAM role in the `components/terraform/eks/karpenter` component (see the next step). +Note that we provision the EC2 Instance Profile for the Karpenter IAM role in the `components/terraform/eks/karpenter` +component (see the next step). -Run the following commands to provision the EKS Fargate Profile for Karpenter and the IAM role for instances launched by Karpenter -on the blue EKS cluster and add the role ARNs to the `aws-auth` ConfigMap: +Run the following commands to provision the EKS Fargate Profile for Karpenter and the IAM role for instances launched by +Karpenter on the blue EKS cluster and add the role ARNs to the `aws-auth` ConfigMap: ```bash atmos terraform plan eks/cluster-blue -s plat-ue2-dev @@ -163,14 +165,14 @@ For more details, refer to: - https://karpenter.sh/v0.18.0/getting-started/getting-started-with-terraform - https://karpenter.sh/v0.18.0/getting-started/getting-started-with-eksctl - ### 2. Provision `karpenter` component In this step, we provision the `components/terraform/eks/karpenter` component, which deploys the following resources: - - EC2 Instance Profile for the nodes launched by Karpenter (note that the IAM role for the Instance Profile is provisioned in the previous step in the `eks/cluster` component) - - Karpenter Kubernetes controller using the Karpenter Helm Chart and the `helm_release` Terraform resource - - EKS IAM role for Kubernetes Service Account for the Karpenter controller (with all the required permissions) +- EC2 Instance Profile for the nodes launched by Karpenter (note that the IAM role for the Instance Profile is + provisioned in the previous step in the `eks/cluster` component) +- Karpenter Kubernetes controller using the Karpenter Helm Chart and the `helm_release` Terraform resource +- EKS IAM role for Kubernetes Service Account for the Karpenter controller (with all the required permissions) Run the following commands to provision the Karpenter component on the blue EKS cluster: @@ -182,22 +184,22 @@ atmos terraform apply eks/karpenter-blue -s plat-ue2-dev Note that the stack config for the blue Karpenter component is defined in `stacks/catalog/eks/clusters/blue.yaml`. ```yaml - eks/karpenter-blue: - metadata: - component: eks/karpenter - inherits: - - eks/karpenter - vars: - eks_component_name: eks/cluster-blue +eks/karpenter-blue: + metadata: + component: eks/karpenter + inherits: + - eks/karpenter + vars: + eks_component_name: eks/cluster-blue ``` ### 3. Provision `karpenter-provisioner` component -In this step, we provision the `components/terraform/eks/karpenter-provisioner` component, which deploys Karpenter [Provisioners](https://karpenter.sh/v0.18.0/aws/provisioning) -using the `kubernetes_manifest` resource. +In this step, we provision the `components/terraform/eks/karpenter-provisioner` component, which deploys Karpenter +[Provisioners](https://karpenter.sh/v0.18.0/aws/provisioning) using the `kubernetes_manifest` resource. -__NOTE:__ We deploy the provisioners in a separate step as a separate component since it uses `kind: Provisioner` CRD which itself is created by -the `karpenter` component in the previous step. +**NOTE:** We deploy the provisioners in a separate step as a separate component since it uses `kind: Provisioner` CRD +which itself is created by the `karpenter` component in the previous step. Run the following commands to deploy the Karpenter provisioners on the blue EKS cluster: @@ -206,84 +208,90 @@ atmos terraform plan eks/karpenter-provisioner-blue -s plat-ue2-dev atmos terraform apply eks/karpenter-provisioner-blue -s plat-ue2-dev ``` -Note that the stack config for the blue Karpenter provisioner component is defined in `stacks/catalog/eks/clusters/blue.yaml`. +Note that the stack config for the blue Karpenter provisioner component is defined in +`stacks/catalog/eks/clusters/blue.yaml`. ```yaml - eks/karpenter-provisioner-blue: - metadata: - component: eks/karpenter-provisioner - inherits: - - eks/karpenter-provisioner - vars: - attributes: - - blue - eks_component_name: eks/cluster-blue +eks/karpenter-provisioner-blue: + metadata: + component: eks/karpenter-provisioner + inherits: + - eks/karpenter-provisioner + vars: + attributes: + - blue + eks_component_name: eks/cluster-blue ``` You can override the default values from the `eks/karpenter-provisioner` base component. -For your cluster, you will need to review the following configurations for the Karpenter provisioners and update it according to your requirements: - - - [requirements](https://karpenter.sh/v0.18.0/provisioner/#specrequirements): - - ```yaml - requirements: - - key: "karpenter.sh/capacity-type" - operator: "In" - values: - - "on-demand" - - "spot" - - key: "node.kubernetes.io/instance-type" - operator: "In" - values: - - "m5.xlarge" - - "m5.large" - - "m5.medium" - - "c5.xlarge" - - "c5.large" - - "c5.medium" - - key: "kubernetes.io/arch" - operator: "In" - values: - - "amd64" - ``` - - - `taints`, `startup_taints`, `ami_family` - - - Resource limits/requests for the Karpenter controller itself: - - ```yaml - resources: - limits: - cpu: "300m" - memory: "1Gi" - requests: - cpu: "100m" - memory: "512Mi" - ``` - - - Total CPU and memory limits for all pods running on the EC2 instances launched by Karpenter: - - ```yaml - total_cpu_limit: "1k" - total_memory_limit: "1000Gi" - ``` - - - Config to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the value to `null` (never scales down if not set): - - ```yaml - ttl_seconds_after_empty: 30 - ``` - - - Config to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` (never expires if not set): - - ```yaml - ttl_seconds_until_expired: 2592000 - ``` +For your cluster, you will need to review the following configurations for the Karpenter provisioners and update it +according to your requirements: + +- [requirements](https://karpenter.sh/v0.18.0/provisioner/#specrequirements): + + ```yaml + requirements: + - key: "karpenter.sh/capacity-type" + operator: "In" + values: + - "on-demand" + - "spot" + - key: "node.kubernetes.io/instance-type" + operator: "In" + values: + - "m5.xlarge" + - "m5.large" + - "m5.medium" + - "c5.xlarge" + - "c5.large" + - "c5.medium" + - key: "kubernetes.io/arch" + operator: "In" + values: + - "amd64" + ``` + +- `taints`, `startup_taints`, `ami_family` + +- Resource limits/requests for the Karpenter controller itself: + + ```yaml + resources: + limits: + cpu: "300m" + memory: "1Gi" + requests: + cpu: "100m" + memory: "512Mi" + ``` + +- Total CPU and memory limits for all pods running on the EC2 instances launched by Karpenter: + + ```yaml + total_cpu_limit: "1k" + total_memory_limit: "1000Gi" + ``` + +- Config to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the + value to `null` (never scales down if not set): + + ```yaml + ttl_seconds_after_empty: 30 + ``` + +- Config to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` + (never expires if not set): + + ```yaml + ttl_seconds_until_expired: 2592000 + ``` ## Node Interruption -Karpenter also supports listening for and responding to Node Interruption events. If interruption handling is enabled, Karpenter will watch for upcoming involuntary interruption events that would cause disruption to your workloads. These interruption events include: +Karpenter also supports listening for and responding to Node Interruption events. If interruption handling is enabled, +Karpenter will watch for upcoming involuntary interruption events that would cause disruption to your workloads. These +interruption events include: - Spot Interruption Warnings - Scheduled Change Health Events (Maintenance Events) @@ -292,25 +300,29 @@ Karpenter also supports listening for and responding to Node Interruption events :::info -The Node Interruption Handler is not the same as the Node Termination Handler. The latter is always enabled and cleanly shuts down the node in 2 minutes in response to a Node Termination event. The former gets advance notice that a node will soon be terminated, so it can have 5-10 minutes to shut down a node. +The Node Interruption Handler is not the same as the Node Termination Handler. The latter is always enabled and cleanly +shuts down the node in 2 minutes in response to a Node Termination event. The former gets advance notice that a node +will soon be terminated, so it can have 5-10 minutes to shut down a node. ::: -For more details, see refer to the [Karpenter docs](https://karpenter.sh/v0.32/concepts/disruption/#interruption) and [FAQ](https://karpenter.sh/v0.32/faq/#interruption-handling) +For more details, see refer to the [Karpenter docs](https://karpenter.sh/v0.32/concepts/disruption/#interruption) and +[FAQ](https://karpenter.sh/v0.32/faq/#interruption-handling) -To enable Node Interruption handling, set `var.interruption_handler_enabled` to `true`. This will create an SQS queue and a set of Event Bridge rules to deliver interruption events to Karpenter. +To enable Node Interruption handling, set `var.interruption_handler_enabled` to `true`. This will create an SQS queue +and a set of Event Bridge rules to deliver interruption events to Karpenter. ## Custom Resource Definition (CRD) Management -Karpenter ships with a few Custom Resource Definitions (CRDs). In earlier versions -of this component, when installing a new version of the `karpenter` helm chart, CRDs -were not be upgraded at the same time, requiring manual steps to upgrade CRDs after deploying the latest chart. -However Karpenter now supports an additional, independent helm chart for CRD management. -This helm chart, `karpenter-crd`, can be installed alongside the `karpenter` helm chart to automatically manage the lifecycle of these CRDs. +Karpenter ships with a few Custom Resource Definitions (CRDs). In earlier versions of this component, when installing a +new version of the `karpenter` helm chart, CRDs were not be upgraded at the same time, requiring manual steps to upgrade +CRDs after deploying the latest chart. However Karpenter now supports an additional, independent helm chart for CRD +management. This helm chart, `karpenter-crd`, can be installed alongside the `karpenter` helm chart to automatically +manage the lifecycle of these CRDs. -To deploy the `karpenter-crd` helm chart, set `var.crd_chart_enabled` to `true`. -(Installing the `karpenter-crd` chart is recommended. `var.crd_chart_enabled` defaults -to `false` to preserve backward compatibility with older versions of this component.) +To deploy the `karpenter-crd` helm chart, set `var.crd_chart_enabled` to `true`. (Installing the `karpenter-crd` chart +is recommended. `var.crd_chart_enabled` defaults to `false` to preserve backward compatibility with older versions of +this component.) ## Troubleshooting @@ -320,14 +332,13 @@ For Karpenter issues, checkout the [Karpenter Troubleshooting Guide](https://kar For more details, refer to: - - https://karpenter.sh/v0.28.0/provisioner/#specrequirements - - https://karpenter.sh/v0.28.0/aws/provisioning - - https://aws.github.io/aws-eks-best-practices/karpenter/#creating-provisioners - - https://aws.github.io/aws-eks-best-practices/karpenter - - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html - - +- https://karpenter.sh/v0.28.0/provisioner/#specrequirements +- https://karpenter.sh/v0.28.0/aws/provisioning +- https://aws.github.io/aws-eks-best-practices/karpenter/#creating-provisioners +- https://aws.github.io/aws-eks-best-practices/karpenter +- https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html + ## Requirements @@ -430,6 +441,7 @@ For more details, refer to: | [instance\_profile](#output\_instance\_profile) | Provisioned EC2 Instance Profile for nodes launched by Karpenter | | [metadata](#output\_metadata) | Block status of the deployed release | + ## References diff --git a/modules/eks/keda/README.md b/modules/eks/keda/README.md index ca8ede1fc..b0dd0b847 100644 --- a/modules/eks/keda/README.md +++ b/modules/eks/keda/README.md @@ -24,9 +24,9 @@ components: chart_version: "2.11.2" chart_values: {} timeout: 180 - ``` + ## Requirements @@ -120,6 +120,9 @@ components: | [service\_account\_role\_name](#output\_service\_account\_role\_name) | IAM role name | | [service\_account\_role\_unique\_id](#output\_service\_account\_role\_unique\_id) | IAM role unique ID | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/keda) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/keda) - + Cloud Posse's upstream component diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 087dff862..000c4347c 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -1,6 +1,7 @@ # Component: `metrics-server` -This component creates a Helm release for [metrics-server](https://github.com/kubernetes-sigs/metrics-server) is a Kubernetes addon that provides resource usage metrics used in particular by other addons such Horizontal Pod Autoscaler. +This component creates a Helm release for [metrics-server](https://github.com/kubernetes-sigs/metrics-server) is a +Kubernetes addon that provides resource usage metrics used in particular by other addons such Horizontal Pod Autoscaler. ## Usage @@ -37,6 +38,7 @@ components: chart_values: {} ``` + ## Requirements @@ -125,6 +127,7 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References diff --git a/modules/eks/platform/README.md b/modules/eks/platform/README.md index a089db808..9c26a6c25 100644 --- a/modules/eks/platform/README.md +++ b/modules/eks/platform/README.md @@ -1,7 +1,7 @@ # Component: `eks/platform` -This component maps another components' outputs into SSM parameter store to declare -platform context used by CI/CD workflows. +This component maps another components' outputs into SSM parameter store to declare platform context used by CI/CD +workflows. ## Usage @@ -20,7 +20,7 @@ The default catalog values `e.g. stacks/catalog/eks/platform.yaml` ```yaml components: terraform: - eks/platform: + eks/platform: metadata: component: eks/platform backend: @@ -43,9 +43,10 @@ components: output: group_name ``` -That would read `group_name` from `eks/alb-controller-ingress-group` component outputs and -put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` +That would read `group_name` from `eks/alb-controller-ingress-group` component outputs and put it into +`/platform/{eks cluster name}/default/default_alb_ingress_group` + ## Requirements @@ -110,5 +111,6 @@ put it into `/platform/{eks cluster name}/default/default_alb_ingress_group` No outputs. + [](https://cpco.io/component) diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 366f618d0..478124de9 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -1,6 +1,7 @@ # Component: `eks/redis-operator` -This component installs `redis-operator` for EKS clusters. Redis Operator creates/configures/manages high availability redis with sentinel automatic failover atop Kubernetes. +This component installs `redis-operator` for EKS clusters. Redis Operator creates/configures/manages high availability +redis with sentinel automatic failover atop Kubernetes. ## Usage @@ -46,7 +47,6 @@ components: image: repository: quay.io/spotahome/redis-operator tag: v1.1.1 - ``` `stacks/catalog/eks/redis-operator/dev` file (derived component for "dev" specific settings): @@ -63,8 +63,9 @@ components: inherits: - eks/redis-operator/defaults vars: {} - ``` + + ## Requirements @@ -153,6 +154,9 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/redis-operator) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/redis-operator) - + Cloud Posse's upstream component diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index 6ac863987..e46c5ea41 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -8,7 +8,6 @@ This component installs `redis` for EKS clusters. This is a Self Hosted Redis Cl Use this in the catalog or use these variables to overwrite the catalog values. - `stacks/catalog/eks/redis/defaults` file (base component for default Redis settings): ```yaml @@ -51,7 +50,6 @@ components: # Disabling Manifest Experiment disables stored metadata with Terraform state # Otherwise, the state will show changes on all plans helm_manifest_experiment_enabled: false - ``` `stacks/catalog/eks/redis/dev` file (derived component for "dev" specific settings): @@ -68,9 +66,9 @@ components: inherits: - eks/redis/defaults vars: {} - ``` + ## Requirements @@ -159,6 +157,9 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/redis) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/redis) - + Cloud Posse's upstream component diff --git a/modules/eks/reloader/README.md b/modules/eks/reloader/README.md index f84ccc99c..8198f4fb2 100644 --- a/modules/eks/reloader/README.md +++ b/modules/eks/reloader/README.md @@ -1,8 +1,7 @@ # Component: `eks/reloader` -This component installs the [Stakater Reloader](https://github.com/stakater/Reloader) for EKS clusters. -`reloader` can watch `ConfigMap`s and `Secret`s for changes -and use these to trigger rolling upgrades on pods and their associated +This component installs the [Stakater Reloader](https://github.com/stakater/Reloader) for EKS clusters. `reloader` can +watch `ConfigMap`s and `Secret`s for changes and use these to trigger rolling upgrades on pods and their associated `DeploymentConfig`s, `Deployment`s, `Daemonset`s `Statefulset`s and `Rollout`s. ## Usage @@ -29,6 +28,7 @@ components: timeout: 180 ``` + ## Requirements @@ -116,7 +116,9 @@ components: |------|-------------| | [metadata](#output\_metadata) | Block status of the deployed release | + ## References -* https://github.com/stakater/Reloader -* https://github.com/stakater/Reloader/tree/master/deployments/kubernetes/chart/reloader + +- https://github.com/stakater/Reloader +- https://github.com/stakater/Reloader/tree/master/deployments/kubernetes/chart/reloader diff --git a/modules/eks/storage-class/README.md b/modules/eks/storage-class/README.md index 9ecb30238..29cf8f602 100644 --- a/modules/eks/storage-class/README.md +++ b/modules/eks/storage-class/README.md @@ -1,36 +1,34 @@ # Component: `eks/storage-class` -This component is responsible for provisioning `StorageClasses` in an EKS cluster. -See the list of guides and references linked at the bottom of this README for more information. +This component is responsible for provisioning `StorageClasses` in an EKS cluster. See the list of guides and references +linked at the bottom of this README for more information. -A StorageClass provides part of the configuration for a PersistentVolumeClaim, -which copies the configuration when it is created. Thus, you can delete a StorageClass -without affecting existing PersistentVolumeClaims, and changes to a StorageClass -do not propagate to existing PersistentVolumeClaims. +A StorageClass provides part of the configuration for a PersistentVolumeClaim, which copies the configuration when it is +created. Thus, you can delete a StorageClass without affecting existing PersistentVolumeClaims, and changes to a +StorageClass do not propagate to existing PersistentVolumeClaims. ## Usage **Stack Level**: Regional, per cluster -This component can create storage classes backed by EBS or EFS, and is intended to be used -with the corresponding EKS add-ons `aws-ebs-csi-driver` and `aws-efs-csi-driver` respectively. -In the case of EFS, this component also requires that you have provisioned an EFS filesystem -in the same region as your cluster, and expects you have used the `efs` (previously `eks/efs`) component to do so. -The EFS storage classes will get the file system ID from the EFS component's output. +This component can create storage classes backed by EBS or EFS, and is intended to be used with the corresponding EKS +add-ons `aws-ebs-csi-driver` and `aws-efs-csi-driver` respectively. In the case of EFS, this component also requires +that you have provisioned an EFS filesystem in the same region as your cluster, and expects you have used the `efs` +(previously `eks/efs`) component to do so. The EFS storage classes will get the file system ID from the EFS component's +output. ### Note: Default Storage Class -Exactly one StorageClass can be designated as the default StorageClass for a cluster. -This default StorageClass is then used by PersistentVolumeClaims that do not specify a storage class. +Exactly one StorageClass can be designated as the default StorageClass for a cluster. This default StorageClass is then +used by PersistentVolumeClaims that do not specify a storage class. -Prior to Kubernetes 1.26, if more than one StorageClass is marked as default, -a PersistentVolumeClaim without `storageClassName` explicitly specified cannot be created. -In Kubernetes 1.26 and later, if more than one StorageClass is marked as default, -the last one created will be used, which means you can get by with just ignoring -the default "gp2" StorageClass that EKS creates for you. +Prior to Kubernetes 1.26, if more than one StorageClass is marked as default, a PersistentVolumeClaim without +`storageClassName` explicitly specified cannot be created. In Kubernetes 1.26 and later, if more than one StorageClass +is marked as default, the last one created will be used, which means you can get by with just ignoring the default "gp2" +StorageClass that EKS creates for you. -EKS always creates a default storage class for the cluster, typically an EBS backed class named `gp2`. Find out -what the default storage class is for your cluster by running this command: +EKS always creates a default storage class for the cluster, typically an EBS backed class named `gp2`. Find out what the +default storage class is for your cluster by running this command: ```bash # You only need to run `set-cluster` when you are changing target clusters @@ -83,28 +81,29 @@ ebs_storage_classes: Here's an example snippet for how to use this component. ```yaml - eks/storage-class: - vars: - ebs_storage_classes: - gp2: - make_default_storage_class: false - include_tags: false - # Preserve values originally set by eks/cluster. - # Set to "" to omit. - provisioner: kubernetes.io/aws-ebs - parameters: - type: gp2 - encrypted: "" - gp3: - make_default_storage_class: true - parameters: - type: gp3 - efs_storage_classes: - efs-sc: - make_default_storage_class: false - efs_component_name: "efs" # Replace with the name of the EFS component, previously "eks/efs" +eks/storage-class: + vars: + ebs_storage_classes: + gp2: + make_default_storage_class: false + include_tags: false + # Preserve values originally set by eks/cluster. + # Set to "" to omit. + provisioner: kubernetes.io/aws-ebs + parameters: + type: gp2 + encrypted: "" + gp3: + make_default_storage_class: true + parameters: + type: gp3 + efs_storage_classes: + efs-sc: + make_default_storage_class: false + efs_component_name: "efs" # Replace with the name of the EFS component, previously "eks/efs" ``` + ## Requirements @@ -183,6 +182,7 @@ Here's an example snippet for how to use this component. |------|-------------| | [storage\_classes](#output\_storage\_classes) | Storage classes created by this module | + ## Related How-to Guides @@ -200,6 +200,7 @@ Here's an example snippet for how to use this component. - [EFS CSI driver (Amazon)](https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html) - [EFS CSI driver (GitHub)](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/README.md#examples) - [EFS CSI StorageClass Parameters](https://github.com/kubernetes-sigs/aws-efs-csi-driver/tree/master/docs#storage-class-parameters-for-dynamic-provisioning) -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/cluster) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/cluster) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md index 73d0a0942..1f33e1a24 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -18,8 +18,8 @@ components: enabled: true name: "elasticache-redis" family: redis6.x - ingress_cidr_blocks: [ ] - egress_cidr_blocks: [ "0.0.0.0/0" ] + ingress_cidr_blocks: [] + egress_cidr_blocks: ["0.0.0.0/0"] port: 6379 at_rest_encryption_enabled: true transit_encryption_enabled: false @@ -61,6 +61,7 @@ components: value: lK ``` + ## Requirements @@ -134,9 +135,11 @@ No resources. |------|-------------| | [redis\_clusters](#output\_redis\_clusters) | Redis cluster objects | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/elasticache-redis) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/elasticache-redis) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index a55401b8b..0458a7433 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -1,6 +1,7 @@ # Component: `elasticsearch` -This component is responsible for provisioning an Elasticsearch cluster with built-in integrations with Kibana and Logstash. +This component is responsible for provisioning an Elasticsearch cluster with built-in integrations with Kibana and +Logstash. ## Usage @@ -27,6 +28,7 @@ components: domain_hostname_enabled: true ``` + ## Requirements @@ -118,8 +120,11 @@ components: | [master\_password\_ssm\_key](#output\_master\_password\_ssm\_key) | SSM key of Elasticsearch master password | | [security\_group\_id](#output\_security\_group\_id) | Security Group ID to control access to the Elasticsearch domain | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/elasticsearch) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/elasticsearch) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/eventbridge/README.md b/modules/eventbridge/README.md index 302d4d009..bbd1c0a95 100644 --- a/modules/eventbridge/README.md +++ b/modules/eventbridge/README.md @@ -1,7 +1,7 @@ # Component: `eventbridge` -The `eventbridge` component is a Terraform module that defines a CloudWatch EventBridge rule. -The rule is pointed at cloudwatch by default. +The `eventbridge` component is a Terraform module that defines a CloudWatch EventBridge rule. The rule is pointed at +cloudwatch by default. ## Usage @@ -28,13 +28,14 @@ components: - WARN - ERROR - agentConnected: - - false + - false - containers: exitCode: - anything-but: - - 0 + - 0 ``` + ## Requirements @@ -101,8 +102,11 @@ components: | [cloudwatch\_logs\_log\_group\_arn](#output\_cloudwatch\_logs\_log\_group\_arn) | The ARN of the CloudWatch Log Group | | [cloudwatch\_logs\_log\_group\_name](#output\_cloudwatch\_logs\_log\_group\_name) | The name of the CloudWatch Log Group | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eventbridge) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eventbridge) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/github-action-token-rotator/README.md b/modules/github-action-token-rotator/README.md index 080320c2a..cdff9ec74 100644 --- a/modules/github-action-token-rotator/README.md +++ b/modules/github-action-token-rotator/README.md @@ -1,6 +1,7 @@ # Component: `github-action-token-rotator` -This component is responsible for provisioning [Github Action Token Rotator](https://github.com/cloudposse/terraform-aws-github-action-token-rotator). +This component is responsible for provisioning +[Github Action Token Rotator](https://github.com/cloudposse/terraform-aws-github-action-token-rotator). This component creates a Lambda to rotate Github Action tokens in SSM Parameter Store. @@ -8,7 +9,8 @@ This component creates a Lambda to rotate Github Action tokens in SSM Parameter **Stack Level**: Regional -Here's an example snippet for how to use this component. This is generally deployed once and to the automation account's primary region. +Here's an example snippet for how to use this component. This is generally deployed once and to the automation account's +primary region. `stacks/catalog/github-action-token-rotator.yaml` file: @@ -25,8 +27,11 @@ components: parameter_store_token_path: /github/runners/my-org/registrationToken ``` -Follow the manual steps using the [guide in the upstream module](https://github.com/cloudposse/terraform-aws-github-action-token-rotator#quick-start) and use `chamber` to add the secrets to the appropriate stage. +Follow the manual steps using the +[guide in the upstream module](https://github.com/cloudposse/terraform-aws-github-action-token-rotator#quick-start) and +use `chamber` to add the secrets to the appropriate stage. + ## Requirements @@ -86,9 +91,11 @@ No resources. |------|-------------| | [github\_action\_token\_rotator](#output\_github\_action\_token\_rotator) | GitHub action token rotator module outputs. | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-action-token-rotator) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-action-token-rotator) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index 08968f5c2..59c17f515 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -1,18 +1,17 @@ # Component: `github-oidc-provider` -This component is responsible for authorizing the GitHub OIDC provider -as an Identity provider for an AWS account. It is meant to be used -in concert with `aws-teams` and `aws-team-roles` and/or with -`github-actions-iam-role.mixin.tf` +This component is responsible for authorizing the GitHub OIDC provider as an Identity provider for an AWS account. It is +meant to be used in concert with `aws-teams` and `aws-team-roles` and/or with `github-actions-iam-role.mixin.tf` ## Usage **Stack Level**: Global Here's an example snippet for how to use this component. + - This must be installed in the `identity` account in order to use standard SAML roles with role chaining. -- This must be installed in each individual account where you want to provision a service role for a GitHub action - that will be assumed directly by the action. +- This must be installed in each individual account where you want to provision a service role for a GitHub action that + will be assumed directly by the action. For security, since this component adds an identity provider, only SuperAdmin can install it. @@ -26,17 +25,16 @@ components: ## Configuring the Github OIDC Provider -This component was created to add the Github OIDC provider so that Github Actions can safely assume roles -without the need to store static credentials in the environment. -The details of the GitHub OIDC provider are hard coded in the component, however at some point -the provider's thumbprint may change, at which point you can use +This component was created to add the Github OIDC provider so that Github Actions can safely assume roles without the +need to store static credentials in the environment. The details of the GitHub OIDC provider are hard coded in the +component, however at some point the provider's thumbprint may change, at which point you can use [get_github_oidc_thumbprint.sh](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh) to get the new thumbprint and add it to the list in `var.thumbprint_list`. This script will pull one of two thumbprints. There are two possible intermediary certificates for the Actions SSL certificate and either can be returned by the GitHub servers, requiring customers to trust both. This is a known -behavior when the intermediary certificates are cross-signed by the CA. Therefore, run this script until both values -are retrieved. Add both to `var.thumbprint_list`. +behavior when the intermediary certificates are cross-signed by the CA. Therefore, run this script until both values are +retrieved. Add both to `var.thumbprint_list`. For more, see https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/ @@ -50,15 +48,16 @@ The following error is very common if the GitHub workflow is missing proper perm Error: User: arn:aws:sts::***:assumed-role/acme-core-use1-auto-actions-runner@actions-runner-system/token-file-web-identity is not authorized to perform: sts:TagSession on resource: arn:aws:iam::999999999999:role/acme-plat-use1-dev-gha ``` -In order to use a web identity, GitHub Action pipelines must have the following permission. -See [GitHub Action documentation for more](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-permissions-settings). +In order to use a web identity, GitHub Action pipelines must have the following permission. See +[GitHub Action documentation for more](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-permissions-settings). ```yaml permissions: id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout + contents: read # This is required for actions/checkout ``` + ## Requirements @@ -118,10 +117,11 @@ permissions: |------|-------------| | [oidc\_provider\_arn](#output\_oidc\_provider\_arn) | GitHub OIDC provider ARN | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-oidc-provider) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-oidc-provider) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/github-oidc-role/README.md b/modules/github-oidc-role/README.md index 7d21a02f4..4c003e77d 100644 --- a/modules/github-oidc-role/README.md +++ b/modules/github-oidc-role/README.md @@ -42,7 +42,7 @@ components: # Note: inherited lists are not merged, they are replaced github_actions_allowed_repos: - "MyOrg/infrastructure" - attributes: [ "gitops" ] + attributes: ["gitops"] iam_policies: - gitops gitops_policy_configuration: @@ -68,7 +68,7 @@ components: enabled: true github_actions_allowed_repos: - MyOrg/example-app-on-lambda-with-gha - attributes: [ "lambda-cicd" ] + attributes: ["lambda-cicd"] iam_policies: - lambda-cicd lambda_cicd_policy_configuration: @@ -98,7 +98,7 @@ components: enabled: true github_actions_allowed_repos: - MyOrg/example-app-on-lambda-with-gha - attributes: [ "custom" ] + attributes: ["custom"] iam_policies: - arn:aws:iam::aws:policy/AdministratorAccess iam_policy: @@ -120,29 +120,33 @@ There are two methods for adding custom policies to the IAM role. #### Defining Custom Policies in Terraform -1. Give the policy a unique name, e.g. `docker-publish`. We will use `NAME` as a placeholder for the name in the instructions below. +1. Give the policy a unique name, e.g. `docker-publish`. We will use `NAME` as a placeholder for the name in the + instructions below. 2. Create a file in the component directory (i.e. `github-oidc-role`) with the name `policy_NAME.tf`. 3. In that file, conditionally (based on need) create a policy document as follows: - ```hcl - locals { - NAME_policy_enabled = contains(var.iam_policies, "NAME") - NAME_policy = local.NAME_policy_enabled ? one(data.aws_iam_policy_document.NAME.*.json) : null - } + ```hcl + locals { + NAME_policy_enabled = contains(var.iam_policies, "NAME") + NAME_policy = local.NAME_policy_enabled ? one(data.aws_iam_policy_document.NAME.*.json) : null + } - data "aws_iam_policy_document" "NAME" { - count = local.NAME_policy_enabled ? 1 : 0 + data "aws_iam_policy_document" "NAME" { + count = local.NAME_policy_enabled ? 1 : 0 + + # Define the policy here + } + ``` - # Define the policy here - } - ``` + Note that you can also add input variables and outputs to this file if desired. Just make sure that all inputs are + optional. - Note that you can also add input variables and outputs to this file if desired. Just make sure that all inputs are optional. 4. Create a file named `additional-policy-map_override.tf` in the component directory (if it does not already exist). This is a [terraform override file](https://developer.hashicorp.com/terraform/language/files/override), meaning its - contents will be merged with the main terraform file, and any locals defined in it will override locals defined in other files. - Having your code in this separate override file makes it possible for the component to provide a placeholder local variable - so that it works without customization, while allowing you to customize the component and still update it without losing your customizations. + contents will be merged with the main terraform file, and any locals defined in it will override locals defined in + other files. Having your code in this separate override file makes it possible for the component to provide a + placeholder local variable so that it works without customization, while allowing you to customize the component and + still update it without losing your customizations. 5. In that file, redefine the local variable `overridable_additional_custom_policy_map` map as follows: ```hcl @@ -153,15 +157,18 @@ There are two methods for adding custom policies to the IAM role. } ``` - If you have multiple custom policies, using just this one file, add each policy document to the map in the form `NAME = local.NAME_policy`. + If you have multiple custom policies, using just this one file, add each policy document to the map in the form + `NAME = local.NAME_policy`. + 6. With that done, you can now attach that policy by adding the name to the `iam_policies` list. For example: - ```yaml - iam_policies: - - "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" - - "NAME" - ``` + ```yaml + iam_policies: + - "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" + - "NAME" + ``` + ## Requirements @@ -232,8 +239,11 @@ There are two methods for adding custom policies to the IAM role. | [github\_actions\_iam\_role\_arn](#output\_github\_actions\_iam\_role\_arn) | ARN of IAM role for GitHub Actions | | [github\_actions\_iam\_role\_name](#output\_github\_actions\_iam\_role\_name) | Name of IAM role for GitHub Actions | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-oidc-role) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-oidc-role) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 933199baa..90fe4d50a 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -2,12 +2,11 @@ This component is responsible for provisioning EC2 instances for GitHub runners. -:::info -We also have a similar component based on [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) for Kubernetes. +:::info We also have a similar component based on +[actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) for Kubernetes. ::: - ## Requirements ## Usage @@ -68,21 +67,27 @@ components: Prior to deployment, the API Token must exist in SSM. -To generate the token, please follow [these instructions](https://cloudposse.atlassian.net/l/c/N4dH05ud). Once generated, write the API token to the SSM key store at the following location within the same AWS account and region where the GitHub Actions runner pool will reside. +To generate the token, please follow [these instructions](https://cloudposse.atlassian.net/l/c/N4dH05ud). Once +generated, write the API token to the SSM key store at the following location within the same AWS account and region +where the GitHub Actions runner pool will reside. ``` assume-role chamber write github/runners/ registration-token ghp_secretstring ``` - ## Background ### Registration -Github Actions Self-Hosted runners can be scoped to the Github Organization, a Single Repository, or a group of Repositories (Github Enterprise-Only). Upon startup, each runner uses a `REGISTRATION_TOKEN` to call the Github API to register itself with the Organization, Repository, or Runner Group (Github Enterprise). + +Github Actions Self-Hosted runners can be scoped to the Github Organization, a Single Repository, or a group of +Repositories (Github Enterprise-Only). Upon startup, each runner uses a `REGISTRATION_TOKEN` to call the Github API to +register itself with the Organization, Repository, or Runner Group (Github Enterprise). ### Running Workflows -Once a Self-Hosted runner is registered, you will have to update your workflow with the `runs-on` attribute specify it should run on a self-hosted runner: + +Once a Self-Hosted runner is registered, you will have to update your workflow with the `runs-on` attribute specify it +should run on a self-hosted runner: ``` name: Test Self Hosted Runners @@ -95,11 +100,25 @@ jobs: ``` ### Workflow Github Permissions (GITHUB_TOKEN) -Each run of the Github Actions Workflow is assigned a GITHUB_TOKEN, which allows your workflow to perform actions against Github itself such as cloning a repo, updating the checks API status, etc., and expires at the end of the workflow run. The GITHUB_TOKEN has two permission "modes" it can operate in `Read and write permissions` ("Permissive" or "Full Access") and `Read repository contents permission` ("Restricted" or "Read-Only"). By default, the GITHUB_TOKEN is granted Full Access permissions, but you can change this via the Organization or Repo settings. If you opt for the Read-Only permissions, you can optionally grant or revoke access to specific APIs via the workflow `yaml` file and a full list of APIs that can be accessed can be found in the [documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) and is shown below in the table. It should be noted that the downside to this permissions model is that any user with write access to the repository can escalate permissions for the workflow by updating the `yaml` file, however, the APIs available via this token are limited. Most notably the GITHUB_TOKEN does not have access to the `users`, `repos`, `apps`, `billing`, or `collaborators` APIs, so the tokens do not have access to modify sensitive settings or add/remove users from the Organization/Repository. + +Each run of the Github Actions Workflow is assigned a GITHUB_TOKEN, which allows your workflow to perform actions +against Github itself such as cloning a repo, updating the checks API status, etc., and expires at the end of the +workflow run. The GITHUB_TOKEN has two permission "modes" it can operate in `Read and write permissions` ("Permissive" +or "Full Access") and `Read repository contents permission` ("Restricted" or "Read-Only"). By default, the GITHUB_TOKEN +is granted Full Access permissions, but you can change this via the Organization or Repo settings. If you opt for the +Read-Only permissions, you can optionally grant or revoke access to specific APIs via the workflow `yaml` file and a +full list of APIs that can be accessed can be found in the +[documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) +and is shown below in the table. It should be noted that the downside to this permissions model is that any user with +write access to the repository can escalate permissions for the workflow by updating the `yaml` file, however, the APIs +available via this token are limited. Most notably the GITHUB_TOKEN does not have access to the `users`, `repos`, +`apps`, `billing`, or `collaborators` APIs, so the tokens do not have access to modify sensitive settings or add/remove +users from the Organization/Repository.
> Example of using escalated permissions for the entire workflow + ``` name: Pull request labeler on: [ pull_request_target ] @@ -116,6 +135,7 @@ jobs: ``` > Example of using escalated permissions for a job + ``` name: Create issue on commit on: [ push ] @@ -139,10 +159,17 @@ jobs: ``` ### Pre-Requisites for Using This Component -In order to use this component, you will have to obtain the `REGISTRATION_TOKEN` mentioned above from your Github Organization or Repository and store it in SSM Parameter store. In addition, it is recommended that you set the permissions “mode” for Self-hosted runners to Read-Only. The instructions for doing both are below. + +In order to use this component, you will have to obtain the `REGISTRATION_TOKEN` mentioned above from your Github +Organization or Repository and store it in SSM Parameter store. In addition, it is recommended that you set the +permissions “mode” for Self-hosted runners to Read-Only. The instructions for doing both are below. #### Workflow Permissions -1. Browse to [https://github.com/organizations/{Org}/settings/actions](https://github.com/organizations/{Org}/settings/actions) (Organization) or [https://github.com/{Org}/{Repo}/settings/actions](https://github.com/{Org}/{Repo}/settings/actions) (Repository) + +1. Browse to + [https://github.com/organizations/{Org}/settings/actions](https://github.com/organizations/{Org}/settings/actions) + (Organization) or + [https://github.com/{Org}/{Repo}/settings/actions](https://github.com/{Org}/{Repo}/settings/actions) (Repository) 2. Set the default permissions for the GITHUB_TOKEN to Read Only @@ -150,16 +177,19 @@ In order to use this component, you will have to obtain the `REGISTRATION_TOKEN` ### Creating Registration Token -:::info -We highly recommend using a GitHub Application with the github-action-token-rotator module to generate the Registration Token. This will ensure that the token is rotated and that the token is stored in SSM Parameter Store encrypted with KMS. -::: +:::info We highly recommend using a GitHub Application with the github-action-token-rotator module to generate the +Registration Token. This will ensure that the token is rotated and that the token is stored in SSM Parameter Store +encrypted with KMS. ::: #### GitHub Application -Follow the quickstart with the upstream module, [cloudposse/terraform-aws-github-action-token-rotator](https://github.com/cloudposse/terraform-aws-github-action-token-rotator#quick-start), or follow the steps below. +Follow the quickstart with the upstream module, +[cloudposse/terraform-aws-github-action-token-rotator](https://github.com/cloudposse/terraform-aws-github-action-token-rotator#quick-start), +or follow the steps below. 1. Create a new GitHub App 1. Add the following permission: + ```diff # Required Permissions for Repository Runners: ## Repository Permissions @@ -175,13 +205,17 @@ Follow the quickstart with the upstream module, [cloudposse/terraform-aws-github ## Organization Permissions + Self-hosted runners (read / write) ``` + 1. Generate a Private Key -If you are working with Cloud Posse, upload this Private Key, GitHub App ID, and Github App Installation ID to 1Password and skip the rest. Otherwise, complete the private key setup in `core--auto`. +If you are working with Cloud Posse, upload this Private Key, GitHub App ID, and Github App Installation ID to 1Password +and skip the rest. Otherwise, complete the private key setup in `core--auto`. -1. Convert the private key to a PEM file using the following command: `openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in {DOWNLOADED_FILE_NAME}.pem -out private-key-pkcs8.key` +1. Convert the private key to a PEM file using the following command: + `openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in {DOWNLOADED_FILE_NAME}.pem -out private-key-pkcs8.key` 1. Upload PEM file key to the specified ssm path: `/github/runners/acme/private-key` in `core--auto` -1. Create another sensitive SSM parameter `/github/runners/acme/registration-token` in `core--auto` with any basic value, such as "foo". This will be overwritten by the rotator. +1. Create another sensitive SSM parameter `/github/runners/acme/registration-token` in `core--auto` with + any basic value, such as "foo". This will be overwritten by the rotator. 1. Update the GitHub App ID and Installation ID in the `github-action-token-rotator` catalog. :::info @@ -191,40 +225,56 @@ If you change the Private Key saved in SSM, redeploy `github-action-token-rotato ::: #### (ClickOps) Obtain the Runner Registration Token -1. Browse to [https://github.com/organizations/{Org}/settings/actions/runners](https://github.com/organizations/{Org}/settings/actions/runners) (Organization) or [https://github.com/{Org}/{Repo}/settings/actions/runners](https://github.com/{Org}/{Repo}/settings/actions/runners) (Repository) + +1. Browse to + [https://github.com/organizations/{Org}/settings/actions/runners](https://github.com/organizations/{Org}/settings/actions/runners) + (Organization) or + [https://github.com/{Org}/{Repo}/settings/actions/runners](https://github.com/{Org}/{Repo}/settings/actions/runners) + (Repository) 2. Click the **New Runner** button (Organization) or **New Self Hosted Runner** button (Repository) -3. Copy the Github Runner token from the next screen. Note that this is the only time you will see this token. Note that if you exit the `New {Self Hosted} Runner` screen and then later return by clicking the `New {Self Hosted} Runner` button again, the registration token will be invalidated and a new token will be generated. +3. Copy the Github Runner token from the next screen. Note that this is the only time you will see this token. Note that + if you exit the `New {Self Hosted} Runner` screen and then later return by clicking the `New {Self Hosted} Runner` + button again, the registration token will be invalidated and a new token will be generated.
-4. Add the `REGISTRATION_TOKEN` to the `/github/token` SSM parameter in the account where Github runners are hosted (usually `automation`), encrypted with KMS. +4. Add the `REGISTRATION_TOKEN` to the `/github/token` SSM parameter in the account where Github runners are hosted + (usually `automation`), encrypted with KMS. ``` chamber write github token ``` - # FAQ ## The GitHub Registration Token is not updated in SSM -The `github-action-token-rotator` runs an AWS Lambda function every 30 minutes. This lambda will attempt to use a private key in its environment configuration to generate a GitHub Registration Token, and then store that token to AWS SSM Parameter Store. +The `github-action-token-rotator` runs an AWS Lambda function every 30 minutes. This lambda will attempt to use a +private key in its environment configuration to generate a GitHub Registration Token, and then store that token to AWS +SSM Parameter Store. -If the GitHub Registration Token parameter, `/github/runners/acme/registration-token`, is not updated, read through the following tips: +If the GitHub Registration Token parameter, `/github/runners/acme/registration-token`, is not updated, read through the +following tips: -1. The private key is stored at the given parameter path: `parameter_store_private_key_path: /github/runners/acme/private-key` -1. The private key is Base 64 encoded. If you pull the key from SSM and decode it, it should begin with `-----BEGIN PRIVATE KEY-----` -1. If the private key has changed, you must _redeploy_ `github-action-token-rotator`. Run a plan against the component to make sure there are not changes required. +1. The private key is stored at the given parameter path: + `parameter_store_private_key_path: /github/runners/acme/private-key` +1. The private key is Base 64 encoded. If you pull the key from SSM and decode it, it should begin with + `-----BEGIN PRIVATE KEY-----` +1. If the private key has changed, you must _redeploy_ `github-action-token-rotator`. Run a plan against the component + to make sure there are not changes required. ## The GitHub Registration Token is valid, but the Runners are not registering with GitHub -If you first deployed the `github-action-token-rotator` component initally with an invalid configuration and then deployed the `github-runners` component, the instance runners will have failed to register with GitHub. +If you first deployed the `github-action-token-rotator` component initally with an invalid configuration and then +deployed the `github-runners` component, the instance runners will have failed to register with GitHub. -After you correct `github-action-token-rotator` and have a valid GitHub Registration Token in SSM, _destroy and recreate_ the `github-runners` component. +After you correct `github-action-token-rotator` and have a valid GitHub Registration Token in SSM, _destroy and +recreate_ the `github-runners` component. -If you cannot see the runners registered in GitHub, check the system logs on one of EC2 Instances in AWS in `core--auto`. +If you cannot see the runners registered in GitHub, check the system logs on one of EC2 Instances in AWS in +`core--auto`. ## I cannot assume the role from GitHub Actions after deploying @@ -234,16 +284,16 @@ The following error is very common if the GitHub workflow is missing proper perm Error: User: arn:aws:sts::***:assumed-role/acme-core-use1-auto-actions-runner@actions-runner-system/token-file-web-identity is not authorized to perform: sts:TagSession on resource: arn:aws:iam::999999999999:role/acme-plat-use1-dev-gha ``` -In order to use a web identity, GitHub Action pipelines must have the following permission. -See [GitHub Action documentation for more](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-permissions-settings). +In order to use a web identity, GitHub Action pipelines must have the following permission. See +[GitHub Action documentation for more](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#adding-permissions-settings). ```yaml permissions: id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout + contents: read # This is required for actions/checkout ``` - + ## Requirements @@ -353,17 +403,20 @@ permissions: | [iam\_role\_arn](#output\_iam\_role\_arn) | The ARN of the IAM role associated with the Autoscaling Group | | [ssm\_document\_arn](#output\_ssm\_document\_arn) | The ARN of the SSM document. | - + ## FAQ ### Can we scope it to a github org with both private and public repos ? -Yes but this requires Github Enterprise Cloud and the usage of runner groups to scope permissions of runners to specific repos. If you set the scope to the entire org without runner groups and if the org has both public and private repos, then the risk of using a self-hosted runner incorrectly is a vulnerability within public repos. +Yes but this requires Github Enterprise Cloud and the usage of runner groups to scope permissions of runners to specific +repos. If you set the scope to the entire org without runner groups and if the org has both public and private repos, +then the risk of using a self-hosted runner incorrectly is a vulnerability within public repos. [https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups) -If you do not have github enterprise cloud and runner groups cannot be utilized, then it’s best to create new github runners per repo or use the summerwind action-runners-controller via a Github App to set the scope to specific repos. +If you do not have github enterprise cloud and runner groups cannot be utilized, then it’s best to create new github +runners per repo or use the summerwind action-runners-controller via a Github App to set the scope to specific repos. ### How can we see the current spot pricing? @@ -371,19 +424,27 @@ Go to [ec2instances.info](http://ec2instances.info/) ### If we don’t use mixed at all does that mean we can’t do spot? -It’s possible to do spot without using mixed instances but you leave yourself open to zero instance availability with a single instance type. +It’s possible to do spot without using mixed instances but you leave yourself open to zero instance availability with a +single instance type. -For example, if you wanted to use spot and use `t3.xlarge` in `us-east-2` and for some reason, AWS ran out of `t3.xlarge`, you wouldn't have the option to choose another instance type and so all the GitHub Action runs would stall until availability returned. If you use on-demand pricing, it’s more expensive, but you’re more likely to get scheduling priority. For guaranteed availability, reserved instances are required. +For example, if you wanted to use spot and use `t3.xlarge` in `us-east-2` and for some reason, AWS ran out of +`t3.xlarge`, you wouldn't have the option to choose another instance type and so all the GitHub Action runs would stall +until availability returned. If you use on-demand pricing, it’s more expensive, but you’re more likely to get scheduling +priority. For guaranteed availability, reserved instances are required. ### Do the overrides apply to both the on-demand and the spot instances, or only the spot instances? -Since the overrides affect the launch template, I believe they will affect both spot instances and override since weighted capacity can be set for either or. The override terraform option is on the ASG’s `launch_template` +Since the overrides affect the launch template, I believe they will affect both spot instances and override since +weighted capacity can be set for either or. The override terraform option is on the ASG’s `launch_template` -> List of nested arguments provides the ability to specify multiple instance types. This will override the same parameter in the launch template. For on-demand instances, Auto Scaling considers the order of preference of instance types to launch based on the order specified in the overrides list. Defined below. -And in the terraform resource for `instances_distribution` +> List of nested arguments provides the ability to specify multiple instance types. This will override the same +> parameter in the launch template. For on-demand instances, Auto Scaling considers the order of preference of instance +> types to launch based on the order specified in the overrides list. Defined below. And in the terraform resource for +> `instances_distribution` -> `spot_max_price` - (Optional) Maximum price per unit hour that the user is willing to pay for the Spot instances. Default: an empty string which means the on-demand price. -For a `mixed_instances_policy`, this will do purely on-demand +> `spot_max_price` - (Optional) Maximum price per unit hour that the user is willing to pay for the Spot instances. +> Default: an empty string which means the on-demand price. For a `mixed_instances_policy`, this will do purely +> on-demand ``` mixed_instances_policy: @@ -405,7 +466,8 @@ This will always do spot unless instances are unavailable, then switch to on-dem spot_max_price: 0.05 ``` -If you want a single instance type, you could still use the mixed instances policy to define that like above, or you can use these other inputs and comment out the `mixed_instances_policy` +If you want a single instance type, you could still use the mixed instances policy to define that like above, or you can +use these other inputs and comment out the `mixed_instances_policy` ``` instance_type: "t3.xlarge" @@ -422,13 +484,14 @@ If you want a single instance type, you could still use the mixed instances poli The `overrides` will override the `instance_type` above. +## References +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-runners) - + Cloud Posse's upstream component +- [AWS: Auto Scaling groups with multiple instance types and purchase options](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-mixed-instances-groups.html) +- [InstancesDistribution](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_InstancesDistribution.html) -## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-runners) - Cloud Posse's upstream component -* [AWS: Auto Scaling groups with multiple instance types and purchase options](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-mixed-instances-groups.html) -* [InstancesDistribution](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_InstancesDistribution.html) -- [MixedInstancesPolicy](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_MixedInstancesPolicy.html) -- [Terraform ASG `Override` Attribute](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#override) +* [MixedInstancesPolicy](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_MixedInstancesPolicy.html) +* [Terraform ASG `Override` Attribute](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#override) [](https://cpco.io/component) diff --git a/modules/github-webhook/README.md b/modules/github-webhook/README.md index 4f2afc916..74b5e13f1 100644 --- a/modules/github-webhook/README.md +++ b/modules/github-webhook/README.md @@ -2,7 +2,8 @@ This component provisions a GitHub webhook for a single GitHub repository. -You may want to use this component if you are provisioning webhooks for multiple ArgoCD deployment repositories across GitHub organizations. +You may want to use this component if you are provisioning webhooks for multiple ArgoCD deployment repositories across +GitHub organizations. ## Usage @@ -65,11 +66,13 @@ components: webhook_github_secret: "abcdefg" ``` - ### ArgoCD Webhooks -For usage with the `eks/argocd` component, see [Creating Webhooks with `github-webhook`](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/eks/argocd/README.md#creating-webhooks-with-github-webhook) in that component's README. +For usage with the `eks/argocd` component, see +[Creating Webhooks with `github-webhook`](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/eks/argocd/README.md#creating-webhooks-with-github-webhook) +in that component's README. + ## Requirements @@ -141,8 +144,11 @@ For usage with the `eks/argocd` component, see [Creating Webhooks with `github-w No outputs. + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components) - Cloud Posse's upstream components + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components) - Cloud Posse's upstream + components [](https://cpco.io/component) diff --git a/modules/gitops/README.md b/modules/gitops/README.md index f5d189c0e..58a0a9404 100644 --- a/modules/gitops/README.md +++ b/modules/gitops/README.md @@ -1,10 +1,11 @@ # Component: `gitops` -This component is used to deploy GitHub OIDC roles for accessing the `gitops` Team. We use this team to run Terraform from GitHub Actions. +This component is used to deploy GitHub OIDC roles for accessing the `gitops` Team. We use this team to run Terraform +from GitHub Actions. Examples: -* [cloudposse/github-action-terraform-plan-storage](https://github.com/cloudposse/github-action-terraform-plan-storage/blob/main/.github/workflows/build-and-test.yml) +- [cloudposse/github-action-terraform-plan-storage](https://github.com/cloudposse/github-action-terraform-plan-storage/blob/main/.github/workflows/build-and-test.yml) ## Usage @@ -45,13 +46,14 @@ components: vars: enabled: true github_actions_iam_role_enabled: true - github_actions_iam_role_attributes: [ "gitops" ] + github_actions_iam_role_attributes: ["gitops"] github_actions_allowed_repos: - "acmeOrg/infra" s3_bucket_component_name: gitops/s3-bucket dynamodb_component_name: gitops/dynamodb ``` + ## Requirements @@ -122,9 +124,11 @@ components: | [github\_actions\_iam\_role\_arn](#output\_github\_actions\_iam\_role\_arn) | ARN of IAM role for GitHub Actions | | [github\_actions\_iam\_role\_name](#output\_github\_actions\_iam\_role\_name) | Name of IAM role for GitHub Actions | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/gitops) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/gitops) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/global-accelerator-endpoint-group/README.md b/modules/global-accelerator-endpoint-group/README.md index 4a7e9f647..fcbedd063 100644 --- a/modules/global-accelerator-endpoint-group/README.md +++ b/modules/global-accelerator-endpoint-group/README.md @@ -2,7 +2,8 @@ This component is responsible for provisioning a Global Accelerator Endpoint Group. -This component assumes that the `global-accelerator` component has already been deployed to the same account in the environment specified by `var.global_accelerator_environment_name`. +This component assumes that the `global-accelerator` component has already been deployed to the same account in the +environment specified by `var.global_accelerator_environment_name`. ## Usage @@ -21,6 +22,7 @@ components: - endpoint_lb_name: my-load-balancer ``` + ## Requirements @@ -78,10 +80,11 @@ No resources. |------|-------------| | [id](#output\_id) | The ID of the Global Accelerator Endpoint Group. | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/global-accelerator-endpoint-group) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/global-accelerator-endpoint-group) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/global-accelerator/README.md b/modules/global-accelerator/README.md index e1bdc94e4..f76093e40 100644 --- a/modules/global-accelerator/README.md +++ b/modules/global-accelerator/README.md @@ -25,6 +25,7 @@ global-accelerator: to_port: 443 ``` + ## Requirements @@ -90,10 +91,11 @@ No resources. | [name](#output\_name) | Name of the Global Accelerator. | | [static\_ips](#output\_static\_ips) | Global Static IPs owned by the Global Accelerator. | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/global-accelerator) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/global-accelerator) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/catalog-database/README.md b/modules/glue/catalog-database/README.md index 5f2711f84..9ed139442 100644 --- a/modules/glue/catalog-database/README.md +++ b/modules/glue/catalog-database/README.md @@ -23,6 +23,7 @@ components: - "ALL" ``` + ## Requirements @@ -95,9 +96,11 @@ components: | [catalog\_database\_id](#output\_catalog\_database\_id) | Catalog database ID | | [catalog\_database\_name](#output\_catalog\_database\_name) | Catalog database name | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/catalog-database) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/catalog-database) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/catalog-table/README.md b/modules/glue/catalog-table/README.md index 1da4ce397..2dbff5cf5 100644 --- a/modules/glue/catalog-table/README.md +++ b/modules/glue/catalog-table/README.md @@ -25,6 +25,7 @@ components: location: "s3://awsglue-datasets/examples/medicare/Medicare_Hospital_Provider.csv" ``` + ## Requirements @@ -105,9 +106,11 @@ components: | [catalog\_table\_id](#output\_catalog\_table\_id) | Catalog table ID | | [catalog\_table\_name](#output\_catalog\_table\_name) | Catalog table name | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/catalog-table) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/catalog-table) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/connection/README.md b/modules/glue/connection/README.md index 3b7adff52..082197fd3 100644 --- a/modules/glue/connection/README.md +++ b/modules/glue/connection/README.md @@ -25,6 +25,7 @@ components: vpc_component_name: "vpc" ``` + ## Requirements @@ -114,9 +115,11 @@ components: | [security\_group\_id](#output\_security\_group\_id) | The ID of the Security Group associated with the Glue connection | | [security\_group\_name](#output\_security\_group\_name) | The name of the Security Group and associated with the Glue connection | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/connection) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/connection) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/crawler/README.md b/modules/glue/crawler/README.md index b551ad2db..9395b5eb1 100644 --- a/modules/glue/crawler/README.md +++ b/modules/glue/crawler/README.md @@ -28,6 +28,7 @@ components: update_behavior: null ``` + ## Requirements @@ -107,9 +108,11 @@ No resources. | [crawler\_id](#output\_crawler\_id) | Crawler ID | | [crawler\_name](#output\_crawler\_name) | Crawler name | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/crawler) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/crawler) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/iam/README.md b/modules/glue/iam/README.md index 737da185f..6de843fc5 100644 --- a/modules/glue/iam/README.md +++ b/modules/glue/iam/README.md @@ -21,6 +21,7 @@ components: - "arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole" ``` + ## Requirements @@ -81,9 +82,11 @@ No resources. | [role\_id](#output\_role\_id) | The ID of the Glue role | | [role\_name](#output\_role\_name) | The name of the Glue role | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/iam) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/iam) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/job/README.md b/modules/glue/job/README.md index b81d700df..edfe7f946 100644 --- a/modules/glue/job/README.md +++ b/modules/glue/job/README.md @@ -28,6 +28,7 @@ components: glue_job_command_python_version: 3 ``` + ## Requirements @@ -114,9 +115,11 @@ components: | [job\_id](#output\_job\_id) | Glue job ID | | [job\_name](#output\_job\_name) | Glue job name | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/job) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/job) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/registry/README.md b/modules/glue/registry/README.md index f87f9a8e7..0fa49a243 100644 --- a/modules/glue/registry/README.md +++ b/modules/glue/registry/README.md @@ -19,6 +19,7 @@ components: registry_description: "Glue registry example" ``` + ## Requirements @@ -78,9 +79,11 @@ No resources. | [registry\_id](#output\_registry\_id) | Glue registry ID | | [registry\_name](#output\_registry\_name) | Glue registry name | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/registry) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/registry) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/schema/README.md b/modules/glue/schema/README.md index 1dc77e5b0..82a58c1fe 100644 --- a/modules/glue/schema/README.md +++ b/modules/glue/schema/README.md @@ -21,6 +21,7 @@ components: glue_registry_component_name: "glue/registry/example" ``` + ## Requirements @@ -89,9 +90,11 @@ No resources. | [schema\_id](#output\_schema\_id) | Glue schema ID | | [schema\_name](#output\_schema\_name) | Glue schema name | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/schema) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/schema) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/trigger/README.md b/modules/glue/trigger/README.md index 88ba77a41..e692e2aa5 100644 --- a/modules/glue/trigger/README.md +++ b/modules/glue/trigger/README.md @@ -26,6 +26,7 @@ components: type: SCHEDULED ``` + ## Requirements @@ -97,9 +98,11 @@ No resources. | [trigger\_id](#output\_trigger\_id) | Glue trigger ID | | [trigger\_name](#output\_trigger\_name) | Glue trigger name | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/trigger) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/trigger) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/workflow/README.md b/modules/glue/workflow/README.md index 77ebe4235..d6adadd7a 100644 --- a/modules/glue/workflow/README.md +++ b/modules/glue/workflow/README.md @@ -19,6 +19,7 @@ components: workflow_description: "Glue workflow example" ``` + ## Requirements @@ -80,9 +81,11 @@ No resources. | [workflow\_id](#output\_workflow\_id) | Glue workflow ID | | [workflow\_name](#output\_workflow\_name) | Glue workflow name | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/workflow) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/glue/workflow) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/guardduty/README.md b/modules/guardduty/README.md index 1cfaf66c0..199691f33 100644 --- a/modules/guardduty/README.md +++ b/modules/guardduty/README.md @@ -45,18 +45,18 @@ with an additional layer of security to proactively identify and respond to pote This component is complex in that it must be deployed multiple times with different variables set to configure the AWS Organization successfully. -It is further complicated by the fact that you must deploy each of the the component instances described below to -every region that existed before March 2019 and to any regions that have been opted-in as described in the [AWS -Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions). +It is further complicated by the fact that you must deploy each of the the component instances described below to every +region that existed before March 2019 and to any regions that have been opted-in as described in the +[AWS Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions). In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization Delegated Administrator account is `security`, both in the `core` tenant. ### Deploy to Delegated Admininstrator Account -First, the component is deployed to the [Delegated -Admininstrator](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) account in each region in -order to configure the central GuardDuty detector that each account will send its findings to. +First, the component is deployed to the +[Delegated Admininstrator](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) account in each +region in order to configure the central GuardDuty detector that each account will send its findings to. ```yaml # core-ue1-security @@ -115,9 +115,9 @@ atmos terraform apply guardduty/root/uw1 -s core-uw1-root ### Deploy Organization Settings in Delegated Administrator Account -Finally, the component is deployed to the Delegated Administrator Account again in order to create the -organization-wide configuration for the AWS Organization, but with `var.admin_delegated` set to `true` to indicate that -the delegation has already been performed from the Organization Management account. +Finally, the component is deployed to the Delegated Administrator Account again in order to create the organization-wide +configuration for the AWS Organization, but with `var.admin_delegated` set to `true` to indicate that the delegation has +already been performed from the Organization Management account. ```yaml # core-ue1-security @@ -141,6 +141,7 @@ atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security # ... other regions ``` + ## Requirements @@ -228,6 +229,7 @@ atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security | [sns\_topic\_name](#output\_sns\_topic\_name) | The name of the SNS topic created by the component | | [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | The SNS topic subscriptions created by the component | + ## References diff --git a/modules/iam-role/README.md b/modules/iam-role/README.md index 7a2c83e96..9976affcf 100644 --- a/modules/iam-role/README.md +++ b/modules/iam-role/README.md @@ -1,6 +1,7 @@ # Component: `iam-role` -This component is responsible for provisioning simple IAM roles. If a more complicated IAM role and policy are desired then it is better to use a separate component specific to that role. +This component is responsible for provisioning simple IAM roles. If a more complicated IAM role and policy are desired +then it is better to use a separate component specific to that role. ## Usage @@ -28,7 +29,7 @@ Use-case: An IAM role for AWS Workspaces Directory since this service does not h ```yaml # stacks/catalog/aws-workspaces/directory/iam-role.yaml import: -- catalog/iam-role + - catalog/iam-role components: terraform: @@ -57,6 +58,7 @@ components: - arn:aws:iam::aws:policy/AmazonWorkSpacesSelfServiceAccess ``` + ## Requirements @@ -126,9 +128,11 @@ No resources. |------|-------------| | [role](#output\_role) | IAM role module outputs | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/iam-role) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/iam-role) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/iam-service-linked-roles/README.md b/modules/iam-service-linked-roles/README.md index f5236791c..b36f0f4f0 100644 --- a/modules/iam-service-linked-roles/README.md +++ b/modules/iam-service-linked-roles/README.md @@ -1,6 +1,7 @@ # Component: `iam-service-linked-roles` -This component is responsible for provisioning [IAM Service-Linked Roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html). +This component is responsible for provisioning +[IAM Service-Linked Roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html). ## Usage @@ -26,16 +27,15 @@ components: ## Service-Linked Roles for EC2 Spot and EC2 Spot Fleet -__Note:__ If you want to use EC2 Spot or Spot Fleet, -you will need to provision the following Service-Linked Roles: +**Note:** If you want to use EC2 Spot or Spot Fleet, you will need to provision the following Service-Linked Roles: - Service-Linked Role for EC2 Spot - Service-Linked Role for EC2 Spot Fleet This is only necessary if this is the first time you're using EC2 Spot and Spot Fleet in the account. -Note that if the Service-Linked Roles already exist in the AWS account (if you used EC2 Spot or Spot Fleet before), -and you try to provision them again, you will see the following errors: +Note that if the Service-Linked Roles already exist in the AWS account (if you used EC2 Spot or Spot Fleet before), and +you try to provision them again, you will see the following errors: ```text An error occurred (InvalidInput) when calling the CreateServiceLinkedRole operation: @@ -46,10 +46,11 @@ Service role name AWSServiceRoleForEC2SpotFleet has been taken in this account, ``` For more details, see: + - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html - https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html - + ## Requirements @@ -108,8 +109,11 @@ For more details, see: |------|-------------| | [service\_linked\_roles](#output\_service\_linked\_roles) | Provisioned Service-Linked roles | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/iam-service-linked-roles) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/iam-service-linked-roles) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ipam/README.md b/modules/ipam/README.md index a26449820..a9b590df5 100644 --- a/modules/ipam/README.md +++ b/modules/ipam/README.md @@ -47,6 +47,7 @@ components: ram_share_accounts: [plat-sandbox] ``` + ## Requirements @@ -119,10 +120,11 @@ components: |------|-------------| | [pool\_configurations](#output\_pool\_configurations) | Pool configurations | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/kinesis-stream/README.md b/modules/kinesis-stream/README.md index 72dc6734c..495403606 100644 --- a/modules/kinesis-stream/README.md +++ b/modules/kinesis-stream/README.md @@ -24,12 +24,11 @@ components: tags: Team: sre Service: kinesis-stream - ``` ```yaml import: -- catalog/kinesis-stream/defaults + - catalog/kinesis-stream/defaults components: terraform: @@ -45,6 +44,7 @@ components: kms_key_id: "alias/aws/kinesis" ``` + ## Requirements @@ -109,8 +109,11 @@ No resources. | [shard\_count](#output\_shard\_count) | Number of shards provisioned. | | [stream\_arn](#output\_stream\_arn) | ARN of the the Kinesis stream. | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/kinesis-stream) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/kinesis-stream) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/kms/README.md b/modules/kms/README.md index dc8f3a404..4be480599 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -19,7 +19,6 @@ components: enabled: true ``` - ## Requirements No requirements. @@ -30,13 +29,13 @@ No providers. ## Modules -| Name | Source | Version | -|------|--------|---------| -| [iam\_roles](#module\_iam\_roles) | git::ssh://git@github.com/spenmo/infrastructure.git//components/terraform/account-map/modules/iam-roles | n/a | -| [introspection](#module\_introspection) | cloudposse/label/null | 0.25.0 | -| [kms\_key](#module\_kms\_key) | cloudposse/kms-key/aws | 0.12.1 | -| [monorepo](#module\_monorepo) | git::ssh://git@github.com/spenmo/infrastructure.git | n/a | -| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| Name | Source | Version | +| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------- | +| [iam_roles](#module_iam_roles) | git::ssh://git@github.com/spenmo/infrastructure.git//components/terraform/account-map/modules/iam-roles | n/a | +| [introspection](#module_introspection) | cloudposse/label/null | 0.25.0 | +| [kms_key](#module_kms_key) | cloudposse/kms-key/aws | 0.12.1 | +| [monorepo](#module_monorepo) | git::ssh://git@github.com/spenmo/infrastructure.git | n/a | +| [this](#module_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -44,48 +43,50 @@ No resources. ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [alias](#input\_alias) | The display name of the alias. The name must start with the word `alias` followed by a forward slash. If not specified, the alias name will be auto-generated. | `string` | n/a | yes | -| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [customer\_master\_key\_spec](#input\_customer\_master\_key\_spec) | Specifies whether the key contains a symmetric key or an asymmetric key pair and the encryption algorithms or signing algorithms that the key supports. Valid values: `SYMMETRIC_DEFAULT`, `RSA_2048`, `RSA_3072`, `RSA_4096`, `ECC_NIST_P256`, `ECC_NIST_P384`, `ECC_NIST_P521`, or `ECC_SECG_P256K1`. | `string` | `"SYMMETRIC_DEFAULT"` | no | -| [deletion\_window\_in\_days](#input\_deletion\_window\_in\_days) | Duration in days after which the key is deleted after destruction of the resource | `number` | `10` | no | -| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [description](#input\_description) | The description of the key as viewed in AWS console | `string` | `"Parameter Store KMS master key"` | no | -| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [enable\_key\_rotation](#input\_enable\_key\_rotation) | Specifies whether key rotation is enabled | `bool` | `true` | no | -| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | -| [key\_usage](#input\_key\_usage) | Specifies the intended use of the key. Valid values: `ENCRYPT_DECRYPT` or `SIGN_VERIFY`. | `string` | `"ENCRYPT_DECRYPT"` | no | -| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | -| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [multi\_region](#input\_multi\_region) | Indicates whether the KMS key is a multi-Region (true) or regional (false) key. | `bool` | `false` | no | -| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | -| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [policy](#input\_policy) | A valid KMS policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy. | `string` | `""` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [required\_tags](#input\_required\_tags) | List of required tag names | `list(string)` | `[]` | no | -| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| Name | Description | Type | Default | Required | +| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | +| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alias](#input_alias) | The display name of the alias. The name must start with the word `alias` followed by a forward slash. If not specified, the alias name will be auto-generated. | `string` | n/a | yes | +| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [customer_master_key_spec](#input_customer_master_key_spec) | Specifies whether the key contains a symmetric key or an asymmetric key pair and the encryption algorithms or signing algorithms that the key supports. Valid values: `SYMMETRIC_DEFAULT`, `RSA_2048`, `RSA_3072`, `RSA_4096`, `ECC_NIST_P256`, `ECC_NIST_P384`, `ECC_NIST_P521`, or `ECC_SECG_P256K1`. | `string` | `"SYMMETRIC_DEFAULT"` | no | +| [deletion_window_in_days](#input_deletion_window_in_days) | Duration in days after which the key is deleted after destruction of the resource | `number` | `10` | no | +| [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input_description) | The description of the key as viewed in AWS console | `string` | `"Parameter Store KMS master key"` | no | +| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enable_key_rotation](#input_enable_key_rotation) | Specifies whether key rotation is enabled | `bool` | `true` | no | +| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import_profile_name](#input_import_profile_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import_role_arn](#input_import_role_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [key_usage](#input_key_usage) | Specifies the intended use of the key. Valid values: `ENCRYPT_DECRYPT` or `SIGN_VERIFY`. | `string` | `"ENCRYPT_DECRYPT"` | no | +| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [multi_region](#input_multi_region) | Indicates whether the KMS key is a multi-Region (true) or regional (false) key. | `bool` | `false` | no | +| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [policy](#input_policy) | A valid KMS policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy. | `string` | `""` | no | +| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input_region) | AWS Region | `string` | n/a | yes | +| [required_tags](#input_required_tags) | List of required tag names | `list(string)` | `[]` | no | +| [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | ## Outputs -| Name | Description | -|------|-------------| -| [kms\_key](#output\_kms\_key) | Output for KMS module | +| Name | Description | +| -------------------------------------------------------- | --------------------- | +| [kms_key](#output_kms_key) | Output for KMS module | ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/kms) - Cloud Posse's upstream component -* [cloudposse/terraform-aws-kms-key](https://github.com/cloudposse/terraform-aws-kms-key) - Cloud Posse's upstream module +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/kms) - + Cloud Posse's upstream component +- [cloudposse/terraform-aws-kms-key](https://github.com/cloudposse/terraform-aws-kms-key) - Cloud Posse's upstream + module [](https://cpco.io/component) diff --git a/modules/lakeformation/README.md b/modules/lakeformation/README.md index 8c1278ed8..2c43b8d5a 100644 --- a/modules/lakeformation/README.md +++ b/modules/lakeformation/README.md @@ -8,7 +8,8 @@ This component is responsible for provisioning Amazon Lake Formation resources. Here are some example snippets for how to use this component: -`stacks/catalog/lakeformation/defaults.yaml` file (base component for all lakeformation deployments with default settings): +`stacks/catalog/lakeformation/defaults.yaml` file (base component for all lakeformation deployments with default +settings): ```yaml components: @@ -28,7 +29,7 @@ components: ```yaml import: -- catalog/lakeformation/defaults + - catalog/lakeformation/defaults components: terraform: @@ -54,6 +55,7 @@ components: left: test1 ``` + ## Requirements @@ -123,8 +125,11 @@ components: |------|-------------| | [lf\_tags](#output\_lf\_tags) | List of LF tags created. | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/lakeformation) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/lakeformation) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/lambda/README.md b/modules/lambda/README.md index 310a65d03..34eaf24cf 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -7,6 +7,7 @@ This component is responsible for provisioning Lambda functions. **Stack Level**: Regional Stack configuration for defaults: + ```yaml components: terraform: @@ -21,6 +22,7 @@ components: ``` Sample App Yaml Entry: + ```yaml import: - catalog/lambda/defaults @@ -74,7 +76,7 @@ components: # s3_key: hello-world-go.zip ``` - + ## Requirements @@ -186,9 +188,11 @@ components: | [role\_arn](#output\_role\_arn) | Lambda IAM role ARN | | [role\_name](#output\_role\_name) | Lambda IAM role name | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/macie/README.md b/modules/macie/README.md index 3bf9b211a..e5ab09ff9 100644 --- a/modules/macie/README.md +++ b/modules/macie/README.md @@ -36,9 +36,9 @@ Delegated Administrator account is `security`, both in the `core` tenant. ### Deploy to Delegated Administrator Account -First, the component is deployed to the [Delegated -Administrator](https://docs.aws.amazon.com/macie/latest/user/accounts-mgmt-ao-integrate.html) account to configure the -central Macie account∑. +First, the component is deployed to the +[Delegated Administrator](https://docs.aws.amazon.com/macie/latest/user/accounts-mgmt-ao-integrate.html) account to +configure the central Macie account∑. ```yaml # core-ue1-security @@ -63,9 +63,9 @@ atmos terraform apply macie/delegated-administrator -s core-ue1-security Next, the component is deployed to the AWS Organization Management, a/k/a `root`, Account in order to set the AWS Organization Designated Admininstrator account. -Note that you must `SuperAdmin` permissions as we are deploying to the AWS Organization Management account. Since -we are using the `SuperAdmin` user, it will already have access to the state bucket, so we set the `role_arn` of the -backend config to null and set `var.privileged` to `true`. +Note that you must `SuperAdmin` permissions as we are deploying to the AWS Organization Management account. Since we are +using the `SuperAdmin` user, it will already have access to the state bucket, so we set the `role_arn` of the backend +config to null and set `var.privileged` to `true`. ```yaml # core-ue1-root @@ -91,9 +91,9 @@ atmos terraform apply macie/root -s core-ue1-root ### Deploy Organization Settings in Delegated Administrator Account -Finally, the component is deployed to the Delegated Administrator Account again in order to create the -organization-wide configuration for the AWS Organization, but with `var.admin_delegated` set to `true` to indicate that -the delegation has already been performed from the Organization Management account. +Finally, the component is deployed to the Delegated Administrator Account again in order to create the organization-wide +configuration for the AWS Organization, but with `var.admin_delegated` set to `true` to indicate that the delegation has +already been performed from the Organization Management account. ```yaml # core-ue1-security @@ -114,6 +114,7 @@ components: atmos terraform apply macie/org-settings/ue1 -s core-ue1-security ``` + ## Requirements @@ -165,8 +166,6 @@ atmos terraform apply macie/org-settings/ue1 -s core-ue1-security | [finding\_publishing\_frequency](#input\_finding\_publishing\_frequency) | Specifies how often to publish updates to policy findings for the account. This includes publishing updates to AWS
Security Hub and Amazon EventBridge (formerly called Amazon CloudWatch Events). For more information, see:

https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings_cloudwatch.html#guardduty_findings_cloudwatch_notification_frequency | `string` | `"FIFTEEN_MINUTES"` | no | | [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | -| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | @@ -192,6 +191,7 @@ atmos terraform apply macie/org-settings/ue1 -s core-ue1-security | [macie\_service\_role\_arn](#output\_macie\_service\_role\_arn) | The Amazon Resource Name (ARN) of the service-linked role that allows Macie to monitor and analyze data in AWS resources for the account. | | [member\_account\_ids](#output\_member\_account\_ids) | The AWS Account IDs of the member accounts | + ## References diff --git a/modules/mq-broker/README.md b/modules/mq-broker/README.md index 2b7317c38..bd763ca48 100644 --- a/modules/mq-broker/README.md +++ b/modules/mq-broker/README.md @@ -27,6 +27,7 @@ components: use_aws_owned_key: true ``` + ## Requirements @@ -125,10 +126,11 @@ No resources. | [secondary\_stomp\_ssl\_endpoint](#output\_secondary\_stomp\_ssl\_endpoint) | AmazonMQ secondary STOMP+SSL endpoint | | [secondary\_wss\_endpoint](#output\_secondary\_wss\_endpoint) | AmazonMQ secondary WSS endpoint | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/mq-broker) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/mq-broker) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/msk/README.md b/modules/msk/README.md index f517bcf6b..6c4d8424a 100644 --- a/modules/msk/README.md +++ b/modules/msk/README.md @@ -1,7 +1,7 @@ # Component: `msk/cluster` -This component is responsible for provisioning [Amazon Managed Streaming](https://aws.amazon.com/msk/) -clusters for [Apache Kafka](https://aws.amazon.com/msk/what-is-kafka/). +This component is responsible for provisioning [Amazon Managed Streaming](https://aws.amazon.com/msk/) clusters for +[Apache Kafka](https://aws.amazon.com/msk/what-is-kafka/). ## Usage @@ -12,7 +12,6 @@ Here's an example snippet for how to use this component. ```yaml components: terraform: - msk: metadata: component: "msk" @@ -69,6 +68,7 @@ components: allowed_cidr_blocks: [] ``` + ## Requirements @@ -193,6 +193,7 @@ No resources. | [zookeeper\_connect\_string](#output\_zookeeper\_connect\_string) | Comma separated list of one or more hostname:port pairs to connect to the Apache Zookeeper cluster | | [zookeeper\_connect\_string\_tls](#output\_zookeeper\_connect\_string\_tls) | Comma separated list of one or more hostname:port pairs to connect to the Apache Zookeeper cluster via TLS | + ## References diff --git a/modules/mwaa/README.md b/modules/mwaa/README.md index 1cc7e069d..e8c816d16 100644 --- a/modules/mwaa/README.md +++ b/modules/mwaa/README.md @@ -14,9 +14,9 @@ Allows the Airflow UI to be access over the public internet to users granted acc Limits access to users within the VPC to users granted access by an IAM policy. -* MWAA creates a VPC interface endpoint for the Airflow webserver and an interface endpoint for the pgsql metadatabase. +- MWAA creates a VPC interface endpoint for the Airflow webserver and an interface endpoint for the pgsql metadatabase. - the endpoints are created in the AZs mapped to your private subnets -* MWAA binds an IP address from your private subnet to the interface endpoint +- MWAA binds an IP address from your private subnet to the interface endpoint ### Managing access to VPC endpoings on MWAA @@ -41,6 +41,7 @@ components: airflow_version: 2.0.2 ``` + ## Requirements @@ -144,10 +145,11 @@ components: | [tags\_all](#output\_tags\_all) | A map of tags assigned to the resource, including those inherited from the provider for the Amazon MWAA Environment | | [webserver\_url](#output\_webserver\_url) | The webserver URL of the Amazon MWAA Environment | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/network-firewall/README.md b/modules/network-firewall/README.md index f72db4820..a7fe6c867 100644 --- a/modules/network-firewall/README.md +++ b/modules/network-firewall/README.md @@ -11,10 +11,11 @@ Example of a Network Firewall with stateful 5-tuple rules: :::info -The "5-tuple" means the five items (columns) that each rule (row, or tuple) in a firewall policy uses to define whether to block or allow traffic: -source and destination IP, source and destination port, and protocol. +The "5-tuple" means the five items (columns) that each rule (row, or tuple) in a firewall policy uses to define whether +to block or allow traffic: source and destination IP, source and destination port, and protocol. -Refer to [Standard stateful rule groups in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-basic.html) +Refer to +[Standard stateful rule groups in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-basic.html) for more details. ::: @@ -90,10 +91,12 @@ Example of a Network Firewall with [Suricata](https://suricata.readthedocs.io/en :::info -For [Suricata](https://suricata.io/) rule group type, you provide match and action settings in a string, in a Suricata compatible specification. -The specification fully defines what the stateful rules engine looks for in a traffic flow and the action to take on the packets in a flow that matches the inspection criteria. +For [Suricata](https://suricata.io/) rule group type, you provide match and action settings in a string, in a Suricata +compatible specification. The specification fully defines what the stateful rules engine looks for in a traffic flow and +the action to take on the packets in a flow that matches the inspection criteria. -Refer to [Suricata compatible rule strings in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-suricata.html) +Refer to +[Suricata compatible rule strings in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-suricata.html) for more details. ::: @@ -197,7 +200,6 @@ components: # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-how-to-provide-rules.html rules_source: - # Suricata rules for the rule group # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-examples.html # https://docs.aws.amazon.com/network-firewall/latest/developerguide/suricata-rule-evaluation-order.html @@ -233,6 +235,7 @@ components: pass ip any any <> any any ( msg: "Allow general traffic"; sid:10000; rev:1; ) ``` + ## Requirements @@ -312,6 +315,7 @@ No resources. | [network\_firewall\_policy\_name](#output\_network\_firewall\_policy\_name) | Network Firewall policy name | | [network\_firewall\_status](#output\_network\_firewall\_status) | Nested list of information about the current status of the Network Firewall | + ## References @@ -323,6 +327,7 @@ No resources. - [How to deploy AWS Network Firewall by using AWS Firewall Manager](https://aws.amazon.com/blogs/security/how-to-deploy-aws-network-firewall-by-using-aws-firewall-manager) - [A Deep Dive into AWS Transit Gateway](https://www.youtube.com/watch?v=a55Iud-66q0) - [Appliance in a shared services VPC](https://docs.aws.amazon.com/vpc/latest/tgw/transit-gateway-appliance-scenario.html) -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/opsgenie-team/CHANGELOG.md b/modules/opsgenie-team/CHANGELOG.md index ed8ac39d2..209f811aa 100644 --- a/modules/opsgenie-team/CHANGELOG.md +++ b/modules/opsgenie-team/CHANGELOG.md @@ -2,15 +2,12 @@ ### `team` replaced with `team_options` -The `team` variable has been replaced with `team_options` to reduce confusion. -The component only ever creates at most one team, with the name -specified in the `name` variable. The `team` variable was introduced to -provide a single object to specify other options, but was not implemented -properly. +The `team` variable has been replaced with `team_options` to reduce confusion. The component only ever creates at most +one team, with the name specified in the `name` variable. The `team` variable was introduced to provide a single object +to specify other options, but was not implemented properly. ### Team membership now managed by this component by default -Previously, the default behavior was to not manage team membership, -allowing users to be managed via the Opsgenie UI. Now the default is to manage -via the `members` input. To restore the previous behavior, set +Previously, the default behavior was to not manage team membership, allowing users to be managed via the Opsgenie UI. +Now the default is to manage via the `members` input. To restore the previous behavior, set `team_options.ignore_members` to `true`. diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index cdcd659ef..06ee77000 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -5,29 +5,32 @@ This component is responsible for provisioning Opsgenie teams and related servic ## Usage #### Pre-requisites -You need an API Key stored in `/opsgenie/opsgenie_api_key` of SSM, this is configurable using the `ssm_parameter_name_format` and `ssm_path` variables. -Opsgenie is now part of Atlassian, so you need to make sure you are creating -an Opsgenie API Key, which looks like `abcdef12-3456-7890-abcd-ef0123456789` -and not an Atlassian API key, which looks like +You need an API Key stored in `/opsgenie/opsgenie_api_key` of SSM, this is configurable using the +`ssm_parameter_name_format` and `ssm_path` variables. + +Opsgenie is now part of Atlassian, so you need to make sure you are creating an Opsgenie API Key, which looks like +`abcdef12-3456-7890-abcd-ef0123456789` and not an Atlassian API key, which looks like ```shell ATAfT3xFfGF0VFXAfl8EmQNPVv1Hlazp3wsJgTmM8Ph7iP-RtQyiEfw-fkDS2LvymlyUOOhc5XiSx46vQWnznCJolq-GMX4KzdvOSPhEWr-BF6LEkJQC4CSjDJv0N7d91-0gVekNmCD2kXY9haUHUSpO4H7X6QxyImUb9VmOKIWTbQi8rf4CF28=63CB21B9 ``` -Generate an API Key by going to Settings -> API key management on your Opsgenie -control panel, which will have an address like `https://.app.opsgenie.com/settings/api-key-management`, -and click the "Add new API key" button. For more information, see the +Generate an API Key by going to Settings -> API key management on your Opsgenie control panel, which will have an +address like `https://.app.opsgenie.com/settings/api-key-management`, and click the "Add new API key" button. +For more information, see the [Opsgenie API key management documentation](https://support.atlassian.com/opsgenie/docs/api-key-management/). -Once you have the key, you'll need to test it with a curl to verify that you are at least -on a Standard plan with OpsGenie: +Once you have the key, you'll need to test it with a curl to verify that you are at least on a Standard plan with +OpsGenie: + ``` curl -X GET 'https://api.opsgenie.com/v2/account' \ --header "Authorization: GenieKey $API_KEY" ``` The result should be something similar to below: + ``` { "data": { @@ -39,9 +42,8 @@ The result should be something similar to below: } ``` -If you see `Free` or `Essentials` in the plan, then you won't be able -to use this component. You can see more details here: -[OpsGenie pricing/features](https://www.atlassian.com/software/opsgenie/pricing#) +If you see `Free` or `Essentials` in the plan, then you won't be able to use this component. You can see more details +here: [OpsGenie pricing/features](https://www.atlassian.com/software/opsgenie/pricing#) #### Getting Started @@ -49,7 +51,8 @@ to use this component. You can see more details here: Here's an example snippet for how to use this component. -This component should only be applied once as the resources it creates are regional, but it works with integrations. This is typically done via the auto or corp stack (e.g. `gbl-auto.yaml`). +This component should only be applied once as the resources it creates are regional, but it works with integrations. +This is typically done via the auto or corp stack (e.g. `gbl-auto.yaml`). ```yaml # 9-5 Mon-Fri @@ -175,8 +178,8 @@ components: notify_type: default delay: 60 recipients: - - type: team - name: otherteam + - type: team + name: otherteam yaep_escalation: enabled: true @@ -187,8 +190,8 @@ components: notify_type: default delay: 90 recipients: - - type: user - name: user@example.com + - type: user + name: user@example.com schedule_escalation: enabled: true @@ -199,8 +202,8 @@ components: notify_type: default delay: 30 recipients: - - type: schedule - name: secondary_on_call + - type: schedule + name: secondary_on_call ``` The API keys relating to the Opsgenie Integrations are stored in SSM Parameter Store and can be accessed via chamber. @@ -210,13 +213,18 @@ AWS_PROFILE=foo chamber list opsgenie-team/ ``` ### ClickOps Work - - After deploying the opsgenie-team component the created team will have a schedule named after the team. This is purposely left to be clickOps’d so the UI can be used to set who is on call, as that is the usual way (not through code). Additionally, we do not want a re-apply of the Terraform to delete or shuffle who is planned to be on call, thus we left who is on-call on a schedule out of the component. + +- After deploying the opsgenie-team component the created team will have a schedule named after the team. This is + purposely left to be clickOps’d so the UI can be used to set who is on call, as that is the usual way (not through + code). Additionally, we do not want a re-apply of the Terraform to delete or shuffle who is planned to be on call, + thus we left who is on-call on a schedule out of the component. ## Known Issues ### Different API Endpoints in Use The problem is there are 3 different api endpoints in use + - `/webapp` - the most robust - only exposed to the UI (that we've seen) - `/v2/` - robust with some differences from `webapp` - `/v1/` - the oldest and furthest from the live UI. @@ -231,11 +239,11 @@ This module does not create users. Users must have already been created to be ad ### Cannot Add Stakeholders - - Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/278 +- Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/278 ### No Resource to create Slack Integration - - Track the issue: https://github.com/DataDog/terraform-provider-datadog/issues/67 +- Track the issue: https://github.com/DataDog/terraform-provider-datadog/issues/67 ### Out of Date Terraform Docs @@ -244,10 +252,12 @@ Another Problem is the terraform docs are not always up to date with the provide The OpsGenie Provider uses a mix of `/v1` and `/v2`. This means there are many things you can only do from the UI. Listed below in no particular order -- Incident Routing cannot add dependent services - in `v1` and `v2` a `service_incident_rule` object has `serviceId` as type string, in webapp this becomes `serviceIds` of type `list(string)` + +- Incident Routing cannot add dependent services - in `v1` and `v2` a `service_incident_rule` object has `serviceId` as + type string, in webapp this becomes `serviceIds` of type `list(string)` - Opsgenie Provider appears to be inconsistent with how it uses `time_restriction`: - - `restrictions` for type `weekday-and-time-of-day` - - `restriction` for type `time-of-day` + - `restrictions` for type `weekday-and-time-of-day` + - `restriction` for type `time-of-day` Unfortunately none of this is in the terraform docs, and was found via errors and digging through source code. @@ -259,24 +269,27 @@ We recommend to use the human readable timezone such as `Europe/London`. - Setting a schedule to a GMT-style timezone with offsets can cause inconsistent plans. - Setting the timezone to `Etc/GMT+1` instead of `Europe/London`, will lead to permadrift as OpsGenie converts the GMT offsets to regional timezones at deploy-time. In the previous deploy, the GMT style get converted to `Atlantic/Cape_Verde`. + Setting the timezone to `Etc/GMT+1` instead of `Europe/London`, will lead to permadrift as OpsGenie converts the GMT + offsets to regional timezones at deploy-time. In the previous deploy, the GMT style get converted to + `Atlantic/Cape_Verde`. - ```hcl - # module.routing["london_schedule"].module.team_routing_rule[0].opsgenie_team_routing_rule.this[0] will be updated in-place - ~ resource "opsgenie_team_routing_rule" "this" { - id = "4b4c4454-8ccf-41a9-b856-02bec6419ba7" - name = "london_schedule" - ~ timezone = "Atlantic/Cape_Verde" -> "Etc/GMT+1" - # (2 unchanged attributes hidden) - ``` + ```hcl + # module.routing["london_schedule"].module.team_routing_rule[0].opsgenie_team_routing_rule.this[0] will be updated in-place + ~ resource "opsgenie_team_routing_rule" "this" { + id = "4b4c4454-8ccf-41a9-b856-02bec6419ba7" + name = "london_schedule" + ~ timezone = "Atlantic/Cape_Verde" -> "Etc/GMT+1" + # (2 unchanged attributes hidden) + ``` - Some GMT styles will not cause a timezone change on subsequent applies such as `Etc/GMT+8` for `Asia/Taipei`. + Some GMT styles will not cause a timezone change on subsequent applies such as `Etc/GMT+8` for `Asia/Taipei`. -- If the calendar date has crossed daylight savings time, the `Etc/GMT+` GMT style will need to be updated to reflect the correct timezone. +- If the calendar date has crossed daylight savings time, the `Etc/GMT+` GMT style will need to be updated to reflect + the correct timezone. Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/258 - + ## Requirements @@ -370,6 +383,7 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ | [team\_members](#output\_team\_members) | Team members | | [team\_name](#output\_team\_name) | Team Name | + ## Related How-to Guides @@ -383,7 +397,8 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ - [How to Implement Incident Management with OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie) ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/opsgenie-team) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/opsgenie-team) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/opsgenie-team/modules/escalation/README.md b/modules/opsgenie-team/modules/escalation/README.md index e5261df7c..d57862655 100644 --- a/modules/opsgenie-team/modules/escalation/README.md +++ b/modules/opsgenie-team/modules/escalation/README.md @@ -1,7 +1,7 @@ ## Escalation -Terraform module to configure [Opsgenie Escalation](https://registry.terraform.io/providers/opsgenie/opsgenie/latest/docs/resources/escalation) - +Terraform module to configure +[Opsgenie Escalation](https://registry.terraform.io/providers/opsgenie/opsgenie/latest/docs/resources/escalation) ## Usage @@ -27,6 +27,7 @@ module "escalation" { } ``` + ## Requirements @@ -89,3 +90,4 @@ module "escalation" { | [escalation\_id](#output\_escalation\_id) | The ID of the Opsgenie Escalation | | [escalation\_name](#output\_escalation\_name) | Name of the Opsgenie Escalation | + diff --git a/modules/opsgenie-team/modules/integration/README.md b/modules/opsgenie-team/modules/integration/README.md index 21faa8bbe..fe7b0a1f8 100644 --- a/modules/opsgenie-team/modules/integration/README.md +++ b/modules/opsgenie-team/modules/integration/README.md @@ -2,6 +2,7 @@ This module creates an OpsGenie integrations for a team. By Default, it creates a Datadog integration. + ## Requirements @@ -67,3 +68,4 @@ This module creates an OpsGenie integrations for a team. By Default, it creates | [ssm\_path](#output\_ssm\_path) | Full SSM path of the team integration key | | [type](#output\_type) | Type of the team integration | + diff --git a/modules/opsgenie-team/modules/routing/README.md b/modules/opsgenie-team/modules/routing/README.md index bf6f14519..a69fa1d28 100644 --- a/modules/opsgenie-team/modules/routing/README.md +++ b/modules/opsgenie-team/modules/routing/README.md @@ -1,8 +1,10 @@ ## Routing -This module creates team routing rules, these are the initial rules that are applied to an alert to determine who gets notified. -This module also creates incident service rules, which determine if an alert is considered a service incident or not. +This module creates team routing rules, these are the initial rules that are applied to an alert to determine who gets +notified. This module also creates incident service rules, which determine if an alert is considered a service incident +or not. + ## Requirements @@ -76,3 +78,4 @@ This module also creates incident service rules, which determine if an alert is | [service\_incident\_rule](#output\_service\_incident\_rule) | Service incident rules for incidents | | [team\_routing\_rule](#output\_team\_routing\_rule) | Team routing rules for alerts | + diff --git a/modules/philips-labs-github-runners/README.md b/modules/philips-labs-github-runners/README.md index 83ad0b33e..cbb0664a2 100644 --- a/modules/philips-labs-github-runners/README.md +++ b/modules/philips-labs-github-runners/README.md @@ -4,11 +4,15 @@ This component is responsible for provisioning the surrounding infrastructure fo ## Prerequisites -* Github App installed on the organization - * For more details see [Philips Lab's Setting up a Github App](https://github.com/philips-labs/terraform-aws-github-runner/tree/main#setup-github-app-part-1) - * Ensure you create a **PRIVATE KEY** and store it in SSM, **NOT** to be confused with a **Client Secret**. Private Keys are created in the GitHub App Configuration and scrolling to the bottom. -* Github App ID and private key stored in SSM under `/pl-github-runners/id` (or the value of `var.github_app_id_ssm_path`) -* Github App Private Key stored in SSM (base64 encoded) under `/pl-github-runners/key` (or the value of `var.github_app_key_ssm_path`) +- Github App installed on the organization + - For more details see + [Philips Lab's Setting up a Github App](https://github.com/philips-labs/terraform-aws-github-runner/tree/main#setup-github-app-part-1) + - Ensure you create a **PRIVATE KEY** and store it in SSM, **NOT** to be confused with a **Client Secret**. Private + Keys are created in the GitHub App Configuration and scrolling to the bottom. +- Github App ID and private key stored in SSM under `/pl-github-runners/id` (or the value of + `var.github_app_id_ssm_path`) +- Github App Private Key stored in SSM (base64 encoded) under `/pl-github-runners/key` (or the value of + `var.github_app_key_ssm_path`) ## Usage @@ -31,8 +35,8 @@ The following will create - SQS Queue - EC2 Launch Template instances -The API Gateway is registered as a webhook within the GitHub app. Which scales up or down, via lambdas, the EC2 Launch Template -by the number of messages in the SQS queue. +The API Gateway is registered as a webhook within the GitHub app. Which scales up or down, via lambdas, the EC2 Launch +Template by the number of messages in the SQS queue. ![Architecture](https://github.com/philips-labs/terraform-aws-github-runner/blob/main/docs/component-overview.svg) @@ -43,12 +47,16 @@ by the number of messages in the SQS queue. This is a fork of https://github.com/philips-labs/terraform-aws-github-runner/tree/main/modules/webhook-github-app. We customized it until this PR is resolved as it does not update the github app webhook until this is merged. -* https://github.com/philips-labs/terraform-aws-github-runner/pull/3625 + +- https://github.com/philips-labs/terraform-aws-github-runner/pull/3625 This module also requires an environment variable -* `GH_TOKEN` - a github token be set -This module also requires the `gh` cli to be installed. Your Dockerfile can be updated to include the following to install it: +- `GH_TOKEN` - a github token be set + +This module also requires the `gh` cli to be installed. Your Dockerfile can be updated to include the following to +install it: + ```dockerfile ARG GH_CLI_VERSION=2.39.1 # ... @@ -57,13 +65,14 @@ RUN apt-get update && apt-get install -y --allow-downgrades \ gh="${GH_CLI_VERSION}-*" ``` -By default, we leave this disabled, as it requires a github token to be set. You can enable it by setting `var.enable_update_github_app_webhook` to `true`. -When enabled, it will update the github app webhook to point to the API Gateway. This can occur if the API Gateway is deleted and recreated. - -When disabled, you will need to manually update the github app webhook to point to the API Gateway. -This is output by the component, and available via the `webhook` output under `endpoint`. +By default, we leave this disabled, as it requires a github token to be set. You can enable it by setting +`var.enable_update_github_app_webhook` to `true`. When enabled, it will update the github app webhook to point to the +API Gateway. This can occur if the API Gateway is deleted and recreated. +When disabled, you will need to manually update the github app webhook to point to the API Gateway. This is output by +the component, and available via the `webhook` output under `endpoint`. + ## Requirements @@ -140,8 +149,11 @@ This is output by the component, and available via the `webhook` output under `e | [ssm\_parameters](#output\_ssm\_parameters) | Information about the SSM parameters to use to register the runner. | | [webhook](#output\_webhook) | Information about the webhook to use to register the runner. | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/philips-labs-github-runners/modules/README.md b/modules/philips-labs-github-runners/modules/README.md index 85f6ef43b..bade162a5 100644 --- a/modules/philips-labs-github-runners/modules/README.md +++ b/modules/philips-labs-github-runners/modules/README.md @@ -5,12 +5,16 @@ This is a fork of https://github.com/philips-labs/terraform-aws-github-runner/tree/main/modules/webhook-github-app. We customized it until this PR is resolved as it does not update the github app webhook until this is merged. - * https://github.com/philips-labs/terraform-aws-github-runner/pull/3625 + +- https://github.com/philips-labs/terraform-aws-github-runner/pull/3625 This module also requires an environment variable - * `GH_TOKEN` - a github token be set -This module also requires the `gh` cli to be installed. Your Dockerfile can be updated to include the following to install it: +- `GH_TOKEN` - a github token be set + +This module also requires the `gh` cli to be installed. Your Dockerfile can be updated to include the following to +install it: + ```dockerfile ARG GH_CLI_VERSION=2.39.1 # ... diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/README.md b/modules/philips-labs-github-runners/modules/webhook-github-app/README.md index ba0ca7190..b6125e471 100644 --- a/modules/philips-labs-github-runners/modules/webhook-github-app/README.md +++ b/modules/philips-labs-github-runners/modules/webhook-github-app/README.md @@ -2,21 +2,23 @@ > This module is using the local executor to run a bash script. -This module updates the GitHub App webhook with the endpoint and secret and can be changed with the root module. See the examples for usages. +This module updates the GitHub App webhook with the endpoint and secret and can be changed with the root module. See the +examples for usages. + ## Requirements -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.3.0 | -| [null](#requirement\_null) | ~> 3 | +| Name | Version | +| ------------------------------------------------------------------------ | -------- | +| [terraform](#requirement_terraform) | >= 1.3.0 | +| [null](#requirement_null) | ~> 3 | ## Providers -| Name | Version | -|------|---------| -| [null](#provider\_null) | ~> 3 | +| Name | Version | +| --------------------------------------------------- | ------- | +| [null](#provider_null) | ~> 3 | ## Modules @@ -24,18 +26,19 @@ No modules. ## Resources -| Name | Type | -|------|------| +| Name | Type | +| ----------------------------------------------------------------------------------------------------------------- | -------- | | [null_resource.update_app](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [github\_app](#input\_github\_app) | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). |
object({
key_base64 = string
id = string
webhook_secret = string
})
| n/a | yes | -| [webhook\_endpoint](#input\_webhook\_endpoint) | The endpoint to use for the webhook, defaults to the endpoint of the runners module. | `string` | n/a | yes | +| Name | Description | Type | Default | Required | +| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------- | :------: | +| [github_app](#input_github_app) | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). |
object({
key_base64 = string
id = string
webhook_secret = string
})
| n/a | yes | +| [webhook_endpoint](#input_webhook_endpoint) | The endpoint to use for the webhook, defaults to the endpoint of the runners module. | `string` | n/a | yes | ## Outputs No outputs. + diff --git a/modules/rds/README.md b/modules/rds/README.md index 9fb828b86..4f8ef7383 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -1,11 +1,13 @@ # Component: `rds` -This component is responsible for provisioning an RDS instance. It seeds relevant database information (hostnames, username, password, etc.) into AWS SSM Parameter Store. +This component is responsible for provisioning an RDS instance. It seeds relevant database information (hostnames, +username, password, etc.) into AWS SSM Parameter Store. ## Security Groups Guidance: -By default this component creates a client security group and adds that security group id to the default attached security group. -Ideally other AWS resources that require RDS access can be granted this client security group. Additionally you can grant access -via specific CIDR blocks or security group ids. + +By default this component creates a client security group and adds that security group id to the default attached +security group. Ideally other AWS resources that require RDS access can be granted this client security group. +Additionally you can grant access via specific CIDR blocks or security group ids. ## Usage @@ -69,24 +71,29 @@ components: # This does not seem to work correctly deletion_protection: false ``` + ### Provisioning from a snapshot -The snapshot identifier variable can be added to provision an instance from a snapshot HOWEVER- -Keep in mind these instances are provisioned from a unique kms key per rds. -For clean terraform runs, you must first provision the key for the destination instance, then copy the snapshot using that kms key. + +The snapshot identifier variable can be added to provision an instance from a snapshot HOWEVER- Keep in mind these +instances are provisioned from a unique kms key per rds. For clean terraform runs, you must first provision the key for +the destination instance, then copy the snapshot using that kms key. Example - I want a new instance `rds-example-new` to be provisioned from a snapshot of `rds-example-old`: + 1. Use the console to manually make a snapshot of rds instance `rds-example-old` 1. provision the kms key for `rds-example-new` - ``` - atmos terraform plan rds-example-new -s ue1-staging '-target=module.kms_key_rds.aws_kms_key.default[0]' - atmos terraform apply rds-example-new -s ue1-staging '-target=module.kms_key_rds.aws_kms_key.default[0]' - ``` + ``` + atmos terraform plan rds-example-new -s ue1-staging '-target=module.kms_key_rds.aws_kms_key.default[0]' + atmos terraform apply rds-example-new -s ue1-staging '-target=module.kms_key_rds.aws_kms_key.default[0]' + ``` 1. Use the console to copy the snapshot to a new name using the above provisioned kms key -1. Add `snapshot_identifier` variable to `rds-example-new` catalog and specify the newly copied snapshot that used the above key +1. Add `snapshot_identifier` variable to `rds-example-new` catalog and specify the newly copied snapshot that used the + above key 1. Post provisioning, remove the `snapshot_idenfier` variable and verify terraform runs clean for the copied instance + ## Requirements @@ -237,10 +244,11 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | [rds\_security\_group\_id](#output\_rds\_security\_group\_id) | ID of the Security Group | | [rds\_subnet\_group\_id](#output\_rds\_subnet\_group\_id) | ID of the created Subnet Group | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/rds) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/rds) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/redshift/CHANGELOG.md b/modules/redshift/CHANGELOG.md index a211c5ef9..055b6d2c9 100644 --- a/modules/redshift/CHANGELOG.md +++ b/modules/redshift/CHANGELOG.md @@ -1,7 +1,7 @@ ## Components PR [Fix components](https://github.com/cloudposse/terraform-aws-components/pull/855) -This is a bug fix and feature enhancement update. -No actions necessary to upgrade. +This is a bug fix and feature enhancement update. No actions necessary to upgrade. ## Fixes -* Fix bug related to the AWS provider `>= 5.0.0` removed `redshift_cluster.cluster_security_groups`. + +- Fix bug related to the AWS provider `>= 5.0.0` removed `redshift_cluster.cluster_security_groups`. diff --git a/modules/redshift/README.md b/modules/redshift/README.md index c0045301b..90e9e6bca 100644 --- a/modules/redshift/README.md +++ b/modules/redshift/README.md @@ -1,6 +1,7 @@ # Component: `redshift` -This component is responsible for provisioning a RedShift instance. It seeds relevant database information (hostnames, username, password, etc.) into AWS SSM Parameter Store. +This component is responsible for provisioning a RedShift instance. It seeds relevant database information (hostnames, +username, password, etc.) into AWS SSM Parameter Store. ## Usage @@ -36,9 +37,9 @@ components: protocol: tcp cidr_blocks: - 10.0.0.0/8 - ``` + ## Requirements @@ -139,10 +140,11 @@ components: | [redshift\_database\_ssm\_key\_prefix](#output\_redshift\_database\_ssm\_key\_prefix) | SSM prefix | | [vpc\_security\_group\_ids](#output\_vpc\_security\_group\_ids) | The VPC security group IDs associated with the cluster | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/redshift) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/redshift) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/route53-resolver-dns-firewall/README.md b/modules/route53-resolver-dns-firewall/README.md index 7206ca7e3..63519378f 100644 --- a/modules/route53-resolver-dns-firewall/README.md +++ b/modules/route53-resolver-dns-firewall/README.md @@ -1,7 +1,9 @@ # Component: `route53-resolver-dns-firewall` -This component is responsible for provisioning [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) -resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging configuration. +This component is responsible for provisioning +[Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) +resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging +configuration. ## Usage @@ -76,6 +78,7 @@ Execute the following command to provision the `route53-resolver-dns-firewall/ex atmos terraform apply route53-resolver-dns-firewall/example -s ``` + ## Requirements @@ -143,6 +146,7 @@ No resources. | [rule\_groups](#output\_rule\_groups) | Route 53 Resolver DNS Firewall rule groups | | [rules](#output\_rules) | Route 53 Resolver DNS Firewall rules | + ## References @@ -156,6 +160,7 @@ No resources. - [Appliance in a shared services VPC](https://docs.aws.amazon.com/vpc/latest/tgw/transit-gateway-appliance-scenario.html) - [Quotas on Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-entities-resolver) - [Unified bad hosts](https://github.com/StevenBlack/hosts) -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index 0647544c6..7e35bf2ed 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -51,12 +51,11 @@ components: days: 90 expiration: days: 120 - ``` ```yaml import: -- catalog/s3/defaults + - catalog/s3/defaults components: terraform: @@ -74,6 +73,7 @@ components: prefix: logs/ ``` + ## Requirements @@ -182,10 +182,11 @@ components: | [bucket\_region](#output\_bucket\_region) | Bucket region | | [bucket\_regional\_domain\_name](#output\_bucket\_regional\_domain\_name) | Bucket region-specific domain name | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/s3-bucket) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/s3-bucket) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/security-hub/README.md b/modules/security-hub/README.md index f2611672c..43bf853ca 100644 --- a/modules/security-hub/README.md +++ b/modules/security-hub/README.md @@ -52,18 +52,18 @@ and effectively manage security compliance across their AWS accounts and resourc This component is complex in that it must be deployed multiple times with different variables set to configure the AWS Organization successfully. -It is further complicated by the fact that you must deploy each of the component instances described below to -every region that existed before March 2019 and to any regions that have been opted-in as described in the [AWS -Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions). +It is further complicated by the fact that you must deploy each of the component instances described below to every +region that existed before March 2019 and to any regions that have been opted-in as described in the +[AWS Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions). In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization Delegated Administrator account is `security`, both in the `core` tenant. ### Deploy to Delegated Administrator Account -First, the component is deployed to the [Delegated -Administrator](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) account in each region to -configure the Security Hub instance to which each account will send its findings. +First, the component is deployed to the +[Delegated Administrator](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) account in each +region to configure the Security Hub instance to which each account will send its findings. ```yaml # core-ue1-security @@ -148,6 +148,7 @@ atmos terraform apply security-hub/org-settings/uw1 -s core-uw1-security # ... other regions ``` + ## Requirements @@ -236,6 +237,7 @@ atmos terraform apply security-hub/org-settings/uw1 -s core-uw1-security | [sns\_topic\_name](#output\_sns\_topic\_name) | The name of the SNS topic created by the component | | [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | The SNS topic subscriptions created by the component | + ## References diff --git a/modules/ses/README.md b/modules/ses/README.md index 4863a0b03..5d99a0c07 100644 --- a/modules/ses/README.md +++ b/modules/ses/README.md @@ -1,6 +1,7 @@ # Component: `ses` -This component is responsible for provisioning SES to act as an SMTP gateway. The credentials used for sending email can be retrieved from SSM. +This component is responsible for provisioning SES to act as an SMTP gateway. The credentials used for sending email can +be retrieved from SSM. ## Usage @@ -26,6 +27,7 @@ components: Service: ses ``` + ## Requirements @@ -97,9 +99,11 @@ components: | [user\_name](#output\_user\_name) | Normalized name of the IAM user with permission to send emails from SES domain | | [user\_unique\_id](#output\_user\_unique\_id) | The unique ID of the IAM user with permission to send emails from SES domain | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ses) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ses) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/sftp/README.md b/modules/sftp/README.md index a3b6d662f..7aad36556 100644 --- a/modules/sftp/README.md +++ b/modules/sftp/README.md @@ -19,6 +19,7 @@ components: enabled: true ``` + ## Requirements @@ -96,9 +97,11 @@ components: |------|-------------| | [sftp](#output\_sftp) | The SFTP module outputs | + ## References -* [cloudposse/terraform-aws-transfer-sftp](https://github.com/cloudposse/terraform-aws-transfer-sftp) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-transfer-sftp](https://github.com/cloudposse/terraform-aws-transfer-sftp) - Cloud Posse's + upstream component [](https://cpco.io/component) diff --git a/modules/snowflake-account/README.md b/modules/snowflake-account/README.md index 89b8932ff..3ad2093f1 100644 --- a/modules/snowflake-account/README.md +++ b/modules/snowflake-account/README.md @@ -1,6 +1,7 @@ # Component: `snowflake-account` -This component sets up the requirements for all other Snowflake components, including creating the Terraform service user. Before running this component, follow the manual, Click-Ops steps below to create a Snowflake subscription. +This component sets up the requirements for all other Snowflake components, including creating the Terraform service +user. Before running this component, follow the manual, Click-Ops steps below to create a Snowflake subscription. ## Deployment Steps @@ -10,7 +11,9 @@ This component sets up the requirements for all other Snowflake components, incl 4. Select "Snowflake Data Cloud" 5. Click "Continue to Subscribe" -6. Fill out the information steps using the following as an example. Note, the provided email cannot use labels such as `mdev+sbx01@example.com`. +6. Fill out the information steps using the following as an example. Note, the provided email cannot use labels such as + `mdev+sbx01@example.com`. + ``` First Name: John Last Name: Smith @@ -18,20 +21,29 @@ This component sets up the requirements for all other Snowflake components, incl Company: Example Country: United States ``` -7. Select "Standard" and the current region. In this example, we chose "US East (Ohio)" which is the same as `us-east-1`. -7. Continue and wait for Sign Up to complete. Note the Snowflake account ID; you can find this in the newly accessible Snowflake console in the top right of the window. -8. Check for the Account Activation email. Note, this may be collected in a Slack notifications channel for easy access. -9. Follow the given link to create the Admin user with username `admin` and a strong password. Be sure to save that password somewhere secure. -10. Upload that password to AWS Parameter Store under `/snowflake/$ACCOUNT/users/admin/password`, where `ACCOUNT` is the value given during the subscription process. This password will only be used to create a private key, and all other authentication will be done with said key. Below is an example of how to do that with a [chamber](https://github.com/segmentio/chamber) command: + +7. Select "Standard" and the current region. In this example, we chose "US East (Ohio)" which is the same as + `us-east-1`. +8. Continue and wait for Sign Up to complete. Note the Snowflake account ID; you can find this in the newly accessible + Snowflake console in the top right of the window. +9. Check for the Account Activation email. Note, this may be collected in a Slack notifications channel for easy access. +10. Follow the given link to create the Admin user with username `admin` and a strong password. Be sure to save that + password somewhere secure. +11. Upload that password to AWS Parameter Store under `/snowflake/$ACCOUNT/users/admin/password`, where `ACCOUNT` is the + value given during the subscription process. This password will only be used to create a private key, and all other + authentication will be done with said key. Below is an example of how to do that with a + [chamber](https://github.com/segmentio/chamber) command: + ``` AWS_PROFILE=$NAMESPACE-$TENANT-gbl-sbx01-admin chamber write /snowflake/$ACCOUNT/users/admin/ admin $PASSWORD ``` + 11. Finally, use atmos to deploy this component: + ``` atmos terraform deploy snowflake/account --stack $TENANT-use2-sbx01 ``` - ## Usage **Stack Level**: Regional @@ -55,6 +67,7 @@ components: Service: snowflake ``` + ## Requirements @@ -150,6 +163,6 @@ components: | [ssm\_path\_terraform\_user\_name](#output\_ssm\_path\_terraform\_user\_name) | The path to the SSM parameter for the Terraform user name. | | [ssm\_path\_terraform\_user\_private\_key](#output\_ssm\_path\_terraform\_user\_private\_key) | The path to the SSM parameter for the Terraform user private key. | - + [](https://cpco.io/component) diff --git a/modules/snowflake-database/README.md b/modules/snowflake-database/README.md index e2800c294..70e340027 100644 --- a/modules/snowflake-database/README.md +++ b/modules/snowflake-database/README.md @@ -1,6 +1,7 @@ # Component: `snowflake-database` -All data in Snowflake is stored in database tables, logically structured as collections of columns and rows. This component will create and control a Snowflake database, schema, and set of tables. +All data in Snowflake is stored in database tables, logically structured as collections of columns and rows. This +component will create and control a Snowflake database, schema, and set of tables. ## Usage @@ -39,6 +40,7 @@ components: select * from "example"; ``` + ## Requirements @@ -122,10 +124,11 @@ components: No outputs. - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/snowflake-database) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/snowflake-database) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/sns-topic/README.md b/modules/sns-topic/README.md index 95c975a88..1c5eee12d 100644 --- a/modules/sns-topic/README.md +++ b/modules/sns-topic/README.md @@ -40,12 +40,12 @@ components: fifo_queue_enabled: false content_based_deduplication: false redrive_policy_max_receiver_count: 5 - redrive_policy: null + redrive_policy: null ``` ```yaml import: -- catalog/sns-topic/defaults + - catalog/sns-topic/defaults components: terraform: @@ -65,6 +65,7 @@ components: endpoint_auto_confirms: true ``` + ## Requirements @@ -144,8 +145,11 @@ No resources. | [sns\_topic\_owner](#output\_sns\_topic\_owner) | SNS topic owner. | | [sns\_topic\_subscriptions](#output\_sns\_topic\_subscriptions) | SNS topic subscription. | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sns-topic) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sns-topic) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/spa-s3-cloudfront/CHANGELOG.md b/modules/spa-s3-cloudfront/CHANGELOG.md index 250187cd3..c2b70dd21 100644 --- a/modules/spa-s3-cloudfront/CHANGELOG.md +++ b/modules/spa-s3-cloudfront/CHANGELOG.md @@ -1,9 +1,9 @@ ## Component PRs [#991](https://github.com/cloudposse/terraform-aws-components/pull/991) and [#995](https://github.com/cloudposse/terraform-aws-components/pull/995) -### Drop `lambda_edge_redirect_404` +### Drop `lambda_edge_redirect_404` -This PRs removes the `lambda_edge_redirect_404` functionality because it leads to significat costs. -Use native CloudFront error pages configs instead. +This PRs removes the `lambda_edge_redirect_404` functionality because it leads to significat costs. Use native +CloudFront error pages configs instead. ```yaml cloudfront_custom_error_response: @@ -16,25 +16,40 @@ cloudfront_custom_error_response: ### Lambda@Edge Submodule Refactor -This PR has significantly refactored how Lambda@Edge functions are managed by Terraform with this component. Previously, the specific use cases for Lambda@Edge functions were handled by submodules `lambda-edge-preview` and `lambda_edge_redirect_404`. These component submodules both called the same Terraform module, `cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge`. These submodules have been replaced with a single Terraform file, `lambda_edge.tf`. +This PR has significantly refactored how Lambda@Edge functions are managed by Terraform with this component. Previously, +the specific use cases for Lambda@Edge functions were handled by submodules `lambda-edge-preview` and +`lambda_edge_redirect_404`. These component submodules both called the same Terraform module, +`cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge`. These submodules have been replaced with a single Terraform +file, `lambda_edge.tf`. -The reason a single file is better than submodules is (1) simplification and (2) adding the ability to deep merge function configuration. Cloudfront Distributions support a single Lambda@Edge function for each origin/viewer request or response. With deep merging, we can define default values for function configuration and provide the ability to overwrite specific values for a given deployment. +The reason a single file is better than submodules is (1) simplification and (2) adding the ability to deep merge +function configuration. Cloudfront Distributions support a single Lambda@Edge function for each origin/viewer request or +response. With deep merging, we can define default values for function configuration and provide the ability to +overwrite specific values for a given deployment. -Specifically, our own use case is using an authorization Lambda@Edge viewer request only if the paywall is enabled. Other deployments use an alternative viewer request to redirect 404. +Specifically, our own use case is using an authorization Lambda@Edge viewer request only if the paywall is enabled. +Other deployments use an alternative viewer request to redirect 404. #### Upgrading with `preview_environment_enabled: true` or `lambda_edge_redirect_404_enabled: true` -If you have `var.preview_environment_enabled` or `var.lambda_edge_redirect_404_enabled` set to `true`, Terraform `moved` will move the previous resource by submodule to the new resource by file. Please give your next Terraform plan a sanity check. Any existing Lambda functions _should not be destroyed_ by this change. +If you have `var.preview_environment_enabled` or `var.lambda_edge_redirect_404_enabled` set to `true`, Terraform `moved` +will move the previous resource by submodule to the new resource by file. Please give your next Terraform plan a sanity +check. Any existing Lambda functions _should not be destroyed_ by this change. #### Upgrading with both `preview_environment_enabled: false` and `lambda_edge_redirect_404_enabled: false` -If you have no Lambda@Edge functions deployed and where both `var.preview_environment_enabled` and `var.lambda_edge_redirect_404_enabled` are `false` (the default value), no change is necessary. +If you have no Lambda@Edge functions deployed and where both `var.preview_environment_enabled` and +`var.lambda_edge_redirect_404_enabled` are `false` (the default value), no change is necessary. ### Lambda Runtime Version -The previous PR [#946](https://github.com/cloudposse/terraform-aws-components/pull/946) introduced the `var.lambda_runtime` input. Previously, the version of node in both submodules was hard-coded to be `nodejs12.x`. This PR renames that variable to `var.lambda_edge_runtime` and sets the default to `nodejs16.x`. +The previous PR [#946](https://github.com/cloudposse/terraform-aws-components/pull/946) introduced the +`var.lambda_runtime` input. Previously, the version of node in both submodules was hard-coded to be `nodejs12.x`. This +PR renames that variable to `var.lambda_edge_runtime` and sets the default to `nodejs16.x`. -If you want to maintain the previous version of Node, set `var.lambda_edge_runtime` to `nodejs12.x`, though be aware that AWS deprecated that version on March 31, 2023, and lambdas using that environment may no longer work. Otherwise, this component will attempt to deploy the functions with runtime `nodejs16.x`. +If you want to maintain the previous version of Node, set `var.lambda_edge_runtime` to `nodejs12.x`, though be aware +that AWS deprecated that version on March 31, 2023, and lambdas using that environment may no longer work. Otherwise, +this component will attempt to deploy the functions with runtime `nodejs16.x`. - [See all available runtimes here](https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime) - [See runtime environment deprecation dates here](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtime-support-policy) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 0badadf0a..a511f7255 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -26,7 +26,7 @@ components: github_runners_component_name: github-runners github_runners_tenant_name: core github_runners_environment_name: ue2 - github_runners_stage_name : auto + github_runners_stage_name: auto origin_force_destroy: false origin_versioning_enabled: true origin_block_public_acls: true @@ -52,16 +52,16 @@ components: name: example-spa site_subdomain: example-spa cloudfront_allowed_methods: - - GET - - HEAD + - GET + - HEAD cloudfront_cached_methods: - - GET - - HEAD + - GET + - HEAD cloudfront_custom_error_response: - - error_caching_min_ttl: 1 - error_code: 403 - response_code: 200 - response_page_path: /index.html + - error_caching_min_ttl: 1 + error_code: 403 + response_code: 200 + response_page_path: /index.html cloudfront_default_ttl: 60 cloudfront_min_ttl: 60 cloudfront_max_ttl: 60 @@ -89,13 +89,15 @@ Failover origins are supported via `var.failover_s3_origin_name` and `var.failov ### Preview Environments -SPA Preview environments (i.e. `subdomain.example.com` mapping to a `/subdomain` path in the S3 bucket) powered by Lambda@Edge -are supported via `var.preview_environment_enabled`. See the both the variable description and inline documentation for -an extensive explanation for how these preview environments work. +SPA Preview environments (i.e. `subdomain.example.com` mapping to a `/subdomain` path in the S3 bucket) powered by +Lambda@Edge are supported via `var.preview_environment_enabled`. See the both the variable description and inline +documentation for an extensive explanation for how these preview environments work. ### Customizing Lambda@Edge -This component supports customizing Lambda@Edge functions for the CloudFront distribution. All Lambda@Edge function configuration is deep merged before being passed to the `cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge` module. You can add additional functions and overwrite existing functions as such: +This component supports customizing Lambda@Edge functions for the CloudFront distribution. All Lambda@Edge function +configuration is deep merged before being passed to the `cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge` module. +You can add additional functions and overwrite existing functions as such: ```yaml import: @@ -124,9 +126,9 @@ components: handler: "index.handler" event_type: "viewer-response" include_body: false - ``` + ## Requirements @@ -190,7 +192,7 @@ components: | [cloudfront\_aws\_waf\_protection\_enabled](#input\_cloudfront\_aws\_waf\_protection\_enabled) | Enable or disable AWS WAF for the CloudFront distribution.

This assumes that the `aws-waf-acl-default-cloudfront` component has been deployed to the regional stack corresponding
to `var.waf_acl_environment`. | `bool` | `true` | no | | [cloudfront\_cached\_methods](#input\_cloudfront\_cached\_methods) | List of cached methods (e.g. GET, PUT, POST, DELETE, HEAD). | `list(string)` |
[
"GET",
"HEAD"
]
| no | | [cloudfront\_compress](#input\_cloudfront\_compress) | Compress content for web requests that include Accept-Encoding: gzip in the request header. | `bool` | `false` | no | -| [cloudfront\_custom\_error\_response](#input\_cloudfront\_custom\_error\_response) | List of one or more custom error response element maps. |
list(object({
error_caching_min_ttl = string
error_code = string
response_code = string
response_page_path = string
}))
| `[]` | no | +| [cloudfront\_custom\_error\_response](#input\_cloudfront\_custom\_error\_response) | List of one or more custom error response element maps. |
list(object({
error_caching_min_ttl = optional(string, "10")
error_code = string
response_code = string
response_page_path = string
}))
| `[]` | no | | [cloudfront\_default\_root\_object](#input\_cloudfront\_default\_root\_object) | Object that CloudFront return when requests the root URL. | `string` | `"index.html"` | no | | [cloudfront\_default\_ttl](#input\_cloudfront\_default\_ttl) | Default amount of time (in seconds) that an object is in a CloudFront cache. | `number` | `60` | no | | [cloudfront\_index\_document](#input\_cloudfront\_index\_document) | Amazon S3 returns this index document when requests are made to the root domain or any of the subfolders. | `string` | `"index.html"` | no | @@ -269,11 +271,12 @@ components: | [origin\_s3\_bucket\_arn](#output\_origin\_s3\_bucket\_arn) | Origin bucket ARN. | | [origin\_s3\_bucket\_name](#output\_origin\_s3\_bucket\_name) | Origin bucket name. | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spa-s3-cloudfront) - Cloud Posse's upstream component -* [How do I use CloudFront to serve a static website hosted on Amazon S3?](https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-serve-static-website/) +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/spa-s3-cloudfront) - + Cloud Posse's upstream component +- [How do I use CloudFront to serve a static website hosted on Amazon S3?](https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-serve-static-website/) [](https://cpco.io/component) diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index a18a0fe99..4adc6c1d4 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -1,14 +1,23 @@ # Spacelift -These components are responsible for setting up Spacelift and include three components: `spacelift/admin-stack`, `spacelift/spaces`, and `spacelift/worker-pool`. +These components are responsible for setting up Spacelift and include three components: `spacelift/admin-stack`, +`spacelift/spaces`, and `spacelift/worker-pool`. -Spacelift is a specialized, Terraform-compatible continuous integration and deployment (CI/CD) platform for infrastructure-as-code. It's designed and implemented by long-time DevOps practitioners based on previous experience with large-scale installations - dozens of teams, hundreds of engineers and tens of thousands of cloud resources. +Spacelift is a specialized, Terraform-compatible continuous integration and deployment (CI/CD) platform for +infrastructure-as-code. It's designed and implemented by long-time DevOps practitioners based on previous experience +with large-scale installations - dozens of teams, hundreds of engineers and tens of thousands of cloud resources. ## Stack Configuration -Spacelift exists outside of the AWS ecosystem, so we define these components as unique to our standard stack organization. Spacelift Spaces are required before tenant-specific stacks are created in Spacelift, and the root administrator stack, referred to as `root-gbl-spacelift-admin-stack`, also does not belong to a specific tenant. Therefore, we define both outside of the standard `core` or `plat` stacks directories. That root administrator stack is responsible for creating the tenant-specific administrator stacks, `core-gbl-spacelift-admin-stack` and `plat-gbl-spacelift-admin-stack`. +Spacelift exists outside of the AWS ecosystem, so we define these components as unique to our standard stack +organization. Spacelift Spaces are required before tenant-specific stacks are created in Spacelift, and the root +administrator stack, referred to as `root-gbl-spacelift-admin-stack`, also does not belong to a specific tenant. +Therefore, we define both outside of the standard `core` or `plat` stacks directories. That root administrator stack is +responsible for creating the tenant-specific administrator stacks, `core-gbl-spacelift-admin-stack` and +`plat-gbl-spacelift-admin-stack`. -Our solution is to define a spacelift-specific configuration file per Spacelift Space. Typically our Spaces would be `root`, `core`, and `plat`, so we add three files: +Our solution is to define a spacelift-specific configuration file per Spacelift Space. Typically our Spaces would be +`root`, `core`, and `plat`, so we add three files: ```diff + stacks/orgs/NAMESPACE/spacelift.yaml @@ -18,28 +27,32 @@ Our solution is to define a spacelift-specific configuration file per Spacelift ### Global Configuration -In order to apply common Spacelift configuration to all stacks, we need to set a few global Spacelift settings. The `pr-comment-triggered` label will be required to trigger stacks with GitHub comments but is not required otherwise. More on triggering Spacelift stacks to follow. +In order to apply common Spacelift configuration to all stacks, we need to set a few global Spacelift settings. The +`pr-comment-triggered` label will be required to trigger stacks with GitHub comments but is not required otherwise. More +on triggering Spacelift stacks to follow. Add the following to `stacks/orgs/NAMESPACE/_defaults.yaml`: + ```yaml - settings: - spacelift: - workspace_enabled: true # enable spacelift by default - before_apply: - - spacelift-configure-paths - before_init: - - spacelift-configure-paths - - spacelift-write-vars - - spacelift-tf-workspace - before_plan: - - spacelift-configure-paths - labels: - - pr-comment-triggered +settings: + spacelift: + workspace_enabled: true # enable spacelift by default + before_apply: + - spacelift-configure-paths + before_init: + - spacelift-configure-paths + - spacelift-write-vars + - spacelift-tf-workspace + before_plan: + - spacelift-configure-paths + labels: + - pr-comment-triggered ``` Furthermore, specify additional tenant-specific Space configuration for both `core` and `plat` tenants. For example, for `core` add the following to `stacks/orgs/NAMESPACE/core/_defaults.yaml`: + ```yaml terraform: settings: @@ -48,6 +61,7 @@ terraform: ``` And for `plat` add the following to `stacks/orgs/NAMESPACE/plat/_defaults.yaml`: + ```yaml terraform: settings: @@ -55,12 +69,14 @@ terraform: space_name: plat ``` - ### Spacelift `root` Space -The `root` Space in Spacelift is responsible for deploying the root adminstrator stack, `admin-stack`, and the Spaces component, `spaces`. This Spaces component also includes Spacelift policies. Since the root adminstrator stack is unique to tenants, we modify the stack context to create a unique stack slug, `root-gbl-spacelift`. +The `root` Space in Spacelift is responsible for deploying the root adminstrator stack, `admin-stack`, and the Spaces +component, `spaces`. This Spaces component also includes Spacelift policies. Since the root adminstrator stack is unique +to tenants, we modify the stack context to create a unique stack slug, `root-gbl-spacelift`. `stacks/orgs/NAMESPACE/spacelift.yaml`: + ```yaml import: - mixins/region/global-region @@ -102,7 +118,6 @@ components: # this creates policies for the children (admin) stacks child_policy_attachments: - TRIGGER Global administrator - ``` #### Deployment @@ -114,6 +129,7 @@ The following steps assume that you've already authenticated with Spacelift loca ::: First deploy Spaces and policies with the `spaces` component: + ```bash atmos terraform apply spaces -s root-gbl-spacelift ``` @@ -121,11 +137,13 @@ atmos terraform apply spaces -s root-gbl-spacelift In the Spacelift UI, you should see each Space and each policy. Next, deploy the `root` `admin-stack` with the following: + ```bash atmos terraform apply admin-stack -s root-gbl-spacelift ``` -Now in the Spacelift UI, you should see the administrator stacks created. Typically these should look similiar to the following: +Now in the Spacelift UI, you should see the administrator stacks created. Typically these should look similiar to the +following: ```diff + root-gbl-spacelift-admin-stack @@ -137,18 +155,23 @@ Now in the Spacelift UI, you should see the administrator stacks created. Typica :::info -The `spacelift/worker-pool` component is deployed to a specific tenant, stage, and region but is still deployed by the root administrator stack. Verify the administrator stack by checking the `managed-by:` label. +The `spacelift/worker-pool` component is deployed to a specific tenant, stage, and region but is still deployed by the +root administrator stack. Verify the administrator stack by checking the `managed-by:` label. ::: Finally, deploy the Spacelift Worker Pool (change the stack-slug to match your configuration): + ```bash atmos terraform apply spacelift/worker-pool -s core-ue1-auto ``` ### Spacelift Tenant-Specific Spaces -A tenant-specific Space in Spacelift, such as `core` or `plat`, includes the administrator stack for that specific Space and _all_ components in the given tenant. This administrator stack uses `var.context_filters` to select all components in the given tenant and create Spacelift stacks for each. Similar to the root adminstrator stack, we again create a unique stack slug for each tenant. For example `core-gbl-spacelift` or `plat-gbl-spacelift`. +A tenant-specific Space in Spacelift, such as `core` or `plat`, includes the administrator stack for that specific Space +and _all_ components in the given tenant. This administrator stack uses `var.context_filters` to select all components +in the given tenant and create Spacelift stacks for each. Similar to the root adminstrator stack, we again create a +unique stack slug for each tenant. For example `core-gbl-spacelift` or `plat-gbl-spacelift`. For example, configure a `core` administrator stack with `stacks/orgs/NAMESPACE/core/spacelift.yaml`. @@ -185,11 +208,14 @@ components: ``` Deploy the `core` `admin-stack` with the following: + ```bash atmos terraform apply admin-stack -s core-gbl-spacelift ``` -Create the same for the `plat` tenant in `stacks/orgs/NAMESPACE/plat/spacelift.yaml`, update the tenant and configuration as necessary, and deploy with the following: +Create the same for the `plat` tenant in `stacks/orgs/NAMESPACE/plat/spacelift.yaml`, update the tenant and +configuration as necessary, and deploy with the following: + ```bash atmos terraform apply admin-stack -s plat-gbl-spacelift ``` @@ -204,20 +230,27 @@ Cloud Posse recommends two options to trigger Spacelift stacks. Historically, all stacks were triggered with three `GIT_PUSH` policies: - 1. [GIT_PUSH Global Administrator](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.administrative.rego) triggers admin stacks - 2. [GIT_PUSH Proposed Run](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.proposed-run.rego) triggers Proposed runs (typically Terraform Plan) for all non-admin stacks on Pull Requests - 3. [GIT_PUSH Tracked Run](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.tracked-run.rego) triggers Tracked runs (typically Terraform Apply) for all non-admin stacks on merges into `main` +1. [GIT_PUSH Global Administrator](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.administrative.rego) + triggers admin stacks +2. [GIT_PUSH Proposed Run](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.proposed-run.rego) + triggers Proposed runs (typically Terraform Plan) for all non-admin stacks on Pull Requests +3. [GIT_PUSH Tracked Run](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/blob/main/catalog/policies/git_push.tracked-run.rego) + triggers Tracked runs (typically Terraform Apply) for all non-admin stacks on merges into `main` Attach these policies to stacks and Spacelift will trigger them on the respective git push. - ### Triggering with GitHub Comments (Preferred) -Atmos support for `atmos describe affected` made it possible to greatly improve Spacelift's triggering workflow. Now we can add a GitHub Action to collect all affected components for a given Pull Request and add a GitHub comment to the given PR with a formatted list of the affected stacks. Then Spacelift can watch for a GitHub comment event and then trigger stacks based on that comment. +Atmos support for `atmos describe affected` made it possible to greatly improve Spacelift's triggering workflow. Now we +can add a GitHub Action to collect all affected components for a given Pull Request and add a GitHub comment to the +given PR with a formatted list of the affected stacks. Then Spacelift can watch for a GitHub comment event and then +trigger stacks based on that comment. -In order to set up GitHub Comment triggers, first add the following `GIT_PUSH Plan Affected` policy to the `spaces` component. +In order to set up GitHub Comment triggers, first add the following `GIT_PUSH Plan Affected` policy to the `spaces` +component. For example, `stacks/catalog/spacelift/spaces.yaml` + ```yaml components: terraform: @@ -232,76 +265,77 @@ components: spaces: root: policies: -... - # This policy will automatically assign itself to stacks and is used to trigger stacks directly from the `cloudposse/github-action-atmos-affected-trigger-spacelift` GitHub action - # This is only used if said GitHub action is set to trigger on "comments" - "GIT_PUSH Plan Affected": - type: GIT_PUSH - labels: - - autoattach:pr-comment-triggered - body: | - package spacelift - - # This policy runs whenever a comment is added to a pull request. It looks for the comment body to contain either: - # /spacelift preview input.stack.id - # /spacelift deploy input.stack.id - # - # If the comment matches those patterns it will queue a tracked run (deploy) or a proposed run (preview). In the case of - # a proposed run, it will also cancel all of the other pending runs for the same branch. - # - # This is being used on conjunction with the GitHub actions `atmos-trigger-spacelift-feature-branch.yaml` and - # `atmos-trigger-spacelift-main-branch.yaml` in .github/workflows to automatically trigger a preview or deploy run based - # on the `atmos describe affected` output. - - track { - commented - contains(input.pull_request.comment, concat(" ", ["/spacelift", "deploy", input.stack.id])) - } - - propose { - commented - contains(input.pull_request.comment, concat(" ", ["/spacelift", "preview", input.stack.id])) - } - - # Ignore if the event is not a comment - ignore { - not commented - } - - # Ignore if the PR has a `spacelift-no-trigger` label - ignore { - input.pull_request.labels[_] = "spacelift-no-trigger" - } - - # Ignore if the PR is a draft and deesnt have a `spacelift-trigger` label - ignore { - input.pull_request.draft - not has_spacelift_trigger_label - } - - has_spacelift_trigger_label { - input.pull_request.labels[_] == "spacelift-trigger" - } - - commented { - input.pull_request.action == "commented" - } - - cancel[run.id] { - run := input.in_progress[_] - run.type == "PROPOSED" - run.state == "QUEUED" - run.branch == input.pull_request.head.branch - } - - # This is a random sample of 10% of the runs - sample { - millis := round(input.request.timestamp_ns / 1e6) - millis % 100 <= 10 - } +--- +# This policy will automatically assign itself to stacks and is used to trigger stacks directly from the `cloudposse/github-action-atmos-affected-trigger-spacelift` GitHub action +# This is only used if said GitHub action is set to trigger on "comments" +"GIT_PUSH Plan Affected": + type: GIT_PUSH + labels: + - autoattach:pr-comment-triggered + body: | + package spacelift + + # This policy runs whenever a comment is added to a pull request. It looks for the comment body to contain either: + # /spacelift preview input.stack.id + # /spacelift deploy input.stack.id + # + # If the comment matches those patterns it will queue a tracked run (deploy) or a proposed run (preview). In the case of + # a proposed run, it will also cancel all of the other pending runs for the same branch. + # + # This is being used on conjunction with the GitHub actions `atmos-trigger-spacelift-feature-branch.yaml` and + # `atmos-trigger-spacelift-main-branch.yaml` in .github/workflows to automatically trigger a preview or deploy run based + # on the `atmos describe affected` output. + + track { + commented + contains(input.pull_request.comment, concat(" ", ["/spacelift", "deploy", input.stack.id])) + } + + propose { + commented + contains(input.pull_request.comment, concat(" ", ["/spacelift", "preview", input.stack.id])) + } + + # Ignore if the event is not a comment + ignore { + not commented + } + + # Ignore if the PR has a `spacelift-no-trigger` label + ignore { + input.pull_request.labels[_] = "spacelift-no-trigger" + } + + # Ignore if the PR is a draft and deesnt have a `spacelift-trigger` label + ignore { + input.pull_request.draft + not has_spacelift_trigger_label + } + + has_spacelift_trigger_label { + input.pull_request.labels[_] == "spacelift-trigger" + } + + commented { + input.pull_request.action == "commented" + } + + cancel[run.id] { + run := input.in_progress[_] + run.type == "PROPOSED" + run.state == "QUEUED" + run.branch == input.pull_request.head.branch + } + + # This is a random sample of 10% of the runs + sample { + millis := round(input.request.timestamp_ns / 1e6) + millis % 100 <= 10 + } ``` -This policy will automatically attach itself to _all_ components that have the `pr-comment-triggered` label, already defined in `stacks/orgs/NAMESPACE/_defaults.yaml` under `settings.spacelift.labels`. +This policy will automatically attach itself to _all_ components that have the `pr-comment-triggered` label, already +defined in `stacks/orgs/NAMESPACE/_defaults.yaml` under `settings.spacelift.labels`. Next, create two new GitHub Action workflows: @@ -310,9 +344,11 @@ Next, create two new GitHub Action workflows: + .github/workflows/atmos-trigger-spacelift-main-branch.yaml ``` -The feature branch workflow will create a comment event in Spacelift to run a Proposed run for a given stack. Whereas the main branch workflow will create a comment event in Spacelift to run a Deploy run for those same stacks. +The feature branch workflow will create a comment event in Spacelift to run a Proposed run for a given stack. Whereas +the main branch workflow will create a comment event in Spacelift to run a Deploy run for those same stacks. #### Feature Branch + ```yaml name: "Plan Affected Spacelift Stacks" @@ -337,11 +373,13 @@ jobs: ``` This will add a GitHub comment such as: + ``` /spacelift preview plat-ue1-sandbox-foobar ``` #### Main Branch + ```yaml name: "Deploy Affected Spacelift Stacks" @@ -366,6 +404,7 @@ jobs: ``` This will add a GitHub comment such as: + ``` /spacelift deploy plat-ue1-sandbox-foobar ``` diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index 08a33fd7e..ce23def69 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -9,7 +9,8 @@ The component uses a series of `context_filters` to select atmos component insta **Stack Level**: Global -The following are example snippets of how to use this component. For more on Spacelift admin stack usage, see the [Spacelift README](https://docs.cloudposse.com/components/library/aws/spacelift/) +The following are example snippets of how to use this component. For more on Spacelift admin stack usage, see the +[Spacelift README](https://docs.cloudposse.com/components/library/aws/spacelift/) First define the default configuration for any admin stack: @@ -135,6 +136,7 @@ components: - TRIGGER Dependencies ``` + ## Requirements @@ -265,3 +267,4 @@ components: | [root\_stack](#output\_root\_stack) | The root stack, if enabled and created by this component | | [root\_stack\_id](#output\_root\_stack\_id) | The stack id | + diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index dafcf34cc..f8773f8ed 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -64,6 +64,7 @@ components: - plat ``` + ## Requirements @@ -120,3 +121,4 @@ No resources. | [policies](#output\_policies) | The policies created by this component | | [spaces](#output\_spaces) | The spaces created by this component | + diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index 7727590ab..2209c8437 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -11,8 +11,7 @@ assume the role via `trusted_role_arns`), and have the following AWS managed IAM - AWSXRayDaemonWriteAccess - CloudWatchAgentServerPolicy -Among other things, this allows workers with SSM agent installed to -be accessed via SSM Session Manager. +Among other things, this allows workers with SSM agent installed to be accessed via SSM Session Manager. ```bash aws ssm start-session --target @@ -75,11 +74,11 @@ components: ### Impacts on billing -While scaling the workload for Spacelift, keep in mind that each agent connection counts -against your quota of self-hosted workers. The number of EC2 instances you have running is _not_ -going to affect your Spacelift bill. As an example, if you had 3 EC2 instances in your Spacelift -worker pool, and you configured `spacelift_agents_per_node` to be `3`, you would see your Spacelift -bill report 9 agents being run. Take care while configuring the worker pool for your Spacelift infrastructure. +While scaling the workload for Spacelift, keep in mind that each agent connection counts against your quota of +self-hosted workers. The number of EC2 instances you have running is _not_ going to affect your Spacelift bill. As an +example, if you had 3 EC2 instances in your Spacelift worker pool, and you configured `spacelift_agents_per_node` to be +`3`, you would see your Spacelift bill report 9 agents being run. Take care while configuring the worker pool for your +Spacelift infrastructure. ## Configuration @@ -92,9 +91,9 @@ has read-only access to the ECR repository. Prior to deployment, the API key must exist in SSM. The key must have admin permissions. -To generate the key, please follow [these -instructions](https://docs.spacelift.io/integrations/api.html#spacelift-api-key-token). Once generated, write the API -key ID and secret to the SSM key store at the following locations within the same AWS account and region where the +To generate the key, please follow +[these instructions](https://docs.spacelift.io/integrations/api.html#spacelift-api-key-token). Once generated, write the +API key ID and secret to the SSM key store at the following locations within the same AWS account and region where the Spacelift worker pool will reside. | Key | SSM Path | Type | @@ -118,6 +117,7 @@ After provisioning the component, you must give the created instance role permis role. This is done by adding `iam_role_arn` from the output to the `trusted_role_arns` list for the `spacelift` role in `aws-teams`. + ## Requirements @@ -260,10 +260,13 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [worker\_pool\_id](#output\_worker\_pool\_id) | Spacelift worker pool ID | | [worker\_pool\_name](#output\_worker\_pool\_name) | Spacelift worker pool name | + ## References -- [cloudposse/terraform-spacelift-cloud-infrastructure-automation](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation) - Cloud Posse's related upstream component -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/spacelift-worker-pool) - Cloud Posse's upstream component +- [cloudposse/terraform-spacelift-cloud-infrastructure-automation](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation) - + Cloud Posse's related upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/spacelift-worker-pool) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index f7114ac04..fffbaed2b 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -19,6 +19,7 @@ components: enabled: true ``` + ## Requirements @@ -89,8 +90,11 @@ No resources. | [name](#output\_name) | The name of the created Amazon SQS queue. | | [url](#output\_url) | The URL of the created Amazon SQS queue. | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sqs-queue) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sqs-queue) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ssm-parameters/README.md b/modules/ssm-parameters/README.md index 45c48d763..911755472 100644 --- a/modules/ssm-parameters/README.md +++ b/modules/ssm-parameters/README.md @@ -1,6 +1,7 @@ # Component: `ssm-parameters` -This component is responsible for provisioning Parameter Store resources against AWS SSM. It supports normal parameter store resources that can be configured directly in YAML OR pulling secret values from a local Sops file. +This component is responsible for provisioning Parameter Store resources against AWS SSM. It supports normal parameter +store resources that can be configured directly in YAML OR pulling secret values from a local Sops file. ## Usage @@ -25,6 +26,7 @@ components: type: String ``` + ## Requirements @@ -89,10 +91,11 @@ components: |------|-------------| | [created\_params](#output\_created\_params) | The keys of created SSM parameter store resources. | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ssm-parameters) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ssm-parameters) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/sso-saml-provider/README.md b/modules/sso-saml-provider/README.md index bab429ff0..008892277 100644 --- a/modules/sso-saml-provider/README.md +++ b/modules/sso-saml-provider/README.md @@ -1,6 +1,6 @@ # Component: `sso-saml-provider` -This component reads sso credentials from SSM Parameter store and provides them as outputs +This component reads sso credentials from SSM Parameter store and provides them as outputs ## Usage diff --git a/modules/strongdm/README.md b/modules/strongdm/README.md index 29709df12..20aa20f0d 100644 --- a/modules/strongdm/README.md +++ b/modules/strongdm/README.md @@ -16,6 +16,7 @@ components: enabled: true ``` + ## Requirements @@ -96,7 +97,9 @@ components: No outputs. + ## References -* https://github.com/spotinst/spotinst-kubernetes-helm-charts -* https://docs.spot.io/ocean/tutorials/spot-kubernetes-controller/ + +- https://github.com/spotinst/spotinst-kubernetes-helm-charts +- https://docs.spot.io/ocean/tutorials/spot-kubernetes-controller/ diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 3dd8e61c2..12f8433cb 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -1,25 +1,30 @@ # Component: `tfstate-backend` -This component is responsible for provisioning an S3 Bucket and DynamoDB table that follow security best practices for usage as a Terraform backend. It also creates IAM roles for access to the Terraform backend. +This component is responsible for provisioning an S3 Bucket and DynamoDB table that follow security best practices for +usage as a Terraform backend. It also creates IAM roles for access to the Terraform backend. -Once the initial S3 backend is configured, this component can create additional backends, allowing you to segregate them and control access to each backend separately. This may be desirable because any secret or sensitive information (such as generated passwords) that Terraform has access to gets stored in the Terraform state backend S3 bucket, so you may wish to restrict who can read the production Terraform state backend S3 bucket. -However, perhaps counter-intuitively, all Terraform users require read access to the most sensitive accounts, such as `root` and `audit`, in order to read security configuration information, so careful planning is required when architecting backend splits. +Once the initial S3 backend is configured, this component can create additional backends, allowing you to segregate them +and control access to each backend separately. This may be desirable because any secret or sensitive information (such +as generated passwords) that Terraform has access to gets stored in the Terraform state backend S3 bucket, so you may +wish to restrict who can read the production Terraform state backend S3 bucket. However, perhaps counter-intuitively, +all Terraform users require read access to the most sensitive accounts, such as `root` and `audit`, in order to read +security configuration information, so careful planning is required when architecting backend splits. -:::info -Part of cold start so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and then to move the state into it. -Follow the guide **[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** to get started. -::: +:::info Part of cold start so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and +then to move the state into it. Follow the guide +**[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** +to get started. ::: ### Access Control -For each backend, this module will create an IAM role with read/write access and, optionally, an IAM role with read-only access. -You can configure who is allowed to assume these roles. +For each backend, this module will create an IAM role with read/write access and, optionally, an IAM role with read-only +access. You can configure who is allowed to assume these roles. -- While read/write access is required for `terraform apply`, the created role only grants read/write access to the Terraform state, - it does not grant permission to create/modify/destroy AWS resources. +- While read/write access is required for `terraform apply`, the created role only grants read/write access to the + Terraform state, it does not grant permission to create/modify/destroy AWS resources. -- Similarly, while the read-only role prohibits making changes to the Terraform state, it does not prevent anyone - from making changes to AWS resources using a different role. +- Similarly, while the read-only role prohibits making changes to the Terraform state, it does not prevent anyone from + making changes to AWS resources using a different role. - Many Cloud Posse components store information about resources they create in the Terraform state via their outputs, and many other components read this information from the Terraform state backend via the CloudPosse `remote-state` @@ -31,74 +36,77 @@ You can configure who is allowed to assume these roles. and `security`, is nevertheless needed by every account, for example to know where to send audit logs, so it is not obvious and can be counter-intuitive which accounts need access to which backends. Plan carefully. -- Atmos provides separate configuration for Terraform state access via the `backend` and `remote_state_backend` - settings. Always configure the `backend` setting with a role that has read/write access (and override that setting - to be `null` for components deployed by SuperAdmin). If a read-only role is available (only helpful if you have - more than one backend), use that role in `remote_state_backend.s3.role_arn`. Otherwise, use the read/write role in +- Atmos provides separate configuration for Terraform state access via the `backend` and `remote_state_backend` + settings. Always configure the `backend` setting with a role that has read/write access (and override that setting to + be `null` for components deployed by SuperAdmin). If a read-only role is available (only helpful if you have more than + one backend), use that role in `remote_state_backend.s3.role_arn`. Otherwise, use the read/write role in `remote_state_backend.s3.role_arn`, to ensure that all components can read the Terraform state, even if `backend.s3.role_arn` is set to `null`, as it is with a few critical components meant to be deployed by SuperAdmin. -- Note that the "read-only" in the "read-only role" refers solely to the S3 bucket that stores the backend data. - That role still has read/write access to the DynamoDB table, which is desirable so that users restricted to the - read-only role can still perform drift detection by running `terraform plan`. The DynamoDB table only stores - checksums and mutual-exclusion lock information, so it is not considered sensitive. The worst a malicious user - could do would be to corrupt the table and cause a denial-of-service (DoS) for Terraform, but such DoS would only - affect making changes to the infrastructure, it would not affect the operation of the existing infrastructure, so - it is an ineffective and therefore unlikely vector of attack. (Also note that the entire DynamoDB table is - optional and can be deleted entirely; Terraform will repopulate it as new activity takes place.) +- Note that the "read-only" in the "read-only role" refers solely to the S3 bucket that stores the backend data. That + role still has read/write access to the DynamoDB table, which is desirable so that users restricted to the read-only + role can still perform drift detection by running `terraform plan`. The DynamoDB table only stores checksums and + mutual-exclusion lock information, so it is not considered sensitive. The worst a malicious user could do would be to + corrupt the table and cause a denial-of-service (DoS) for Terraform, but such DoS would only affect making changes to + the infrastructure, it would not affect the operation of the existing infrastructure, so it is an ineffective and + therefore unlikely vector of attack. (Also note that the entire DynamoDB table is optional and can be deleted + entirely; Terraform will repopulate it as new activity takes place.) -- For convenience, the component automatically grants access to the backend to the user deploying it. This is - helpful because it allows that user, presumably SuperAdmin, to deploy the normal components that expect - the user does not have direct access to Terraform state. +- For convenience, the component automatically grants access to the backend to the user deploying it. This is helpful + because it allows that user, presumably SuperAdmin, to deploy the normal components that expect the user does not have + direct access to Terraform state. ### Quotas -When allowing access to both SAML and AWS SSO users, the trust policy for the IAM roles created by this component -can exceed the default 2048 character limit. If you encounter this error, you can increase the limit by -requesting a quota increase [here](https://us-east-1.console.aws.amazon.com/servicequotas/home/services/iam/quotas/L-C07B4B0D). -Note that this is the IAM limit on "The maximum number of characters in an IAM role trust policy" and it must be -configured in the `us-east-1` region, regardless of what region you are deploying to. Normally 3072 characters -is sufficient, and is recommended so that you still have room to expand the trust policy in the future while -perhaps considering how to reduce its size. +When allowing access to both SAML and AWS SSO users, the trust policy for the IAM roles created by this component can +exceed the default 2048 character limit. If you encounter this error, you can increase the limit by requesting a quota +increase [here](https://us-east-1.console.aws.amazon.com/servicequotas/home/services/iam/quotas/L-C07B4B0D). Note that +this is the IAM limit on "The maximum number of characters in an IAM role trust policy" and it must be configured in the +`us-east-1` region, regardless of what region you are deploying to. Normally 3072 characters is sufficient, and is +recommended so that you still have room to expand the trust policy in the future while perhaps considering how to reduce +its size. ## Usage -**Stack Level**: Regional (because DynamoDB is region-specific), but deploy only in a single region and only in the `root` account -**Deployment**: Must be deployed by SuperAdmin using `atmos` CLI +**Stack Level**: Regional (because DynamoDB is region-specific), but deploy only in a single region and only in the +`root` account **Deployment**: Must be deployed by SuperAdmin using `atmos` CLI -This component configures the shared Terraform backend, and as such is the first component that must be deployed, since all other components depend on it. In fact, this component even depends on itself, so special deployment procedures are needed for the initial deployment (documented in the "Cold Start" procedures). +This component configures the shared Terraform backend, and as such is the first component that must be deployed, since +all other components depend on it. In fact, this component even depends on itself, so special deployment procedures are +needed for the initial deployment (documented in the "Cold Start" procedures). Here's an example snippet for how to use this component. ```yaml - terraform: - tfstate-backend: - backend: - s3: - role_arn: null - settings: - spacelift: - workspace_enabled: false - vars: - enable_server_side_encryption: true - enabled: true - force_destroy: false - name: tfstate - prevent_unencrypted_uploads: true - access_roles: - default: &tfstate-access-template - write_enabled: true - allowed_roles: - core-identity: ["devops", "developers", "managers", "spacelift"] - core-root: ["admin"] - denied_roles: {} - allowed_permission_sets: - core-identity: ["AdministratorAccess"] - denied_permission_sets: {} - allowed_principal_arns: [] - denied_principal_arns: [] +terraform: + tfstate-backend: + backend: + s3: + role_arn: null + settings: + spacelift: + workspace_enabled: false + vars: + enable_server_side_encryption: true + enabled: true + force_destroy: false + name: tfstate + prevent_unencrypted_uploads: true + access_roles: + default: &tfstate-access-template + write_enabled: true + allowed_roles: + core-identity: ["devops", "developers", "managers", "spacelift"] + core-root: ["admin"] + denied_roles: {} + allowed_permission_sets: + core-identity: ["AdministratorAccess"] + denied_permission_sets: {} + allowed_principal_arns: [] + denied_principal_arns: [] ``` + ## Requirements @@ -174,7 +182,9 @@ Here's an example snippet for how to use this component. | [tfstate\_backend\_s3\_bucket\_domain\_name](#output\_tfstate\_backend\_s3\_bucket\_domain\_name) | Terraform state S3 bucket domain name | | [tfstate\_backend\_s3\_bucket\_id](#output\_tfstate\_backend\_s3\_bucket\_id) | Terraform state S3 bucket ID | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tfstate-backend) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tfstate-backend) - + Cloud Posse's upstream component diff --git a/modules/tgw/CHANGELOG.md b/modules/tgw/CHANGELOG.md index 41575194d..9c2e9a799 100644 --- a/modules/tgw/CHANGELOG.md +++ b/modules/tgw/CHANGELOG.md @@ -10,19 +10,27 @@ Components PR [#804](https://github.com/cloudposse/terraform-aws-components/pull ### Summary -This change to the Transit Gateway components, [PR #804](https://github.com/cloudposse/terraform-aws-components/pull/804), added support for cross-region connections. +This change to the Transit Gateway components, +[PR #804](https://github.com/cloudposse/terraform-aws-components/pull/804), added support for cross-region connections. -As part of that change, we've added `environment` to the component identifier used in the Terraform Output created by `tgw/hub`. Because of that map key change, all resources in Terraform now have a new resource identifier and therefore must be recreated with Terraform or removed from state and imported into the new resource ID. +As part of that change, we've added `environment` to the component identifier used in the Terraform Output created by +`tgw/hub`. Because of that map key change, all resources in Terraform now have a new resource identifier and therefore +must be recreated with Terraform or removed from state and imported into the new resource ID. -Recreating the resources is the easiest solution but means that Transit Gateway connectivity will be lost while the changes apply, which typically takes an hour. Alternatively, removing the resources from state and importing back into the new resource ID is much more complex operationally but means no lost Transit Gateway connectivity. +Recreating the resources is the easiest solution but means that Transit Gateway connectivity will be lost while the +changes apply, which typically takes an hour. Alternatively, removing the resources from state and importing back into +the new resource ID is much more complex operationally but means no lost Transit Gateway connectivity. -Since we use Transit Gateway for VPN and GitHub Automation runner access, a temporarily lost connection is not a significant concern, so we choose to accept lost connectivity and recreate all `tgw/spoke` resources. +Since we use Transit Gateway for VPN and GitHub Automation runner access, a temporarily lost connection is not a +significant concern, so we choose to accept lost connectivity and recreate all `tgw/spoke` resources. ### Steps 1. Notify your team of a temporary VPN and Automation outage for accessing private networks -2. Deploy all `tgw/hub` components. There should be a hub component in each region of your network account connected to Transit Gateway -3. Deploy all `tgw/spoke` components. There should be a spoke component in every account and every region connected to Transit Gateway +2. Deploy all `tgw/hub` components. There should be a hub component in each region of your network account connected to + Transit Gateway +3. Deploy all `tgw/spoke` components. There should be a spoke component in every account and every region connected to + Transit Gateway #### Tips diff --git a/modules/tgw/README.md b/modules/tgw/README.md index 68103f870..8d191c74d 100644 --- a/modules/tgw/README.md +++ b/modules/tgw/README.md @@ -1,27 +1,37 @@ # Transit Gateway: `tgw` -AWS Transit Gateway connects your Amazon Virtual Private Clouds (VPCs) and on-premises networks through a central hub. This connection simplifies your network and puts an end to complex peering relationships. Transit Gateway acts as a highly scalable cloud router—each new connection is made only once. +AWS Transit Gateway connects your Amazon Virtual Private Clouds (VPCs) and on-premises networks through a central hub. +This connection simplifies your network and puts an end to complex peering relationships. Transit Gateway acts as a +highly scalable cloud router—each new connection is made only once. For more on Transit Gateway, see [the AWS documentation](https://aws.amazon.com/transit-gateway/). ## Requirements -In order to connect accounts with Transit Gateway, we deploy Transit Gateway to a central account, typically `core-network`, and then deploy Transit Gateway attachments for each connected account. Each connected accounts needs a Transit Gateway attachment for the given account's VPC, either by VPC attachment or by Peering Connection attachment. Furthermore, each private subnet in each connected VPC needs to explicitly list the CIDRs for all allowed connections. +In order to connect accounts with Transit Gateway, we deploy Transit Gateway to a central account, typically +`core-network`, and then deploy Transit Gateway attachments for each connected account. Each connected accounts needs a +Transit Gateway attachment for the given account's VPC, either by VPC attachment or by Peering Connection attachment. +Furthermore, each private subnet in each connected VPC needs to explicitly list the CIDRs for all allowed connections. ## Solution -First we deploy the Transit Gateway Hub, `tgw/hub`, to a central network account. The component prepares the Transit Gateway network with the following steps: +First we deploy the Transit Gateway Hub, `tgw/hub`, to a central network account. The component prepares the Transit +Gateway network with the following steps: 1. Provision Transit Gateway in the network account 2. Collect VPC and EKS component output from every account connected to Transit Gateway 3. Share the Transit Gateway with the Organization using Resource Access Manager (RAM) -By using the `tgw/hub` component to collect Terraform output from connected accounts, only this single component requires access to the Terraform state of all connected accounts. +By using the `tgw/hub` component to collect Terraform output from connected accounts, only this single component +requires access to the Terraform state of all connected accounts. -Next we deploy `tgw/spoke` to the network account and then to every connected account. This spoke component connects the given account to the central hub and any listed connection with the following steps: +Next we deploy `tgw/spoke` to the network account and then to every connected account. This spoke component connects the +given account to the central hub and any listed connection with the following steps: -1. Create a Transit Gateway VPC attachment in the spoke account. This connects the account's VPC to the shared Transit Gateway from the hub account. -2. Define all allowed routes for private subnets. Each private subnet in an account's VPC has it's own route table. This route table needs to explicitly list any allowed connection to another account's VPC CIDR. +1. Create a Transit Gateway VPC attachment in the spoke account. This connects the account's VPC to the shared Transit + Gateway from the hub account. +2. Define all allowed routes for private subnets. Each private subnet in an account's VPC has it's own route table. This + route table needs to explicitly list any allowed connection to another account's VPC CIDR. 3. (Optional) Create an EKS Cluster Security Group rule to allow traffic to the cluster in the given account. ## Implementation @@ -150,7 +160,6 @@ tgw/spoke: - account: tenant: core stage: auto - ``` ### Alternate Regions @@ -161,15 +170,20 @@ In order to connect any account to the network, the given account needs: 2. An attachment for the given Transit Gateway hub 3. Routes to and from each private subnet -However, sharing the Transit Gateway hub via RAM is only supported in the same region as the primary hub. Therefore, we must instead deploy a new hub in the alternate region and create a [Transit Gateway Peering Connection](https://docs.aws.amazon.com/vpc/latest/tgw/tgw-peering.html) between the two Transit Gateway hubs. +However, sharing the Transit Gateway hub via RAM is only supported in the same region as the primary hub. Therefore, we +must instead deploy a new hub in the alternate region and create a +[Transit Gateway Peering Connection](https://docs.aws.amazon.com/vpc/latest/tgw/tgw-peering.html) between the two +Transit Gateway hubs. -Furthermore, since this Transit Gateway hub for the alternate region is now peered, we must create a Peering Transit Gateway attachment, opposed to a VPC Transit Gateway Attachment. +Furthermore, since this Transit Gateway hub for the alternate region is now peered, we must create a Peering Transit +Gateway attachment, opposed to a VPC Transit Gateway Attachment. #### Cross Region Deployment 1. Deploy `tgw/hub` and `tgw/spoke` into the primary region as described in [Implementation](#implementation) -2. Deploy `tgw/hub` and `tgw/cross-region-hub` into the new region in the network account. See the following configuration: +2. Deploy `tgw/hub` and `tgw/cross-region-hub` into the new region in the network account. See the following + configuration: ```yaml # stacks/catalog/tgw/cross-region-hub @@ -343,13 +357,17 @@ tgw/spoke: ## Destruction -When destroying Transit Gateway components, order of operations matters. Always destroy any removed `tgw/spoke` components before removing a connection from the `tgw/hub` component. +When destroying Transit Gateway components, order of operations matters. Always destroy any removed `tgw/spoke` +components before removing a connection from the `tgw/hub` component. -The `tgw/hub` component creates map of VPC resources that each `tgw/spoke` component references. If the required reference is removed before the `tgw/spoke` is destroyed, Terraform will fail to destroy the given `tgw/spoke` component. +The `tgw/hub` component creates map of VPC resources that each `tgw/spoke` component references. If the required +reference is removed before the `tgw/spoke` is destroyed, Terraform will fail to destroy the given `tgw/spoke` +component. :::info Pro Tip! -[Atmos Workflows](https://atmos.tools/core-concepts/workflows/) make applying and destroying Transit Gateway much easier! For example, to destroy components in the correct order, use a workflow similiar to the following: +[Atmos Workflows](https://atmos.tools/core-concepts/workflows/) make applying and destroying Transit Gateway much +easier! For example, to destroy components in the correct order, use a workflow similiar to the following: ```yaml # stacks/workflows/network.yaml @@ -395,4 +413,5 @@ Releasing state lock. This may take a few moments... exit status 1 ``` -This is caused by Terraform attempting to create the replacement VPC attachment before the original is completely destroyed. Retry the apply. Now you should see only "create" actions. +This is caused by Terraform attempting to create the replacement VPC attachment before the original is completely +destroyed. Retry the apply. Now you should see only "create" actions. diff --git a/modules/tgw/cross-region-hub-connector/README.md b/modules/tgw/cross-region-hub-connector/README.md index 5efbd3bb4..1de5a593a 100644 --- a/modules/tgw/cross-region-hub-connector/README.md +++ b/modules/tgw/cross-region-hub-connector/README.md @@ -1,8 +1,11 @@ # Component: `cross-region-hub-connector` -This component is responsible for provisioning an [AWS Transit Gateway Peering Connection](https://aws.amazon.com/transit-gateway) to connect TGWs from different accounts and(or) regions. +This component is responsible for provisioning an +[AWS Transit Gateway Peering Connection](https://aws.amazon.com/transit-gateway) to connect TGWs from different accounts +and(or) regions. -Transit Gateway does not support sharing the Transit Gateway hub across regions. You must deploy a Transit Gateway hub for each region and connect the alternate hub to the primary hub. +Transit Gateway does not support sharing the Transit Gateway hub across regions. You must deploy a Transit Gateway hub +for each region and connect the alternate hub to the primary hub. ## Usage @@ -10,8 +13,9 @@ Transit Gateway does not support sharing the Transit Gateway hub across regions. This component is deployed to each alternate region with `tgw/hub`. -For example if your primary region is `us-east-1` and your alternate region is `us-west-2`, deploy another `tgw/hub` in `us-west-2` -and peer the two with `tgw/cross-region-hub-connector` with the following stack config, imported into `us-west-2` +For example if your primary region is `us-east-1` and your alternate region is `us-west-2`, deploy another `tgw/hub` in +`us-west-2` and peer the two with `tgw/cross-region-hub-connector` with the following stack config, imported into +`us-west-2` ```yaml import: @@ -54,6 +58,7 @@ components: primary_tgw_hub_region: us-east-1 ``` + ## Requirements @@ -126,9 +131,11 @@ components: |------|-------------| | [aws\_ec2\_transit\_gateway\_peering\_attachment\_id](#output\_aws\_ec2\_transit\_gateway\_peering\_attachment\_id) | Transit Gateway Peering Attachment ID | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw/cross-region-hub-connector) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw/cross-region-hub-connector) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 135e22281..1ada2debe 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -1,6 +1,7 @@ # Component: `tgw/hub` -This component is responsible for provisioning an [AWS Transit Gateway](https://aws.amazon.com/transit-gateway) `hub` that acts as a centralized gateway for connecting VPCs from other `spoke` accounts. +This component is responsible for provisioning an [AWS Transit Gateway](https://aws.amazon.com/transit-gateway) `hub` +that acts as a centralized gateway for connecting VPCs from other `spoke` accounts. ## Usage @@ -76,6 +77,7 @@ atmos terraform plan tgw/hub -s --network atmos terraform apply tgw/hub -s --network ``` + ## Requirements @@ -145,9 +147,11 @@ No resources. | [transit\_gateway\_route\_table\_id](#output\_transit\_gateway\_route\_table\_id) | Transit Gateway route table ID | | [vpcs](#output\_vpcs) | Accounts with VPC and VPCs information | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw/hub) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw/hub) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index de63a4e58..acc6ce0ba 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -1,6 +1,7 @@ # Component: `tgw/spoke` -This component is responsible for provisioning [AWS Transit Gateway](https://aws.amazon.com/transit-gateway) attachments to connect VPCs in a `spoke` account to different accounts through a central `hub`. +This component is responsible for provisioning [AWS Transit Gateway](https://aws.amazon.com/transit-gateway) attachments +to connect VPCs in a `spoke` account to different accounts through a central `hub`. ## Usage @@ -88,6 +89,7 @@ atmos terraform plan tgw/spoke -s -- atmos terraform apply tgw/spoke -s -- ``` + ## Requirements @@ -164,9 +166,11 @@ atmos terraform apply tgw/spoke -s -- No outputs. + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/tgw) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/vpc-flow-logs-bucket/README.md b/modules/vpc-flow-logs-bucket/README.md index 7ab0ca610..143e0d10c 100644 --- a/modules/vpc-flow-logs-bucket/README.md +++ b/modules/vpc-flow-logs-bucket/README.md @@ -8,7 +8,8 @@ This component is responsible for provisioning an encrypted S3 bucket which is c Here's an example snippet for how to use this component. -**IMPORTANT**: This component expects the `aws_flow_log` resource to be created externally. Typically that is accomplished through [the `vpc` component](../vpc/). +**IMPORTANT**: This component expects the `aws_flow_log` resource to be created externally. Typically that is +accomplished through [the `vpc` component](../vpc/). ```yaml components: @@ -23,6 +24,7 @@ components: expiration_days: 365 ``` + ## Requirements @@ -88,10 +90,11 @@ No resources. | [vpc\_flow\_logs\_bucket\_arn](#output\_vpc\_flow\_logs\_bucket\_arn) | VPC Flow Logs bucket ARN | | [vpc\_flow\_logs\_bucket\_id](#output\_vpc\_flow\_logs\_bucket\_id) | VPC Flow Logs bucket ID | - + ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc-flow-logs-bucket) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc-flow-logs-bucket) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/vpc-peering/README.md b/modules/vpc-peering/README.md index a7cc67e65..3238f64d2 100644 --- a/modules/vpc-peering/README.md +++ b/modules/vpc-peering/README.md @@ -50,42 +50,43 @@ components: Use case: Peering v2 accounts to v2 ```yaml - vpc-peering/-vpc0: - metadata: - component: vpc-peering - inherits: - - vpc-peering/defaults - vars: - requester_vpc_component_name: vpc - accepter_region: us-east-1 - accepter_stage_name: - accepter_vpc: - tags: - # Fill in with your own information - Name: acme---- +vpc-peering/-vpc0: + metadata: + component: vpc-peering + inherits: + - vpc-peering/defaults + vars: + requester_vpc_component_name: vpc + accepter_region: us-east-1 + accepter_stage_name: + accepter_vpc: + tags: + # Fill in with your own information + Name: acme---- ``` ## Legacy Account Configuration The `vpc-peering` component peers the `dev`, `prod`, `sandbox` and `staging` VPCs to a VPC in the legacy account. -The `dev`, `prod`, `sandbox` and `staging` VPCs are the requesters of the VPC peering connection, -while the legacy VPC is the accepter of the peering connection. - -To provision VPC peering and all related resources with Terraform, we need the following information from the legacy account: +The `dev`, `prod`, `sandbox` and `staging` VPCs are the requesters of the VPC peering connection, while the legacy VPC +is the accepter of the peering connection. - - Legacy account ID - - Legacy VPC ID - - Legacy AWS region - - Legacy IAM role (the role must be created in the legacy account with permissions to create VPC peering and routes). - The name of the role could be `acme-vpc-peering` and the ARN of the role should look like `arn:aws:iam:::role/acme-vpc-peering` +To provision VPC peering and all related resources with Terraform, we need the following information from the legacy +account: +- Legacy account ID +- Legacy VPC ID +- Legacy AWS region +- Legacy IAM role (the role must be created in the legacy account with permissions to create VPC peering and routes). + The name of the role could be `acme-vpc-peering` and the ARN of the role should look like + `arn:aws:iam:::role/acme-vpc-peering` ### Legacy Account IAM Role In the legacy account, create IAM role `acme-vpc-peering` with the following policy: -__NOTE:__ Replace `` with the ID of the legacy account. +**NOTE:** Replace `` with the ID of the legacy account. ```json { @@ -93,10 +94,7 @@ __NOTE:__ Replace `` with the ID of the legacy account. "Statement": [ { "Effect": "Allow", - "Action": [ - "ec2:CreateRoute", - "ec2:DeleteRoute" - ], + "Action": ["ec2:CreateRoute", "ec2:DeleteRoute"], "Resource": "arn:aws:ec2:*::route-table/*" }, { @@ -126,10 +124,7 @@ __NOTE:__ Replace `` with the ID of the legacy account. }, { "Effect": "Allow", - "Action": [ - "ec2:DeleteTags", - "ec2:CreateTags" - ], + "Action": ["ec2:DeleteTags", "ec2:CreateTags"], "Resource": "arn:aws:ec2:*::vpc-peering-connection/*" } ] @@ -138,7 +133,7 @@ __NOTE:__ Replace `` with the ID of the legacy account. Add the following trust policy to the IAM role: -__NOTE:__ Replace `` with the ID of the `identity` account in the new infrastructure. +**NOTE:** Replace `` with the ID of the `identity` account in the new infrastructure. ```json { @@ -147,26 +142,22 @@ __NOTE:__ Replace `` with the ID of the `identity` account { "Effect": "Allow", "Principal": { - "AWS": [ - "arn:aws:iam:::root" - ] + "AWS": ["arn:aws:iam:::root"] }, - "Action": [ - "sts:AssumeRole", - "sts:TagSession" - ], + "Action": ["sts:AssumeRole", "sts:TagSession"], "Condition": {} } ] } ``` -The trust policy allows the `identity` account to assume the role (and provision all the resources in the legacy account). +The trust policy allows the `identity` account to assume the role (and provision all the resources in the legacy +account). ## Provisioning -Provision the VPC peering connections in the `dev`, `prod`, `sandbox` and `staging` accounts by executing -the following commands: +Provision the VPC peering connections in the `dev`, `prod`, `sandbox` and `staging` accounts by executing the following +commands: ```sh atmos terraform plan vpc-peering -s ue1-sandbox @@ -182,6 +173,7 @@ atmos terraform plan vpc-peering -s ue1-prod atmos terraform apply vpc-peering -s ue1-prod ``` + ## Requirements @@ -249,7 +241,9 @@ atmos terraform apply vpc-peering -s ue1-prod |------|-------------| | [vpc\_peering](#output\_vpc\_peering) | VPC peering outputs | + -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc-peering) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc-peering) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/vpc/README.md b/modules/vpc/README.md index a72901b3c..64dde8cd3 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -1,6 +1,8 @@ # Component: `vpc` -This component is responsible for provisioning a VPC and corresponding Subnets. Additionally, VPC Flow Logs can optionally be enabled for auditing purposes. See the existing VPC configuration documentation for the provisioned subnets. +This component is responsible for provisioning a VPC and corresponding Subnets. Additionally, VPC Flow Logs can +optionally be enabled for auditing purposes. See the existing VPC configuration documentation for the provisioned +subnets. ## Usage @@ -52,6 +54,7 @@ components: ipv4_primary_cidr_block: "10.111.0.0/18" ``` + ## Requirements @@ -164,9 +167,11 @@ components: | [vpc\_default\_security\_group\_id](#output\_vpc\_default\_security\_group\_id) | The ID of the security group created by default on VPC creation | | [vpc\_id](#output\_vpc\_id) | VPC ID | + ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/vpc) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/waf/README.md b/modules/waf/README.md index 3a2564dd9..3538f19fc 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -1,7 +1,7 @@ # Component: `aws-waf-acl` -This component is responsible for provisioning an AWS Web Application Firewall (WAF) with an associated managed rule group. - +This component is responsible for provisioning an AWS Web Application Firewall (WAF) with an associated managed rule +group. ## Usage @@ -24,21 +24,22 @@ components: metric_name: "default" sampled_requests_enabled: false managed_rule_group_statement_rules: - - name: "OWASP-10" - # Rules are processed in order based on the value of priority, lowest number first - priority: 1 - - statement: - name: AWSManagedRulesCommonRuleSet - vendor_name: AWS - - visibility_config: - # Defines and enables Amazon CloudWatch metrics and web request sample collection. - cloudwatch_metrics_enabled: false - metric_name: "OWASP-10" - sampled_requests_enabled: false + - name: "OWASP-10" + # Rules are processed in order based on the value of priority, lowest number first + priority: 1 + + statement: + name: AWSManagedRulesCommonRuleSet + vendor_name: AWS + + visibility_config: + # Defines and enables Amazon CloudWatch metrics and web request sample collection. + cloudwatch_metrics_enabled: false + metric_name: "OWASP-10" + sampled_requests_enabled: false ``` + ## Requirements @@ -128,10 +129,11 @@ components: | [id](#output\_id) | The ID of the WAF WebACL. | | [logging\_config\_id](#output\_logging\_config\_id) | The ARN of the WAFv2 Web ACL logging configuration. | - + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/waf) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/waf) - + Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index 97843df14..c736f0109 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -2,7 +2,9 @@ This component is responsible for provisioning ZScaler Private Access Connector instances on Amazon Linux 2 AMIs. -Prior to provisioning this component, it is required that a SecureString SSM Parameter containing the ZScaler App Connector Provisioning Key is populated in each account corresponding to the regional stack the component is deployed to, with the name of the SSM Parameter matching the value of `var.zscaler_key`. +Prior to provisioning this component, it is required that a SecureString SSM Parameter containing the ZScaler App +Connector Provisioning Key is populated in each account corresponding to the regional stack the component is deployed +to, with the name of the SSM Parameter matching the value of `var.zscaler_key`. This parameter should be populated using `chamber`, which is included in the geodesic image: @@ -10,7 +12,8 @@ This parameter should be populated using `chamber`, which is included in the geo chamber write zscaler key ``` -Where `` is the ZScaler App Connector Provisioning Key. For more information on how to generate this key, see: [ZScaler documentation on Configuring App Connectors](https://help.zscaler.com/zpa/configuring-connectors). +Where `` is the ZScaler App Connector Provisioning Key. For more information on how to generate this key, see: +[ZScaler documentation on Configuring App Connectors](https://help.zscaler.com/zpa/configuring-connectors). ## Usage @@ -26,7 +29,8 @@ components: zscaler_count: 2 ``` -Preferably, regional stack configurations can be kept _DRY_ by importing `catalog/zscaler` via the `imports` list at the top of the configuration. +Preferably, regional stack configurations can be kept _DRY_ by importing `catalog/zscaler` via the `imports` list at the +top of the configuration. ``` import: @@ -34,6 +38,7 @@ import: - catalog/zscaler ``` + ## Requirements @@ -106,8 +111,11 @@ import: | [instance\_id](#output\_instance\_id) | Instance ID | | [private\_ip](#output\_private\_ip) | Private IP of the instance | + ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/zscaler) - Cloud Posse's upstream component + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/zscaler) - + Cloud Posse's upstream component [](https://cpco.io/component) From 4acf5f7e094ceed898bc8cdfad186443ec521fdb Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 11 Mar 2024 11:09:02 -0700 Subject: [PATCH 379/501] [`ecs-service`] update ecs service with s3 templating change for GHA (#997) --- modules/ecs-service/README.md | 3 + .../ecs-service/github-actions-iam-policy.tf | 4 +- modules/ecs-service/main.tf | 88 ++++++++++++++----- modules/ecs-service/outputs.tf | 5 ++ 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 567f832bd..027af5a73 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -311,10 +311,12 @@ components: | [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_kinesis_stream.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_stream) | resource | +| [aws_s3_bucket_object.task_definition_template](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_object) | resource | | [aws_security_group_rule.custom_sg_rules](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_service_discovery_service.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_service) | resource | | [aws_ssm_parameter.full_urls](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_ecs_task_definition.created_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_task_definition) | data source | | [aws_iam_policy_document.github_actions_iam_ecspresso_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.github_actions_iam_platform_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.github_actions_iam_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -456,6 +458,7 @@ components: | [ssm\_key\_prefix](#output\_ssm\_key\_prefix) | SSM prefix | | [ssm\_parameters](#output\_ssm\_parameters) | SSM parameters for the ECS Service | | [subnet\_ids](#output\_subnet\_ids) | Selected subnet IDs | +| [task\_template](#output\_task\_template) | The task template rendered | | [vpc\_id](#output\_vpc\_id) | Selected VPC ID | | [vpc\_sg\_id](#output\_vpc\_sg\_id) | Selected VPC SG ID | diff --git a/modules/ecs-service/github-actions-iam-policy.tf b/modules/ecs-service/github-actions-iam-policy.tf index be17d952e..1500ce8aa 100644 --- a/modules/ecs-service/github-actions-iam-policy.tf +++ b/modules/ecs-service/github-actions-iam-policy.tf @@ -164,7 +164,9 @@ data "aws_iam_policy_document" "github_actions_iam_ecspresso_policy" { content { effect = "Allow" actions = [ - "s3:PutObject" + "s3:PutObject", + "s3:GetObject", + "s3:HeadObject", ] resources = [ format("%s/%s/%s/*", lookup(module.s3[0].outputs, "bucket_arn", null), module.ecs_cluster.outputs.cluster_name, module.this.id) diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 4830ebc7b..7511f81cc 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -14,10 +14,12 @@ locals { for container in module.container_definition : container.json_map_object ], - [for container in module.datadog_container_definition : + [ + for container in module.datadog_container_definition : container.json_map_object ], - var.datadog_log_method_is_firelens ? [for container in module.datadog_fluent_bit_container_definition : + var.datadog_log_method_is_firelens ? [ + for container in module.datadog_fluent_bit_container_definition : container.json_map_object ] : [], ) @@ -48,19 +50,22 @@ locals { efs_component_remote_state = { for efs in local.efs_component_volumes : efs["name"] => module.efs[efs["name"]].outputs } - efs_component_merged = [for efs_volume_name, efs_component_output in local.efs_component_remote_state : { - host_path = local.efs_component_map[efs_volume_name].host_path - name = efs_volume_name - efs_volume_configuration = [ #again this is a hardcoded array because AWS does not support multiple configurations per volume - { - file_system_id = efs_component_output.efs_id - root_directory = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].root_directory - transit_encryption = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].transit_encryption - transit_encryption_port = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].transit_encryption_port - authorization_config = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].authorization_config - } - ] - }] + efs_component_merged = [ + for efs_volume_name, efs_component_output in local.efs_component_remote_state : { + host_path = local.efs_component_map[efs_volume_name].host_path + name = efs_volume_name + efs_volume_configuration = [ + #again this is a hardcoded array because AWS does not support multiple configurations per volume + { + file_system_id = efs_component_output.efs_id + root_directory = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].root_directory + transit_encryption = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].transit_encryption + transit_encryption_port = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].transit_encryption_port + authorization_config = local.efs_component_map[efs_volume_name].efs_volume_configuration[0].authorization_config + } + ] + } + ] efs_volumes = concat(lookup(local.task, "efs_volumes", []), local.efs_component_merged) } @@ -105,19 +110,23 @@ module "roles_to_principals" { } locals { - container_chamber = { for name, result in data.aws_ssm_parameters_by_path.default : + container_chamber = { + for name, result in data.aws_ssm_parameters_by_path.default : name => { for key, value in zipmap(result.names, result.values) : element(reverse(split("/", key)), 0) => value } } - container_aliases = { for name, settings in var.containers : + container_aliases = { + for name, settings in var.containers : settings["name"] => name if local.enabled } - container_s3 = { for item in lookup(local.task_definition_s3, "containerDefinitions", []) : + container_s3 = { + for item in lookup(local.task_definition_s3, "containerDefinitions", []) : local.container_aliases[item.name] => { container_definition = item } } - containers = { for name, settings in var.containers : + containers = { + for name, settings in var.containers : name => merge(settings, local.container_chamber[name], lookup(local.container_s3, name, {})) if local.enabled } @@ -309,7 +318,10 @@ module "ecs_alb_service_task" { } resource "aws_security_group_rule" "custom_sg_rules" { - for_each = local.enabled && var.custom_security_group_rules != [] ? { for sg_rule in var.custom_security_group_rules : format("%s_%s_%s", sg_rule.protocol, sg_rule.from_port, sg_rule.to_port) => sg_rule } : {} + for_each = local.enabled && var.custom_security_group_rules != [] ? { + for sg_rule in var.custom_security_group_rules : + format("%s_%s_%s", sg_rule.protocol, sg_rule.from_port, sg_rule.to_port) => sg_rule + } : {} description = each.value.description type = each.value.type from_port = each.value.from_port @@ -327,8 +339,10 @@ module "alb_ingress" { vpc_id = local.vpc_id unauthenticated_listener_arns = [local.lb_listener_https_arn] - unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", var.vanity_domain), local.full_domain] : concat([local.full_domain], var.vanity_alias, var.additional_targets) - unauthenticated_paths = flatten(var.unauthenticated_paths) + unauthenticated_hosts = var.lb_catch_all ? [format("*.%s", var.vanity_domain), local.full_domain] : concat([ + local.full_domain + ], var.vanity_alias, var.additional_targets) + unauthenticated_paths = flatten(var.unauthenticated_paths) # When set to catch-all, make priority super high to make sure last to match unauthenticated_priority = var.lb_catch_all ? 99 : var.unauthenticated_priority default_target_group_enabled = true @@ -537,3 +551,33 @@ resource "aws_kinesis_stream" "default" { ] } } + +data "aws_ecs_task_definition" "created_task" { + task_definition = module.ecs_alb_service_task[0].task_definition_family +} + +locals { + created_task_definition = data.aws_ecs_task_definition.created_task + task_template = merge( + { + containerDefinitions = local.container_definition + family = lookup(local.created_task_definition, "family", null), + taskRoleArn = lookup(local.created_task_definition, "task_role_arn", null), + executionRoleArn = lookup(local.created_task_definition, "execution_role_arn", null), + networkMode = lookup(local.created_task_definition, "network_mode", null), + # we explicitly do not put the volumes here. That should be merged in by GHA + requiresCompatibilities = [lookup(local.task, "launch_type", "FARGATE")] + cpu = tostring(lookup(local.task, "task_cpu", null)) + memory = tostring(lookup(local.task, "task_memory", null)) + + } + ) +} + +resource "aws_s3_bucket_object" "task_definition_template" { + count = local.s3_mirroring_enabled ? 1 : 0 + bucket = lookup(module.s3[0].outputs, "bucket_id", null) + key = format("%s/%s/task-template.json", module.ecs_cluster.outputs.cluster_name, module.this.id) + content = jsonencode(local.task_template) + server_side_encryption = "AES256" +} diff --git a/modules/ecs-service/outputs.tf b/modules/ecs-service/outputs.tf index 38d59b45d..f5a4acf11 100644 --- a/modules/ecs-service/outputs.tf +++ b/modules/ecs-service/outputs.tf @@ -52,3 +52,8 @@ output "service_image" { value = try(nonsensitive(local.containers.service.image), null) description = "The image of the service container" } + +output "task_template" { + value = jsondecode(nonsensitive(jsonencode(local.task_template))) + description = "The task template rendered" +} From a001f8936eb6f2a6ef5485bdc1732b9ce3a48542 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 11 Mar 2024 13:55:45 -0700 Subject: [PATCH 380/501] Fix: `ecs-service` perma-drift for `key_id` (#999) --- modules/ecs-service/systems-manager.tf | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/ecs-service/systems-manager.tf b/modules/ecs-service/systems-manager.tf index 5a40afa07..9c84cf79d 100644 --- a/modules/ecs-service/systems-manager.tf +++ b/modules/ecs-service/systems-manager.tf @@ -51,9 +51,11 @@ resource "aws_ssm_parameter" "full_urls" { name = each.key description = each.value.description type = each.value.type - key_id = var.kms_alias_name_ssm - value = each.value.value - overwrite = true + # key_id is only used with SecureStrings. + # With other types Terraform will pass but will constantly suggest adding the key_id (perma drift) + key_id = each.value.type == "SecureString" ? var.kms_alias_name_ssm : null + value = each.value.value + overwrite = true tags = module.this.tags } From 37d8a5bfa04054231a04bf31cb66a575978352c8 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 11 Mar 2024 14:51:30 -0700 Subject: [PATCH 381/501] hotfix: `ecs-service` S3 Mirroring conditional (#1000) --- modules/ecs-service/main.tf | 32 +++++++++++++++++--------------- modules/ecs-service/outputs.tf | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 7511f81cc..993df324e 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -553,25 +553,27 @@ resource "aws_kinesis_stream" "default" { } data "aws_ecs_task_definition" "created_task" { + count = local.s3_mirroring_enabled ? 1 : 0 task_definition = module.ecs_alb_service_task[0].task_definition_family + depends_on = [ + module.ecs_alb_service_task + ] } locals { - created_task_definition = data.aws_ecs_task_definition.created_task - task_template = merge( - { - containerDefinitions = local.container_definition - family = lookup(local.created_task_definition, "family", null), - taskRoleArn = lookup(local.created_task_definition, "task_role_arn", null), - executionRoleArn = lookup(local.created_task_definition, "execution_role_arn", null), - networkMode = lookup(local.created_task_definition, "network_mode", null), - # we explicitly do not put the volumes here. That should be merged in by GHA - requiresCompatibilities = [lookup(local.task, "launch_type", "FARGATE")] - cpu = tostring(lookup(local.task, "task_cpu", null)) - memory = tostring(lookup(local.task, "task_memory", null)) - - } - ) + created_task_definition = local.s3_mirroring_enabled ? data.aws_ecs_task_definition.created_task[0] : {} + task_template = local.s3_mirroring_enabled ? { + containerDefinitions = local.container_definition + family = lookup(local.created_task_definition, "family", null), + taskRoleArn = lookup(local.created_task_definition, "task_role_arn", null), + executionRoleArn = lookup(local.created_task_definition, "execution_role_arn", null), + networkMode = lookup(local.created_task_definition, "network_mode", null), + # we explicitly do not put the volumes here. That should be merged in by GHA + requiresCompatibilities = [lookup(local.task, "launch_type", "FARGATE")] + cpu = tostring(lookup(local.task, "task_cpu", null)) + memory = tostring(lookup(local.task, "task_memory", null)) + + } : null } resource "aws_s3_bucket_object" "task_definition_template" { diff --git a/modules/ecs-service/outputs.tf b/modules/ecs-service/outputs.tf index f5a4acf11..50b9a3661 100644 --- a/modules/ecs-service/outputs.tf +++ b/modules/ecs-service/outputs.tf @@ -54,6 +54,6 @@ output "service_image" { } output "task_template" { - value = jsondecode(nonsensitive(jsonencode(local.task_template))) + value = local.s3_mirroring_enabled ? jsondecode(nonsensitive(jsonencode(local.task_template))) : null description = "The task template rendered" } From f3cce4d5a5dbd7b96dcdec63a344fb2bb3daf0c0 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 13 Mar 2024 18:19:10 +0100 Subject: [PATCH 382/501] Pin peter-evans/create-pull-request v6 (#1001) --- .github/workflows/update-changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index a06764b92..0e2daaa0f 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -164,7 +164,7 @@ jobs: fs.writeFileSync(filePath, updatedContent, 'utf-8'); - name: Create Pull Request - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: title: 'Update Changelog for `${{ steps.current-release.outputs.tag }}`' body: 'Update Changelog for [`${{ steps.current-release.outputs.tag }}`](${{ github.event.release.html_url }})' From d0c8030c1332b5cb260a236ba0e2373095579c08 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 19 Mar 2024 11:42:31 -0400 Subject: [PATCH 383/501] chore: update module in datadog-monitor (#1003) --- modules/datadog-monitor/README.md | 71 +- .../catalog/monitors/aurora.yaml | 40 +- .../datadog-monitor/catalog/monitors/ec2.yaml | 40 +- .../datadog-monitor/catalog/monitors/efs.yaml | 161 +++-- .../datadog-monitor/catalog/monitors/elb.yaml | 32 +- .../catalog/monitors/host.yaml | 152 ++-- .../datadog-monitor/catalog/monitors/k8s.yaml | 667 +++++++++--------- .../monitors/lambda-log-forwarder.yaml | 41 +- .../catalog/monitors/lambda.yaml | 26 +- .../catalog/monitors/rabbitmq.yaml | 114 +-- .../datadog-monitor/catalog/monitors/rds.yaml | 204 +++--- modules/datadog-monitor/main.tf | 2 +- 12 files changed, 778 insertions(+), 772 deletions(-) diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 95774e6c3..631402515 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -31,13 +31,18 @@ components: - Use the `catalog` convention to define a step of alerts. You can use ours or define your own. [https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors](https://github.com/cloudposse/terraform-datadog-platform/tree/master/catalog/monitors) +- The monitors catalog for the datadog-monitor component support datadog monitor exports. You can use + [the status page of a monitor to export it from 'settings'](https://docs.datadoghq.com/monitors/manage/status/#settings). + You can add the export to existing files or make new ones. Because the export is json formatted, it's also yaml + compatible. If you prefer, you can convert the export to yaml using your text editor or a cli tool like `yq`. + ## Adjust Thresholds per Stack Since there are so many parameters that may be adjusted for a given monitor, we define all monitors through YAML. By convention, we define the **default monitors** that should apply to all environments, and then adjust the thresholds per -environment. This is accomplished using the `datadog-monitor` components variable `datadog_monitors_config_paths` which -defines the path to the YAML configuration files. By passing a path for `dev` and `prod`, we can define configurations -that are different per environment. +environment. This is accomplished using the `datadog-monitor` components variable `local_datadog_monitors_config_paths` +which defines the path to the YAML configuration files. By passing a path for `dev` and `prod`, we can define +configurations that are different per environment. For example, you might have the following settings defined for `prod` and `dev` stacks that override the defaults. @@ -49,7 +54,7 @@ components: datadog-monitor: vars: # Located in the components/terraform/datadog-monitor directory - datadog_monitors_config_paths: + local_datadog_monitors_config_paths: - catalog/monitors/*.yaml - catalog/monitors/dev/*.yaml # note this line ``` @@ -62,7 +67,7 @@ components: datadog-monitor: vars: # Located in the components/terraform/datadog-monitor directory - datadog_monitors_config_paths: + local_datadog_monitors_config_paths: - catalog/monitors/*.yaml - catalog/monitors/prod/*.yaml # note this line ``` @@ -89,29 +94,28 @@ elb-lb-httpcode-5xx-notify: Check LB escalation_message: "" tags: {} + options: + renotify_interval: 60 + notify_audit: false + require_full_window: true + include_tags: true + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + new_group_delay: 0 + groupby_simple_monitor: false + renotify_occurrences: 0 + renotify_statuses: [] + validate: true + notify_no_data: false + no_data_timeframe: 5 + priority: 3 + threshold_windows: {} + thresholds: + critical: 50 + warning: 20 priority: 3 - renotify_interval: 60 - notify_audit: false - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - new_group_delay: 0 - groupby_simple_monitor: false - renotify_occurrences: 0 - renotify_statuses: [] - validate: true - notify_no_data: false - no_data_timeframe: 5 - priority: 3 - threshold_windows: {} - thresholds: - critical: 50 - warning: 20 + restricted_roles: null ``` **components/terraform/datadog-monitor/catalog/monitors/dev/elb.yaml** @@ -121,9 +125,10 @@ elb-lb-httpcode-5xx-notify: query: | avg(last_15m):max:aws.elb.httpcode_elb_5xx{${context_dd_tags}} by {env,host} > 30 priority: 2 - thresholds: - critical: 30 - warning: 10 + options: + thresholds: + critical: 30 + warning: 10 ``` ## Key Notes @@ -148,7 +153,7 @@ components: datadog-monitor: vars: # Located in the components/terraform/datadog-monitor directory - datadog_monitors_config_paths: + local_datadog_monitors_config_paths: - catalog/monitors/*.yaml - catalog/monitors/dev/*.yaml # templatefile() is used for all yaml config paths with these variables. @@ -165,7 +170,7 @@ components: terraform: datadog-monitor: vars: - datadog_monitors_config_paths: + local_datadog_monitors_config_paths: - https://raw.githubusercontent.com/cloudposse/terraform-datadog-platform/0.27.0/catalog/monitors/ec2.yaml - catalog/monitors/ec2.yaml ``` @@ -198,7 +203,7 @@ No providers. | Name | Source | Version | |------|--------|---------| | [datadog\_configuration](#module\_datadog\_configuration) | ../datadog-configuration/modules/datadog_keys | n/a | -| [datadog\_monitors](#module\_datadog\_monitors) | cloudposse/platform/datadog//modules/monitors | 1.2.0 | +| [datadog\_monitors](#module\_datadog\_monitors) | cloudposse/platform/datadog//modules/monitors | 1.4.1 | | [datadog\_monitors\_merge](#module\_datadog\_monitors\_merge) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [local\_datadog\_monitors\_yaml\_config](#module\_local\_datadog\_monitors\_yaml\_config) | cloudposse/config/yaml | 1.0.2 | diff --git a/modules/datadog-monitor/catalog/monitors/aurora.yaml b/modules/datadog-monitor/catalog/monitors/aurora.yaml index 81e887166..6efa79fb5 100644 --- a/modules/datadog-monitor/catalog/monitors/aurora.yaml +++ b/modules/datadog-monitor/catalog/monitors/aurora.yaml @@ -17,23 +17,23 @@ aurora-replica-lag: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 1000 - warning: 500 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 1000 + warning: 500 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/ec2.yaml b/modules/datadog-monitor/catalog/monitors/ec2.yaml index f8fea361c..7a29fc85e 100644 --- a/modules/datadog-monitor/catalog/monitors/ec2.yaml +++ b/modules/datadog-monitor/catalog/monitors/ec2.yaml @@ -11,23 +11,23 @@ ec2-failed-status-check: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 0 - #warning: - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 0 + #warning: + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/efs.yaml b/modules/datadog-monitor/catalog/monitors/efs.yaml index bece80607..b4136df6e 100644 --- a/modules/datadog-monitor/catalog/monitors/efs.yaml +++ b/modules/datadog-monitor/catalog/monitors/efs.yaml @@ -11,26 +11,26 @@ efs-throughput-utilization-check: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 75 - warning: 50 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 75 + warning: 50 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null # The official Datadog API documentation with available query parameters & alert types: # https://docs.datadoghq.com/api/v1/monitors/#create-a-monitor @@ -45,27 +45,26 @@ efs-burst-balance: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 100000000000 # 100 GB - warning: 1000000000000 # 1TB - #unknown: - #ok: - #critical_recovery: - #warning_recovery: - + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 100000000000 # 100 GB + warning: 1000000000000 # 1TB + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null efs-io-percent-limit: name: "(EFS) ${tenant} ${ stage } - I/O limit has been reached (> 90%)" @@ -77,26 +76,26 @@ efs-io-percent-limit: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 90 - warning: 50 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 90 + warning: 50 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null efs-client-connection-anomaly: name: "(EFS) ${tenant} ${ stage } - Client Connection Anomaly" @@ -108,23 +107,23 @@ efs-client-connection-anomaly: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 1 - critical_recovery: 0 - #warning: - #unknown: - #ok: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 1 + critical_recovery: 0 + #warning: + #unknown: + #ok: + #warning_recovery: + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/elb.yaml b/modules/datadog-monitor/catalog/monitors/elb.yaml index a54ef0c77..0b38f34d1 100644 --- a/modules/datadog-monitor/catalog/monitors/elb.yaml +++ b/modules/datadog-monitor/catalog/monitors/elb.yaml @@ -15,19 +15,19 @@ elb-lb-httpcode-5xx-notify: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: {} - thresholds: - critical: 50 - warning: 20 + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: {} + thresholds: + critical: 50 + warning: 20 + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/host.yaml b/modules/datadog-monitor/catalog/monitors/host.yaml index 020536227..2a7cb94f0 100644 --- a/modules/datadog-monitor/catalog/monitors/host.yaml +++ b/modules/datadog-monitor/catalog/monitors/host.yaml @@ -10,22 +10,22 @@ host-io-wait-times: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 50 - warning: 30 + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 50 + warning: 30 + priority: 3 + restricted_roles: null host-disk-use: name: "(Host) ${tenant} ${ stage } - Host Disk Usage" @@ -36,26 +36,26 @@ host-disk-use: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 90 - warning: 80 - #unknown: - #ok: - critical_recovery: 85 - warning_recovery: 75 + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 90 + warning: 80 + #unknown: + #ok: + critical_recovery: 85 + warning_recovery: 75 + priority: 3 + restricted_roles: null host-high-mem-use: name: "(Host) ${tenant} ${ stage } - Memory Utilization" @@ -66,26 +66,26 @@ host-high-mem-use: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 0.1 - warning: 0.15 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 0.1 + warning: 0.15 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null host-high-load-avg: name: "(Host) ${tenant} ${ stage } - High System Load Average" @@ -96,23 +96,23 @@ host-high-load-avg: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 0.8 - warning: 0.75 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 0.8 + warning: 0.75 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/k8s.yaml b/modules/datadog-monitor/catalog/monitors/k8s.yaml index f0d6f1dc6..349ea221c 100644 --- a/modules/datadog-monitor/catalog/monitors/k8s.yaml +++ b/modules/datadog-monitor/catalog/monitors/k8s.yaml @@ -11,21 +11,21 @@ k8s-deployment-replica-pod-down: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 5 - threshold_windows: { } - thresholds: - critical: 2 + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 5 + threshold_windows: { } + thresholds: + critical: 2 + priority: 3 + restricted_roles: null k8s-pod-restarting: name: "(k8s) ${tenant} ${ stage } - Pods are restarting multiple times" @@ -37,22 +37,22 @@ k8s-pod-restarting: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 5 - warning: 3 + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 5 + warning: 3 + priority: 3 + restricted_roles: null k8s-statefulset-replica-down: name: "(k8s) ${tenant} ${ stage } - StatefulSet Replica Pod is down" @@ -64,23 +64,22 @@ k8s-statefulset-replica-down: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - warning: 1 - critical: 2 - + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + warning: 1 + critical: 2 + priority: 3 + restricted_roles: null k8s-daemonset-pod-down: name: "(k8s) ${tenant} ${ stage } - DaemonSet Pod is down" @@ -92,21 +91,21 @@ k8s-daemonset-pod-down: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 1 + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 1 + priority: 3 + restricted_roles: null k8s-crashloopBackOff: name: "(k8s) ${tenant} ${ stage } - CrashloopBackOff detected" @@ -118,21 +117,21 @@ k8s-crashloopBackOff: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 1 + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 1 + priority: 3 + restricted_roles: null k8s-multiple-pods-failing: name: "(k8s) ${tenant} ${ stage } - Multiple Pods are failing" @@ -144,22 +143,22 @@ k8s-multiple-pods-failing: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - warning: 5 - critical: 10 + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + warning: 5 + critical: 10 + priority: 3 + restricted_roles: null k8s-unavailable-deployment-replica: name: "(k8s) ${tenant} ${ stage } - Unavailable Deployment Replica(s) detected" @@ -171,26 +170,26 @@ k8s-unavailable-deployment-replica: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 0 - #warning: - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 0 + #warning: + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-unavailable-statefulset-replica: name: "(k8s) ${tenant} ${ stage } - Unavailable Statefulset Replica(s) detected" @@ -202,26 +201,26 @@ k8s-unavailable-statefulset-replica: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 0 - #warning: - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 0 + #warning: + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-node-status-unschedulable: name: "(k8s) ${tenant} ${ stage } - Detected Unschedulable Node(s)" @@ -233,26 +232,26 @@ k8s-node-status-unschedulable: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 80 - warning: 90 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 80 + warning: 90 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-imagepullbackoff: name: "(k8s) ${tenant} ${ stage } - ImagePullBackOff detected" @@ -264,26 +263,26 @@ k8s-imagepullbackoff: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 1 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 1 - #warning: - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 1 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 1 + #warning: + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-high-cpu-usage: name: "(k8s) ${tenant} ${ stage } - High CPU Usage Detected" @@ -295,26 +294,26 @@ k8s-high-cpu-usage: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 90 - warning: 60 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 90 + warning: 60 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-high-disk-usage: name: "(k8s) ${tenant} ${ stage } - High Disk Usage Detected" @@ -326,26 +325,26 @@ k8s-high-disk-usage: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 90 - warning: 75 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 90 + warning: 75 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-high-memory-usage: name: "(k8s) ${tenant} ${ stage } - High Memory Usage Detected" @@ -363,26 +362,26 @@ k8s-high-memory-usage: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 90 - warning: 80 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 90 + warning: 80 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-high-filesystem-usage: name: "(k8s) ${tenant} ${ stage } - High Filesystem Usage Detected" @@ -400,26 +399,26 @@ k8s-high-filesystem-usage: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 90 - warning: 80 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 90 + warning: 80 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-network-tx-errors: name: "(k8s) ${tenant} ${ stage } - High Network TX (send) Errors" @@ -437,26 +436,26 @@ k8s-network-tx-errors: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 100 - warning: 10 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 100 + warning: 10 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-network-rx-errors: name: "(k8s) ${tenant} ${ stage } - High Network RX (receive) Errors" @@ -474,26 +473,26 @@ k8s-network-rx-errors: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 100 - warning: 10 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 100 + warning: 10 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-increased-pod-crash: name: "(k8s) ${tenant} ${ stage } - Increased Pod Crashes" @@ -505,26 +504,26 @@ k8s-increased-pod-crash: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: false - require_full_window: false - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 3 - #warning: - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: false + require_full_window: false + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 3 + #warning: + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null k8s-pending-pods: name: "(k8s) ${tenant} ${ stage } - Pending Pods" @@ -537,23 +536,23 @@ k8s-pending-pods: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: false - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 1 - #warning: - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: false + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 1 + #warning: + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml b/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml index e4a83061a..76238267e 100644 --- a/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml +++ b/modules/datadog-monitor/catalog/monitors/lambda-log-forwarder.yaml @@ -13,27 +13,26 @@ datadog-lambda-forwarder-config-modification: Event title: {{ event.title }} Lambda function name: {{ event.tags.functionname }} Event ID: {{ event.id }} - escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 1 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 1 - #warning: - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 1 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 1 + #warning: + #unknown: + #ok: + #critical_recovery: + #warning_recovery + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/lambda.yaml b/modules/datadog-monitor/catalog/monitors/lambda.yaml index 77683e878..3346fe6eb 100644 --- a/modules/datadog-monitor/catalog/monitors/lambda.yaml +++ b/modules/datadog-monitor/catalog/monitors/lambda.yaml @@ -9,14 +9,18 @@ lambda-errors: Lambda {{functionname.name}} in ({{tenant.name}}-{{environment.name}}-{{stage.name}}) has {{value}} errors over the last 5 minutes. - tags: [] - threshold_windows: { } - thresholds: - critical: 0 - notify_audit: false - require_full_window: false - notify_no_data: false - renotify_interval: 0 - include_tags: true - evaluation_delay: 900 - new_group_delay: 60 + tags: + managed-by: Terraform + options: + notify_audit: false + require_full_window: false + notify_no_data: false + renotify_interval: 0 + include_tags: true + evaluation_delay: 900 + new_group_delay: 60 + threshold_windows: { } + thresholds: + critical: 0 + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml b/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml index 2e6299c2b..df8e106e9 100644 --- a/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml +++ b/modules/datadog-monitor/catalog/monitors/rabbitmq.yaml @@ -13,26 +13,26 @@ rabbitmq-messages-unacknowledged-rate-too-high: tags: managed-by: Terraform integration: rabbitmq - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: null - threshold_windows: { } - thresholds: - critical: 1 - critical_recovery: 0 - #warning: - #unknown: - #ok: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: null + threshold_windows: { } + thresholds: + critical: 1 + critical_recovery: 0 + #warning: + #unknown: + #ok: + #warning_recovery: + priority: 3 + restricted_roles: null rabbitmq-memory-utilization: name: "[RabbitMQ] ${tenant} ${ stage } - Memory Utilization: {{broker.name}}" @@ -48,25 +48,25 @@ rabbitmq-memory-utilization: tags: managed-by: Terraform integration: rabbitmq - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: null - threshold_windows: { } - thresholds: - critical: 0.50 - critical_recovery: 0.40 - #unknown: - #ok: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: null + threshold_windows: { } + thresholds: + critical: 0.50 + critical_recovery: 0.40 + #unknown: + #ok: + #warning_recovery: + priority: 3 + restricted_roles: null rabbitmq-disk-utilization: name: "[RabbitMQ] ${tenant} ${ stage } - Disk Utilization: {{broker.name}}" @@ -82,21 +82,21 @@ rabbitmq-disk-utilization: tags: managed-by: Terraform integration: rabbitmq - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 0 - timeout_h: 0 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: null - threshold_windows: { } - thresholds: - critical: 100000000000 - #unknown: - #ok: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 0 + timeout_h: 0 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: null + threshold_windows: { } + thresholds: + critical: 100000000000 + #unknown: + #ok: + #warning_recovery: + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/catalog/monitors/rds.yaml b/modules/datadog-monitor/catalog/monitors/rds.yaml index eb8765575..946861130 100644 --- a/modules/datadog-monitor/catalog/monitors/rds.yaml +++ b/modules/datadog-monitor/catalog/monitors/rds.yaml @@ -17,26 +17,26 @@ rds-cpuutilization: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 90 - warning: 85 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 90 + warning: 85 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null rds-disk-queue-depth: name: "(RDS) ${tenant} ${ stage } - Disk queue depth above 64" @@ -54,26 +54,26 @@ rds-disk-queue-depth: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 64 - warning: 48 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 64 + warning: 48 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null rds-freeable-memory: name: "(RDS) ${tenant} ${ stage } - Freeable memory below 256 MB" @@ -91,26 +91,26 @@ rds-freeable-memory: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 256000000 - warning: 512000000 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 256000000 + warning: 512000000 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null rds-swap-usage: name: "(RDS) ${tenant} ${ stage } - Swap usage above 256 MB" @@ -128,26 +128,26 @@ rds-swap-usage: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: { } - thresholds: - critical: 256000000 - warning: 128000000 - #unknown: - #ok: - #critical_recovery: - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: { } + thresholds: + critical: 256000000 + warning: 128000000 + #unknown: + #ok: + #critical_recovery: + #warning_recovery: + priority: 3 + restricted_roles: null rds-database-connections: name: "(RDS) ${tenant} ${ stage } - Anomaly of a large variance in RDS connection count" @@ -165,25 +165,25 @@ rds-database-connections: escalation_message: "" tags: managed-by: Terraform - notify_no_data: false - notify_audit: true - require_full_window: true - enable_logs_sample: false - force_delete: true - include_tags: true - locked: false - renotify_interval: 60 - timeout_h: 24 - evaluation_delay: 60 - new_host_delay: 300 - no_data_timeframe: 10 - threshold_windows: - trigger_window: "last_15m" - recovery_window: "last_15m" - thresholds: - critical: 1 - #warning: - #unknown: - #ok: - critical_recovery: 0 - #warning_recovery: + options: + notify_no_data: false + notify_audit: true + require_full_window: true + include_tags: true + renotify_interval: 60 + timeout_h: 24 + evaluation_delay: 60 + new_host_delay: 300 + no_data_timeframe: 10 + threshold_windows: + trigger_window: "last_15m" + recovery_window: "last_15m" + thresholds: + critical: 1 + #warning: + #unknown: + #ok: + critical_recovery: 0 + #warning_recovery: + priority: 3 + restricted_roles: null diff --git a/modules/datadog-monitor/main.tf b/modules/datadog-monitor/main.tf index 1bacd8555..45a267646 100644 --- a/modules/datadog-monitor/main.tf +++ b/modules/datadog-monitor/main.tf @@ -100,7 +100,7 @@ module "datadog_monitors" { count = local.datadog_monitors_enabled ? 1 : 0 source = "cloudposse/platform/datadog//modules/monitors" - version = "1.2.0" + version = "1.4.1" datadog_monitors = local.datadog_monitors From b97ced118b4954b56afb8d9d871215fd3ca4bc00 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 20 Mar 2024 23:57:46 +0100 Subject: [PATCH 384/501] Auto release fix (#1002) --- .github/settings.yml | 13 +++++++++++++ .github/workflows/auto-release.yml | 13 +++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 .github/settings.yml diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 000000000..8148a5c3b --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,13 @@ +# These settings are synced to GitHub by https://probot.github.io/apps/settings/ +extends: cloudposse/.github + +repository: + # A URL with more information about the repository + homepage: https://cloudposse.com + + # Either `true` to enable projects for this repository, or `false` to disable them. + # If projects are disabled for the organization, passing `true` will cause an API error. + has_projects: false + + # Either `true` to enable the wiki for this repository, `false` to disable it. + has_wiki: false diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index a0ae99042..0929676d2 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -8,11 +8,8 @@ on: - production jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: cloudposse/github-action-auto-release@v1 - with: - prerelease: false - publish: true - config-name: auto-release.yml + auto: + uses: cloudposse/.github/.github/workflows/shared-auto-release.yml@main + with: + publish: true + secrets: inherit From 3204f06999735042b065743085b41ef8fec76157 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Mon, 25 Mar 2024 17:28:29 -0400 Subject: [PATCH 385/501] fix: prettier format errors (#1005) --- modules/eks/actions-runner-controller/README.md | 4 +++- modules/eks/datadog-agent/README.md | 5 +++-- modules/github-runners/README.md | 4 +++- modules/tfstate-backend/README.md | 4 +++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index bedb6ddd9..b134ab617 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -313,7 +313,9 @@ on the capacity reservation until enough reservations ahead of it are removed fo and active job. Although there are some edge cases regarding `webhook_startup_timeout` that seem not to be covered properly (see [actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)), they only -merit adding a few extra minutes to the timeout. ::: +merit adding a few extra minutes to the timeout. + +::: ### Recommended `webhook_startup_timeout` Duration diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index d212a078c..e203a45f8 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -107,9 +107,10 @@ for `.yaml`. :::caution The key of a filename must match datadog docs, which is `.yaml` [Datadog Cluster Checks](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) +::: -::: Cluster Checks **can** be used for external URL testing (loadbalancer endpoints), whereas annotations **must** be -used for kubernetes services. +Cluster Checks **can** be used for external URL testing (loadbalancer endpoints), whereas annotations **must** be used +for kubernetes services. ``` http_check.yaml: diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 90fe4d50a..04da5bf8f 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -179,7 +179,9 @@ permissions “mode” for Self-hosted runners to Read-Only. The instructions fo :::info We highly recommend using a GitHub Application with the github-action-token-rotator module to generate the Registration Token. This will ensure that the token is rotated and that the token is stored in SSM Parameter Store -encrypted with KMS. ::: +encrypted with KMS. + +::: #### GitHub Application diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 12f8433cb..396115419 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -13,7 +13,9 @@ security configuration information, so careful planning is required when archite :::info Part of cold start so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and then to move the state into it. Follow the guide **[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** -to get started. ::: +to get started. + +::: ### Access Control From 5316202c54d07106b245fdecf05cf7a95f443d2b Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 26 Mar 2024 12:35:33 -0700 Subject: [PATCH 386/501] Bugfix: ecs private locations for datadog (#1007) --- modules/datadog-private-location-ecs/main.tf | 4 ++-- modules/datadog-private-location-ecs/remote-state.tf | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/datadog-private-location-ecs/main.tf b/modules/datadog-private-location-ecs/main.tf index 3e2a730fb..9bb93edcc 100644 --- a/modules/datadog-private-location-ecs/main.tf +++ b/modules/datadog-private-location-ecs/main.tf @@ -6,7 +6,7 @@ locals { container.json_map_object ], ) - datadog_location_config = jsondecode(datadog_synthetics_private_location.private_location.config) + datadog_location_config = try(jsondecode(datadog_synthetics_private_location.private_location[0].config), null) } @@ -30,7 +30,7 @@ module "container_definition" { depends_on = [datadog_synthetics_private_location.private_location] - for_each = var.containers + for_each = { for k, v in var.containers : k => v if local.enabled } container_name = lookup(each.value, "name") diff --git a/modules/datadog-private-location-ecs/remote-state.tf b/modules/datadog-private-location-ecs/remote-state.tf index 131679f57..2a3b6161b 100644 --- a/modules/datadog-private-location-ecs/remote-state.tf +++ b/modules/datadog-private-location-ecs/remote-state.tf @@ -22,7 +22,7 @@ module "ecs_cluster" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" - component = "ecs" + component = "ecs/cluster" context = module.this.context } From 47f25214b92d792d5d0937e6047358961d554432 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 27 Mar 2024 14:50:21 -0700 Subject: [PATCH 387/501] `ecs-service` better task definition merging (#1008) --- modules/ecs-service/CHANGELOG.md | 11 +++++++++ modules/ecs-service/README.md | 2 ++ modules/ecs-service/main.tf | 31 +++++++++++++++++--------- modules/ecs-service/outputs.tf | 12 +++++++++- modules/ecs-service/systems-manager.tf | 8 +++---- 5 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 modules/ecs-service/CHANGELOG.md diff --git a/modules/ecs-service/CHANGELOG.md b/modules/ecs-service/CHANGELOG.md new file mode 100644 index 000000000..df4f4de64 --- /dev/null +++ b/modules/ecs-service/CHANGELOG.md @@ -0,0 +1,11 @@ +## PR [#1008](https://github.com/cloudposse/terraform-aws-components/pull/1008) + +### Possible Breaking Change + +- Refactored how S3 Task Definitions and the Terraform Task definition are merged. + - Introduced local `local.containers_priority_terraform` to be referenced whenever terraform Should take priority + - Introduced local `local.containers_priority_s3` to be referenced whenever S3 Should take priority +- `map_secrets` pulled out from container definition to local where it can be better maintained. Used Terraform as + priority as it is a calculated as a map of arns. +- `s3_mirror_name` now automatically uploads a task-template.json to s3 mirror where it can be pulled from GitHub + Actions. diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 027af5a73..0df29a8a9 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -458,6 +458,8 @@ components: | [ssm\_key\_prefix](#output\_ssm\_key\_prefix) | SSM prefix | | [ssm\_parameters](#output\_ssm\_parameters) | SSM parameters for the ECS Service | | [subnet\_ids](#output\_subnet\_ids) | Selected subnet IDs | +| [task\_definition\_arn](#output\_task\_definition\_arn) | The task definition ARN | +| [task\_definition\_revision](#output\_task\_definition\_revision) | The task definition revision | | [task\_template](#output\_task\_template) | The task template rendered | | [vpc\_id](#output\_vpc\_id) | Selected VPC ID | | [vpc\_sg\_id](#output\_vpc\_sg\_id) | Selected VPC SG ID | diff --git a/modules/ecs-service/main.tf b/modules/ecs-service/main.tf index 993df324e..33914deaa 100644 --- a/modules/ecs-service/main.tf +++ b/modules/ecs-service/main.tf @@ -125,7 +125,12 @@ locals { local.container_aliases[item.name] => { container_definition = item } } - containers = { + containers_priority_terraform = { + for name, settings in var.containers : + name => merge(local.container_chamber[name], lookup(local.container_s3, name, {}), settings, ) + if local.enabled + } + containers_priority_s3 = { for name, settings in var.containers : name => merge(settings, local.container_chamber[name], lookup(local.container_s3, name, {})) if local.enabled @@ -168,13 +173,18 @@ locals { for k, v in data.template_file.envs : k => v.rendered } + map_secrets = { for k, v in local.containers_priority_terraform : k => lookup(v, "map_secrets", null) != null ? zipmap( + keys(lookup(v, "map_secrets", null)), + formatlist("%s/%s", format("arn:aws:ssm:%s:%s:parameter", var.region, module.roles_to_principals.full_account_map[format("%s-%s", var.tenant, var.stage)]), + values(lookup(v, "map_secrets", null))) + ) : null } } module "container_definition" { source = "cloudposse/ecs-container-definition/aws" version = "0.61.1" - for_each = { for k, v in local.containers : k => v if local.enabled } + for_each = { for k, v in local.containers_priority_terraform : k => v if local.enabled } container_name = each.value["name"] @@ -182,8 +192,8 @@ module "container_definition" { "%s.dkr.ecr.%s.amazonaws.com/%s", module.roles_to_principals.full_account_map[var.ecr_stage_name], coalesce(var.ecr_region, var.region), - lookup(each.value, "ecr_image", null) - ) : lookup(each.value, "image") + lookup(local.containers_priority_s3[each.key], "ecr_image", null) + ) : lookup(local.containers_priority_s3[each.key], "image") container_memory = each.value["memory"] container_memory_reservation = each.value["memory_reservation"] @@ -203,14 +213,12 @@ module "container_definition" { "DD_SERVICE_NAME" = var.name, "DD_ENV" = var.stage, "DD_PROFILING_EXPORTERS" = "agent" - } : {} + } : {}, + lookup(each.value, "map_environment", null) ) : null - map_secrets = lookup(each.value, "map_secrets", null) != null ? zipmap( - keys(lookup(each.value, "map_secrets", null)), - formatlist("%s/%s", format("arn:aws:ssm:%s:%s:parameter", var.region, module.roles_to_principals.full_account_map[format("%s-%s", var.tenant, var.stage)]), - values(lookup(each.value, "map_secrets", null))) - ) : null + map_secrets = local.map_secrets[each.key] + port_mappings = each.value["port_mappings"] command = each.value["command"] entrypoint = each.value["entrypoint"] @@ -235,7 +243,8 @@ module "container_definition" { # escape hatch for anything not specifically described above or unsupported by the upstream module - container_definition = lookup(each.value, "container_definition", {}) + # March 2024: Removing this as it always prioritizes the s3 task definition + # container_definition = lookup(each.value, "container_definition", {}) } locals { diff --git a/modules/ecs-service/outputs.tf b/modules/ecs-service/outputs.tf index 50b9a3661..aeb99f783 100644 --- a/modules/ecs-service/outputs.tf +++ b/modules/ecs-service/outputs.tf @@ -49,7 +49,7 @@ output "environment_map" { } output "service_image" { - value = try(nonsensitive(local.containers.service.image), null) + value = try(nonsensitive(local.containers_priority_terraform.service.image), null) description = "The image of the service container" } @@ -57,3 +57,13 @@ output "task_template" { value = local.s3_mirroring_enabled ? jsondecode(nonsensitive(jsonencode(local.task_template))) : null description = "The task template rendered" } + +output "task_definition_arn" { + value = one(module.ecs_alb_service_task[*].task_definition_arn) + description = "The task definition ARN" +} + +output "task_definition_revision" { + value = one(module.ecs_alb_service_task[*].task_definition_revision) + description = "The task definition revision" +} diff --git a/modules/ecs-service/systems-manager.tf b/modules/ecs-service/systems-manager.tf index 9c84cf79d..5a40afa07 100644 --- a/modules/ecs-service/systems-manager.tf +++ b/modules/ecs-service/systems-manager.tf @@ -51,11 +51,9 @@ resource "aws_ssm_parameter" "full_urls" { name = each.key description = each.value.description type = each.value.type - # key_id is only used with SecureStrings. - # With other types Terraform will pass but will constantly suggest adding the key_id (perma drift) - key_id = each.value.type == "SecureString" ? var.kms_alias_name_ssm : null - value = each.value.value - overwrite = true + key_id = var.kms_alias_name_ssm + value = each.value.value + overwrite = true tags = module.this.tags } From 687ad3a5363acae7a091e843d7e75e086f25dc02 Mon Sep 17 00:00:00 2001 From: "Erik Osterman (CEO @ Cloud Posse)" Date: Mon, 1 Apr 2024 10:24:42 -0700 Subject: [PATCH 388/501] Add GitHub Settings (#1009) --- .github/settings.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 8148a5c3b..941b10f30 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,13 +1,14 @@ # These settings are synced to GitHub by https://probot.github.io/apps/settings/ -extends: cloudposse/.github - +# Upstream changes from _extends are only recognized when modifications are made to this file in the default branch. +_extends: .github repository: # A URL with more information about the repository - homepage: https://cloudposse.com - + homepage: https://docs.cloudposse.com/components/ # Either `true` to enable projects for this repository, or `false` to disable them. # If projects are disabled for the organization, passing `true` will cause an API error. has_projects: false - # Either `true` to enable the wiki for this repository, `false` to disable it. has_wiki: false + name: terraform-aws-components + description: Opinionated, self-contained Terraform root modules that each solve one, specific problem + topics: terraform, terraform-module, geodesic, reference-implementation, reference-architecture, aws, service-catalog, catalog, library, examples, terraform-modules, stacks, blueprints, itil, catalogue, components, component-library From c2eaf4171a2d7908d3e2ff524d94a3fbe58f2cb8 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 3 Apr 2024 08:40:00 -0700 Subject: [PATCH 389/501] EKS: `actions-runner-controller` readme update for dockerhub auth (#1010) Co-authored-by: Dan Miller --- .../eks/actions-runner-controller/README.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index b134ab617..6b20ebed1 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -247,6 +247,63 @@ Store this key in AWS SSM under the same path specified by `ssm_github_webhook_s ssm_github_webhook_secret_token_path: "/github_runners/github_webhook_secret" ``` +### Dockerhub Authentication + +Authenticating with Dockerhub is optional but when enabled can ensure stability by increasing the number of pulls +allowed from your runners. + +To get started set `docker_config_json_enabled` to `true` and `ssm_docker_config_json_path` to the SSM path where the +credentials are stored, for example `github_runners/docker`. + +To create the credentials file, fill out a JSON file locally with the following content: + +```json +{ + "auths": { + "https://index.docker.io/v1/": { + "username": "your_username", + "password": "your_password", + "email": "your_email", + "auth": "$(echo "your_username: your_password" | base64)" + } + } +} +``` + +Then write the file to SSM with the following Atmos Workflow: + +```yaml +save/docker-config-json: + description: Prompt for uploading Docker Config JSON to the AWS SSM Parameter Store + steps: + - type: shell + command: |- + echo "Please enter the Docker Config JSON file path" + echo "See https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry for information on how to create the file" + read -p "Docker Config JSON file path: " -r DOCKER_CONFIG_JSON_FILE_PATH + if [ -z "DOCKER_CONFIG_JSON_FILE_PATH" ] + then + echo 'Inputs cannot be blank please try again!' + exit 0 + fi + + DOCKER_CONFIG_JSON=$(<$DOCKER_CONFIG_JSON_FILE_PATH); + ENCODED_DOCKER_CONFIG_JSON=$(echo "$DOCKER_CONFIG_JSON" | base64 -w 0 ); + + echo $DOCKER_CONFIG_JSON + echo $ENCODED_DOCKER_CONFIG_JSON + + AWS_PROFILE=acme-core-gbl-auto-admin + + set -e + + chamber write github_runners/docker config-json -- "$ENCODED_DOCKER_CONFIG_JSON" + + echo 'Saved Docker Config JSON to the AWS SSM Parameter Store' +``` + +Don't forget to update the AWS Profile in the script. + ### Using Runner Groups GitHub supports grouping runners into distinct From 7b501b5b4e42ac6c0c2699f4d66309308387505d Mon Sep 17 00:00:00 2001 From: Nuru Date: Sat, 6 Apr 2024 18:56:00 -0700 Subject: [PATCH 390/501] [tfstate-backend] Recommend explicit configuration of SuperAdmin (#1013) --- modules/tfstate-backend/README.md | 12 ++++++++---- modules/tfstate-backend/iam.tf | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 396115419..70da2f2ba 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -10,9 +10,11 @@ wish to restrict who can read the production Terraform state backend S3 bucket. all Terraform users require read access to the most sensitive accounts, such as `root` and `audit`, in order to read security configuration information, so careful planning is required when architecting backend splits. -:::info Part of cold start so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and -then to move the state into it. Follow the guide -**[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** +:::info + +Part of cold start, so it has to initially be run with `SuperAdmin`, multiple +times: to create the S3 bucket and then to move the state into it. Follow +the guide **[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** to get started. ::: @@ -56,7 +58,9 @@ access. You can configure who is allowed to assume these roles. - For convenience, the component automatically grants access to the backend to the user deploying it. This is helpful because it allows that user, presumably SuperAdmin, to deploy the normal components that expect the user does not have - direct access to Terraform state. + direct access to Terraform state, without requiring custom configuration. However, you may want to explicitly + grant SuperAdmin access to the backend in the `allowed_principal_arns` configuration, to ensure that SuperAdmin + can always access the backend, even if the component is later updated by the `root-admin` role. ### Quotas diff --git a/modules/tfstate-backend/iam.tf b/modules/tfstate-backend/iam.tf index 4376c50fe..5881b3998 100644 --- a/modules/tfstate-backend/iam.tf +++ b/modules/tfstate-backend/iam.tf @@ -35,7 +35,7 @@ module "assume_role" { denied_roles = each.value.denied_roles # Allow whatever user or role is running Terraform to manage the backend to assume any backend access role - allowed_principal_arns = concat(each.value.allowed_principal_arns, [local.caller_arn]) + allowed_principal_arns = distinct(concat(each.value.allowed_principal_arns, [local.caller_arn])) denied_principal_arns = each.value.denied_principal_arns # Permission sets are for AWS SSO, which is optional allowed_permission_sets = try(each.value.allowed_permission_sets, {}) From cdd25a27f6b0ac727a07ecc4e13abb24127c3615 Mon Sep 17 00:00:00 2001 From: "Erik Osterman (CEO @ Cloud Posse)" Date: Wed, 10 Apr 2024 08:31:19 -0500 Subject: [PATCH 391/501] Use GitHub Action Workflows from `cloudposse/.github` Repo (#1015) --- .github/workflows/release-branch.yml | 19 +++++++++++++++++++ .github/workflows/release-published.yml | 13 +++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 .github/workflows/release-branch.yml create mode 100644 .github/workflows/release-published.yml diff --git a/.github/workflows/release-branch.yml b/.github/workflows/release-branch.yml new file mode 100644 index 000000000..3593cea51 --- /dev/null +++ b/.github/workflows/release-branch.yml @@ -0,0 +1,19 @@ +--- +name: release-branch +on: + push: + branches: + - main + - release/v* + paths-ignore: + - '.github/**' + - 'docs/**' + - 'examples/**' + - 'test/**' + +permissions: {} + +jobs: + terraform-module: + uses: cloudposse/github-actions-workflows-terraform-module/.github/workflows/release-branch.yml@main + secrets: inherit diff --git a/.github/workflows/release-published.yml b/.github/workflows/release-published.yml new file mode 100644 index 000000000..1b0aaca73 --- /dev/null +++ b/.github/workflows/release-published.yml @@ -0,0 +1,13 @@ +--- +name: release-published +on: + release: + types: + - published + +permissions: {} + +jobs: + terraform-module: + uses: cloudposse/github-actions-workflows-terraform-module/.github/workflows/release-published.yml@main + secrets: inherit From add978eb5cf2c24a4de2ba080367bf0fdc97847d Mon Sep 17 00:00:00 2001 From: Nuru Date: Fri, 12 Apr 2024 11:17:27 -0700 Subject: [PATCH 392/501] [eks/echo-server] Fix resource settings via chart inputs (#1014) --- .../charts/echo-server/templates/deployment.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/eks/echo-server/charts/echo-server/templates/deployment.yaml b/modules/eks/echo-server/charts/echo-server/templates/deployment.yaml index 7d690e39c..1eade38de 100644 --- a/modules/eks/echo-server/charts/echo-server/templates/deployment.yaml +++ b/modules/eks/echo-server/charts/echo-server/templates/deployment.yaml @@ -45,10 +45,14 @@ spec: successThreshold: 1 {{- with index .Values "resources" }} resources: + {{- with index . "limits" }} limits: - cpu: {{ index . "limits.cpu" | default "50m" }} - memory: {{ index . "limits.memory" | default "128Mi" }} + cpu: {{ index . "cpu" | default "50m" }} + memory: {{ index . "memory" | default "128Mi" }} + {{- end }} + {{- with index . "requests" }} requests: - cpu: {{ index . "requests.cpu" | default "50m" }} - memory: {{ index . "requests.memory" | default "128Mi" }} + cpu: {{ index . "cpu" | default "50m" }} + memory: {{ index . "memory" | default "128Mi" }} + {{- end }} {{- end }} From fd5598ed7cf1784e81ea9f71d8d74b58c89b875d Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 17 Apr 2024 11:18:08 -0700 Subject: [PATCH 393/501] Update Keda default chart (#1021) --- modules/eks/keda/README.md | 4 ++-- modules/eks/keda/main.tf | 4 ++-- modules/eks/keda/variables.tf | 14 +++----------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/modules/eks/keda/README.md b/modules/eks/keda/README.md index b0dd0b847..31a12d755 100644 --- a/modules/eks/keda/README.md +++ b/modules/eks/keda/README.md @@ -21,7 +21,7 @@ components: kubernetes_namespace: "keda" chart_repository: "https://kedacore.github.io/charts" chart: "keda" - chart_version: "2.11.2" + chart_version: "2.13.2" chart_values: {} timeout: 180 ``` @@ -99,7 +99,7 @@ components: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [repository](#input\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://kedacore.github.io/charts"` | no | -| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | +| [resources](#input\_resources) | A sub-nested map of deployment to resources. e.g. { operator = { requests = { cpu = 100m, memory = 100Mi }, limits = { cpu = 200m, memory = 200Mi } } } | `any` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/eks/keda/main.tf b/modules/eks/keda/main.tf index 857148b8c..fad7fe50d 100644 --- a/modules/eks/keda/main.tf +++ b/modules/eks/keda/main.tf @@ -37,11 +37,11 @@ module "keda" { serviceAccount = { name = module.this.name } - resources = var.resources rbac = { create = var.rbac_enabled } - }) + }), + var.resources != null ? yamlencode({ resources = var.resources }) : "", ]) context = module.this.context diff --git a/modules/eks/keda/variables.tf b/modules/eks/keda/variables.tf index 44ab15af5..d461f86d4 100644 --- a/modules/eks/keda/variables.tf +++ b/modules/eks/keda/variables.tf @@ -16,17 +16,9 @@ variable "eks_component_name" { } variable "resources" { - type = object({ - limits = object({ - cpu = string - memory = string - }) - requests = object({ - cpu = string - memory = string - }) - }) - description = "The cpu and memory of the deployment's limits and requests." + type = any + description = "A sub-nested map of deployment to resources. e.g. { operator = { requests = { cpu = 100m, memory = 100Mi }, limits = { cpu = 200m, memory = 200Mi } } }" + default = null } variable "kubernetes_namespace" { From 9b08a3400f0eaa9f35a540000339a950b16b6d86 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 17 Apr 2024 17:07:45 -0700 Subject: [PATCH 394/501] fix: `api-gateway-rest-api` Default Value (#1022) --- modules/api-gateway-rest-api/remote-state.tf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/api-gateway-rest-api/remote-state.tf b/modules/api-gateway-rest-api/remote-state.tf index 5f5b11668..6f671e748 100644 --- a/modules/api-gateway-rest-api/remote-state.tf +++ b/modules/api-gateway-rest-api/remote-state.tf @@ -15,6 +15,10 @@ module "acm" { component = "acm" ignore_errors = true + defaults = { + domain_name = "" + } + context = module.this.context } From 57b74313e42733edc8d803abc0caf7e646273cbc Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 22 Apr 2024 12:39:34 -0700 Subject: [PATCH 395/501] feat: `ec2-instance` Component (#1024) --- modules/ec2-instance/README.md | 97 ++++++ modules/ec2-instance/context.tf | 279 ++++++++++++++++++ modules/ec2-instance/main.tf | 51 ++++ modules/ec2-instance/outputs.tf | 9 + modules/ec2-instance/providers.tf | 19 ++ modules/ec2-instance/remote-state.tf | 8 + .../ec2-instance/templates/userdata.sh.tmpl | 3 + modules/ec2-instance/variables.tf | 64 ++++ modules/ec2-instance/versions.tf | 14 + 9 files changed, 544 insertions(+) create mode 100644 modules/ec2-instance/README.md create mode 100644 modules/ec2-instance/context.tf create mode 100644 modules/ec2-instance/main.tf create mode 100644 modules/ec2-instance/outputs.tf create mode 100644 modules/ec2-instance/providers.tf create mode 100644 modules/ec2-instance/remote-state.tf create mode 100644 modules/ec2-instance/templates/userdata.sh.tmpl create mode 100644 modules/ec2-instance/variables.tf create mode 100644 modules/ec2-instance/versions.tf diff --git a/modules/ec2-instance/README.md b/modules/ec2-instance/README.md new file mode 100644 index 000000000..6959a329f --- /dev/null +++ b/modules/ec2-instance/README.md @@ -0,0 +1,97 @@ +# Component: `ec2-instance` + +This component is responsible for provisioning a single EC2 instance. + +## Usage + +**Stack Level**: Regional + +The typical stack configuration for this component is as follows: + +```yaml +components: + terraform: + ec2-instance: + vars: + enabled: true + name: ec2 +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [template](#requirement\_template) | >= 2.2 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [template](#provider\_template) | >= 2.2 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [ec2\_instance](#module\_ec2\_instance) | cloudposse/ec2-instance/aws | 1.4.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ami.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | +| [template_file.userdata](https://registry.terraform.io/providers/cloudposse/template/latest/docs/data-sources/file) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [ami\_filters](#input\_ami\_filters) | A list of AMI filters for finding the latest AMI |
list(object({
name = string
values = list(string)
}))
|
[
{
"name": "architecture",
"values": [
"x86_64"
]
},
{
"name": "virtualization-type",
"values": [
"hvm"
]
}
]
| no | +| [ami\_name\_regex](#input\_ami\_name\_regex) | The regex used to match the latest AMI to be used for the EC2 instance. | `string` | `"^amzn2-ami-hvm.*"` | no | +| [ami\_owner](#input\_ami\_owner) | The owner of the AMI used for the ZScaler EC2 instances. | `string` | `"amazon"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [instance\_type](#input\_instance\_type) | The instance family to use for the EC2 instance | `string` | `"t3a.micro"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS region | `string` | n/a | yes | +| [security\_group\_rules](#input\_security\_group\_rules) | A list of maps of Security Group rules.
The values of map is fully complated with `aws_security_group_rule` resource.
To get more info see [security\_group\_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). | `list(any)` |
[
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 0,
"protocol": "-1",
"to_port": 65535,
"type": "egress"
}
]
| no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [user\_data](#input\_user\_data) | User data to be included with this EC2 instance | `string` | `"echo \"hello user data\""` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [instance\_id](#output\_instance\_id) | Instance ID | +| [private\_ip](#output\_private\_ip) | Private IP of the instance | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ec2-instance) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/ec2-instance/context.tf b/modules/ec2-instance/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/ec2-instance/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/ec2-instance/main.tf b/modules/ec2-instance/main.tf new file mode 100644 index 000000000..8ac773d12 --- /dev/null +++ b/modules/ec2-instance/main.tf @@ -0,0 +1,51 @@ +locals { + enabled = module.this.enabled + + vpc_id = module.vpc.outputs.vpc_id + # basic usage picks the first private subnet from the vpc component + vpc_private_subnet_ids = sort(module.vpc.outputs.private_subnet_ids) + subnet_id = local.vpc_private_subnet_ids[0] +} + +data "aws_ami" "this" { + count = local.enabled ? 1 : 0 + + most_recent = true + owners = [var.ami_owner] + name_regex = var.ami_name_regex + + dynamic "filter" { + for_each = toset(var.ami_filters) + content { + name = filter.value.name + values = filter.value.values + } + } +} + +data "template_file" "userdata" { + count = local.enabled ? 1 : 0 + template = file("${path.module}/templates/userdata.sh.tmpl") + + vars = { + user_data = var.user_data + } +} + +module "ec2_instance" { + source = "cloudposse/ec2-instance/aws" + version = "1.4.0" + + enabled = local.enabled + + ami = local.enabled ? data.aws_ami.this[0].id : "" + ami_owner = var.ami_owner + instance_type = var.instance_type + user_data_base64 = local.enabled ? base64encode(data.template_file.userdata[0].rendered) : "" + + subnet = local.subnet_id + vpc_id = local.vpc_id + security_group_rules = var.security_group_rules + + context = module.this.context +} diff --git a/modules/ec2-instance/outputs.tf b/modules/ec2-instance/outputs.tf new file mode 100644 index 000000000..bdee37c9b --- /dev/null +++ b/modules/ec2-instance/outputs.tf @@ -0,0 +1,9 @@ +output "instance_id" { + value = module.ec2_instance[*].id + description = "Instance ID" +} + +output "private_ip" { + value = module.ec2_instance[*].private_ip + description = "Private IP of the instance" +} diff --git a/modules/ec2-instance/providers.tf b/modules/ec2-instance/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/ec2-instance/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/ec2-instance/remote-state.tf b/modules/ec2-instance/remote-state.tf new file mode 100644 index 000000000..757ef9067 --- /dev/null +++ b/modules/ec2-instance/remote-state.tf @@ -0,0 +1,8 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "vpc" + + context = module.this.context +} diff --git a/modules/ec2-instance/templates/userdata.sh.tmpl b/modules/ec2-instance/templates/userdata.sh.tmpl new file mode 100644 index 000000000..bcf19e6d5 --- /dev/null +++ b/modules/ec2-instance/templates/userdata.sh.tmpl @@ -0,0 +1,3 @@ +#!/bin/bash + +${user_data} diff --git a/modules/ec2-instance/variables.tf b/modules/ec2-instance/variables.tf new file mode 100644 index 000000000..879e224e0 --- /dev/null +++ b/modules/ec2-instance/variables.tf @@ -0,0 +1,64 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "ami_owner" { + type = string + description = "The owner of the AMI used for the ZScaler EC2 instances." + default = "amazon" +} + +variable "ami_name_regex" { + type = string + description = "The regex used to match the latest AMI to be used for the EC2 instance." + default = "^amzn2-ami-hvm.*" +} + +variable "ami_filters" { + type = list(object({ + name = string + values = list(string) + })) + default = [ + { + name = "architecture" + values = ["x86_64"] + }, + { + name = "virtualization-type" + values = ["hvm"] + } + ] + description = "A list of AMI filters for finding the latest AMI" +} + +variable "instance_type" { + type = string + default = "t3a.micro" + description = "The instance family to use for the EC2 instance" +} + +variable "security_group_rules" { + type = list(any) + default = [ + { + type = "egress" + from_port = 0 + to_port = 65535 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + ] + description = <<-EOT + A list of maps of Security Group rules. + The values of map is fully complated with `aws_security_group_rule` resource. + To get more info see [security_group_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). + EOT +} + +variable "user_data" { + type = string + default = "echo \"hello user data\"" + description = "User data to be included with this EC2 instance" +} diff --git a/modules/ec2-instance/versions.tf b/modules/ec2-instance/versions.tf new file mode 100644 index 000000000..7a90cef78 --- /dev/null +++ b/modules/ec2-instance/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + template = { + source = "cloudposse/template" + version = ">= 2.2" + } + } +} From ede20ee8d756fe2f1119813bdd61131553227df9 Mon Sep 17 00:00:00 2001 From: Matt Gowie Date: Mon, 22 Apr 2024 15:58:43 -0600 Subject: [PATCH 396/501] feat(aurora-pg): allow passing extra SGs to allow (#1019) Co-authored-by: Dan Miller --- modules/aurora-postgres/README.md | 3 +++ modules/aurora-postgres/main.tf | 10 +++++++++- modules/aurora-postgres/outputs.tf | 5 +++++ modules/aurora-postgres/variables.tf | 6 ++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 56a9e59d2..b2a691029 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -280,6 +280,7 @@ components: | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.kms_key_rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_security_groups.allowed](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_groups) | data source | ## Inputs @@ -291,6 +292,7 @@ components: | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.
e.g.
{
environment = "ue2",
stage = "auto",
tenant = "core"
}

Defaults to the "vpc" component in the given account |
list(object({
vpc = optional(string, "vpc")
environment = optional(string)
stage = optional(string)
tenant = optional(string)
}))
| `[]` | no | | [allow\_major\_version\_upgrade](#input\_allow\_major\_version\_upgrade) | Enable to allow major engine version upgrades when changing engine versions. Defaults to false. | `bool` | `false` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDRs allowed to access the database (in addition to security groups and subnets) | `list(string)` | `[]` | no | +| [allowed\_security\_group\_names](#input\_allowed\_security\_group\_names) | List of security group names (tags) that should be allowed access to the database | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [autoscaling\_enabled](#input\_autoscaling\_enabled) | Whether to enable cluster autoscaling | `bool` | `false` | no | | [autoscaling\_max\_capacity](#input\_autoscaling\_max\_capacity) | Maximum number of instances to be maintained by the autoscaler | `number` | `5` | no | @@ -355,6 +357,7 @@ components: | Name | Description | |------|-------------| | [admin\_username](#output\_admin\_username) | Postgres admin username | +| [allowed\_security\_groups](#output\_allowed\_security\_groups) | The resulting list of security group IDs that are allowed to connect to the Aurora Postgres cluster. | | [cluster\_identifier](#output\_cluster\_identifier) | Postgres cluster identifier | | [config\_map](#output\_config\_map) | Map containing information pertinent to a PostgreSQL client configuration. | | [database\_name](#output\_database\_name) | Postgres database name | diff --git a/modules/aurora-postgres/main.tf b/modules/aurora-postgres/main.tf index 77191700a..6c8818162 100644 --- a/modules/aurora-postgres/main.tf +++ b/modules/aurora-postgres/main.tf @@ -5,10 +5,11 @@ locals { private_subnet_ids = module.vpc.outputs.private_subnet_ids eks_security_group_enabled = local.enabled && var.eks_security_group_enabled - allowed_security_groups = [ + allowed_eks_security_groups = [ for eks in module.eks : eks.outputs.eks_cluster_managed_security_group_id ] + allowed_security_groups = concat(data.aws_security_groups.allowed.ids, local.allowed_eks_security_groups) zone_id = module.dns_gbl_delegated.outputs.default_dns_zone_id @@ -29,6 +30,13 @@ locals { ) } +data "aws_security_groups" "allowed" { + filter { + name = "tag:Name" + values = var.allowed_security_group_names + } +} + module "cluster" { source = "cloudposse/label/null" version = "0.25.0" diff --git a/modules/aurora-postgres/outputs.tf b/modules/aurora-postgres/outputs.tf index 6f740c835..3e199f377 100644 --- a/modules/aurora-postgres/outputs.tf +++ b/modules/aurora-postgres/outputs.tf @@ -47,3 +47,8 @@ output "kms_key_arn" { value = module.kms_key_rds.key_arn description = "KMS key ARN for Aurora Postgres" } + +output "allowed_security_groups" { + value = local.allowed_security_groups + description = "The resulting list of security group IDs that are allowed to connect to the Aurora Postgres cluster." +} diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 3c3de97ea..f2137b617 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -259,6 +259,12 @@ variable "snapshot_identifier" { description = "Specifies whether or not to create this cluster from a snapshot" } +variable "allowed_security_group_names" { + type = list(string) + description = "List of security group names (tags) that should be allowed access to the database" + default = [] +} + variable "eks_security_group_enabled" { type = bool description = "Use the eks default security group" From d35e16433f6e9dc6e1f45a04742e85e48eea3de5 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:25:39 -0500 Subject: [PATCH 397/501] feat: support for multiple Argo CD service types (#1020) Co-authored-by: Andriy Knysh --- modules/eks/argocd/README.md | 1 + modules/eks/argocd/main.tf | 1 + modules/eks/argocd/resources/argocd-values.yaml.tpl | 2 +- modules/eks/argocd/variables-argocd.tf | 11 +++++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 47bc24a74..0ea68f4b7 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -580,6 +580,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [saml\_enabled](#input\_saml\_enabled) | Toggles SAML integration in the deployed chart | `bool` | `false` | no | | [saml\_rbac\_scopes](#input\_saml\_rbac\_scopes) | SAML RBAC scopes to request | `string` | `"[email,groups]"` | no | | [saml\_sso\_providers](#input\_saml\_sso\_providers) | SAML SSO providers components |
map(object({
component = string
environment = optional(string, null)
}))
| `{}` | no | +| [service\_type](#input\_service\_type) | Service type for exposing the ArgoCD service. The available type values and their behaviors are:
ClusterIP: Exposes the Service on a cluster-internal IP. Choosing this value makes the Service only reachable from within the cluster.
NodePort: Exposes the Service on each Node's IP at a static port (the NodePort).
LoadBalancer: Exposes the Service externally using a cloud provider's load balancer. | `string` | `"NodePort"` | no | | [slack\_notifications](#input\_slack\_notifications) | ArgoCD Slack notification configuration. Requires Slack Bot created with token stored at the given SSM Parameter path.

See: https://argocd-notifications.readthedocs.io/en/stable/services/slack/ |
object({
token_ssm_path = optional(string, "/argocd/notifications/notifiers/slack/token")
api_url = optional(string, null)
username = optional(string, "ArgoCD")
icon = optional(string, null)
})
| `{}` | no | | [slack\_notifications\_enabled](#input\_slack\_notifications\_enabled) | Whether or not to enable Slack notifications. See `var.slack_notifications.` | `bool` | `false` | no | | [ssm\_github\_api\_key](#input\_ssm\_github\_api\_key) | SSM path to the GitHub API key | `string` | `"/argocd/github/api_key"` | no | diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index fbbec8a2c..2249cd38d 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -162,6 +162,7 @@ module "argocd" { oidc_rbac_scopes = var.oidc_rbac_scopes saml_enabled = local.saml_enabled saml_rbac_scopes = var.saml_rbac_scopes + service_type = var.service_type rbac_default_policy = var.argocd_rbac_default_policy rbac_policies = var.argocd_rbac_policies rbac_groups = var.argocd_rbac_groups diff --git a/modules/eks/argocd/resources/argocd-values.yaml.tpl b/modules/eks/argocd/resources/argocd-values.yaml.tpl index 26d3ae928..0b869d6f4 100644 --- a/modules/eks/argocd/resources/argocd-values.yaml.tpl +++ b/modules/eks/argocd/resources/argocd-values.yaml.tpl @@ -66,7 +66,7 @@ server: https: false service: - type: NodePort + type: ${service_type} secret: create: true diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index cda437076..43d9f5dc1 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -143,6 +143,17 @@ variable "saml_rbac_scopes" { default = "[email,groups]" } +variable "service_type" { + type = string + default = "NodePort" + description = <<-EOT + Service type for exposing the ArgoCD service. The available type values and their behaviors are: + ClusterIP: Exposes the Service on a cluster-internal IP. Choosing this value makes the Service only reachable from within the cluster. + NodePort: Exposes the Service on each Node's IP at a static port (the NodePort). + LoadBalancer: Exposes the Service externally using a cloud provider's load balancer. + EOT +} + variable "argocd_rbac_policies" { type = list(string) default = [] From 026f7030bbb379315858538869e7cbe118563155 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:48:23 -0500 Subject: [PATCH 398/501] feat: Provide Anonymous ArgoCD Access (#1017) Co-authored-by: Andriy Knysh --- modules/eks/argocd/README.md | 1 + modules/eks/argocd/main.tf | 1 + modules/eks/argocd/resources/argocd-values.yaml.tpl | 1 + modules/eks/argocd/variables-argocd.tf | 6 ++++++ 4 files changed, 9 insertions(+) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 0ea68f4b7..334d30b5a 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -511,6 +511,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [alb\_logs\_bucket](#input\_alb\_logs\_bucket) | The name of the bucket for ALB access logs. The bucket must have policy allowing the ELB logging principal | `string` | `""` | no | | [alb\_logs\_prefix](#input\_alb\_logs\_prefix) | `alb_logs_bucket` s3 bucket prefix | `string` | `""` | no | | [alb\_name](#input\_alb\_name) | The name of the ALB (e.g. `argocd`) provisioned by `alb-controller`. Works together with `var.alb_group_name` | `string` | `null` | no | +| [anonymous\_enabled](#input\_anonymous\_enabled) | Toggles anonymous user access using default rbac setting (defaults to readonly) | `bool` | `false` | no | | [argocd\_apps\_chart](#input\_argocd\_apps\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"argocd-apps"` | no | | [argocd\_apps\_chart\_description](#input\_argocd\_apps\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"A Helm chart for managing additional Argo CD Applications and Projects"` | no | | [argocd\_apps\_chart\_repository](#input\_argocd\_apps\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://argoproj.github.io/argo-helm"` | no | diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 2249cd38d..4277299d6 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -148,6 +148,7 @@ module "argocd" { "${path.module}/resources/argocd-values.yaml.tpl", { admin_enabled = var.admin_enabled + anonymous_enabled = var.anonymous_enabled alb_group_name = var.alb_group_name == null ? "" : var.alb_group_name alb_logs_bucket = var.alb_logs_bucket alb_logs_prefix = var.alb_logs_prefix diff --git a/modules/eks/argocd/resources/argocd-values.yaml.tpl b/modules/eks/argocd/resources/argocd-values.yaml.tpl index 0b869d6f4..6c12148d2 100644 --- a/modules/eks/argocd/resources/argocd-values.yaml.tpl +++ b/modules/eks/argocd/resources/argocd-values.yaml.tpl @@ -74,6 +74,7 @@ server: config: url: https://${argocd_host} admin.enabled: "${admin_enabled}" + users.anonymous_enabled: "${anonymous_enabled}" # https://github.com/argoproj/argo-cd/issues/7835 kustomize.buildOptions: --enable-helm diff --git a/modules/eks/argocd/variables-argocd.tf b/modules/eks/argocd/variables-argocd.tf index 43d9f5dc1..ca4d54496 100644 --- a/modules/eks/argocd/variables-argocd.tf +++ b/modules/eks/argocd/variables-argocd.tf @@ -101,6 +101,12 @@ variable "admin_enabled" { default = false } +variable "anonymous_enabled" { + type = bool + description = "Toggles anonymous user access using default RBAC setting (Defaults to read-only)" + default = false +} + variable "oidc_enabled" { type = bool description = "Toggles OIDC integration in the deployed chart" From ca093aae8c4a8b7c4b48d0703a0fd0b1ac4985ce Mon Sep 17 00:00:00 2001 From: David Moran <23364162+wavemoran@users.noreply.github.com> Date: Wed, 1 May 2024 14:30:42 -0700 Subject: [PATCH 399/501] feat: add input_storage rds variable (#1026) --- modules/rds/README.md | 1 + modules/rds/main.tf | 1 + modules/rds/rds-variables.tf | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/modules/rds/README.md b/modules/rds/README.md index 4f8ef7383..ce94af2cd 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -217,6 +217,7 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | [ssm\_key\_user](#input\_ssm\_key\_user) | The SSM key to save the user. See `var.ssm_path_format`. | `string` | `"admin/db_user"` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [storage\_encrypted](#input\_storage\_encrypted) | (Optional) Specifies whether the DB instance is encrypted. The default is false if not specified | `bool` | `true` | no | +| [storage\_throughput](#input\_storage\_throughput) | The storage throughput value for the DB instance. Can only be set when `storage_type` is `gp3`. Cannot be specified if the `allocated_storage` value is below a per-engine threshold. | `number` | `null` | no | | [storage\_type](#input\_storage\_type) | One of 'standard' (magnetic), 'gp2' (general purpose SSD), or 'io1' (provisioned IOPS SSD) | `string` | `"standard"` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/rds/main.tf b/modules/rds/main.tf index 4f08e901c..b385b5820 100644 --- a/modules/rds/main.tf +++ b/modules/rds/main.tf @@ -86,6 +86,7 @@ module "rds_instance" { skip_final_snapshot = var.skip_final_snapshot snapshot_identifier = var.snapshot_identifier storage_encrypted = var.storage_encrypted + storage_throughput = var.storage_throughput storage_type = var.storage_type subnet_ids = local.subnet_ids timezone = var.timezone diff --git a/modules/rds/rds-variables.tf b/modules/rds/rds-variables.tf index 7f725a9e1..5e93be7b2 100644 --- a/modules/rds/rds-variables.tf +++ b/modules/rds/rds-variables.tf @@ -108,6 +108,12 @@ variable "iops" { default = 0 } +variable "storage_throughput" { + type = number + description = "The storage throughput value for the DB instance. Can only be set when `storage_type` is `gp3`. Cannot be specified if the `allocated_storage` value is below a per-engine threshold." + default = null +} + variable "allocated_storage" { type = number description = "The allocated storage in GBs" From 26c3a7519b41a472d90999977e522c54b928e9f9 Mon Sep 17 00:00:00 2001 From: Kevin Mahoney Date: Thu, 2 May 2024 18:16:20 +0200 Subject: [PATCH 400/501] feat(aurora-postgres): allow additional cluster parameters (#1004) --- modules/aurora-postgres/cluster-regional.tf | 4 ++-- modules/aurora-postgres/variables.tf | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/aurora-postgres/cluster-regional.tf b/modules/aurora-postgres/cluster-regional.tf index ccd911d6c..cc9f8a3f1 100644 --- a/modules/aurora-postgres/cluster-regional.tf +++ b/modules/aurora-postgres/cluster-regional.tf @@ -54,7 +54,7 @@ module "aurora_postgres_cluster" { allow_major_version_upgrade = var.allow_major_version_upgrade ca_cert_identifier = var.ca_cert_identifier - cluster_parameters = [ + cluster_parameters = concat([ { apply_method = "immediate" name = "log_statement" @@ -65,7 +65,7 @@ module "aurora_postgres_cluster" { name = "log_min_duration_statement" value = "0" } - ] + ], var.cluster_parameters) context = module.cluster.context } diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index f2137b617..3702a5f41 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -330,3 +330,13 @@ variable "intra_security_group_traffic_enabled" { default = false description = "Whether to allow traffic between resources inside the database's security group." } + +variable "cluster_parameters" { + type = list(object({ + apply_method = string + name = string + value = string + })) + default = [] + description = "List of DB cluster parameters to apply" +} From 4978caf3e5c89bd3535110596123637fd1078d22 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 2 May 2024 12:21:06 -0700 Subject: [PATCH 401/501] `[New Docs]` ECS Partial Task Definiton (#1027) --- .../docs/ecs-partial-task-definitions.md | 206 ++++++++++++++++++ .../docs/ecs-partial-task-defintions.png | Bin 0 -> 119726 bytes 2 files changed, 206 insertions(+) create mode 100644 modules/ecs-service/docs/ecs-partial-task-definitions.md create mode 100644 modules/ecs-service/docs/ecs-partial-task-defintions.png diff --git a/modules/ecs-service/docs/ecs-partial-task-definitions.md b/modules/ecs-service/docs/ecs-partial-task-definitions.md new file mode 100644 index 000000000..e67802b1e --- /dev/null +++ b/modules/ecs-service/docs/ecs-partial-task-definitions.md @@ -0,0 +1,206 @@ +# ECS Partial Task Definitions + +This document describes what partial task definitions are and how we can use them to set up ECS services using Terraform +and GitHub Actions. + +## The Problem + +Managing ECS Services is challenging. Ideally, we want our services to be managed by Terraform so everything is living +in code. However, we also want to update the task definition via GitOps as through the GitHub release lifecycle. This is +challenging because Terraform can create the task definition, but if updated by the application repository, the task +definition will be out of sync with the Terraform state. + +Managing it entirely through Terraform means we cannot easily update the newly built image by the application repository +unless we directly commit to the infrastructure repository, which is not ideal. + +Managing it entirely through the application repository means we cannot codify the infrastructure and have to hardcode +ARNs, secrets, and other infrastructure-specific configurations. + +## Introduction + +ECS Partial task definitions is the idea of breaking the task definition into smaller parts. This allows for easier +management of the task definition and makes it easier to update the task definition. + +We do this by setting up Terraform to manage a portion of the task definition, and the application repository to manage +another portion. + +The Terraform (infrastructure) portion is created first. It will create an ECS Service in ECS, and then upload the task +definition JSON to S3 as `task-template.json`.The application repository will have a `task-definition.json` git +controlled, during the development lifecycle, the application repository will download the task definition from S3, +merge the task definitions, then update the ECS Service with the new task definition. Finally, GitHub actions will +update the S3 bucket with the deployed task definition under `task-definition.json`. If Terraform is planned again, it +will use the new task definition as the base for the next deployment, thus not resetting the image or application +configuration. + +![how-does-partial-task-definition-work](./ecs-partial-task-defintions.png) + +### Pros + +The **benefit** to using this approach is that we can manage the task definition portion in Terraform with the +infrastructure, meaning secrets, volumes, and other ARNs can be managed in Terraform. If a filesystem ID updates we can +re-apply Terraform to update the task definition with the new filesystem ID. The application repository can manage the +container definitions, environment variables, and other application-specific configurations. This allows developers who +are closer to the application to quickly update the environment variables or other configuration. + +### Cons + +The drawback to this approach is that it is more complex than managing the task definition entirely in Terraform or the +application repository. It requires more setup and more moving parts. It can be confusing for a developer who is not +familiar with the setup to understand how the task definition is being managed and deployed. + +This also means that when something goes wrong, it becomes harder to troubleshoot as there are more moving parts. + +### Getting Setup + +#### Pre-requisites + +- Application Repository - [Cloud Posse Example ECS Application](https://github.com/cloudposse-examples/app-on-ecs) +- Infrastructure Repository +- ECS Cluster - [Cloud Posse Docs](https://docs.cloudposse.com/components/library/aws/ecs/) - + [Component](https://github.com/cloudposse/Terraform-aws-components/tree/main/modules/ecs). +- `ecs-service` - [Cloud Posse Docs](https://docs.cloudposse.com/components/library/aws/ecs-service/) - + [Component](https://github.com/cloudposse/Terraform-aws-components/tree/main/modules/ecs-service). + - **Must** use the Cloud Posse Component. + - [`v1.416.0`](https://github.com/cloudposse/Terraform-aws-components/releases/tag/1.416.0) or later. +- S3 Bucket - [Cloud Posse Docs](https://docs.cloudposse.com/components/library/aws/s3-bucket/) - + [Component](https://github.com/cloudposse/Terraform-aws-components/tree/main/modules/s3-bucket). + +#### Steps + +1. Set up the S3 Bucket that will store the task definition. + +
This bucket should be in the same account as the ECS Cluster. + +
S3 Bucket Default Definition + + ```yaml + components: + Terraform: + s3-bucket/defaults: + metadata: + type: abstract + vars: + enabled: true + account_map_tenant_name: core + # Suggested configuration for all buckets + user_enabled: false + acl: "private" + grants: null + force_destroy: false + versioning_enabled: false + allow_encrypted_uploads_only: true + block_public_acls: true + block_public_policy: true + ignore_public_acls: true + restrict_public_buckets: true + allow_ssl_requests_only: true + lifecycle_configuration_rules: + - id: default + enabled: true + abort_incomplete_multipart_upload_days: 90 + filter_and: + prefix: "" + tags: {} + # Move to Glacier after 2 years + transition: + - storage_class: GLACIER + days: 730 + # Never expire + expiration: {} + # Versioning isnt enabled, but these default values are still required + noncurrent_version_transition: + - storage_class: GLACIER + days: 90 + noncurrent_version_expiration: {} + ``` + +
+ + ```yaml + import: + - catalog/s3-bucket/defaults + + components: + Terraform: + s3-bucket/ecs-tasks-mirror: #NOTE this is the component instance name. + metadata: + component: s3-bucket + inherits: + - s3-bucket/defaults + vars: + enabled: true + name: ecs-tasks-mirror + ``` + +2. Create an ECS Service in Terraform + +
Set up the ECS Service in Terraform using the + [`ecs-service` component](https://github.com/cloudposse/Terraform-aws-components/tree/main/modules/ecs-service). This + will create the ECS Service and upload the task definition to the S3 bucket. + +
To enable Partial Task Definitions, set the variable `s3_mirror_name` to be the component instance name of the + bucket to mirror to. For example `s3-bucket/ecs-tasks-mirror` + + ```yaml + components: + Terraform: + ecs-services/defaults: + metadata: + component: ecs-service + type: abstract + vars: + enabled: true + ecs_cluster_name: "ecs/cluster" + s3_mirror_name: s3-bucket/ecs-tasks-mirror + ``` + +3. Set up an Application repository with GitHub workflows. + + An example application repository can be found [here](https://github.com/cloudposse-examples/app-on-ecs). + +
Two things need to be pulled from this repository: + + - The `task-definition.json` file under `deploy/task-definition.json` + - The GitHub Workflows. + + An important note about the GitHub Workflows, in the example repository they all live under `.github/workflows`. This + is done so development of workflows can be fast, however we recommend moving the shared workflows to a separate + repository and calling them from the application repository. The application repository should only contain the + workflows `main-branch.yaml`, `release.yaml` and `feature-branch.yml`. + +
To enable Partial Task Definitions in the workflows, the call to + [`cloudposse/github-action-run-ecspresso` (link)](https://github.com/cloudposse-examples/app-on-ecs/blob/main/.github/workflows/workflow-cd-ecspresso.yml#L133-L147) + should have the input `mirror_to_s3_bucket` set to the S3 bucket name. the variable `use_partial_taskdefinition` + should be set to `'true'` + +
Example GitHub Action Step + + ```yaml + - name: Deploy + uses: cloudposse/github-action-deploy-ecspresso@0.6.0 + continue-on-error: true + if: ${{ steps.db_migrate.outcome != 'failure' }} + id: deploy + with: + image: ${{ steps.image.outputs.out }} + image-tag: ${{ inputs.tag }} + region: ${{ steps.environment.outputs.region }} + operation: deploy + debug: false + cluster: ${{ steps.environment.outputs.cluster }} + application: ${{ steps.environment.outputs.name }} + taskdef-path: ${{ inputs.path }} + mirror_to_s3_bucket: ${{ steps.environment.outputs.s3-bucket }} + use_partial_taskdefinition: "true" + timeout: 10m + ``` + +
+ +## Operation + +Changes through Terraform will not immediately be reflected in the ECS Service. This is because the task template has +been updated, but whatever was in the `task-definition.json` file in the S3 bucket will be used for deployment. + +To update the ECS Service after updating the Terraform for it, you must deploy through GitHub Actions. This will then +download the new template and create a new updated `task-defintion.json` to store in s3. diff --git a/modules/ecs-service/docs/ecs-partial-task-defintions.png b/modules/ecs-service/docs/ecs-partial-task-defintions.png new file mode 100644 index 0000000000000000000000000000000000000000..cbdfeef9519addf1df6776eb2592ffe7a36fd5c1 GIT binary patch literal 119726 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

_ypw|mLR5PWu+=78Tphk?>cpZ7W|6GZGm3zq8 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%0D}X?j*q=3ZJqRO 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 Date: Thu, 25 Jan 2024 20:29:54 -0800 Subject: [PATCH 351/501] feat: ArgoCD Repository Name and Chart Values (#960) --- modules/argocd-repo/README.md | 1 + modules/argocd-repo/outputs.tf | 5 +++++ modules/eks/argocd/README.md | 1 + modules/eks/argocd/main.tf | 5 +++-- modules/eks/argocd/variables-argocd-apps.tf | 6 ++++++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index d5ffb546e..bc74f4878 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -170,6 +170,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" |------|-------------| | [deploy\_keys\_ssm\_path\_format](#output\_deploy\_keys\_ssm\_path\_format) | SSM Parameter Store path format for the repository's deploy keys | | [deploy\_keys\_ssm\_paths](#output\_deploy\_keys\_ssm\_paths) | SSM Parameter Store paths for the repository's deploy keys | +| [repository](#output\_repository) | Repository name | | [repository\_default\_branch](#output\_repository\_default\_branch) | Repository default branch | | [repository\_description](#output\_repository\_description) | Repository description | | [repository\_git\_clone\_url](#output\_repository\_git\_clone\_url) | Repository git clone URL | diff --git a/modules/argocd-repo/outputs.tf b/modules/argocd-repo/outputs.tf index 49f29ca59..19430b352 100644 --- a/modules/argocd-repo/outputs.tf +++ b/modules/argocd-repo/outputs.tf @@ -8,6 +8,11 @@ output "deploy_keys_ssm_path_format" { value = local.enabled ? var.ssm_github_deploy_key_format : null } +output "repository" { + description = "Repository name" + value = local.enabled && var.create_repo ? module.this.name : var.name +} + output "repository_description" { description = "Repository description" value = local.github_repository.description diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 456c7b772..dfc45b349 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -460,6 +460,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [argocd\_apps\_chart](#input\_argocd\_apps\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"argocd-apps"` | no | | [argocd\_apps\_chart\_description](#input\_argocd\_apps\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"A Helm chart for managing additional Argo CD Applications and Projects"` | no | | [argocd\_apps\_chart\_repository](#input\_argocd\_apps\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://argoproj.github.io/argo-helm"` | no | +| [argocd\_apps\_chart\_values](#input\_argocd\_apps\_chart\_values) | Additional values to yamlencode as `helm_release` values for the argocd\_apps chart | `any` | `{}` | no | | [argocd\_apps\_chart\_version](#input\_argocd\_apps\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"0.0.3"` | no | | [argocd\_apps\_enabled](#input\_argocd\_apps\_enabled) | Enable argocd apps | `bool` | `true` | no | | [argocd\_create\_namespaces](#input\_argocd\_create\_namespaces) | ArgoCD create namespaces policy | `bool` | `false` | no | diff --git a/modules/eks/argocd/main.tf b/modules/eks/argocd/main.tf index 6781c7605..52c66fa52 100644 --- a/modules/eks/argocd/main.tf +++ b/modules/eks/argocd/main.tf @@ -6,7 +6,7 @@ locals { oidc_enabled_count = local.oidc_enabled ? 1 : 0 saml_enabled = local.enabled && var.saml_enabled argocd_repositories = local.enabled ? { - for k, v in var.argocd_repositories : k => { + for k, v in var.argocd_repositories : module.argocd_repo[k].outputs.repository => { clone_url = module.argocd_repo[k].outputs.repository_ssh_clone_url github_deploy_key = data.aws_ssm_parameter.github_deploy_key[k].value } @@ -222,7 +222,8 @@ module "argocd_apps" { stage = var.stage attributes = var.attributes } - ) + ), + yamlencode(var.argocd_apps_chart_values) ]) depends_on = [ diff --git a/modules/eks/argocd/variables-argocd-apps.tf b/modules/eks/argocd/variables-argocd-apps.tf index 77a8d42bd..7ecab3742 100644 --- a/modules/eks/argocd/variables-argocd-apps.tf +++ b/modules/eks/argocd/variables-argocd-apps.tf @@ -27,3 +27,9 @@ variable "argocd_apps_enabled" { description = "Enable argocd apps" default = true } + +variable "argocd_apps_chart_values" { + type = any + description = "Additional values to yamlencode as `helm_release` values for the argocd_apps chart" + default = {} +} From 1710c2f7b21f83cbb058066032c24cfefc494df5 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Fri, 26 Jan 2024 14:16:39 -0500 Subject: [PATCH 352/501] Update `eks/cluster` component (#961) Co-authored-by: cloudpossebot --- modules/eks/cluster/README.md | 4 ++-- modules/eks/cluster/addons.tf | 17 +++++++++-------- modules/eks/cluster/main.tf | 2 +- modules/eks/cluster/variables.tf | 11 ++++++----- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 7ea65c62d..a880aca45 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -447,7 +447,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [aws\_efs\_csi\_driver\_eks\_iam\_role](#module\_aws\_efs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.1 | | [coredns\_fargate\_profile](#module\_coredns\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 2.9.0 | +| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 3.0.0 | | [fargate\_pod\_execution\_role](#module\_fargate\_pod\_execution\_role) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [iam\_arns](#module\_iam\_arns) | ../../account-map/modules/roles-to-principals | n/a | @@ -487,7 +487,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |

Ki4x6q_4fdO4 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⪙ z%x-z&JicA7;ZXkhyw+)<(%TYIW-j;a?-^~rmUH6Np*V59DrboVhs<~fEcTinetwAj zqsgI4^}$rXPjiq%X8>mPN^{CXM@q_pnp87a2O!LTM;rSn`JD{vP&Ge4fAZ$qJ2AW< z&phukG=e;`1Q>f(E?gmfOEZ7&b{Qg59{`My5=_wRGnA#2Du6_-jn=G8t3aW*vN5k( z<3~6Dj?o)6a5A z4FMu*iKz^ts_K$z5e!Uhs1OFGQYnmXfu(hf)m@DHP2Z-)U!ZZ*&}waNoE}e==Rqpv zWq|MS2O8+A=x7dgO%-S$T2FzOpIZ93rP?7;enr3?t5ypHLuLLmR_kI-`=kypPIR@j zSh(^P2W4GvQ5qHOs`8)7VBUG6c5Tsosaa)4aH-VD(yOzsq|pN148%DvjOr0u4f6`` zr`Fu5s^&E?{hnn%1#ZBF8~wxu(+eQqi-&S2n)DEdbbH zd*!<B6_Ax!J#p+F|R6t3NJut9dm@T@mwzR69Q>?c04&@ zNhis}_|)TLZ4@BAnlAZxTIHEw-}ybqywdJlL(567HaEvwc?9n6kmGmSWEqJkpQ%*= z6m|*Hp8$c5ocL|tjjBtR6_xNp#mNg6%P`TlI@5R?Y1{1tee` z$rsY7`_}1)om$f_lA8WHoxF_aqMjSD+kETySzz`uvyDYLom0M@H${+FEe&0c^;Ql36c0F%4whDSK-LiGYttM=^`S+4&at}uZhXY6DrxLf-7?o)+ zAb+1*&wG)F%0UUZb0k$V8g{t(`z4cPh%? zBR^il$wL=-S6kM&mta6`x#fYCd261L&hYG2JYU%*0?I5%2jn-!uqy`lGUbo%lcITLd$1E8Q zyZUKCi)-9PH?vt&SFvMT^l=78W-biMlu-j$m@m8bLZ6qKQJBkT%wnoaP8JG5fMgIUOG9-Cr8<58ySmB&U{5fb3EWD#q>+W34JxFwYem?zF zops&wX9nq}vR6O4!LX9++9-ge#?@;sz2{ap7h;*4yomIeDUihf+8nN+dXaQlyXB`; z`vHQZ=cBcR+YZ+=431-M&1Hi=W`0rk8$YiOcWb`$Pyjq-)o1KcMQ8C(7S3JeKqdAe z_>`(^Z+X!Kbl8pZI%xWerX}rGvTt%-a-Hopjow@+A~QYrc;pnJQ*wYPMQ~M6t?ZEt zSab_MmkE%>Y3R65Lw|Q((Q6@wsV^|f3X?6cr}8veu`lrbw4VE&SK~+6$Bb1pEq~hx zwr~lI?)Kb)Ol{XA#0f1Q4ousJuASTD(YXYYK})#nTgwkorIVJSdWOOab5XF; zUo34(R_VGq0tnQub+Yk3r+t4_ni=)@%tE1I4m~NP?ECKVMM%_xCfmdq9^LC|EIH(( zDBU3ptn{vsqNJ_?iZcMMKUCUofqvWL8EMjO0RYO>Bh7xTa(>c!Gi@35M(EcdEiz3x z#pRYlUwfOIZ^?z-RL&ep*_Q!ML+?QC;;DhC25fCW%^lqD>6j^YT)xJG*W>FPe~VK{Edx zUCR-|5scHUOjsLdal^Xg&pxd!%`+vJb6f+DI8Scm@WiE9h@Lur68*MK?i!0pgr(|j zQjcy2LB%}RF_G7p))S~GP44u*(zmnq`wYjAHgX5>#>u5@go^=JmJO>6hh z;ZMuB=aUy0mhZexZKLU7R%jGFE>@J5GD!n)blUdX3i78%74go~ znIYLdK|&_7K_7xh7Ub&6<7tPVr8v=K8}7>c`ePck*wJCZbAu1JEYj_Qv2=h zT-&_gl=->Z5^jtlukUG^nf?pfp|YS%vmn)xv?L|(awt&XANk1=g*%ABl32)HJuf-g zYc6I^oXYIMqn#2g6s>@PZNx;#B6y7!^fXMEwN;*na^`qfp~Gid6=1?f1IAdElr(T& z(vC0aDwwzvMND$j-=o#xpet)^Xlo%_U2N7rdC^4ScHV4O&s9pCjQ_{mBPDTaD@rsw z*L>$-)qnzQj6Xa1Moz-&gZTIhI1bga{4dydm;D-hXWUG@x8g0*_=~Kb z;i{*2zU1$!VuQ^%C7bD`VjWw`vmocvP!H_+vPnwuciwY&4<;v?_F@Jnm`Hp8020^4 zm?GTxZ>D_cYTEt$84KH(g%EwWD4XLU1dnvi&`ujAqDdEbQ@HEqbL5ssPn)lY1*A<~ z>fX%ao1rzZFYTcpOK`sUJBpHB;bd(530+w}RY~pN=7}Nn5Tl0j1I_o$vHVopspkpT zCY0Qpnu|qkE?{znYJM%dTBk5&3&&?W``zj&YS*$1U#K)35#0EE!}|=PlzpN>)~-x? zTgOT_zYN`hNh30aFT~FKc!TjnKH^6(@&Ov}!R<(7jJ{?=8AdUPCVyZ;j3cZcV&kGp z`--;m$4C+<+&EK_8raH|%^B1P3FLbEG6wtcm5OBE8;D zotY|w(LEDzlTU;m-n{2R5hXLuE+YYy|cg(v`f+J3uXyIsw&`p6Z)9cpn>zF1unGdKx5CD z1oPRy0+*xnpqAxWb1=6OcV)7B5E&q;wFa}%2Vq=G*AQQ>&5aFK++4@KES%$z}gP8I30eI z-EEdv2O~UQhO)eEa+?pw3?FpNZ34lOBDGF6ju{);`1D=k}v|LP77_67WVA59y%PR*?mD807gr&oNdZ(m>Lw&2`yF$;A1Rj}LT^ zid>;?b#DR+o+G}E#NC0{iZ6!14jGb~2|Ipk;NkrkUB&J=!;=RUh&pv^cNP0*(cfSa zFEh2zIB^}=o>nJI9G=n~A%<0Z(OFgvB{ zo}X8^Z)K_45wY-STq*EJVG`Og28?;qX8?g$eT|l?)+*US{q7+ka^gk7+n*5Pt~SJd z!yAA7Rx4Urmo-49>l+dhOG8NPZX09$6yqMb00~SLWJZoJpC{la9682B0%t>2hU3ny zx-CORy%$&rCg6QX6Pn0idLO(KWgN=mH2{?6UWOU2WqTSFT2)4T;v*Ku$J-c7_`d)k zc~OzGxa`sR`(&kB829M~6fN7>YQRklBM677wVP$*0^03H5$jJkFn;HKCrNC1tn{&k z8>U)%Z#qS|v3vtgpK_RhSstY#qdH4KcljfAX=Y#mBER?QuSg-Ei|zFxSx_#!GKF1V z{gHU&;;ISJc@cq?ERjb5M{?OQ*)KsoXlJ5HK6coV=qGr+6ua=aET|Tyfw}+?pY^Ha zl-eh?)mhCYxCcKUmP5D(e*=EJ4e-TpVVWB_@r4xRF0*XvkzUpk#-L)!h?*;?Aasn6G-#rs8y6n_o3KvI z*$~VD&1Hb#jDSwgq?&alwLP?zNei94foT9+c))f4tBnvd9Zob{L1wG{136+=9sUU1 z(Q5dotEx);bo<1%b00=mw(%~-4h&U&$U9MpOBGTpUWaN$7@D}?uLI_sO!d1H@ zVZ+UM$_@<53Z7P-A_y+vw^%t4X^p(KIs%U4BIa){HWXJ92u$JRw(yN)QwvsZzqr`- z6(0TOXOSXo6w*(8*Zqt?8?}XvzU=Lm+6<50a!U|=ksUm>Ui*D0{p2udY_PK9>Lm6t zZ1|Y^qBcY5RdBp>IPar>HhKy+nkW$P1_h61yr&5=cY~*P?^ojbBXcmDvP^i(+y5Ld zR{?C)>P$@J#zrGR)|@^U3--W9kD$ESK;{M@7y$`ZjSZP2$(q)eBG=1IKf@O`v2H7e ziQTH=VmWy0S?$-;?+}&2$TNle%52}*Rv~PstTh7T3YU2T4X=sxMmscmL6*Ahe6+1t z2*wg*GokU&=Z`HnfIUA7|8YH^{cj#>#(lhqdVl@q}ZuGaC0A+jxA12Q}w}RyX@pXNAuXGr8ND#y5j>c~M3L__c zF-}XilFb^pw7ZVoY z)dj4ehH}@&onkYEGd2IFktPpH<^I{FZQv7%Hz^( za9>9G|Nn6RC$8oH&cpSn5v4qN@=_WKd9-hBub?V3nJ9Y5{CQ*Y<+pq!K!nt?N^nB4 z(sKzoc7@R;&@*e!x1xMn0}?6YNCQaj{;S*lGt#$X6#~{u7>7_^Oa0bsfF1^RV85;W zM~{v8uPn{@0~pl@z;I|*X==`=rC+GJWVKI7^#SPZ_B>%(Lwp`Tv-SRHlC<^?C zg~fz>bn8hFhxORX_+l(OJ-k5;!g{zOVV)ky>q6$JDslJJ*Tb;QNq%`T0umeG0zSN> zTYTMESl8=2X_gP$l#d%k5xijirGV~Fy4@h22qC1zYVW(e2ex_T?xP*Lh(o4O>@U&R z#r0sF=9I6ky85m<>ct= zvEs0Pw(P92l&GrxR{LXY;uf$zyZNjd-O(M`fg)xR@97C{7FCV1%y3|)LR@mj)02;n zzOKg&a?LS6kQ+}(DUOsAWadWF%&VIYh|q0U9C){L)OX_B!U!p^Nk(XpRvWo37^ z(AUMmk=}35kLx(8d&W$YZyOUk#P*Wap>g=%#u0N8j`x7`CGMNYb66=d5!Y!pW4^^fR@hxCA(xfG*^_?QvSqTT-oj*NOw-3)q^Zg2ngxhdT=;2S8gI6 z+)$2jP4OGRh~@{Yq4)A@22!|6sL1kA^*eb_-=Tl>RXE65oQK0}jGASDq7HhjZDvSKy?!GQg;D@$l?9Ik z&%1479HWTV!z6_tX#RIwOY|hv#SfTWe+=Qv%N?!WSDF%2eW`kCMAuyu;79tETH2dT z5N$|7vqWy0V1DD zhbG?k4E5@xGIxHVPtsN85FC}XpV$j*vExVi43bxh5b}XmJGbzKa Date: Fri, 3 May 2024 09:34:40 -0500 Subject: [PATCH 402/501] Update GitHub workflows (#1028) --- .github/workflows/feature-branch.yml | 18 ++++++++++++++++++ .github/workflows/release-branch.yml | 1 + .github/workflows/scheduled.yml | 16 ++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 .github/workflows/feature-branch.yml create mode 100644 .github/workflows/scheduled.yml diff --git a/.github/workflows/feature-branch.yml b/.github/workflows/feature-branch.yml new file mode 100644 index 000000000..456793945 --- /dev/null +++ b/.github/workflows/feature-branch.yml @@ -0,0 +1,18 @@ +--- +name: feature-branch +on: + pull_request: + branches: + - main + - release/** + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + pull-requests: write + id-token: write + contents: write + +jobs: + terraform-module: + uses: cloudposse/github-actions-workflows-terraform-module/.github/workflows/feature-branch.yml@main + secrets: inherit diff --git a/.github/workflows/release-branch.yml b/.github/workflows/release-branch.yml index 3593cea51..852d5e3ea 100644 --- a/.github/workflows/release-branch.yml +++ b/.github/workflows/release-branch.yml @@ -10,6 +10,7 @@ on: - 'docs/**' - 'examples/**' - 'test/**' + - 'README.md' permissions: {} diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml new file mode 100644 index 000000000..7bc09ab9d --- /dev/null +++ b/.github/workflows/scheduled.yml @@ -0,0 +1,16 @@ +--- +name: scheduled +on: + workflow_dispatch: { } # Allows manually trigger this workflow + schedule: + - cron: "0 3 * * *" + +permissions: + pull-requests: write + id-token: write + contents: write + +jobs: + scheduled: + uses: cloudposse/github-actions-workflows-terraform-module/.github/workflows/scheduled.yml@main + secrets: inherit From 86035c388bac5c8e45d02d1f8d25c912798f4f3e Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Mon, 6 May 2024 10:29:15 -0500 Subject: [PATCH 403/501] fix: update spacelift worker installation for latest images (#1023) Co-authored-by: Andriy Knysh --- modules/spacelift/worker-pool/main.tf | 7 +- .../worker-pool/templates/user-data.sh | 70 +++++++++++-------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/modules/spacelift/worker-pool/main.tf b/modules/spacelift/worker-pool/main.tf index 8f90dc894..320154565 100644 --- a/modules/spacelift/worker-pool/main.tf +++ b/modules/spacelift/worker-pool/main.tf @@ -118,8 +118,11 @@ module "autoscale_group" { # The instance refresh definition # If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated - instance_refresh = var.instance_refresh - launch_template_version = var.launch_template_version # this has to be empty for the instance refresh to work + instance_refresh = var.instance_refresh + # Note: instance refresh settings are IGNORED unless template version is empty + # See the second "NOTE" in the "instance_refresh" documentation + # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#instance_refresh + launch_template_version = var.instance_refresh == null ? "$Latest" : "" context = module.this.context } diff --git a/modules/spacelift/worker-pool/templates/user-data.sh b/modules/spacelift/worker-pool/templates/user-data.sh index d145a8059..7c4528b2f 100644 --- a/modules/spacelift/worker-pool/templates/user-data.sh +++ b/modules/spacelift/worker-pool/templates/user-data.sh @@ -3,24 +3,30 @@ spacelift() { ( set -e - aws ecr get-login-password --region ${ecr_region} \ - | docker login --username AWS --password-stdin ${ecr_account_id}.dkr.ecr.${ecr_region}.amazonaws.com - - docker pull ${spacelift_runner_image} - echo "Updating packages (security)" | tee -a /var/log/spacelift/info.log yum update-minimal --security -y 1>>/var/log/spacelift/info.log 2>>/var/log/spacelift/error.log -%{ if github_netrc_enabled } + if ! which docker-credential-ecr-login; then + yum install -y amazon-ecr-credential-helper + fi + # Due to https://github.com/docker/cli/issues/2738 + # we need to create the config.json file for all users + for home in /root $(ls /home); do + mkdir -p $home/.docker + echo '{"credsStore": "ecr-login"}' >$home/.docker/config.json + done + docker pull ${spacelift_runner_image} + + %{ if github_netrc_enabled } export GITHUB_TOKEN=$(aws ssm get-parameters --region=${region} --name ${github_netrc_ssm_path_token} --with-decryption --query "Parameters[0].Value" --output text) export GITHUB_USER=$(aws ssm get-parameters --region=${region} --name ${github_netrc_ssm_path_user} --with-decryption --query "Parameters[0].Value" --output text) # Allows downloading terraform modules using a GitHub PAT NETRC_FILE="/root/.netrc" echo "Creating $NETRC_FILE" - printf "machine github.com\n" > "$NETRC_FILE" - printf "login %s\n" "$GITHUB_USER" >> "$NETRC_FILE" - printf "password %s\n" "$GITHUB_TOKEN" >> "$NETRC_FILE" + printf "machine github.com\n" >"$NETRC_FILE" + printf "login %s\n" "$GITHUB_USER" >>"$NETRC_FILE" + printf "password %s\n" "$GITHUB_TOKEN" >>"$NETRC_FILE" echo "Created $NETRC_FILE" # Converts ssh clones into https clones to take advantage of the GitHub PAT @@ -36,17 +42,19 @@ spacelift() { ( # Mount the .netrc and .gitconfig files into the container export SPACELIFT_WORKER_EXTRA_MOUNTS=$NETRC_FILE:/conf/.netrc,$GIT_CONFIG:/conf/.gitconfig -%{ endif } -%{ if infracost_enabled } + %{ endif } + + %{ if infracost_enabled } export INFRACOST_API_KEY=$(aws ssm get-parameters --region=${region} --name ${infracost_api_token_ssm_path} --with-decryption --query "Parameters[0].Value" --output text) export INFRACOST_CLI_ARGS=${infracost_cli_args} export INFRACOST_WARN_ON_FAILURE=${infracost_warn_on_failure} -%{ endif } + %{ endif } + export SPACELIFT_POOL_PRIVATE_KEY=${spacelift_worker_pool_private_key} export SPACELIFT_TOKEN=${spacelift_worker_pool_config} - # This is a comma separated list of all the environment variables to read from the env file + # This is a comma separated list of all the environment variables to read from the env file export SPACELIFT_WHITELIST_ENVS=AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN,AWS_SDK_LOAD_CONFIG,AWS_CONFIG_FILE,AWS_PROFILE,GITHUB_TOKEN,INFRACOST_API_KEY,ATMOS_BASE_PATH,TF_VAR_terraform_user - # This is a comma separated list of all the sensitive environment variables that will show up masked if printed during a run + # This is a comma separated list of all the sensitive environment variables that will show up masked if printed during a run export SPACELIFT_MASK_ENVS=AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN,GITHUB_TOKEN,INFRACOST_API_KEY export SPACELIFT_LAUNCHER_LOGS_TIMEOUT=30m export SPACELIFT_LAUNCHER_RUN_TIMEOUT=120m @@ -79,23 +87,23 @@ spacelift() { ( sudo mkdir -p "/etc/spacelift" sudo touch "$env_file" sudo chmod 744 "$env_file" - printf "SPACELIFT_POOL_PRIVATE_KEY=%s\n" "$SPACELIFT_POOL_PRIVATE_KEY" > "$env_file" - printf "SPACELIFT_TOKEN=%s\n" "$SPACELIFT_TOKEN" >> "$env_file" - printf "SPACELIFT_WHITELIST_ENVS=%s\n" "$SPACELIFT_WHITELIST_ENVS" >> "$env_file" - printf "SPACELIFT_MASK_ENVS=%s\n" "$SPACELIFT_MASK_ENVS" >> "$env_file" - printf "SPACELIFT_LAUNCHER_LOGS_TIMEOUT=%s\n" "$SPACELIFT_LAUNCHER_LOGS_TIMEOUT" >> "$env_file" - printf "SPACELIFT_LAUNCHER_RUN_TIMEOUT=%s\n" "$SPACELIFT_LAUNCHER_RUN_TIMEOUT" >> "$env_file" - printf "SPACELIFT_METADATA_instance_id=%s\n" "$SPACELIFT_METADATA_instance_id" >> "$env_file" - printf "SPACELIFT_METADATA_asg_id=%s\n" "$SPACELIFT_METADATA_asg_id" >> "$env_file" - printf "AWS_SDK_LOAD_CONFIG=%s\n" "$TMP_AWS_SDK_LOAD_CONFIG" >> "$env_file" - printf "AWS_CONFIG_FILE=%s\n" "$TMP_AWS_CONFIG_FILE" >> "$env_file" - printf "AWS_PROFILE=%s\n" "$TMP_AWS_PROFILE" >> "$env_file" - printf "ATMOS_BASE_PATH=%s\n" "/mnt/workspace/source" >> "$env_file" - printf "TF_VAR_terraform_user=%s\n" "spacelift" >> "$env_file" - [[ ! -z "$GITHUB_TOKEN" ]] && printf "GITHUB_TOKEN=%s\n" "$GITHUB_TOKEN" >> "$env_file" - [[ ! -z "$GITHUB_USER" ]] && printf "GITHUB_USER=%s\n" "$GITHUB_USER" >> "$env_file" - [[ ! -z "$SPACELIFT_WORKER_EXTRA_MOUNTS" ]] && printf "SPACELIFT_WORKER_EXTRA_MOUNTS=%s\n" "$SPACELIFT_WORKER_EXTRA_MOUNTS" >> "$env_file" - [[ ! -z "$INFRACOST_API_KEY" ]] && printf "INFRACOST_API_KEY=%s\n" "$INFRACOST_API_KEY" >> "$env_file" + printf "SPACELIFT_POOL_PRIVATE_KEY=%s\n" "$SPACELIFT_POOL_PRIVATE_KEY" >"$env_file" + printf "SPACELIFT_TOKEN=%s\n" "$SPACELIFT_TOKEN" >>"$env_file" + printf "SPACELIFT_WHITELIST_ENVS=%s\n" "$SPACELIFT_WHITELIST_ENVS" >>"$env_file" + printf "SPACELIFT_MASK_ENVS=%s\n" "$SPACELIFT_MASK_ENVS" >>"$env_file" + printf "SPACELIFT_LAUNCHER_LOGS_TIMEOUT=%s\n" "$SPACELIFT_LAUNCHER_LOGS_TIMEOUT" >>"$env_file" + printf "SPACELIFT_LAUNCHER_RUN_TIMEOUT=%s\n" "$SPACELIFT_LAUNCHER_RUN_TIMEOUT" >>"$env_file" + printf "SPACELIFT_METADATA_instance_id=%s\n" "$SPACELIFT_METADATA_instance_id" >>"$env_file" + printf "SPACELIFT_METADATA_asg_id=%s\n" "$SPACELIFT_METADATA_asg_id" >>"$env_file" + printf "AWS_SDK_LOAD_CONFIG=%s\n" "$TMP_AWS_SDK_LOAD_CONFIG" >>"$env_file" + printf "AWS_CONFIG_FILE=%s\n" "$TMP_AWS_CONFIG_FILE" >>"$env_file" + printf "AWS_PROFILE=%s\n" "$TMP_AWS_PROFILE" >>"$env_file" + printf "ATMOS_BASE_PATH=%s\n" "/mnt/workspace/source" >>"$env_file" + printf "TF_VAR_terraform_user=%s\n" "spacelift" >>"$env_file" + [[ ! -z "$GITHUB_TOKEN" ]] && printf "GITHUB_TOKEN=%s\n" "$GITHUB_TOKEN" >>"$env_file" + [[ ! -z "$GITHUB_USER" ]] && printf "GITHUB_USER=%s\n" "$GITHUB_USER" >>"$env_file" + [[ ! -z "$SPACELIFT_WORKER_EXTRA_MOUNTS" ]] && printf "SPACELIFT_WORKER_EXTRA_MOUNTS=%s\n" "$SPACELIFT_WORKER_EXTRA_MOUNTS" >>"$env_file" + [[ ! -z "$INFRACOST_API_KEY" ]] && printf "INFRACOST_API_KEY=%s\n" "$INFRACOST_API_KEY" >>"$env_file" echo "Enabling Spacelift agent services" | tee -a /var/log/spacelift/info.log sudo systemctl enable spacelift@{1..${spacelift_agents_per_node}}.service From ee59fd85ba19fd7854f21d8c23cfd404c402f865 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Mon, 6 May 2024 15:04:34 -0500 Subject: [PATCH 404/501] feat: Spacelift worker pool enhancements (#1029) --- modules/spacelift/worker-pool/README.md | 4 +++- modules/spacelift/worker-pool/data.tf | 2 +- modules/spacelift/worker-pool/main.tf | 7 +++---- modules/spacelift/worker-pool/variables.tf | 14 +++++++++++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index 2209c8437..bf2ad4acc 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -174,6 +174,7 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [account\_map\_stage\_name](#input\_account\_map\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | | [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [architecture](#input\_architecture) | OS architecture of the EC2 instance AMI | `list(string)` |
[
"x86_64"
]
| no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [aws\_config\_file](#input\_aws\_config\_file) | The AWS\_CONFIG\_FILE used by the worker. Can be overridden by `/.spacelift/config.yml`. | `string` | `"/etc/aws-config/aws-config-spacelift"` | no | | [aws\_profile](#input\_aws\_profile) | The AWS\_PROFILE used by the worker. If not specified, `"${var.namespace}-identity"` will be used.
Can be overridden by `/.spacelift/config.yml`. | `string` | `null` | no | @@ -205,13 +206,14 @@ role. This is done by adding `iam_role_arn` from the output to the `trusted_role | [infracost\_cli\_args](#input\_infracost\_cli\_args) | These are the CLI args passed to infracost | `string` | `""` | no | | [infracost\_enabled](#input\_infracost\_enabled) | Whether to enable infracost for Spacelift stacks | `bool` | `false` | no | | [infracost\_warn\_on\_failure](#input\_infracost\_warn\_on\_failure) | A failure executing Infracost, or a non-zero exit code being returned from the command will cause runs to fail. If this is true, this will only warn instead of failing the stack. | `bool` | `true` | no | +| [instance\_lifetime](#input\_instance\_lifetime) | Number of seconds after which the instance will be terminated. The default is set to 14 days. | `number` | `1209600` | no | | [instance\_refresh](#input\_instance\_refresh) | The instance refresh definition. If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated |
object({
strategy = string
preferences = object({
instance_warmup = optional(number, null)
min_healthy_percentage = optional(number, null)
skip_matching = optional(bool, null)
auto_rollback = optional(bool, null)
})
triggers = optional(list(string), [])
})
| `null` | no | | [instance\_type](#input\_instance\_type) | EC2 instance type to use for workers | `string` | `"r5n.large"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [launch\_template\_version](#input\_launch\_template\_version) | Launch template version to use for workers | `string` | `"$Latest"` | no | +| [launch\_template\_version](#input\_launch\_template\_version) | Launch template version to use for workers. Note that instance refresh settings are IGNORED unless template version is empty | `string` | `"$Latest"` | no | | [max\_size](#input\_max\_size) | The maximum size of the autoscale group | `number` | n/a | yes | | [min\_size](#input\_min\_size) | The minimum size of the autoscale group | `number` | n/a | yes | | [mixed\_instances\_policy](#input\_mixed\_instances\_policy) | Policy to use a mixed group of on-demand/spot of different types. Launch template is automatically generated. https://www.terraform.io/docs/providers/aws/r/autoscaling_group.html#mixed_instances_policy-1 |
object({
instances_distribution = object({
on_demand_allocation_strategy = string
on_demand_base_capacity = number
on_demand_percentage_above_base_capacity = number
spot_allocation_strategy = string
spot_instance_pools = number
spot_max_price = string
})
override = list(object({
instance_type = string
weighted_capacity = number
}))
})
| `null` | no | diff --git a/modules/spacelift/worker-pool/data.tf b/modules/spacelift/worker-pool/data.tf index b069e0725..462c52953 100644 --- a/modules/spacelift/worker-pool/data.tf +++ b/modules/spacelift/worker-pool/data.tf @@ -34,6 +34,6 @@ data "aws_ami" "spacelift" { filter { name = "architecture" - values = ["x86_64"] + values = var.architecture } } diff --git a/modules/spacelift/worker-pool/main.tf b/modules/spacelift/worker-pool/main.tf index 320154565..7abbb3434 100644 --- a/modules/spacelift/worker-pool/main.tf +++ b/modules/spacelift/worker-pool/main.tf @@ -116,12 +116,11 @@ module "autoscale_group" { cpu_utilization_high_threshold_percent = var.cpu_utilization_high_threshold_percent cpu_utilization_low_threshold_percent = var.cpu_utilization_low_threshold_percent + max_instance_lifetime = var.instance_lifetime + # The instance refresh definition # If this block is configured, an Instance Refresh will be started when the Auto Scaling Group is updated - instance_refresh = var.instance_refresh - # Note: instance refresh settings are IGNORED unless template version is empty - # See the second "NOTE" in the "instance_refresh" documentation - # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#instance_refresh + instance_refresh = var.instance_refresh launch_template_version = var.instance_refresh == null ? "$Latest" : "" context = module.this.context diff --git a/modules/spacelift/worker-pool/variables.tf b/modules/spacelift/worker-pool/variables.tf index 7b6da34df..4ebd092c0 100644 --- a/modules/spacelift/worker-pool/variables.tf +++ b/modules/spacelift/worker-pool/variables.tf @@ -60,6 +60,12 @@ variable "cpu_utilization_low_threshold_percent" { description = "CPU utilization low threshold" } +variable "instance_lifetime" { + type = number + default = 1209600 + description = "Number of seconds after which the instance will be terminated. The default is set to 14 days." +} + variable "default_cooldown" { type = number description = "The amount of time, in seconds, after a scaling activity completes before another scaling activity can start" @@ -196,6 +202,12 @@ variable "custom_spacelift_ami" { default = false } +variable "architecture" { + type = list(string) + description = "OS architecture of the EC2 instance AMI" + default = ["x86_64"] +} + variable "spacelift_domain_name" { type = string description = "Top-level domain name to use for pulling the launcher binary" @@ -210,7 +222,7 @@ variable "iam_attributes" { variable "launch_template_version" { type = string - description = "Launch template version to use for workers" + description = "Launch template version to use for workers. Note that instance refresh settings are IGNORED unless template version is empty" default = "$Latest" } From a9252bc79d5fed90edec6ddb147bd61287de0620 Mon Sep 17 00:00:00 2001 From: "Erik Osterman (CEO @ Cloud Posse)" Date: Tue, 7 May 2024 15:54:37 -0500 Subject: [PATCH 405/501] Update GitHub Workflows to Fix ReviewDog TFLint Action (#1030) --- .github/workflows/feature-branch.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/feature-branch.yml b/.github/workflows/feature-branch.yml index 456793945..ebd8854f1 100644 --- a/.github/workflows/feature-branch.yml +++ b/.github/workflows/feature-branch.yml @@ -11,6 +11,7 @@ permissions: pull-requests: write id-token: write contents: write + issues: write jobs: terraform-module: From de476410e081437169110c95381a087d56e7cd07 Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 14 May 2024 13:35:17 -0700 Subject: [PATCH 406/501] [eks/cluster] Update to use AWS Auth API (#1033) --- .pre-commit-config.yaml | 3 + modules/eks/cluster/CHANGELOG.md | 219 ++++++++++- modules/eks/cluster/README.md | 348 ++++++++++-------- modules/eks/cluster/aws-sso.tf | 21 +- modules/eks/cluster/eks-node-groups.tf | 4 - modules/eks/cluster/main.tf | 160 ++++---- .../modules/node_group_by_region/variables.tf | 2 +- modules/eks/cluster/outputs.tf | 7 +- modules/eks/cluster/variables-deprecated.tf | 58 +++ modules/eks/cluster/variables.tf | 102 +++-- modules/eks/cluster/versions.tf | 9 + 11 files changed, 602 insertions(+), 331 deletions(-) create mode 100644 modules/eks/cluster/variables-deprecated.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bce86cb2a..f26946b9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,6 +44,9 @@ repos: name: prettier entry: prettier --write --prose-wrap always --print-width 120 types: ["markdown"] + # If prettier chokes on the output of the `terraform-docs` command for a file, don't + # exclude it here. Instead, wrap the terraform-docs comment in the README with + # `` and `` comments. exclude: | (?x)^( README.md | diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index cafcdb448..8351f297b 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,11 +1,211 @@ -## Components PR [#910](https://github.com/cloudposse/terraform-aws-components/pull/910) +## Breaking Changes: Components PR [#1033](https://github.com/cloudposse/terraform-aws-components/pull/1033) + +### Major Breaking Changes + +:::warning Major Breaking Changes, Manual Intervention Required + +This release includes a major breaking change that requires manual intervention to migrate existing clusters. The change +is necessary to support the new AWS Access Control API, which is more secure and more reliable than the old `aws-auth` +ConfigMap. + +::: + +This release drops support for the `aws-auth` ConfigMap and switches to managing access control with the new AWS Access +Control API. This change allows for more secure and reliable access control, and removes the requirement that Terraform +operations on the EKS cluster itself require network access to the EKS control plane. + +In this release, this component only supports assigning "team roles" to Kubernetes RBAC groups. Support for AWS EKS +Access Policies is not yet implemented. However, if you specify `system:masters` as a group, that will be translated +into assigning the `AmazonEKSClusterAdminPolicy` to the role. Any other `system:*` group will cause an error. + +:::tip Network Access Considerations + +Previously, this component required network access to the EKS control plane to manage the `aws-auth` ConfigMap. This +meant having the EKS control plane accessible from the public internet, or using a bastion host or VPN to access the +control plane. With the new AWS Access Control API, Terraform operations on the EKS cluster no longer require network +access to the EKS control plane. + +This may seem like it makes it easier to secure the EKS control plane, but Terraform users will still require network +access to the EKS control plane to manage any deployments or other Kubernetes resources in the cluster. This means that +this upgrade does not substantially change the need for network access. + +::: + +### Minor Changes + +With the fixes included and AWS Terraform Provider v5.43.0 and Karpenter v0.33.0, the +`legacy_do_not_create_karpenter_instance_profile` is now obsolete. After upgrading both this component and the +`eks/karpenter` component, if you had it in your configuration, you can remove it. If you had previously set it to +`false`, removing it may cause an error when you apply the changes. If you see an error about the +`aws_iam_instance_profile` resource being destroyed (cannot be destroyed because it is in use, has dependencies, and/or +has role attached), you can simply remove the resource from the Terraform state with `[atmos] terraform state rm`, +because it will be managed by the Karpenter controller instead of Terraform. + +### Access Control API Migration Procedure + +Full details of the migration process can be found in the `cloudposse/terraform-aws-eks-cluster` +[migration document](https://github.com/cloudposse/terraform-aws-eks-cluster/blob/main/docs/migration-v3-v4.md). This +section is a streamlined version for users of this `eks/cluster` component. + +:::important + +The commands below assume the component is named "eks/cluster". If you are using a different name, replace "eks/cluster" +with the correct component name. + +::: + +#### Prepare for Migration + +Make sure you have `kubectl` access to the cluster, preferably using the `aws eks get-token` command configured into +your `$KUBECONFIG` file. Geodesic users can usually set this up with + +```shell +atmos aws eks update-kubeconfig eks/cluster -s= +# or +set-cluster -- +``` + +Where `` is the "tenant" name, a.k.a. the "org" name, e.g. "core", and should be omitted (along with the hyphen) +if your organization does not use a tenant name. `` is the AWS region abbreviation your organization is using, +e.g. "usw2" or "uw2", and `` is the "stage" or "account" name, e.g. "auto" or "prod". + +Test your access with `kubectl` + +```shell +# check if you have any access at all. Should output "yes". +kubectl auth can-i -A create selfsubjectaccessreviews.authorization.k8s.io + +# Do you have full cluster administrator access? +kubectl auth can-i '*' '*' + +# Show me what I can and cannot do (if `rakkess` is installed) +rakkess + +``` + +#### Migrate + +1. Update the component (already done if you see this document). +2. Run `atmos terraform plan eks/cluster -s ` + +See this error: + +```plaintext +To work with module.eks_cluster.kubernetes_config_map.aws_auth_ignore_changes[0] (orphan) its original provider configuration +``` + +Note, in other documentation, the exact "address" of the orphaned resource may be different, and the documentation may +say to refer to the address of the resource in the error message. In this case, because we are using this component as +the root module, the address should be exactly as shown above. (Possibly ending with `aws_auth[0]` instead of +`aws_auth_ignore_changes[0]`.) + +3. Remove the orphaned resource from the state file with + +``` +atmos terraform state rm eks/cluster 'module.eks_cluster.kubernetes_config_map.aws_auth_ignore_changes[0]' -s +``` + +4. `atmos terraform plan eks/cluster -s ` + +Verify: + +- `module.eks_cluster.aws_eks_cluster.default[0]` will be updated in-place + - access_config.authentication_mode = "CONFIG_MAP" -> "API_AND_CONFIG_MAP" + +Stop and ask for help if you see `module.eks_cluster.aws_eks_cluster.default[0]` will be destroyed. Expect to see a lot +of IAM changes due to the potential for the EKS OIDC thumbprint to change, and a lot of `aws_eks_access_entry` +additions. You may also see: + +- `aws_security_group_rule` resources replaced by `aws_vpc_security_group_ingress_rule` resources +- `null_resource` resources destroyed + +5. Apply the plan with `atmos terraform apply eks/cluster -s --from-plan` + +**EXPECT AN ERROR**. Something like: + +```plaintext +│ Error: creating EKS Access Entry +(eg-core-usw2-auto-eks-cluster:arn:aws:iam::123456789012:role/eg-core-gbl-auto-terraform): operation error EKS: CreateAccessEntry, https response error StatusCode: 409, RequestID: 97a40994-4223-4af1-977e-42ec57eb3ad6, ResourceInUseException: The specified access entry resource is already in use on this cluster. +│ +│ with module.eks_cluster.aws_eks_access_entry.map["arn:aws:iam::123456789012:role/eg-core-gbl-auto-terraform"], +│ on .terraform/modules/eks_cluster/auth.tf line 60, in resource "aws_eks_access_entry" "map": +│ 60: resource "aws_eks_access_entry" "map" { +``` + +This is expected. The access entry is something we want to control, but a duplicate is automatically created by AWS +during the conversion. Import the created entry. You may get other errors, but they are likely transient and will be +fixed automatically after fixing this one. + +The `access entry ID` to import is given in the error message in parentheses. In the example above, the ID is +`eg-core-usw2-auto-eks-cluster:arn:aws:iam::123456789012:role/eg-core-gbl-auto-terraform`. + +The Terraform `resource address` for the resource will also be in the error message: it is the part after "with". In the +example above, the address is + +```plaintext +module.eks_cluster.aws_eks_access_entry.map["arn:aws:iam::123456789012:role/eg-core-gbl-auto-terraform"] +``` + +Import the resource with + +```bash +atmos terraform import eks/cluster '' '' -s +``` + +It is critical to use single quotes around the resource address and access entry ID to prevent the shell from +interpreting the square brackets and colons and to preserve the double quotes in the resource address. + +After successfully importing the resource, run + +``` +atmos terraform apply eks/cluster -s ` +``` + +to apply tags to the entry and finish up any changes interrupted by the error. It should apply cleanly this time. + +#### Verify + +Verify that you still have access to the cluster with `kubectl`, just as you did in the "Prepare" section. + +#### Cleanup + +Either one cluster at a time, or later in an organization-wide cleanup, migrate all clusters from `API_AND_CONFIG_MAP` +to `API` authentication mode. + +At this point you have both the old and new access control methods enabled, but nothing is managing the `aws-auth` +ConfigMap. The `aws-auth` ConfigMap has been abandoned by this module and will no longer have entries added or, +crucially, removed. In order to remove this lingering unmanaged grant of access, migrate the cluster to `API` +authentication mode, and manually remove the `aws-auth` ConfigMap. + +- Update the `access.config.authentication_mode` to "API" in your configuration: + + ```yaml + access_config: + authentication_mode: API + ``` + + and run `atmos terraform apply` again. This will cause EKS to ignore the `aws-auth` ConfigMap, but will not remove it. + Again, this will cause a lot of IAM changes due to the potential for the EKS OIDC thumbprint to change, but this is + not a problem. + +- Manually remove the `aws-auth` ConfigMap. You can do this with + `kubectl delete configmap aws-auth --namespace kube-system`. This will not affect the cluster, because it is now being + managed by the new access control API, but it will reduce the possibility of confusion in the future. + +### End of Access Control API Migration + +--- + +## Changes in `v1.349.0` + +Components PR [#910](https://github.com/cloudposse/terraform-aws-components/pull/910) Bug fix and updates to Changelog, no action required. Fixed: Error about managed node group ARNs list being null, which could happen when adding a managed node group to an existing cluster that never had one. -## Upgrading to `v1.303.0` +## Changes in `v1.303.0` Components PR [#852](https://github.com/cloudposse/terraform-aws-components/pull/852) @@ -20,13 +220,14 @@ single node, removing the high availability that comes from having a node per Av spread across those nodes. As a result, we now recommend deploying a minimal node group with a single instance (currently recommended to be a -`c6a.large`) in each of 3 Availability Zones. This will provide the compute power needed to initialize add-ons, and will -provide high availability for the cluster. As a bonus, it will also remove the need to deploy Karpenter to Fargate. - -**NOTE about instance type**: The `c6a.large` instance type is relatively new. If you have deployed an old version of -our ServiceControlPolicy `DenyEC2NonNitroInstances`, `DenyNonNitroInstances` (obsolete, replaced by -`DenyEC2NonNitroInstances`), and/or `DenyEC2InstancesWithoutEncryptionInTransit`, you will want to update them to -v0.12.0 or choose a difference instance type. +`c7a.medium`) in each of 3 Availability Zones. This will provide the compute power needed to initialize add-ons, and +will provide high availability for the cluster. As a bonus, it will also remove the need to deploy Karpenter to Fargate. + +**NOTE about instance type**: The `c7a.medium` instance type is relatively new. If you have deployed an old version of +our [ServiceControlPolicy](https://github.com/cloudposse/terraform-aws-service-control-policies) +`DenyEC2NonNitroInstances`, `DenyNonNitroInstances` (obsolete, replaced by `DenyEC2NonNitroInstances`), and/or +`DenyEC2InstancesWithoutEncryptionInTransit`, you will want to update them to v0.14.1 or choose a different instance +type. ### Migration procedure diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index d000e4bd5..182cade47 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -3,13 +3,12 @@ This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups and Fargate profiles. -:::warning +:::note Windows not supported -This component should only be deployed after logging into AWS via Federated login with SAML (e.g. GSuite) or assuming an -IAM role (e.g. from a CI/CD system). It should not be deployed if you log into AWS via AWS SSO, the reason being that on -initial deployment, the EKS cluster will be owned by the assumed role that provisioned it, and AWS SSO roles are -ephemeral (replaced on every configuration change). If this were to be the AWS SSO Role, then we risk losing access to -the EKS cluster once the ARN of the AWS SSO Role eventually changes. +This component has not been tested with Windows worker nodes of any launch type. Although upstream modules support +Windows nodes, there are likely issues around incorrect or insufficient IAM permissions or other configuration that +would need to be resolved for this component to properly configure the upstream modules for Windows nodes. If you need +Windows nodes, please experiment and be on the lookout for issues, and then report any issues to Cloud Posse. ::: @@ -30,15 +29,96 @@ For more on these requirements, see [GitHub OIDC component](https://docs.cloudposse.com/components/catalog/aws/github-oidc-provider/), and the [Karpenter component](https://docs.cloudposse.com/components/catalog/aws/eks/karpenter/). +### Mixin pattern for Kubernetes version + +We recommend separating out the Kubernetes and related addons versions into a separate mixin (one per Kubernetes minor +version), to make it easier to run different versions in different environments, for example while testing a new +version. + +We also recommend leaving "resolve conflicts" settings unset and therefore using the default "OVERWRITE" setting because +any custom configuration that you would want to preserve should be managed by Terraform configuring the add-ons +directly. + +For example, create `catalog/eks/cluster/mixins/k8s-1-29.yaml` with the following content: + ```yaml +components: + terraform: + eks/cluster: + vars: + cluster_kubernetes_version: "1.29" + + # You can set all the add-on versions to `null` to use the latest version, + # but that introduces drift as new versions are released. As usual, we recommend + # pinning the versions to a specific version and upgrading when convenient. + + # Determine the latest version of the EKS add-ons for the specified Kubernetes version + # EKS_K8S_VERSION=1.29 # replace with your cluster version + # ADD_ON=vpc-cni # replace with the add-on name + # echo "${ADD_ON}:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name $ADD_ON \ + # --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table + + # To see versions for all the add-ons, wrap the above command in a for loop: + # for ADD_ON in vpc-cni kube-proxy coredns aws-ebs-csi-driver aws-efs-csi-driver; do + # echo "${ADD_ON}:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name $ADD_ON \ + # --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table + # done + + # To see the custom configuration schema for an add-on, run the following command: + # aws eks describe-addon-configuration --addon-name aws-ebs-csi-driver \ + # --addon-version v1.20.0-eksbuild.1 | jq '.configurationSchema | fromjson' + # See the `coredns` configuration below for an example of how to set a custom configuration. + + # https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html + # https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on + addons: + # https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html + # https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html + # https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role + # https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/#deploy-vpc-cni-managed-add-on + vpc-cni: + addon_version: "v1.16.0-eksbuild.1" # set `addon_version` to `null` to use the latest version + # https://docs.aws.amazon.com/eks/latest/userguide/managing-kube-proxy.html + kube-proxy: + addon_version: "v1.29.0-eksbuild.1" # set `addon_version` to `null` to use the latest version + # https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html + coredns: + addon_version: "v1.11.1-eksbuild.4" # set `addon_version` to `null` to use the latest version + ## override default replica count of 2. In very large clusters, you may want to increase this. + configuration_values: '{"replicaCount": 3}' + + # https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html + # https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons + # https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role + # https://github.com/kubernetes-sigs/aws-ebs-csi-driver + aws-ebs-csi-driver: + addon_version: "v1.27.0-eksbuild.1" # set `addon_version` to `null` to use the latest version + # If you are not using [volume snapshots](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/#how-to-use-volume-snapshots) + # (and you probably are not), disable the EBS Snapshotter + # See https://github.com/aws/containers-roadmap/issues/1919 + configuration_values: '{"sidecars":{"snapshotter":{"forceEnable":false}}}' + + aws-efs-csi-driver: + addon_version: "v1.7.7-eksbuild.1" # set `addon_version` to `null` to use the latest version + # Set a short timeout in case of conflict with an existing efs-controller deployment + create_timeout: "7m" +``` + +### Common settings for all Kubernetes versions + +In your main stack configuration, you can then set the Kubernetes version by importing the appropriate mixin: + +```yaml +# +import: + - catalog/eks/cluster/mixins/k8s-1-29 + components: terraform: eks/cluster: vars: enabled: true name: eks - cluster_kubernetes_version: "1.27" - vpc_component_name: "vpc" eks_component_name: "eks/cluster" @@ -73,24 +153,73 @@ components: github_actions_allowed_repos: - acme/infra - # We use karpenter to provision nodes - # See below for using node_groups - managed_node_groups_enabled: false - node_groups: {} - - # EKS IAM Authentication settings - # By default, you can authenticate to EKS cluster only by assuming the role that created the cluster. - # After the Auth Config Map is applied, the other IAM roles in - # `primary_iam_roles`, `delegated_iam_roles`, and `sso_iam_roles` will be able to authenticate. - apply_config_map_aws_auth: true + # We recommend, at a minimum, deploying 1 managed node group, + # with the same number of instances as availability zones (typically 3). + managed_node_groups_enabled: true + node_groups: # for most attributes, setting null here means use setting from node_group_defaults + main: + # availability_zones = null will create one autoscaling group + # in every private subnet in the VPC + availability_zones: null + + # Tune the desired and minimum group size according to your baseload requirements. + # We recommend no autoscaling for the main node group, so it will + # stay at the specified desired group size, with additional + # capacity provided by Karpenter. Nevertheless, we recommend + # deploying enough capacity in the node group to handle your + # baseload requirements, and in production, we recommend you + # have a large enough node group to handle 3/2 (1.5) times your + # baseload requirements, to handle the loss of a single AZ. + desired_group_size: 3 # number of instances to start with, should be >= number of AZs + min_group_size: 3 # must be >= number of AZs + max_group_size: 3 + + # Can only set one of ami_release_version or kubernetes_version + # Leave both null to use latest AMI for Cluster Kubernetes version + kubernetes_version: null # use cluster Kubernetes version + ami_release_version: null # use latest AMI for Kubernetes version + + attributes: [] + create_before_destroy: true + cluster_autoscaler_enabled: true + instance_types: + # Tune the instance type according to your baseload requirements. + - c7a.medium + ami_type: AL2_x86_64 # use "AL2_x86_64" for standard instances, "AL2_x86_64_GPU" for GPU instances + block_device_map: + # EBS volume for local ephemeral storage + # IGNORED if legacy `disk_encryption_enabled` or `disk_size` are set! + # Use "/dev/xvda" for most of the instances (without local NVMe) + # using most of the Linuxes, "/dev/xvdb" for BottleRocket + "/dev/xvda": + ebs: + volume_size: 100 # number of GB + volume_type: gp3 + + kubernetes_labels: {} + kubernetes_taints: {} + resources_to_tag: + - instance + - volume + tags: null + + # The abbreviation method used for Availability Zones in your project. + # Used for naming resources in managed node groups. + # Either "short" or "fixed". availability_zone_abbreviation_type: fixed + cluster_private_subnets_only: true cluster_encryption_config_enabled: true cluster_endpoint_private_access: true cluster_endpoint_public_access: false cluster_log_retention_period: 90 - # List of `aws-teams-roles` (in the account where the EKS cluster is deployed) to map to Kubernetes RBAC groups + # List of `aws-team-roles` (in the account where the EKS cluster is deployed) to map to Kubernetes RBAC groups + # You cannot set `system:*` groups here, except for `system:masters`. + # The `idp:*` roles referenced here are created by the `eks/idp-roles` component. + # While set here, the `idp:*` roles will have no effect until after + # the `eks/idp-roles` component is applied, which must be after the + # `eks/cluster` component is deployed. aws_team_roles_rbac: - aws_team_role: admin groups: @@ -98,15 +227,12 @@ components: - aws_team_role: poweruser groups: - idp:poweruser - - system:authenticated - aws_team_role: observer groups: - idp:observer - - system:authenticated - aws_team_role: planner groups: - idp:observer - - system:authenticated - aws_team: terraform groups: - system:masters @@ -117,53 +243,15 @@ components: - aws_sso_permission_set: PowerUserAccess groups: - idp:poweruser - - system:authenticated - # Fargate Profiles for Karpenter - fargate_profiles: - karpenter: - kubernetes_namespace: karpenter - kubernetes_labels: null + # Set to false if you are not using Karpenter karpenter_iam_role_enabled: true - # If you are using Karpenter, disable the legacy instance profile created by the eks/karpenter component - # and use the one created by this component instead by setting the legacy flags to false in both components. - # This is recommended for all new clusters. - legacy_do_not_create_karpenter_instance_profile: false + # All Fargate Profiles will use the same IAM Role when `legacy_fargate_1_role_per_profile_enabled` is set to false. # Recommended for all new clusters, but will damage existing clusters provisioned with the legacy component. legacy_fargate_1_role_per_profile_enabled: false # While it is possible to deploy add-ons to Fargate Profiles, it is not recommended. Use a managed node group instead. deploy_addons_to_fargate: false - - # EKS addons - # https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html - # Configuring EKS addons: https://aws.amazon.com/blogs/containers/amazon-eks-add-ons-advanced-configuration/ - addons: - # https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html - vpc-cni: - addon_version: v1.13.4-eksbuild.1 # set `addon_version` to `null` to use the latest version - # https://docs.aws.amazon.com/eks/latest/userguide/managing-kube-proxy.html - kube-proxy: - addon_version: "v1.27.1-eksbuild.1" # set `addon_version` to `null` to use the latest version - # https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html - coredns: - addon_version: "v1.10.1-eksbuild.1" # set `addon_version` to `null` to use the latest version - # https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons - # https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html - # https://github.com/kubernetes-sigs/aws-ebs-csi-driver - aws-ebs-csi-driver: - addon_version: "v1.20.0-eksbuild.1" # set `addon_version` to `null` to use the latest version - # If you are not using [volume snapshots](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/#how-to-use-volume-snapshots) - # (and you probably are not), disable the EBS Snapshotter with: - configuration_values: '{"sidecars":{"snapshotter":{"forceEnable":false}}}' - # Only install the EFS driver if you are using EFS. - # Create an EFS file system with the `efs` component. - # Create an EFS StorageClass with the `eks/storage-class` component. - # https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html - aws-efs-csi-driver: - addon_version: "v1.5.8-eksbuild.1" - # Set a short timeout in case of conflict with an existing efs-controller deployment - create_timeout: "7m" ``` ### Amazon EKS End-of-Life Dates @@ -171,81 +259,28 @@ components: When picking a Kubernetes version, be sure to review the [end-of-life dates for Amazon EKS](https://endoflife.date/amazon-eks). Refer to the chart below: -| cycle | release | latest | latest release | eol | -| :---- | :--------: | :---------- | :------------: | :--------: | -| 1.28 | 2023-09-26 | 1.28-eks-1 | 2023-09-26 | 2024-11-01 | -| 1.27 | 2023-05-24 | 1.27-eks-5 | 2023-08-30 | 2024-07-01 | -| 1.26 | 2023-04-11 | 1.26-eks-6 | 2023-08-30 | 2024-06-01 | -| 1.25 | 2023-02-21 | 1.25-eks-7 | 2023-08-30 | 2024-05-01 | -| 1.24 | 2022-11-15 | 1.24-eks-10 | 2023-08-30 | 2024-01-31 | -| 1.23 | 2022-08-11 | 1.23-eks-12 | 2023-08-30 | 2023-10-11 | -| 1.22 | 2022-04-04 | 1.22-eks-14 | 2023-06-30 | 2023-06-04 | -| 1.21 | 2021-07-19 | 1.21-eks-18 | 2023-06-09 | 2023-02-15 | -| 1.20 | 2021-05-18 | 1.20-eks-14 | 2023-05-05 | 2022-11-01 | -| 1.19 | 2021-02-16 | 1.19-eks-11 | 2022-08-15 | 2022-08-01 | -| 1.18 | 2020-10-13 | 1.18-eks-13 | 2022-08-15 | 2022-08-15 | - -\*This Chart was updated as of 10/16/2023 and is generated with -[the `eol` tool](https://github.com/hugovk/norwegianblue). Check the latest updates by running `eol amazon-eks` locally -or [on the website directly](https://endoflife.date/amazon-eks). +| cycle | release | latest | latest release | eol | extended support | +| :---- | :--------: | :---------- | :------------: | :--------: | :--------------: | +| 1.29 | 2024-01-23 | 1.29-eks-6 | 2024-04-18 | 2025-03-23 | 2026-03-23 | +| 1.28 | 2023-09-26 | 1.28-eks-12 | 2024-04-18 | 2024-11-26 | 2025-11-26 | +| 1.27 | 2023-05-24 | 1.27-eks-16 | 2024-04-18 | 2024-07-24 | 2025-07-24 | +| 1.26 | 2023-04-11 | 1.26-eks-17 | 2024-04-18 | 2024-06-11 | 2025-06-11 | +| 1.25 | 2023-02-21 | 1.25-eks-18 | 2024-04-18 | 2024-05-01 | 2025-05-01 | +| 1.24 | 2022-11-15 | 1.24-eks-21 | 2024-04-18 | 2024-01-31 | 2025-01-31 | +| 1.23 | 2022-08-11 | 1.23-eks-23 | 2024-04-18 | 2023-10-11 | 2024-10-11 | +| 1.22 | 2022-04-04 | 1.22-eks-14 | 2023-06-30 | 2023-06-04 | 2024-09-01 | +| 1.21 | 2021-07-19 | 1.21-eks-18 | 2023-06-09 | 2023-02-16 | 2024-07-15 | +| 1.20 | 2021-05-18 | 1.20-eks-14 | 2023-05-05 | 2022-11-01 | False | +| 1.19 | 2021-02-16 | 1.19-eks-11 | 2022-08-15 | 2022-08-01 | False | +| 1.18 | 2020-10-13 | 1.18-eks-13 | 2022-08-15 | 2022-08-15 | False | + +\* This Chart was generated 2024-05-12 with [the `eol` tool](https://github.com/hugovk/norwegianblue). Install it with +`python3 -m pip install --upgrade norwegianblue` and create a new table by running `eol --md amazon-eks` locally, or +view the information by visiting [the endoflife website](https://endoflife.date/amazon-eks). You can also view the release and support timeline for [the Kubernetes project itself](https://endoflife.date/kubernetes). -### Usage with Node Groups - -The `eks/cluster` component also supports managed Node Groups. In order to add a set of nodes to provision with the -cluster, provide values for `var.managed_node_groups_enabled` and `var.node_groups`. - -:::info - -You can use managed Node Groups in conjunction with Karpenter. We recommend provisioning a managed node group with as -many nodes as Availability Zones used by your cluster (typically 3), to ensure a minimum support for a high-availability -set of daemons, and then using Karpenter to provision additional nodes as needed. - -::: - -For example: - -```yaml -managed_node_groups_enabled: true -node_groups: # for most attributes, setting null here means use setting from node_group_defaults - main: - # availability_zones = null will create one autoscaling group - # in every private subnet in the VPC - availability_zones: null - - desired_group_size: 3 # number of instances to start with, must be >= number of AZs - min_group_size: 3 # must be >= number of AZs - max_group_size: 6 - - # Can only set one of ami_release_version or kubernetes_version - # Leave both null to use latest AMI for Cluster Kubernetes version - kubernetes_version: null # use cluster Kubernetes version - ami_release_version: null # use latest AMI for Kubernetes version - - attributes: [] - create_before_destroy: true - cluster_autoscaler_enabled: true - instance_types: - - t3.medium - ami_type: AL2_x86_64 # use "AL2_x86_64" for standard instances, "AL2_x86_64_GPU" for GPU instances - block_device_map: - # EBS volume for local ephemeral storage - # IGNORED if legacy `disk_encryption_enabled` or `disk_size` are set! - # "/dev/xvda" most of the instances (without local NVMe) and most of the Linuxes, "/dev/xvdb" BottleRocket - "/dev/xvda": - ebs: - volume_size: 100 # number of GB - volume_type: gp3 - kubernetes_labels: {} - kubernetes_taints: {} - resources_to_tag: - - instance - - volume - tags: null -``` - ### Using Addons EKS clusters support “Addons” that can be automatically installed on a cluster. Install these addons with the @@ -261,20 +296,20 @@ details. ::: ```shell -EKS_K8S_VERSION=1.27 # replace with your cluster version +EKS_K8S_VERSION=1.29 # replace with your cluster version aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION \ --query 'addons[].{MarketplaceProductUrl: marketplaceInformation.productUrl, Name: addonName, Owner: owner Publisher: publisher, Type: type}' --output table ``` :::info -You can see which versions are available for each addon by executing the following commands. Replace 1.27 with the +You can see which versions are available for each addon by executing the following commands. Replace 1.29 with the version of your cluster. ::: ```shell -EKS_K8S_VERSION=1.27 # replace with your cluster version +EKS_K8S_VERSION=1.29 # replace with your cluster version echo "vpc-cni:" && aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION --addon-name vpc-cni \ --query 'addons[].addonVersions[].{Version: addonVersion, Defaultversion: compatibilities[0].defaultVersion}' --output table @@ -327,8 +362,8 @@ addons: # https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html coredns: addon_version: "v1.9.3-eksbuild.2" # set `addon_version` to `null` to use the latest version - # Uncomment to override default replica count of 2 - # configuration_values: '{"replicaCount": 3}' + # Override default replica count of 2, to have one in each AZ + configuration_values: '{"replicaCount": 3}' # https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html # https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons # https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role @@ -353,8 +388,12 @@ addons: :::warning -Addons may not be suitable for all use-cases! For example, if you are using Karpenter to provision nodes, these nodes -will never be available before the cluster component is deployed. +Addons may not be suitable for all use-cases! For example, if you are deploying Karpenter to Fargate and using Karpenter +to provision all nodes, these nodes will never be available before the cluster component is deployed if you are using +the CoreDNS addon (for example). + +This is one of the reasons we recommend deploying a managed node group: to ensure that the addons will become fully +functional during deployment of the cluster. ::: @@ -363,7 +402,8 @@ For more information on upgrading EKS Addons, see ### Adding and Configuring a new EKS Addon -Add a new EKS addon to the `addons` map (`addons` variable): +The component already supports all the EKS addons shown in the configurations above. To add a new EKS addon, not +supported by the cluster, add it to the `addons` map (`addons` variable): ```yaml addons: @@ -373,7 +413,7 @@ addons: If the new addon requires an EKS IAM Role for Kubernetes Service Account, perform the following steps: -- Add a file `addons-custom.tf` to the `eks/cluster` folder +- Add a file `addons-custom.tf` to the `eks/cluster` folder if not already present - In the file, add an IAM policy document with the permissions required for the addon, and use the `eks-iam-role` module to provision an IAM Role for Kubernetes Service Account for the addon: @@ -407,9 +447,9 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor } ``` - For reference on how to configure the IAM role and IAM permissions for EKS addons, see [addons.tf](addons.tf). + For examples of how to configure the IAM role and IAM permissions for EKS addons, see [addons.tf](addons.tf). -- Add a file `additional-addon-support_override.tf` to the `eks/cluster` folder +- Add a file `additional-addon-support_override.tf` to the `eks/cluster` folder if not already present - In the file, add the IAM Role for Kubernetes Service Account for the addon to the `overridable_additional_addon_service_account_role_arn_map` map: @@ -455,7 +495,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [aws\_efs\_csi\_driver\_eks\_iam\_role](#module\_aws\_efs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.1 | | [coredns\_fargate\_profile](#module\_coredns\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 3.0.0 | +| [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 4.1.0 | | [fargate\_pod\_execution\_role](#module\_fargate\_pod\_execution\_role) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [iam\_arns](#module\_iam\_arns) | ../../account-map/modules/roles-to-principals | n/a | @@ -494,18 +534,18 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [access\_config](#input\_access\_config) | Access configuration for the EKS cluster |
object({
authentication_mode = optional(string, "API")
bootstrap_cluster_creator_admin_permissions = optional(bool, false)
})
| `{}` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [addons](#input\_addons) | Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources |
map(object({
enabled = optional(bool, true)
addon_version = optional(string, null)
# configuration_values is a JSON string, such as '{"computeType": "Fargate"}'.
configuration_values = optional(string, null)
# Set default resolve_conflicts to OVERWRITE because it is required on initial installation of
# add-ons that have self-managed versions installed by default (e.g. vpc-cni, coredns), and
# because any custom configuration that you would want to preserve should be managed by Terraform.
resolve_conflicts_on_create = optional(string, "OVERWRITE")
resolve_conflicts_on_update = optional(string, "OVERWRITE")
service_account_role_arn = optional(string, null)
create_timeout = optional(string, null)
update_timeout = optional(string, null)
delete_timeout = optional(string, null)
}))
| `{}` | no | | [addons\_depends\_on](#input\_addons\_depends\_on) | If set `true` (recommended), all addons will depend on managed node groups provisioned by this component and therefore not be installed until nodes are provisioned.
See [issue #170](https://github.com/cloudposse/terraform-aws-eks-cluster/issues/170) for more details. | `bool` | `true` | no | | [allow\_ingress\_from\_vpc\_accounts](#input\_allow\_ingress\_from\_vpc\_accounts) | List of account contexts to pull VPC ingress CIDR and add to cluster security group.

e.g.

{
environment = "ue2",
stage = "auto",
tenant = "core"
} | `any` | `[]` | no | | [allowed\_cidr\_blocks](#input\_allowed\_cidr\_blocks) | List of CIDR blocks to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | | [allowed\_security\_groups](#input\_allowed\_security\_groups) | List of Security Group IDs to be allowed to connect to the EKS cluster | `list(string)` | `[]` | no | -| [apply\_config\_map\_aws\_auth](#input\_apply\_config\_map\_aws\_auth) | Whether to execute `kubectl apply` to apply the ConfigMap to allow worker nodes to join the EKS cluster | `bool` | `true` | no | +| [apply\_config\_map\_aws\_auth](#input\_apply\_config\_map\_aws\_auth) | (Obsolete) Whether to execute `kubectl apply` to apply the ConfigMap to allow worker nodes to join the EKS cluster.
This input is included to avoid breaking existing configurations that set it to `true`;
a value of `false` is no longer allowed.
This input is obsolete and will be removed in a future release. | `bool` | `true` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [availability\_zone\_abbreviation\_type](#input\_availability\_zone\_abbreviation\_type) | Type of Availability Zone abbreviation (either `fixed` or `short`) to use in names. See https://github.com/cloudposse/terraform-aws-utils for details. | `string` | `"fixed"` | no | | [availability\_zone\_ids](#input\_availability\_zone\_ids) | List of Availability Zones IDs where subnets will be created. Overrides `availability_zones`.
Can be the full name, e.g. `use1-az1`, or just the part after the AZ ID region code, e.g. `-az1`,
to allow reusable values across regions. Consider contention for resources and spot pricing in each AZ when selecting.
Useful in some regions when using only some AZs and you want to use the same ones across multiple accounts. | `list(string)` | `[]` | no | | [availability\_zones](#input\_availability\_zones) | AWS Availability Zones in which to deploy multi-AZ resources.
Ignored if `availability_zone_ids` is set.
Can be the full name, e.g. `us-east-1a`, or just the part after the region, e.g. `a` to allow reusable values across regions.
If not provided, resources will be provisioned in every zone with a private subnet in the VPC. | `list(string)` | `[]` | no | -| [aws\_auth\_yaml\_strip\_quotes](#input\_aws\_auth\_yaml\_strip\_quotes) | If true, remove double quotes from the generated aws-auth ConfigMap YAML to reduce spurious diffs in plans | `bool` | `true` | no | | [aws\_ssm\_agent\_enabled](#input\_aws\_ssm\_agent\_enabled) | Set true to attach the required IAM policy for AWS SSM agent to each EC2 instance's IAM Role | `bool` | `false` | no | | [aws\_sso\_permission\_sets\_rbac](#input\_aws\_sso\_permission\_sets\_rbac) | (Not Recommended): AWS SSO (IAM Identity Center) permission sets in the EKS deployment account to add to `aws-auth` ConfigMap.
Unfortunately, `aws-auth` ConfigMap does not support SSO permission sets, so we map the generated
IAM Role ARN corresponding to the permission set at the time Terraform runs. This is subject to change
when any changes are made to the AWS SSO configuration, invalidating the mapping, and requiring a
`terraform apply` in this project to update the `aws-auth` ConfigMap and restore access. |
list(object({
aws_sso_permission_set = string
groups = list(string)
}))
| `[]` | no | | [aws\_team\_roles\_rbac](#input\_aws\_team\_roles\_rbac) | List of `aws-team-roles` (in the target AWS account) to map to Kubernetes RBAC groups. |
list(object({
aws_team_role = string
groups = list(string)
}))
| `[]` | no | @@ -534,20 +574,17 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [fargate\_profiles](#input\_fargate\_profiles) | Fargate Profiles config |
map(object({
kubernetes_namespace = string
kubernetes_labels = map(string)
}))
| `{}` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [karpenter\_iam\_role\_enabled](#input\_karpenter\_iam\_role\_enabled) | Flag to enable/disable creation of IAM role for EC2 Instance Profile that is attached to the nodes launched by Karpenter | `bool` | `false` | no | -| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use. Defaults to the current caller's role. | `string` | `null` | no | -| [kubeconfig\_file](#input\_kubeconfig\_file) | Name of `kubeconfig` file to use to configure Kubernetes provider | `string` | `""` | no | -| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | Set true to configure Kubernetes provider with a `kubeconfig` file specified by `kubeconfig_file`.
Mainly for when the standard configuration produces a Terraform error. | `bool` | `false` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [legacy\_do\_not\_create\_karpenter\_instance\_profile](#input\_legacy\_do\_not\_create\_karpenter\_instance\_profile) | When `true` (the default), suppresses creation of the IAM Instance Profile
for nodes launched by Karpenter, to preserve the legacy behavior of
the `eks/karpenter` component creating it.
Set to `false` to enable creation of the IAM Instance Profile, which
ensures that both the role and the instance profile have the same lifecycle,
and avoids AWS Provider issue [#32671](https://github.com/hashicorp/terraform-provider-aws/issues/32671).
Use in conjunction with `eks/karpenter` component `legacy_create_karpenter_instance_profile`. | `bool` | `true` | no | +| [legacy\_do\_not\_create\_karpenter\_instance\_profile](#input\_legacy\_do\_not\_create\_karpenter\_instance\_profile) | **Obsolete:** The issues this was meant to mitigate were fixed in AWS Terraform Provider v5.43.0
and Karpenter v0.33.0. This variable will be removed in a future release.
Remove this input from your configuration and leave it at default.
**Old description:** When `true` (the default), suppresses creation of the IAM Instance Profile
for nodes launched by Karpenter, to preserve the legacy behavior of
the `eks/karpenter` component creating it.
Set to `false` to enable creation of the IAM Instance Profile, which
ensures that both the role and the instance profile have the same lifecycle,
and avoids AWS Provider issue [#32671](https://github.com/hashicorp/terraform-provider-aws/issues/32671).
Use in conjunction with `eks/karpenter` component `legacy_create_karpenter_instance_profile`. | `bool` | `true` | no | | [legacy\_fargate\_1\_role\_per\_profile\_enabled](#input\_legacy\_fargate\_1\_role\_per\_profile\_enabled) | Set to `false` for new clusters to create a single Fargate Pod Execution role for the cluster.
Set to `true` for existing clusters to preserve the old behavior of creating
a Fargate Pod Execution role for each Fargate Profile. | `bool` | `true` | no | | [managed\_node\_groups\_enabled](#input\_managed\_node\_groups\_enabled) | Set false to prevent the creation of EKS managed node groups. | `bool` | `true` | no | -| [map\_additional\_aws\_accounts](#input\_map\_additional\_aws\_accounts) | Additional AWS account numbers to add to `aws-auth` ConfigMap | `list(string)` | `[]` | no | -| [map\_additional\_iam\_roles](#input\_map\_additional\_iam\_roles) | Additional IAM roles to add to `config-map-aws-auth` ConfigMap |
list(object({
rolearn = string
username = string
groups = list(string)
}))
| `[]` | no | -| [map\_additional\_iam\_users](#input\_map\_additional\_iam\_users) | Additional IAM users to add to `aws-auth` ConfigMap |
list(object({
userarn = string
username = string
groups = list(string)
}))
| `[]` | no | -| [map\_additional\_worker\_roles](#input\_map\_additional\_worker\_roles) | AWS IAM Role ARNs of worker nodes to add to `aws-auth` ConfigMap | `list(string)` | `[]` | no | +| [map\_additional\_aws\_accounts](#input\_map\_additional\_aws\_accounts) | (Obsolete) Additional AWS accounts to grant access to the EKS cluster.
This input is included to avoid breaking existing configurations that
supplied an empty list, but the list is no longer allowed to have entries.
(It is not clear that it worked properly in earlier versions in any case.)
This component now only supports EKS access entries, which require full principal ARNs.
This input is deprecated and will be removed in a future release. | `list(string)` | `[]` | no | +| [map\_additional\_iam\_roles](#input\_map\_additional\_iam\_roles) | Additional IAM roles to grant access to the cluster.
*WARNING*: Full Role ARN, including path, is required for `rolearn`.
In earlier versions (with `aws-auth` ConfigMap), only the path
had to be removed from the Role ARN. The path is now required.
`username` is now ignored. This input is planned to be replaced
in a future release with a more flexible input structure that consolidates
`map_additional_iam_roles` and `map_additional_iam_users`. |
list(object({
rolearn = string
username = optional(string)
groups = list(string)
}))
| `[]` | no | +| [map\_additional\_iam\_users](#input\_map\_additional\_iam\_users) | Additional IAM roles to grant access to the cluster.
`username` is now ignored. This input is planned to be replaced
in a future release with a more flexible input structure that consolidates
`map_additional_iam_roles` and `map_additional_iam_users`. |
list(object({
userarn = string
username = optional(string)
groups = list(string)
}))
| `[]` | no | +| [map\_additional\_worker\_roles](#input\_map\_additional\_worker\_roles) | (Deprecated) AWS IAM Role ARNs of unmanaged Linux worker nodes to grant access to the EKS cluster.
In earlier versions, this could be used to grant access to worker nodes of any type
that were not managed by the EKS cluster. Now EKS requires that unmanaged worker nodes
be classified as Linux or Windows servers, in this input is temporarily retained
with the assumption that all worker nodes are Linux servers. (It is likely that
earlier versions did not work properly with Windows worker nodes anyway.)
This input is deprecated and will be removed in a future release.
In the future, this component will either have a way to separate Linux and Windows worker nodes,
or drop support for unmanaged worker nodes entirely. | `list(string)` | `[]` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = optional(string, null)
ami_type = optional(string, null)
attributes = optional(list(string), null)
availability_zones = optional(list(string)) # set to null to use var.availability_zones
cluster_autoscaler_enabled = optional(bool, null)
create_before_destroy = optional(bool, null)
desired_group_size = optional(number, null)
instance_types = optional(list(string), null)
kubernetes_labels = optional(map(string), {})
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), [])
kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version
max_group_size = optional(number, null)
min_group_size = optional(number, null)
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 50) # disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)
})
|
{
"block_device_map": {
"/dev/xvda": {
"ebs": {
"encrypted": true,
"volume_size": 20,
"volume_type": "gp2"
}
}
},
"desired_group_size": 1,
"instance_types": [
"t3.medium"
],
"kubernetes_version": null,
"max_group_size": 100
}
| no | @@ -567,6 +604,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | Name | Description | |------|-------------| | [availability\_zones](#output\_availability\_zones) | Availability Zones in which the cluster is provisioned | +| [eks\_addons\_versions](#output\_eks\_addons\_versions) | Map of enabled EKS Addons names and versions | | [eks\_auth\_worker\_roles](#output\_eks\_auth\_worker\_roles) | List of worker IAM roles that were included in the `auth-map` ConfigMap. | | [eks\_cluster\_arn](#output\_eks\_cluster\_arn) | The Amazon Resource Name (ARN) of the cluster | | [eks\_cluster\_certificate\_authority\_data](#output\_eks\_cluster\_certificate\_authority\_data) | The Kubernetes cluster certificate authority data | diff --git a/modules/eks/cluster/aws-sso.tf b/modules/eks/cluster/aws-sso.tf index f13e575c2..48a398b0f 100644 --- a/modules/eks/cluster/aws-sso.tf +++ b/modules/eks/cluster/aws-sso.tf @@ -3,22 +3,11 @@ locals { - # EKS does not accept the actual role ARN of the permission set, - # but instead requires the ARN of the role with the path prefix removed. - # Unfortunately, the path prefix is not always the same. - # Usually it is only "/aws-reserved/sso.amazonaws.com/" - # but sometimes it includes a region, like "/aws-reserved/sso.amazonaws.com/ap-southeast-1/" - # Adapted from https://registry.terraform.io/providers/hashicorp/aws/3.75.1/docs/data-sources/iam_roles#role-arns-with-paths-removed - aws_sso_permission_set_to_eks_role_arn_map = { for k, v in data.aws_iam_roles.sso_roles : k => [ - for parts in [split("/", one(v.arns[*]))] : - format("%s/%s", parts[0], element(parts, length(parts) - 1)) - ][0] } - - aws_sso_iam_roles_auth = [for role in var.aws_sso_permission_sets_rbac : { - rolearn = local.aws_sso_permission_set_to_eks_role_arn_map[role.aws_sso_permission_set] - username = format("%s-%s", local.this_account_name, role.aws_sso_permission_set) - groups = role.groups - }] + aws_sso_access_entry_map = { + for role in var.aws_sso_permission_sets_rbac : data.aws_iam_roles.sso_roles[role.aws_sso_permission_set] => { + kubernetes_groups = role.groups + } + } } data "aws_iam_roles" "sso_roles" { diff --git a/modules/eks/cluster/eks-node-groups.tf b/modules/eks/cluster/eks-node-groups.tf index c22fe2484..997801d1c 100644 --- a/modules/eks/cluster/eks-node-groups.tf +++ b/modules/eks/cluster/eks-node-groups.tf @@ -52,10 +52,6 @@ module "region_node_group" { vpc_id = local.vpc_id block_device_map = lookup(local.legacy_converted_block_device_map, each.key, local.block_device_map_w_defaults[each.key]) - - # See "Ensure ordering of resource creation" comment above for explanation - # of "module_depends_on" - module_depends_on = module.eks_cluster.kubernetes_config_map_id } : null context = module.this.context diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index ebe8466b2..b86aba520 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -10,48 +10,72 @@ locals { role_map = { (local.this_account_name) = var.aws_team_roles_rbac[*].aws_team_role } aws_team_roles_auth = [for role in var.aws_team_roles_rbac : { - rolearn = module.iam_arns.principals_map[local.this_account_name][role.aws_team_role] - username = format("%s-%s", local.this_account_name, role.aws_team_role) - groups = role.groups + rolearn = module.iam_arns.principals_map[local.this_account_name][role.aws_team_role] + groups = role.groups }] - # Existing Fargate Profile role ARNs - fargate_profile_role_arns = local.eks_outputs.fargate_profile_role_arns - - map_fargate_profile_roles = [ - for role_arn in local.fargate_profile_role_arns : { - rolearn : role_arn - username : "system:node:{{SessionName}}" - groups : [ - "system:bootstrappers", - "system:nodes", - # `system:node-proxier` is required by Fargate (and it's added automatically to the `aws-auth` ConfigMap when a Fargate Profile gets created, so we need to add it back) - # Allows access to the resources required by the `kube-proxy` component - # https://kubernetes.io/docs/reference/access-authn-authz/rbac/ - "system:node-proxier" - ] + aws_team_roles_access_entry_map = { + for role in local.aws_team_roles_auth : role.rolearn => { + kubernetes_groups = role.groups } - ] + } - map_additional_iam_roles = concat( - local.aws_team_roles_auth, - local.aws_sso_iam_roles_auth, - var.map_additional_iam_roles, - local.map_fargate_profile_roles, - ) + ## For future reference, as we enhance support for EKS Policies + ## and namespace limits, here are some examples of entries: + # access_entry_map = { + # "arn:aws:iam:::role/prefix-admin" = { + # access_policy_associations = { + # ClusterAdmin = {} + # } + # } + # "arn:aws:iam:::role/prefix-observer" = { + # kubernetes_groups = ["view"] + # } + # } + # + # access_entry_map = merge({ for role in local.aws_team_roles_auth : role.rolearn => { + # kubernetes_groups = role.groups + # } }, {for role in module.eks_workers[*].workers_role_arn : role => { + # type = "EC2_LINUX" + # }}) + + iam_roles_access_entry_map = { + for role in var.map_additional_iam_roles : role.rolearn => { + kubernetes_groups = role.groups + } + } + + iam_users_access_entry_map = { + for role in var.map_additional_iam_users : role.rolearn => { + kubernetes_groups = role.groups + } + } - # Existing managed worker role ARNs - managed_worker_role_arns = coalesce(local.eks_outputs.eks_managed_node_workers_role_arns, []) + access_entry_map = merge(local.aws_team_roles_access_entry_map, local.aws_sso_access_entry_map, local.iam_roles_access_entry_map, local.iam_users_access_entry_map) - # If Karpenter IAM role is enabled, add it to the `aws-auth` ConfigMap to allow the nodes launched by Karpenter to join the EKS cluster + # If Karpenter IAM role is enabled, give it access to the cluster to allow the nodes launched by Karpenter to join the EKS cluster karpenter_role_arn = one(aws_iam_role.karpenter[*].arn) - worker_role_arns = compact(concat( + linux_worker_role_arns = compact(concat( var.map_additional_worker_roles, - local.managed_worker_role_arns, - [local.karpenter_role_arn] + # As of Karpenter v0.35.0, there is no entry in the official Karpenter documentation + # stating how to configure Karpenter node roles via EKS Access Entries. + # However, it is launching unmanaged worker nodes, so it makes sense that they + # be configured as EC2_LINUX unmanaged worker nodes. Of course, this probably + # does not work if they are Windows nodes, but at the moment, this component + # probably has other deficiencies that would prevent it from working with Windows nodes, + # so we will stick with just saying Windows is not supported until we have some need for it. + [local.karpenter_role_arn], )) + # For backwards compatibility, we need to add the unmanaged worker role ARNs, but + # historically we did not care whether they were LINUX or WINDOWS. + # Best we can do is guess that they are LINUX. The `eks-cluster` module + # did not give them all the support needed to run Windows anyway. + access_entries_for_nodes = length(local.linux_worker_role_arns) > 0 ? { + EC2_LINUX = local.linux_worker_role_arns + } : {} + subnet_type_tag_key = var.subnet_type_tag_key != null ? var.subnet_type_tag_key : local.vpc_outputs.vpc.subnet_type_tag_key allowed_cidr_blocks = concat( @@ -87,18 +111,25 @@ locals { az_id_map = length(local.availability_zone_ids_expanded) > 0 ? try(zipmap(data.aws_availability_zones.default[0].zone_ids, data.aws_availability_zones.default[0].names), {}) : {} availability_zones_normalized = length(local.availability_zone_ids_expanded) > 0 ? [ - for v in local.availability_zone_ids_expanded : local.az_id_map[v]] : local.availability_zones_expanded + for v in local.availability_zone_ids_expanded : local.az_id_map[v] + ] : local.availability_zones_expanded # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` # `az_public_subnets_map` is a map of AZ names to list of public subnet IDs in the AZs # LEGACY SUPPORT for legacy VPC with no az_public_subnets_map - public_subnet_ids = try(flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]), + public_subnet_ids = try(flatten([ + for k, v in local.vpc_outputs.az_public_subnets_map : v + if contains(var.availability_zones, k) || length(var.availability_zones) == 0 + ]), local.vpc_outputs.public_subnet_ids) # Get only the private subnets that correspond to the AZs provided in `var.availability_zones` # `az_private_subnets_map` is a map of AZ names to list of private subnet IDs in the AZs # LEGACY SUPPORT for legacy VPC with no az_public_subnets_map - private_subnet_ids = try(flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k) || length(var.availability_zones) == 0]), + private_subnet_ids = try(flatten([ + for k, v in local.vpc_outputs.az_private_subnets_map : v + if contains(var.availability_zones, k) || length(var.availability_zones) == 0 + ]), local.vpc_outputs.private_subnet_ids) # Infer the availability zones from the private subnets if var.availability_zones is empty: @@ -129,40 +160,27 @@ module "utils" { module "eks_cluster" { source = "cloudposse/eks-cluster/aws" - version = "3.0.0" + version = "4.1.0" region = var.region attributes = local.attributes - kube_data_auth_enabled = false - # exec_auth is more reliable than data_auth when the aws CLI is available - # Details at https://github.com/cloudposse/terraform-aws-eks-cluster/releases/tag/0.42.0 - kube_exec_auth_enabled = !var.kubeconfig_file_enabled - # If using `exec` method (recommended) for authentication, provide an explicit - # IAM role ARN to exec as for authentication to EKS cluster. - kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) - kube_exec_auth_role_arn_enabled = true - # Path to KUBECONFIG file to use to access the EKS cluster - kubeconfig_path = var.kubeconfig_file - kubeconfig_path_enabled = var.kubeconfig_file_enabled - - allowed_security_groups = var.allowed_security_groups + access_config = var.access_config + access_entry_map = local.access_entry_map + access_entries_for_nodes = local.access_entries_for_nodes + + + allowed_security_group_ids = var.allowed_security_groups allowed_cidr_blocks = local.allowed_cidr_blocks - apply_config_map_aws_auth = local.enabled && var.apply_config_map_aws_auth cluster_log_retention_period = var.cluster_log_retention_period enabled_cluster_log_types = var.enabled_cluster_log_types endpoint_private_access = var.cluster_endpoint_private_access endpoint_public_access = var.cluster_endpoint_public_access kubernetes_version = var.cluster_kubernetes_version oidc_provider_enabled = var.oidc_provider_enabled - map_additional_aws_accounts = var.map_additional_aws_accounts - map_additional_iam_roles = local.map_additional_iam_roles - map_additional_iam_users = var.map_additional_iam_users public_access_cidrs = var.public_access_cidrs subnet_ids = var.cluster_private_subnets_only ? local.private_subnet_ids : concat(local.private_subnet_ids, local.public_subnet_ids) - vpc_id = local.vpc_id - kubernetes_config_map_ignore_role_changes = false # EKS addons addons = local.addons @@ -172,40 +190,6 @@ module "eks_cluster" { values(local.final_addon_service_account_role_arn_map) ) : null - # Managed Node Groups do not expose nor accept any Security Groups. - # Instead, EKS creates a Security Group and applies it to ENI that is attached to EKS Control Plane master nodes and to any managed workloads. - #workers_security_group_ids = compact([local.vpn_allowed_cidr_sg]) - - # Ensure ordering of resource creation: - # 1. Create the EKS cluster - # 2. Create any resources OTHER THAN MANAGED NODE GROUPS that need to be added to the - # Kubernetes `aws-auth` configMap by our Terraform - # 3. Use Terraform to create the Kubernetes `aws-auth` configMap we need - # 4. Create managed node groups. AWS EKS will automatically add newly created - # managed node groups to the Kubernetes `aws-auth` configMap. - # - # We must execute steps in this order because: - # - 1 before 3 because we cannot add a configMap to a cluster that does not exist - # - 2 before 3 because Terraform will not create and update the configMap in separate steps, so it must have - # all the data to add before it creates the configMap - # - 3 before 4 because EKS will create the Kubernetes `aws-auth` configMap if it does not exist - # when it creates the first managed node group, and Terraform will not modify a resource it did not create - # - # We count on the EKS cluster module to ensure steps 1-3 are done in the right order. - # We then depend on the kubernetes_config_map_id, using the `module_depends_on` feature of the node-group module, - # to ensure we do not proceed to step 4 until after step 3 is completed. - - # workers_role_arns is part of the data that needs to be collected/created in step 2 above - # because it goes into the `aws-auth` configMap created in step 3. However, because of the - # ordering requirements, we cannot wait for new managed node groups to be created. Fortunately, - # this is not necessary, because AWS EKS will automatically add node groups to the `aws-auth` configMap - # when they are created. However, after they are created, they will not be replaced if they are - # later removed, and in step 3 we replace the entire configMap. So we have to add the pre-existing - # managed node groups here, and we get that by reading our current (pre plan or apply) Terraform state. - workers_role_arns = local.worker_role_arns - - aws_auth_yaml_strip_quotes = var.aws_auth_yaml_strip_quotes - cluster_encryption_config_enabled = var.cluster_encryption_config_enabled cluster_encryption_config_kms_key_id = var.cluster_encryption_config_kms_key_id cluster_encryption_config_kms_key_enable_key_rotation = var.cluster_encryption_config_kms_key_enable_key_rotation diff --git a/modules/eks/cluster/modules/node_group_by_region/variables.tf b/modules/eks/cluster/modules/node_group_by_region/variables.tf index a8be3618c..cd9afe73d 100644 --- a/modules/eks/cluster/modules/node_group_by_region/variables.tf +++ b/modules/eks/cluster/modules/node_group_by_region/variables.tf @@ -32,7 +32,7 @@ variable "cluster_context" { effect = string })) kubernetes_version = string - module_depends_on = any + module_depends_on = optional(any) resources_to_tag = list(string) subnet_type_tag_key = string aws_ssm_agent_enabled = bool diff --git a/modules/eks/cluster/outputs.tf b/modules/eks/cluster/outputs.tf index 6430de8f9..186669f3e 100644 --- a/modules/eks/cluster/outputs.tf +++ b/modules/eks/cluster/outputs.tf @@ -60,7 +60,7 @@ output "eks_node_group_role_names" { output "eks_auth_worker_roles" { description = "List of worker IAM roles that were included in the `auth-map` ConfigMap." - value = local.worker_role_arns + value = local.linux_worker_role_arns } output "eks_node_group_statuses" { @@ -107,3 +107,8 @@ output "availability_zones" { description = "Availability Zones in which the cluster is provisioned" value = local.availability_zones } + +output "eks_addons_versions" { + description = "Map of enabled EKS Addons names and versions" + value = module.eks_cluster.eks_addons_versions +} diff --git a/modules/eks/cluster/variables-deprecated.tf b/modules/eks/cluster/variables-deprecated.tf new file mode 100644 index 000000000..a6c952846 --- /dev/null +++ b/modules/eks/cluster/variables-deprecated.tf @@ -0,0 +1,58 @@ +variable "apply_config_map_aws_auth" { + type = bool + description = <<-EOT + (Obsolete) Whether to execute `kubectl apply` to apply the ConfigMap to allow worker nodes to join the EKS cluster. + This input is included to avoid breaking existing configurations that set it to `true`; + a value of `false` is no longer allowed. + This input is obsolete and will be removed in a future release. + EOT + default = true + nullable = false + validation { + condition = var.apply_config_map_aws_auth == true + error_message = <<-EOT + This component no longer supports the `aws-auth` ConfigMap and always updates the access. + This input is obsolete and will be removed in a future release. + EOT + } +} + +variable "map_additional_aws_accounts" { + type = list(string) + description = <<-EOT + (Obsolete) Additional AWS accounts to grant access to the EKS cluster. + This input is included to avoid breaking existing configurations that + supplied an empty list, but the list is no longer allowed to have entries. + (It is not clear that it worked properly in earlier versions in any case.) + This component now only supports EKS access entries, which require full principal ARNs. + This input is deprecated and will be removed in a future release. + EOT + default = [] + nullable = false + validation { + condition = length(var.map_additional_aws_accounts) == 0 + error_message = <<-EOT + This component no longer supports `map_additional_aws_accounts`. + (It is not clear that it worked properly in earlier versions in any case.) + This component only supports EKS access entries, which require full principal ARNs. + This input is deprecated and will be removed in a future release. + EOT + } +} + +variable "map_additional_worker_roles" { + type = list(string) + description = <<-EOT + (Deprecated) AWS IAM Role ARNs of unmanaged Linux worker nodes to grant access to the EKS cluster. + In earlier versions, this could be used to grant access to worker nodes of any type + that were not managed by the EKS cluster. Now EKS requires that unmanaged worker nodes + be classified as Linux or Windows servers, in this input is temporarily retained + with the assumption that all worker nodes are Linux servers. (It is likely that + earlier versions did not work properly with Windows worker nodes anyway.) + This input is deprecated and will be removed in a future release. + In the future, this component will either have a way to separate Linux and Windows worker nodes, + or drop support for unmanaged worker nodes entirely. + EOT + default = [] + nullable = false +} diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index dc15e9c8e..232fe782e 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -93,27 +93,11 @@ variable "cluster_log_retention_period" { nullable = false } -variable "apply_config_map_aws_auth" { - type = bool - description = "Whether to execute `kubectl apply` to apply the ConfigMap to allow worker nodes to join the EKS cluster" - default = true - nullable = false -} - -variable "map_additional_aws_accounts" { - type = list(string) - description = "Additional AWS account numbers to add to `aws-auth` ConfigMap" - default = [] - nullable = false -} - -variable "map_additional_worker_roles" { - type = list(string) - description = "AWS IAM Role ARNs of worker nodes to add to `aws-auth` ConfigMap" - default = [] - nullable = false -} - +# TODO: +# - Support EKS Access Policies +# - Support namespaced access limits +# - Support roles from other accounts +# - Either combine with Permission Sets or similarly enhance Permission Set support variable "aws_team_roles_rbac" { type = list(object({ aws_team_role = string @@ -143,14 +127,26 @@ variable "aws_sso_permission_sets_rbac" { nullable = false } +# TODO: +# - Support EKS Access Policies +# - Support namespaced access limits +# - Combine with`map_additional_iam_users` into new input variable "map_additional_iam_roles" { type = list(object({ rolearn = string - username = string + username = optional(string) groups = list(string) })) - description = "Additional IAM roles to add to `config-map-aws-auth` ConfigMap" + description = <<-EOT + Additional IAM roles to grant access to the cluster. + *WARNING*: Full Role ARN, including path, is required for `rolearn`. + In earlier versions (with `aws-auth` ConfigMap), only the path + had to be removed from the Role ARN. The path is now required. + `username` is now ignored. This input is planned to be replaced + in a future release with a more flexible input structure that consolidates + `map_additional_iam_roles` and `map_additional_iam_users`. + EOT default = [] nullable = false } @@ -158,11 +154,16 @@ variable "map_additional_iam_roles" { variable "map_additional_iam_users" { type = list(object({ userarn = string - username = string + username = optional(string) groups = list(string) })) - description = "Additional IAM users to add to `aws-auth` ConfigMap" + description = <<-EOT + Additional IAM roles to grant access to the cluster. + `username` is now ignored. This input is planned to be replaced + in a future release with a more flexible input structure that consolidates + `map_additional_iam_roles` and `map_additional_iam_users`. + EOT default = [] nullable = false } @@ -406,37 +407,6 @@ variable "aws_ssm_agent_enabled" { nullable = false } -variable "kubeconfig_file" { - type = string - description = "Name of `kubeconfig` file to use to configure Kubernetes provider" - default = "" -} - -variable "kubeconfig_file_enabled" { - type = bool - - description = <<-EOF - Set true to configure Kubernetes provider with a `kubeconfig` file specified by `kubeconfig_file`. - Mainly for when the standard configuration produces a Terraform error. - EOF - - default = false - nullable = false -} - -variable "kube_exec_auth_role_arn" { - type = string - description = "The role ARN for `aws eks get-token` to use. Defaults to the current caller's role." - default = null -} - -variable "aws_auth_yaml_strip_quotes" { - type = bool - description = "If true, remove double quotes from the generated aws-auth ConfigMap YAML to reduce spurious diffs in plans" - default = true - nullable = false -} - variable "cluster_private_subnets_only" { type = bool description = "Whether or not to enable private subnets or both public and private subnets" @@ -563,7 +533,10 @@ variable "legacy_fargate_1_role_per_profile_enabled" { variable "legacy_do_not_create_karpenter_instance_profile" { type = bool description = <<-EOT - When `true` (the default), suppresses creation of the IAM Instance Profile + **Obsolete:** The issues this was meant to mitigate were fixed in AWS Terraform Provider v5.43.0 + and Karpenter v0.33.0. This variable will be removed in a future release. + Remove this input from your configuration and leave it at default. + **Old description:** When `true` (the default), suppresses creation of the IAM Instance Profile for nodes launched by Karpenter, to preserve the legacy behavior of the `eks/karpenter` component creating it. Set to `false` to enable creation of the IAM Instance Profile, which @@ -573,3 +546,18 @@ variable "legacy_do_not_create_karpenter_instance_profile" { EOT default = true } + +variable "access_config" { + type = object({ + authentication_mode = optional(string, "API") + bootstrap_cluster_creator_admin_permissions = optional(bool, false) + }) + description = "Access configuration for the EKS cluster" + default = {} + nullable = false + + validation { + condition = !contains(["CONFIG_MAP"], var.access_config.authentication_mode) + error_message = "The CONFIG_MAP authentication_mode is not supported." + } +} diff --git a/modules/eks/cluster/versions.tf b/modules/eks/cluster/versions.tf index 7ed8e615b..601150b50 100644 --- a/modules/eks/cluster/versions.tf +++ b/modules/eks/cluster/versions.tf @@ -10,5 +10,14 @@ terraform { source = "hashicorp/random" version = ">= 3.0" } + # We no longer use the Kubernetes provider, so we can remove it, + # but since there are bugs in the current version, we keep this as a comment. + # kubernetes = { + # source = "hashicorp/kubernetes" + # # Version 2.25 and higher have bugs, so we cannot allow them, + # # but automation enforces that we have no upper limit. + # # It is less critical here, because the Kubernetes provider is being removed entirely. + # version = "2.24" + # } } } From 6e0bd4226a061cce409e5ee7d0933811f6952bfb Mon Sep 17 00:00:00 2001 From: Nuru Date: Tue, 14 May 2024 15:06:24 -0700 Subject: [PATCH 407/501] Better support for KUBECONFIG file authentication (#1034) --- mixins/provider-helm.tf | 69 ++++++++++++++----- .../README.md | 3 +- .../provider-helm.tf | 69 ++++++++++++++----- .../eks/actions-runner-controller/README.md | 3 +- .../provider-helm.tf | 69 ++++++++++++++----- .../alb-controller-ingress-class/README.md | 3 +- .../provider-helm.tf | 69 ++++++++++++++----- modules/eks/alb-controller/README.md | 3 +- modules/eks/alb-controller/provider-helm.tf | 69 ++++++++++++++----- modules/eks/argocd/README.md | 5 +- modules/eks/argocd/provider-helm.tf | 69 ++++++++++++++----- .../aws-node-termination-handler/README.md | 3 +- .../provider-helm.tf | 69 ++++++++++++++----- modules/eks/cert-manager/README.md | 3 +- modules/eks/cert-manager/provider-helm.tf | 69 ++++++++++++++----- modules/eks/datadog-agent/README.md | 3 +- modules/eks/datadog-agent/provider-helm.tf | 69 ++++++++++++++----- modules/eks/echo-server/README.md | 3 +- modules/eks/echo-server/provider-helm.tf | 69 ++++++++++++++----- modules/eks/external-dns/README.md | 3 +- modules/eks/external-dns/provider-helm.tf | 69 ++++++++++++++----- .../eks/external-secrets-operator/README.md | 3 +- .../provider-helm.tf | 69 ++++++++++++++----- modules/eks/github-actions-runner/README.md | 3 +- .../github-actions-runner/provider-helm.tf | 69 ++++++++++++++----- modules/eks/idp-roles/README.md | 3 +- modules/eks/idp-roles/provider-helm.tf | 69 ++++++++++++++----- modules/eks/karpenter-provisioner/README.md | 3 +- .../karpenter-provisioner/provider-helm.tf | 69 ++++++++++++++----- modules/eks/karpenter/README.md | 3 +- modules/eks/karpenter/provider-helm.tf | 69 ++++++++++++++----- modules/eks/keda/README.md | 3 +- modules/eks/keda/provider-helm.tf | 69 ++++++++++++++----- modules/eks/metrics-server/README.md | 3 +- modules/eks/metrics-server/provider-helm.tf | 69 ++++++++++++++----- modules/eks/redis-operator/README.md | 3 +- modules/eks/redis-operator/provider-helm.tf | 69 ++++++++++++++----- modules/eks/redis/README.md | 3 +- modules/eks/redis/provider-helm.tf | 69 ++++++++++++++----- modules/eks/reloader/README.md | 3 +- modules/eks/reloader/provider-helm.tf | 69 ++++++++++++++----- modules/eks/storage-class/README.md | 3 +- modules/eks/storage-class/provider-helm.tf | 69 ++++++++++++++----- 43 files changed, 1187 insertions(+), 396 deletions(-) diff --git a/mixins/provider-helm.tf b/mixins/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/mixins/provider-helm.tf +++ b/mixins/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index 3e79812f7..54209576a 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -186,7 +186,8 @@ Environment variables: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/datadog-synthetics-private-location/provider-helm.tf b/modules/datadog-synthetics-private-location/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/datadog-synthetics-private-location/provider-helm.tf +++ b/modules/datadog-synthetics-private-location/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 6b20ebed1..7ea596356 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -533,7 +533,8 @@ documentation for further details. | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/actions-runner-controller/provider-helm.tf b/modules/eks/actions-runner-controller/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/actions-runner-controller/provider-helm.tf +++ b/modules/eks/actions-runner-controller/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/alb-controller-ingress-class/README.md b/modules/eks/alb-controller-ingress-class/README.md index e0fa8c847..cb821739e 100644 --- a/modules/eks/alb-controller-ingress-class/README.md +++ b/modules/eks/alb-controller-ingress-class/README.md @@ -80,7 +80,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/alb-controller-ingress-class/provider-helm.tf b/modules/eks/alb-controller-ingress-class/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/alb-controller-ingress-class/provider-helm.tf +++ b/modules/eks/alb-controller-ingress-class/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 64d6f6c86..33c2d4d2f 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -125,7 +125,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/alb-controller/provider-helm.tf b/modules/eks/alb-controller/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/alb-controller/provider-helm.tf +++ b/modules/eks/alb-controller/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 334d30b5a..054d16ca7 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -511,7 +511,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [alb\_logs\_bucket](#input\_alb\_logs\_bucket) | The name of the bucket for ALB access logs. The bucket must have policy allowing the ELB logging principal | `string` | `""` | no | | [alb\_logs\_prefix](#input\_alb\_logs\_prefix) | `alb_logs_bucket` s3 bucket prefix | `string` | `""` | no | | [alb\_name](#input\_alb\_name) | The name of the ALB (e.g. `argocd`) provisioned by `alb-controller`. Works together with `var.alb_group_name` | `string` | `null` | no | -| [anonymous\_enabled](#input\_anonymous\_enabled) | Toggles anonymous user access using default rbac setting (defaults to readonly) | `bool` | `false` | no | +| [anonymous\_enabled](#input\_anonymous\_enabled) | Toggles anonymous user access using default RBAC setting (Defaults to read-only) | `bool` | `false` | no | | [argocd\_apps\_chart](#input\_argocd\_apps\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"argocd-apps"` | no | | [argocd\_apps\_chart\_description](#input\_argocd\_apps\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"A Helm chart for managing additional Argo CD Applications and Projects"` | no | | [argocd\_apps\_chart\_repository](#input\_argocd\_apps\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://argoproj.github.io/argo-helm"` | no | @@ -555,7 +555,8 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/argocd/provider-helm.tf b/modules/eks/argocd/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/argocd/provider-helm.tf +++ b/modules/eks/argocd/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 15a838604..11acd350a 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -104,7 +104,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/aws-node-termination-handler/provider-helm.tf b/modules/eks/aws-node-termination-handler/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/aws-node-termination-handler/provider-helm.tf +++ b/modules/eks/aws-node-termination-handler/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index d7302cc6b..0fab762d9 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -124,7 +124,8 @@ cert_manager_resources: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/cert-manager/provider-helm.tf b/modules/eks/cert-manager/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/cert-manager/provider-helm.tf +++ b/modules/eks/cert-manager/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index e203a45f8..6885ab282 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -238,7 +238,8 @@ https-checks: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/datadog-agent/provider-helm.tf b/modules/eks/datadog-agent/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/datadog-agent/provider-helm.tf +++ b/modules/eks/datadog-agent/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index 7add6693e..15867e75d 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -162,7 +162,8 @@ In rare cases where some ingress controllers do not support the `ingressClassNam | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/echo-server/provider-helm.tf b/modules/eks/echo-server/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/echo-server/provider-helm.tf +++ b/modules/eks/echo-server/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index d4b630c06..3a8df4f93 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -114,7 +114,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/external-dns/provider-helm.tf b/modules/eks/external-dns/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/external-dns/provider-helm.tf +++ b/modules/eks/external-dns/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index e2a0d2332..8f2a90600 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -156,7 +156,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/external-secrets-operator/provider-helm.tf b/modules/eks/external-secrets-operator/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/external-secrets-operator/provider-helm.tf +++ b/modules/eks/external-secrets-operator/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/github-actions-runner/README.md b/modules/eks/github-actions-runner/README.md index d6fde7712..e9b2cd90b 100644 --- a/modules/eks/github-actions-runner/README.md +++ b/modules/eks/github-actions-runner/README.md @@ -420,7 +420,8 @@ implementation. | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/github-actions-runner/provider-helm.tf b/modules/eks/github-actions-runner/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/github-actions-runner/provider-helm.tf +++ b/modules/eks/github-actions-runner/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index a1acb1f44..a5cf79006 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -81,7 +81,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/idp-roles/provider-helm.tf b/modules/eks/idp-roles/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/idp-roles/provider-helm.tf +++ b/modules/eks/idp-roles/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/karpenter-provisioner/README.md b/modules/eks/karpenter-provisioner/README.md index 88dbdc0ef..3003bb610 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/modules/eks/karpenter-provisioner/README.md @@ -155,7 +155,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/karpenter-provisioner/provider-helm.tf b/modules/eks/karpenter-provisioner/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/karpenter-provisioner/provider-helm.tf +++ b/modules/eks/karpenter-provisioner/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 1437929e2..1ad01d35b 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -412,7 +412,8 @@ For more details, refer to: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/karpenter/provider-helm.tf b/modules/eks/karpenter/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/karpenter/provider-helm.tf +++ b/modules/eks/karpenter/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/keda/README.md b/modules/eks/keda/README.md index 31a12d755..1546853ca 100644 --- a/modules/eks/keda/README.md +++ b/modules/eks/keda/README.md @@ -84,7 +84,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/keda/provider-helm.tf b/modules/eks/keda/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/keda/provider-helm.tf +++ b/modules/eks/keda/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 000c4347c..9c66f80c5 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -100,7 +100,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/metrics-server/provider-helm.tf b/modules/eks/metrics-server/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/metrics-server/provider-helm.tf +++ b/modules/eks/metrics-server/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 478124de9..a2e51ed93 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -127,7 +127,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/redis-operator/provider-helm.tf b/modules/eks/redis-operator/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/redis-operator/provider-helm.tf +++ b/modules/eks/redis-operator/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index e46c5ea41..d488ba944 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -130,7 +130,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/redis/provider-helm.tf b/modules/eks/redis/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/redis/provider-helm.tf +++ b/modules/eks/redis/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/reloader/README.md b/modules/eks/reloader/README.md index 8198f4fb2..3119f1f3b 100644 --- a/modules/eks/reloader/README.md +++ b/modules/eks/reloader/README.md @@ -89,7 +89,8 @@ components: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/reloader/provider-helm.tf b/modules/eks/reloader/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/reloader/provider-helm.tf +++ b/modules/eks/reloader/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/storage-class/README.md b/modules/eks/storage-class/README.md index 29cf8f602..4ada75221 100644 --- a/modules/eks/storage-class/README.md +++ b/modules/eks/storage-class/README.md @@ -160,7 +160,8 @@ eks/storage-class: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/storage-class/provider-helm.tf b/modules/eks/storage-class/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/storage-class/provider-helm.tf +++ b/modules/eks/storage-class/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" From f27333d91ef4e66ce635e93f210d88baf9d3c49b Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 15 May 2024 08:59:37 -0700 Subject: [PATCH 408/501] `sqs-queue`: Update to include SQS Policy (#1035) --- modules/sqs-queue/README.md | 12 ++++++-- modules/sqs-queue/main.tf | 50 +++++++++++++++++++++++++++++++++- modules/sqs-queue/variables.tf | 41 ++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index fffbaed2b..189561351 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -30,19 +30,25 @@ components: ## Providers -No providers. +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | ## Modules | Name | Source | Version | |------|--------|---------| | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [queue\_policy](#module\_queue\_policy) | cloudposse/iam-policy/aws | 2.0.1 | | [sqs\_queue](#module\_sqs\_queue) | ./modules/terraform-aws-sqs-queue | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_sqs_queue_policy.sqs_queue_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | ## Inputs @@ -60,6 +66,8 @@ No resources. | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [fifo\_queue](#input\_fifo\_queue) | Boolean designating a FIFO queue. If not set, it defaults to false making it standard. | `bool` | `false` | no | | [fifo\_throughput\_limit](#input\_fifo\_throughput\_limit) | Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group. Valid values are perQueue and perMessageGroupId. This can be specified if fifo\_queue is true. | `list(string)` | `[]` | no | +| [iam\_policy](#input\_iam\_policy) | IAM policy as list of Terraform objects, compatible with Terraform `aws_iam_policy_document` data source
except that `source_policy_documents` and `override_policy_documents` are not included.
Use inputs `iam_source_policy_documents` and `iam_override_policy_documents` for that. |
list(object({
policy_id = optional(string, null)
version = optional(string, null)
statements = list(object({
sid = optional(string, null)
effect = optional(string, null)
actions = optional(list(string), null)
not_actions = optional(list(string), null)
resources = optional(list(string), null)
not_resources = optional(list(string), null)
conditions = optional(list(object({
test = string
variable = string
values = list(string)
})), [])
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
not_principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
}))
}))
| `[]` | no | +| [iam\_policy\_limit\_to\_current\_account](#input\_iam\_policy\_limit\_to\_current\_account) | Boolean designating whether the IAM policy should be limited to the current account. | `bool` | `true` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [kms\_data\_key\_reuse\_period\_seconds](#input\_kms\_data\_key\_reuse\_period\_seconds) | The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. An integer representing seconds, between 60 seconds (1 minute) and 86,400 seconds (24 hours). The default is 300 (5 minutes). | `number` | `300` | no | | [kms\_master\_key\_id](#input\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see Key Terms. | `list(string)` |
[
"alias/aws/sqs"
]
| no | diff --git a/modules/sqs-queue/main.tf b/modules/sqs-queue/main.tf index 8d350131a..6c51af149 100644 --- a/modules/sqs-queue/main.tf +++ b/modules/sqs-queue/main.tf @@ -1,5 +1,11 @@ locals { - enabled = module.this.enabled + enabled = module.this.enabled + aws_account_number = one(data.aws_caller_identity.current[*].account_id) + policy_enabled = local.enabled && length(var.iam_policy) > 0 +} + +data "aws_caller_identity" "current" { + count = local.enabled ? 1 : 0 } module "sqs_queue" { @@ -21,3 +27,45 @@ module "sqs_queue" { context = module.this.context } + +module "queue_policy" { + count = local.policy_enabled ? 1 : 0 + + source = "cloudposse/iam-policy/aws" + version = "2.0.1" + + iam_policy = [ + for policy in var.iam_policy : { + policy_id = policy.policy_id + version = policy.version + + statements = [ + for statement in policy.statements : + merge( + statement, + { + resources = [module.sqs_queue.arn] + }, + var.iam_policy_limit_to_current_account ? { + conditions = concat(statement.conditions, [ + { + test = "StringEquals" + variable = "aws:SourceAccount" + values = [local.aws_account_number] + } + ]) + } : {} + ) + ] + } + ] + + context = module.this.context +} + +resource "aws_sqs_queue_policy" "sqs_queue_policy" { + count = local.policy_enabled ? 1 : 0 + + queue_url = module.sqs_queue.url + policy = one(module.queue_policy[*].json) +} diff --git a/modules/sqs-queue/variables.tf b/modules/sqs-queue/variables.tf index 0d5cf7c01..e4054ca95 100644 --- a/modules/sqs-queue/variables.tf +++ b/modules/sqs-queue/variables.tf @@ -80,3 +80,44 @@ variable "deduplication_scope" { description = "Specifies whether message deduplication occurs at the message group or queue level. Valid values are messageGroup and queue. This can be specified if fifo_queue is true." default = [] } + +variable "iam_policy_limit_to_current_account" { + type = bool + description = "Boolean designating whether the IAM policy should be limited to the current account." + default = true +} + +variable "iam_policy" { + type = list(object({ + policy_id = optional(string, null) + version = optional(string, null) + statements = list(object({ + sid = optional(string, null) + effect = optional(string, null) + actions = optional(list(string), null) + not_actions = optional(list(string), null) + resources = optional(list(string), null) + not_resources = optional(list(string), null) + conditions = optional(list(object({ + test = string + variable = string + values = list(string) + })), []) + principals = optional(list(object({ + type = string + identifiers = list(string) + })), []) + not_principals = optional(list(object({ + type = string + identifiers = list(string) + })), []) + })) + })) + description = <<-EOT + IAM policy as list of Terraform objects, compatible with Terraform `aws_iam_policy_document` data source + except that `source_policy_documents` and `override_policy_documents` are not included. + Use inputs `iam_source_policy_documents` and `iam_override_policy_documents` for that. + EOT + default = [] + nullable = false +} From fe8544caff332a17903108fd545e1592c7068b52 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 16 May 2024 09:41:27 -0700 Subject: [PATCH 409/501] `sqs-queue` better support dead-letter queues (#1037) --- modules/sqs-queue/README.md | 18 ++++++++++++++++-- modules/sqs-queue/main.tf | 17 ++++++++++++++--- modules/sqs-queue/remote-state.tf | 11 +++++++++++ modules/sqs-queue/variables.tf | 22 ++++++++++++++++++++-- 4 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 modules/sqs-queue/remote-state.tf diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index 189561351..b3194b578 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -17,6 +17,15 @@ components: workspace_enabled: true vars: enabled: true + dead_letter_sqs_component_name: "sqs-queue/dead-letter" + dead_letter_max_receive_count: 4 + + sqs-queue/dead-letter: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true ``` @@ -38,6 +47,7 @@ components: | Name | Source | Version | |------|--------|---------| +| [dead\_letter\_sqs\_remote\_state](#module\_dead\_letter\_sqs\_remote\_state) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [queue\_policy](#module\_queue\_policy) | cloudposse/iam-policy/aws | 2.0.1 | | [sqs\_queue](#module\_sqs\_queue) | ./modules/terraform-aws-sqs-queue | n/a | @@ -48,6 +58,7 @@ components: | Name | Type | |------|------| | [aws_sqs_queue_policy.sqs_queue_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | +| [aws_sqs_queue_redrive_policy.dead_letter_queue](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_redrive_policy) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | ## Inputs @@ -58,6 +69,9 @@ components: | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [content\_based\_deduplication](#input\_content\_based\_deduplication) | Enables content-based deduplication for FIFO queues. For more information, see the [related documentation](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html#FIFO-queues-exactly-once-processing) | `bool` | `false` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [dead\_letter\_max\_receive\_count](#input\_dead\_letter\_max\_receive\_count) | The number of times a message can be unsuccessfully dequeued before being moved to the Dead Letter Queue. | `number` | `5` | no | +| [dead\_letter\_sqs\_arn](#input\_dead\_letter\_sqs\_arn) | The SQS url of the Dead Letter Queue. This is used to create the redrive policy. | `string` | `null` | no | +| [dead\_letter\_sqs\_component\_name](#input\_dead\_letter\_sqs\_component\_name) | The name of the component that will be looked up for the ARN and be used as the Dead Letter Queue. | `string` | `null` | no | | [deduplication\_scope](#input\_deduplication\_scope) | Specifies whether message deduplication occurs at the message group or queue level. Valid values are messageGroup and queue. This can be specified if fifo\_queue is true. | `list(string)` | `[]` | no | | [delay\_seconds](#input\_delay\_seconds) | The time in seconds that the delivery of all messages in the queue will be delayed. An integer from 0 to 900 (15 minutes). The default for this attribute is 0 seconds. | `number` | `0` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | @@ -79,9 +93,9 @@ components: | [message\_retention\_seconds](#input\_message\_retention\_seconds) | The number of seconds Amazon SQS retains a message. Integer representing seconds, from 60 (1 minute) to 1209600 (14 days). The default for this attribute is 345600 (4 days). | `number` | `345600` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [policy](#input\_policy) | The JSON policy for the SQS queue. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy). | `list(string)` | `[]` | no | +| [policy](#input\_policy) | The JSON policy for the SQS Queue. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy). | `list(string)` | `[]` | no | | [receive\_wait\_time\_seconds](#input\_receive\_wait\_time\_seconds) | The time for which a ReceiveMessage call will wait for a message to arrive (long polling) before returning. An integer from 0 to 20 (seconds). The default for this attribute is 0, meaning that the call will return immediately. | `number` | `0` | no | -| [redrive\_policy](#input\_redrive\_policy) | The JSON policy to set up the Dead Letter Queue, see AWS docs. Note: when specifying maxReceiveCount, you must specify it as an integer (5), and not a string ("5"). | `list(string)` | `[]` | no | +| [redrive\_policy](#input\_redrive\_policy) | **DEPRECATED** This is deprecated and is left as an escape hatch, please use `dead_letter_sqs_component_name` or `dead_letter_sqs_arn` instead. The JSON policy to set up the Dead Letter Queue, see AWS docs. Note: when specifying maxReceiveCount, you must specify it as an integer (5), and not a string ("5"). | `list(string)` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/sqs-queue/main.tf b/modules/sqs-queue/main.tf index 6c51af149..351b88108 100644 --- a/modules/sqs-queue/main.tf +++ b/modules/sqs-queue/main.tf @@ -1,7 +1,8 @@ locals { - enabled = module.this.enabled - aws_account_number = one(data.aws_caller_identity.current[*].account_id) - policy_enabled = local.enabled && length(var.iam_policy) > 0 + enabled = module.this.enabled + aws_account_number = one(data.aws_caller_identity.current[*].account_id) + policy_enabled = local.enabled && length(var.iam_policy) > 0 + redrive_policy_enabled = local.enabled && (var.dead_letter_sqs_component_name != null || var.dead_letter_sqs_arn != null) } data "aws_caller_identity" "current" { @@ -69,3 +70,13 @@ resource "aws_sqs_queue_policy" "sqs_queue_policy" { queue_url = module.sqs_queue.url policy = one(module.queue_policy[*].json) } + +resource "aws_sqs_queue_redrive_policy" "dead_letter_queue" { + count = local.redrive_policy_enabled != null ? 1 : 0 + + queue_url = module.sqs_queue.url + redrive_policy = jsonencode({ + deadLetterTargetArn = var.dead_letter_sqs_arn != null ? var.dead_letter_sqs_arn : one(module.dead_letter_sqs_remote_state[*].outputs.arn) + maxReceiveCount = var.dead_letter_max_receive_count + }) +} diff --git a/modules/sqs-queue/remote-state.tf b/modules/sqs-queue/remote-state.tf new file mode 100644 index 000000000..0efbb43cc --- /dev/null +++ b/modules/sqs-queue/remote-state.tf @@ -0,0 +1,11 @@ + +module "dead_letter_sqs_remote_state" { + count = var.dead_letter_sqs_component_name != null ? 1 : 0 + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.dead_letter_sqs_component_name + + context = module.this.context +} diff --git a/modules/sqs-queue/variables.tf b/modules/sqs-queue/variables.tf index e4054ca95..a67b4580b 100644 --- a/modules/sqs-queue/variables.tf +++ b/modules/sqs-queue/variables.tf @@ -35,16 +35,34 @@ variable "receive_wait_time_seconds" { variable "policy" { type = list(string) - description = "The JSON policy for the SQS queue. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy)." + description = "The JSON policy for the SQS Queue. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy)." default = [] } variable "redrive_policy" { type = list(string) - description = "The JSON policy to set up the Dead Letter Queue, see AWS docs. Note: when specifying maxReceiveCount, you must specify it as an integer (5), and not a string (\"5\")." + description = "**DEPRECATED** This is deprecated and is left as an escape hatch, please use `dead_letter_sqs_component_name` or `dead_letter_sqs_arn` instead. The JSON policy to set up the Dead Letter Queue, see AWS docs. Note: when specifying maxReceiveCount, you must specify it as an integer (5), and not a string (\"5\")." default = [] } +variable "dead_letter_sqs_arn" { + type = string + description = "The SQS url of the Dead Letter Queue. This is used to create the redrive policy." + default = null +} + +variable "dead_letter_sqs_component_name" { + type = string + description = "The name of the component that will be looked up for the ARN and be used as the Dead Letter Queue." + default = null +} + +variable "dead_letter_max_receive_count" { + type = number + description = "The number of times a message can be unsuccessfully dequeued before being moved to the Dead Letter Queue." + default = 5 +} + variable "fifo_queue" { type = bool description = "Boolean designating a FIFO queue. If not set, it defaults to false making it standard." From 276cedc0b0591380681ec2bce47b9cbbacd9f0cb Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 16 May 2024 22:03:31 -0400 Subject: [PATCH 410/501] fix: Remove `feature-branch` GitHub Actions workflow (#1038) --- .github/workflows/feature-branch.yml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 .github/workflows/feature-branch.yml diff --git a/.github/workflows/feature-branch.yml b/.github/workflows/feature-branch.yml deleted file mode 100644 index ebd8854f1..000000000 --- a/.github/workflows/feature-branch.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: feature-branch -on: - pull_request: - branches: - - main - - release/** - types: [opened, synchronize, reopened, labeled, unlabeled] - -permissions: - pull-requests: write - id-token: write - contents: write - issues: write - -jobs: - terraform-module: - uses: cloudposse/github-actions-workflows-terraform-module/.github/workflows/feature-branch.yml@main - secrets: inherit From e12c80525ceecc4588fd715262d4661a3491004c Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 17 May 2024 09:16:05 -0700 Subject: [PATCH 411/501] feat(`rds`): `psql` Connection Command as Output (#1036) --- modules/rds/README.md | 1 + modules/rds/main.tf | 2 ++ modules/rds/outputs.tf | 17 +++++++++++++++++ modules/rds/systems-manager.tf | 5 +++-- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/modules/rds/README.md b/modules/rds/README.md index ce94af2cd..438b806c1 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -231,6 +231,7 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | Name | Description | |------|-------------| | [exports](#output\_exports) | Map of exports for use in deployment configuration templates | +| [psql\_helper](#output\_psql\_helper) | A helper output to use with psql for connecting to this RDS instance. | | [rds\_address](#output\_rds\_address) | Address of the instance | | [rds\_arn](#output\_rds\_arn) | ARN of the instance | | [rds\_database\_ssm\_key\_prefix](#output\_rds\_database\_ssm\_key\_prefix) | SSM prefix | diff --git a/modules/rds/main.tf b/modules/rds/main.tf index b385b5820..6843f89fc 100644 --- a/modules/rds/main.tf +++ b/modules/rds/main.tf @@ -18,6 +18,8 @@ locals { local.eks_security_groups, var.security_group_ids ) + + psql_access_enabled = local.enabled && (var.engine == "postgres") } module "rds_client_sg" { diff --git a/modules/rds/outputs.tf b/modules/rds/outputs.tf index 3dee17fa4..d5c29821f 100644 --- a/modules/rds/outputs.tf +++ b/modules/rds/outputs.tf @@ -1,3 +1,15 @@ +locals { + ssm_path_as_list = split("/", local.rds_database_password_path) + ssm_path_app = trim(join("/", slice(local.ssm_path_as_list, 0, length(local.ssm_path_as_list) - 1)), "/") + ssm_path_password_value = element(local.ssm_path_as_list, length(local.ssm_path_as_list) - 1) + psql_message = < Date: Mon, 20 May 2024 11:15:58 -0500 Subject: [PATCH 412/501] feat(vpc): add named subnets (#1032) Signed-off-by: nitrocode <7775707+nitrocode@users.noreply.github.com> Co-authored-by: Dan Miller --- modules/vpc/README.md | 2 ++ modules/vpc/main.tf | 2 ++ modules/vpc/variables.tf | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 64dde8cd3..2d0030d9b 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -131,6 +131,8 @@ components: | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [subnet\_type\_tag\_key](#input\_subnet\_type\_tag\_key) | Key for subnet type tag to provide information about the type of subnets, e.g. `cpco/subnet/type=private` or `cpcp/subnet/type=public` | `string` | n/a | yes | +| [subnets\_per\_az\_count](#input\_subnets\_per\_az\_count) | The number of subnet of each type (public or private) to provision per Availability Zone. | `number` | `1` | no | +| [subnets\_per\_az\_names](#input\_subnets\_per\_az\_names) | The subnet names of each type (public or private) to provision per Availability Zone.
This variable is optional.
If a list of names is provided, the list items will be used as keys in the outputs `named_private_subnets_map`, `named_public_subnets_map`,
`named_private_route_table_ids_map` and `named_public_route_table_ids_map` | `list(string)` |
[
"common"
]
| no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [vpc\_flow\_logs\_bucket\_environment\_name](#input\_vpc\_flow\_logs\_bucket\_environment\_name) | The name of the environment where the VPC Flow Logs bucket is provisioned | `string` | `""` | no | diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index da314dca3..24d4a2a18 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -157,6 +157,8 @@ module "subnets" { public_subnets_additional_tags = local.public_subnets_additional_tags private_subnets_additional_tags = local.private_subnets_additional_tags vpc_id = module.vpc.vpc_id + subnets_per_az_count = var.subnets_per_az_count + subnets_per_az_names = var.subnets_per_az_names context = module.this.context } diff --git a/modules/vpc/variables.tf b/modules/vpc/variables.tf index 683f0c514..6e9940acb 100644 --- a/modules/vpc/variables.tf +++ b/modules/vpc/variables.tf @@ -207,3 +207,30 @@ variable "interface_vpc_endpoints" { description = "A list of Interface VPC Endpoints to provision into the VPC." default = [] } + +variable "subnets_per_az_count" { + type = number + description = <<-EOT + The number of subnet of each type (public or private) to provision per Availability Zone. + EOT + default = 1 + nullable = false + validation { + condition = var.subnets_per_az_count > 0 + # Validation error messages must be on a single line, among other restrictions. + # See https://github.com/hashicorp/terraform/issues/24123 + error_message = "The `subnets_per_az` value must be greater than 0." + } +} + +variable "subnets_per_az_names" { + type = list(string) + description = <<-EOT + The subnet names of each type (public or private) to provision per Availability Zone. + This variable is optional. + If a list of names is provided, the list items will be used as keys in the outputs `named_private_subnets_map`, `named_public_subnets_map`, + `named_private_route_table_ids_map` and `named_public_route_table_ids_map` + EOT + default = ["common"] + nullable = false +} From 5313f427bf659849d7049880967aa9c8830b5ab3 Mon Sep 17 00:00:00 2001 From: David Moran <23364162+wavemoran@users.noreply.github.com> Date: Tue, 21 May 2024 15:07:08 -0700 Subject: [PATCH 413/501] chore: bump elasticache-redis module version (#1040) --- modules/elasticache-redis/main.tf | 17 ++++++++++------- .../modules/redis_cluster/main.tf | 5 ++++- .../modules/redis_cluster/variables.tf | 12 ++++++++++++ modules/elasticache-redis/variables.tf | 6 ++++++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/modules/elasticache-redis/main.tf b/modules/elasticache-redis/main.tf index 2f4003da5..2e5f8ead9 100644 --- a/modules/elasticache-redis/main.tf +++ b/modules/elasticache-redis/main.tf @@ -38,6 +38,7 @@ locals { vpc_id = module.vpc.outputs.vpc_id subnets = module.vpc.outputs.private_subnet_ids availability_zones = var.availability_zones + multi_az_enabled = var.multi_az_enabled allowed_security_groups = local.allowed_security_groups additional_security_group_rules = local.additional_security_group_rules @@ -65,13 +66,15 @@ module "redis_clusters" { cluster_name = lookup(each.value, "cluster_name", replace(each.key, "_", "-")) dns_subdomain = join(".", [lookup(each.value, "cluster_name", replace(each.key, "_", "-")), module.this.environment]) - instance_type = each.value.instance_type - num_replicas = lookup(each.value, "num_replicas", 1) - num_shards = lookup(each.value, "num_shards", 0) - replicas_per_shard = lookup(each.value, "replicas_per_shard", 0) - engine_version = each.value.engine_version - parameters = each.value.parameters - cluster_attributes = local.cluster_attributes + instance_type = each.value.instance_type + num_replicas = lookup(each.value, "num_replicas", 1) + num_shards = lookup(each.value, "num_shards", 0) + replicas_per_shard = lookup(each.value, "replicas_per_shard", 0) + engine_version = each.value.engine_version + create_parameter_group = lookup(each.value, "create_parameter_group", true) + parameters = lookup(each.value, "parameters", null) + parameter_group_name = lookup(each.value, "parameter_group_name", null) + cluster_attributes = local.cluster_attributes context = module.this.context } diff --git a/modules/elasticache-redis/modules/redis_cluster/main.tf b/modules/elasticache-redis/modules/redis_cluster/main.tf index ec7e8eb48..c7a9cf02d 100644 --- a/modules/elasticache-redis/modules/redis_cluster/main.tf +++ b/modules/elasticache-redis/modules/redis_cluster/main.tf @@ -10,7 +10,7 @@ locals { module "redis" { source = "cloudposse/elasticache-redis/aws" - version = "0.52.0" + version = "1.2.2" name = var.cluster_name @@ -22,6 +22,7 @@ module "redis" { auth_token = local.auth_token automatic_failover_enabled = var.cluster_attributes.automatic_failover_enabled availability_zones = var.cluster_attributes.availability_zones + multi_az_enabled = var.cluster_attributes.multi_az_enabled cluster_mode_enabled = var.num_shards > 0 cluster_mode_num_node_groups = var.num_shards cluster_mode_replicas_per_node_group = var.replicas_per_shard @@ -30,7 +31,9 @@ module "redis" { engine_version = var.engine_version family = var.cluster_attributes.family instance_type = var.instance_type + create_parameter_group = var.create_parameter_group parameter = var.parameters + parameter_group_name = var.parameter_group_name port = var.cluster_attributes.port subnets = var.cluster_attributes.subnets transit_encryption_enabled = var.cluster_attributes.transit_encryption_enabled diff --git a/modules/elasticache-redis/modules/redis_cluster/variables.tf b/modules/elasticache-redis/modules/redis_cluster/variables.tf index 3366c1b2b..9a16924a9 100644 --- a/modules/elasticache-redis/modules/redis_cluster/variables.tf +++ b/modules/elasticache-redis/modules/redis_cluster/variables.tf @@ -5,6 +5,12 @@ variable "cluster_name" { description = "Elasticache Cluster name" } +variable "create_parameter_group" { + type = bool + default = true + description = "Whether new parameter group should be created. Set to false if you want to use existing parameter group" +} + variable "engine_version" { type = string description = "Redis Version" @@ -66,6 +72,12 @@ variable "parameters" { description = "Parameters to configure cluster parameter group" } +variable "parameter_group_name" { + type = string + default = null + description = "Override the default parameter group name" +} + variable "kms_alias_name_ssm" { default = "alias/aws/ssm" description = "KMS alias name for SSM" diff --git a/modules/elasticache-redis/variables.tf b/modules/elasticache-redis/variables.tf index 3b25582c5..6f9739ff3 100644 --- a/modules/elasticache-redis/variables.tf +++ b/modules/elasticache-redis/variables.tf @@ -9,6 +9,12 @@ variable "availability_zones" { default = [] } +variable "multi_az_enabled" { + type = bool + default = false + description = "Multi AZ (Automatic Failover must also be enabled. If Cluster Mode is enabled, Multi AZ is on by default, and this setting is ignored)" +} + variable "family" { type = string description = "Redis family" From 0fb45d81e3e56590d1bfb9b54c93f5d1a4c96566 Mon Sep 17 00:00:00 2001 From: David Moran <23364162+wavemoran@users.noreply.github.com> Date: Wed, 22 May 2024 09:08:35 -0700 Subject: [PATCH 414/501] fix: add missing `multi_az_enabled` variable (#1041) --- modules/elasticache-redis/modules/redis_cluster/variables.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/elasticache-redis/modules/redis_cluster/variables.tf b/modules/elasticache-redis/modules/redis_cluster/variables.tf index 9a16924a9..09bbf26e9 100644 --- a/modules/elasticache-redis/modules/redis_cluster/variables.tf +++ b/modules/elasticache-redis/modules/redis_cluster/variables.tf @@ -55,6 +55,7 @@ variable "cluster_attributes" { family = string port = number zone_id = string + multi_az_enabled = bool at_rest_encryption_enabled = bool transit_encryption_enabled = bool apply_immediately = bool From cd7eee3364da1093eed4f4a17b7ab19c7e2c9a68 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 22 May 2024 10:12:41 -0700 Subject: [PATCH 415/501] SQS Component Refactor (#1042) --- modules/sqs-queue/CHANGELOG.md | 23 ++ modules/sqs-queue/README.md | 90 ++++-- modules/sqs-queue/main.tf | 65 ++-- .../terraform-aws-sqs-queue/context.tf | 279 ------------------ .../modules/terraform-aws-sqs-queue/main.tf | 24 -- .../terraform-aws-sqs-queue/outputs.tf | 19 -- .../terraform-aws-sqs-queue/variables.tf | 125 -------- .../terraform-aws-sqs-queue/versions.tf | 10 - modules/sqs-queue/outputs.tf | 21 +- modules/sqs-queue/remote-state.tf | 11 - modules/sqs-queue/variables.tf | 114 +++++-- 11 files changed, 217 insertions(+), 564 deletions(-) create mode 100644 modules/sqs-queue/CHANGELOG.md delete mode 100644 modules/sqs-queue/modules/terraform-aws-sqs-queue/context.tf delete mode 100644 modules/sqs-queue/modules/terraform-aws-sqs-queue/main.tf delete mode 100644 modules/sqs-queue/modules/terraform-aws-sqs-queue/outputs.tf delete mode 100644 modules/sqs-queue/modules/terraform-aws-sqs-queue/variables.tf delete mode 100644 modules/sqs-queue/modules/terraform-aws-sqs-queue/versions.tf delete mode 100644 modules/sqs-queue/remote-state.tf diff --git a/modules/sqs-queue/CHANGELOG.md b/modules/sqs-queue/CHANGELOG.md new file mode 100644 index 000000000..9bc0c026d --- /dev/null +++ b/modules/sqs-queue/CHANGELOG.md @@ -0,0 +1,23 @@ +## Pull Request [#1042](https://github.com/cloudposse/terraform-aws-components/pull/1042) - Refactor `sqs-queue` Component + +Components PR [#1042](https://github.com/cloudposse/terraform-aws-components/pull/1042) + +### Affected Components + +- `sqs-queue` + +### Summary + +This change to the sqs-queue component, [#1042](https://github.com/cloudposse/terraform-aws-components/pull/1042), +refactored the `sqs-queue` component to use the AWS Module for queues, this provides better support for Dead-Letter +Queues and easy policy attachment. + +As part of that change, we've changed some variables: + +- `policy` - **Removed** +- `redrive_policy` - **Removed** +- `dead_letter_sqs_arn` - **Removed** +- `dead_letter_component_name` - **Removed** +- `dead_letter_max_receive_count` - Renamed to `dlq_max_receive_count` +- `fifo_throughput_limit` **type changed** from `list(string)` to type `string` +- `kms_master_key_id` **type changed** from `list(string)` to type `string` diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index b3194b578..d8efdbfac 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -11,21 +11,53 @@ Here's an example snippet for how to use this component. ```yaml components: terraform: - sqs-queue: - settings: - spacelift: - workspace_enabled: true + sqs-queue/defaults: vars: enabled: true - dead_letter_sqs_component_name: "sqs-queue/dead-letter" - dead_letter_max_receive_count: 4 + # org defaults - sqs-queue/dead-letter: - settings: - spacelift: - workspace_enabled: true + sqs-queue: + metadata: + component: sqs-queue + inherits: + - sqs-queue/defaults vars: - enabled: true + name: sqs + visibility_timeout_seconds: 30 + message_retention_seconds: 86400 # 1 day + delay_seconds: 0 + max_message_size_bytes: 262144 + receive_wait_time_seconds: 0 + fifo_queue: false + content_based_deduplication: false + dlq_enabled: true + dlq_name_suffix: "dead-letter" # default is dlq + dlq_max_receive_count: 1 + dlq_kms_data_key_reuse_period_seconds: 86400 # 1 day + kms_data_key_reuse_period_seconds: 86400 # 1 day + sqs_managed_sse_enabled: true # SSE vs KMS + iam_policy_limit_to_current_account: true # default true + iam_policy: + - version: 2012-10-17 + policy_id: Allow-S3-Event-Notifications + statements: + - sid: Allow-S3-Event-Notifications + effect: Allow + principals: + - type: Service + identifiers: ["s3.amazonaws.com"] + actions: + - SQS:SendMessage + resources: [] # auto includes this queue's ARN + conditions: + ## this is included when `iam_policy_limit_to_current_account` is true + #- test: StringEquals + # variable: aws:SourceAccount + # value: "1234567890" + - test: ArnLike + variable: aws:SourceArn + values: + - "arn:aws:s3:::*" ``` @@ -47,10 +79,9 @@ components: | Name | Source | Version | |------|--------|---------| -| [dead\_letter\_sqs\_remote\_state](#module\_dead\_letter\_sqs\_remote\_state) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [queue\_policy](#module\_queue\_policy) | cloudposse/iam-policy/aws | 2.0.1 | -| [sqs\_queue](#module\_sqs\_queue) | ./modules/terraform-aws-sqs-queue | n/a | +| [sqs](#module\_sqs) | terraform-aws-modules/sqs/aws | 4.2.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources @@ -58,7 +89,6 @@ components: | Name | Type | |------|------| | [aws_sqs_queue_policy.sqs_queue_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | -| [aws_sqs_queue_redrive_policy.dead_letter_queue](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_redrive_policy) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | ## Inputs @@ -69,22 +99,34 @@ components: | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [content\_based\_deduplication](#input\_content\_based\_deduplication) | Enables content-based deduplication for FIFO queues. For more information, see the [related documentation](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html#FIFO-queues-exactly-once-processing) | `bool` | `false` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [dead\_letter\_max\_receive\_count](#input\_dead\_letter\_max\_receive\_count) | The number of times a message can be unsuccessfully dequeued before being moved to the Dead Letter Queue. | `number` | `5` | no | -| [dead\_letter\_sqs\_arn](#input\_dead\_letter\_sqs\_arn) | The SQS url of the Dead Letter Queue. This is used to create the redrive policy. | `string` | `null` | no | -| [dead\_letter\_sqs\_component\_name](#input\_dead\_letter\_sqs\_component\_name) | The name of the component that will be looked up for the ARN and be used as the Dead Letter Queue. | `string` | `null` | no | -| [deduplication\_scope](#input\_deduplication\_scope) | Specifies whether message deduplication occurs at the message group or queue level. Valid values are messageGroup and queue. This can be specified if fifo\_queue is true. | `list(string)` | `[]` | no | +| [create\_dlq\_redrive\_allow\_policy](#input\_create\_dlq\_redrive\_allow\_policy) | Determines whether to create a redrive allow policy for the dead letter queue. | `bool` | `true` | no | +| [deduplication\_scope](#input\_deduplication\_scope) | Specifies whether message deduplication occurs at the message group or queue level | `string` | `null` | no | | [delay\_seconds](#input\_delay\_seconds) | The time in seconds that the delivery of all messages in the queue will be delayed. An integer from 0 to 900 (15 minutes). The default for this attribute is 0 seconds. | `number` | `0` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dlq\_content\_based\_deduplication](#input\_dlq\_content\_based\_deduplication) | Enables content-based deduplication for FIFO queues | `bool` | `null` | no | +| [dlq\_deduplication\_scope](#input\_dlq\_deduplication\_scope) | Specifies whether message deduplication occurs at the message group or queue level | `string` | `null` | no | +| [dlq\_delay\_seconds](#input\_dlq\_delay\_seconds) | The time in seconds that the delivery of all messages in the queue will be delayed. An integer from 0 to 900 (15 minutes) | `number` | `null` | no | +| [dlq\_enabled](#input\_dlq\_enabled) | Boolean designating whether the Dead Letter Queue should be created by this component. | `bool` | `false` | no | +| [dlq\_kms\_data\_key\_reuse\_period\_seconds](#input\_dlq\_kms\_data\_key\_reuse\_period\_seconds) | The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. An integer representing seconds, between 60 seconds (1 minute) and 86,400 seconds (24 hours) | `number` | `null` | no | +| [dlq\_kms\_master\_key\_id](#input\_dlq\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK | `string` | `null` | no | +| [dlq\_max\_receive\_count](#input\_dlq\_max\_receive\_count) | The number of times a message can be unsuccessfully dequeued before being moved to the Dead Letter Queue. | `number` | `5` | no | +| [dlq\_message\_retention\_seconds](#input\_dlq\_message\_retention\_seconds) | The number of seconds Amazon SQS retains a message. Integer representing seconds, from 60 (1 minute) to 1209600 (14 days) | `number` | `null` | no | +| [dlq\_name\_suffix](#input\_dlq\_name\_suffix) | The suffix of the Dead Letter Queue. | `string` | `"dlq"` | no | +| [dlq\_receive\_wait\_time\_seconds](#input\_dlq\_receive\_wait\_time\_seconds) | The time for which a ReceiveMessage call will wait for a message to arrive (long polling) before returning. An integer from 0 to 20 (seconds) | `number` | `null` | no | +| [dlq\_redrive\_allow\_policy](#input\_dlq\_redrive\_allow\_policy) | The JSON policy to set up the Dead Letter Queue redrive permission, see AWS docs. | `any` | `{}` | no | +| [dlq\_sqs\_managed\_sse\_enabled](#input\_dlq\_sqs\_managed\_sse\_enabled) | Boolean to enable server-side encryption (SSE) of message content with SQS-owned encryption keys | `bool` | `true` | no | +| [dlq\_tags](#input\_dlq\_tags) | A mapping of additional tags to assign to the dead letter queue | `map(string)` | `{}` | no | +| [dlq\_visibility\_timeout\_seconds](#input\_dlq\_visibility\_timeout\_seconds) | The visibility timeout for the queue. An integer from 0 to 43200 (12 hours) | `number` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [fifo\_queue](#input\_fifo\_queue) | Boolean designating a FIFO queue. If not set, it defaults to false making it standard. | `bool` | `false` | no | -| [fifo\_throughput\_limit](#input\_fifo\_throughput\_limit) | Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group. Valid values are perQueue and perMessageGroupId. This can be specified if fifo\_queue is true. | `list(string)` | `[]` | no | +| [fifo\_throughput\_limit](#input\_fifo\_throughput\_limit) | Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group. Valid values are perQueue and perMessageGroupId. This can be specified if fifo\_queue is true. | `string` | `null` | no | | [iam\_policy](#input\_iam\_policy) | IAM policy as list of Terraform objects, compatible with Terraform `aws_iam_policy_document` data source
except that `source_policy_documents` and `override_policy_documents` are not included.
Use inputs `iam_source_policy_documents` and `iam_override_policy_documents` for that. |
list(object({
policy_id = optional(string, null)
version = optional(string, null)
statements = list(object({
sid = optional(string, null)
effect = optional(string, null)
actions = optional(list(string), null)
not_actions = optional(list(string), null)
resources = optional(list(string), null)
not_resources = optional(list(string), null)
conditions = optional(list(object({
test = string
variable = string
values = list(string)
})), [])
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
not_principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
}))
}))
| `[]` | no | | [iam\_policy\_limit\_to\_current\_account](#input\_iam\_policy\_limit\_to\_current\_account) | Boolean designating whether the IAM policy should be limited to the current account. | `bool` | `true` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [kms\_data\_key\_reuse\_period\_seconds](#input\_kms\_data\_key\_reuse\_period\_seconds) | The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. An integer representing seconds, between 60 seconds (1 minute) and 86,400 seconds (24 hours). The default is 300 (5 minutes). | `number` | `300` | no | -| [kms\_master\_key\_id](#input\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see Key Terms. | `list(string)` |
[
"alias/aws/sqs"
]
| no | +| [kms\_master\_key\_id](#input\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see Key Terms. | `string` | `"alias/aws/sqs"` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | @@ -93,11 +135,10 @@ components: | [message\_retention\_seconds](#input\_message\_retention\_seconds) | The number of seconds Amazon SQS retains a message. Integer representing seconds, from 60 (1 minute) to 1209600 (14 days). The default for this attribute is 345600 (4 days). | `number` | `345600` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [policy](#input\_policy) | The JSON policy for the SQS Queue. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy). | `list(string)` | `[]` | no | | [receive\_wait\_time\_seconds](#input\_receive\_wait\_time\_seconds) | The time for which a ReceiveMessage call will wait for a message to arrive (long polling) before returning. An integer from 0 to 20 (seconds). The default for this attribute is 0, meaning that the call will return immediately. | `number` | `0` | no | -| [redrive\_policy](#input\_redrive\_policy) | **DEPRECATED** This is deprecated and is left as an escape hatch, please use `dead_letter_sqs_component_name` or `dead_letter_sqs_arn` instead. The JSON policy to set up the Dead Letter Queue, see AWS docs. Note: when specifying maxReceiveCount, you must specify it as an integer (5), and not a string ("5"). | `list(string)` | `[]` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [sqs\_managed\_sse\_enabled](#input\_sqs\_managed\_sse\_enabled) | Boolean to enable server-side encryption (SSE) of message content with SQS-owned encryption keys | `bool` | `true` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | @@ -107,10 +148,7 @@ components: | Name | Description | |------|-------------| -| [arn](#output\_arn) | The ARN of the created Amazon SQS queue | -| [id](#output\_id) | The ID of the created Amazon SQS queue. Same as the URL. | -| [name](#output\_name) | The name of the created Amazon SQS queue. | -| [url](#output\_url) | The URL of the created Amazon SQS queue. | +| [sqs\_queue](#output\_sqs\_queue) | The SQS queue. | diff --git a/modules/sqs-queue/main.tf b/modules/sqs-queue/main.tf index 351b88108..31e81bd6e 100644 --- a/modules/sqs-queue/main.tf +++ b/modules/sqs-queue/main.tf @@ -1,32 +1,51 @@ locals { - enabled = module.this.enabled - aws_account_number = one(data.aws_caller_identity.current[*].account_id) - policy_enabled = local.enabled && length(var.iam_policy) > 0 - redrive_policy_enabled = local.enabled && (var.dead_letter_sqs_component_name != null || var.dead_letter_sqs_arn != null) + enabled = module.this.enabled + aws_account_number = one(data.aws_caller_identity.current[*].account_id) + policy_enabled = local.enabled && length(var.iam_policy) > 0 } -data "aws_caller_identity" "current" { - count = local.enabled ? 1 : 0 -} +module "sqs" { + source = "terraform-aws-modules/sqs/aws" + version = "4.2.0" + + name = module.this.id -module "sqs_queue" { - source = "./modules/terraform-aws-sqs-queue" + create_dlq = var.dlq_enabled + dlq_name = "${module.this.id}-${var.dlq_name_suffix}" + dlq_content_based_deduplication = var.dlq_content_based_deduplication + dlq_deduplication_scope = var.dlq_deduplication_scope + dlq_kms_master_key_id = var.dlq_kms_master_key_id + dlq_delay_seconds = var.dlq_delay_seconds + dlq_kms_data_key_reuse_period_seconds = var.dlq_kms_data_key_reuse_period_seconds + dlq_message_retention_seconds = var.dlq_message_retention_seconds + dlq_receive_wait_time_seconds = var.dlq_receive_wait_time_seconds + create_dlq_redrive_allow_policy = var.create_dlq_redrive_allow_policy + dlq_redrive_allow_policy = var.dlq_redrive_allow_policy + dlq_sqs_managed_sse_enabled = var.dlq_sqs_managed_sse_enabled + dlq_visibility_timeout_seconds = var.dlq_visibility_timeout_seconds + dlq_tags = merge(module.this.tags, var.dlq_tags) + redrive_policy = var.dlq_enabled ? { + maxReceiveCount = var.dlq_max_receive_count + } : {} visibility_timeout_seconds = var.visibility_timeout_seconds message_retention_seconds = var.message_retention_seconds - max_message_size = var.max_message_size delay_seconds = var.delay_seconds receive_wait_time_seconds = var.receive_wait_time_seconds - policy = try([var.policy[0]], []) - redrive_policy = try([var.redrive_policy[0]], []) + max_message_size = var.max_message_size fifo_queue = var.fifo_queue - fifo_throughput_limit = try([var.fifo_throughput_limit[0]], []) content_based_deduplication = var.content_based_deduplication - kms_master_key_id = try([var.kms_master_key_id[0]], []) + kms_master_key_id = var.kms_master_key_id kms_data_key_reuse_period_seconds = var.kms_data_key_reuse_period_seconds - deduplication_scope = try([var.deduplication_scope[0]], []) + sqs_managed_sse_enabled = var.sqs_managed_sse_enabled + fifo_throughput_limit = var.fifo_throughput_limit + deduplication_scope = var.deduplication_scope - context = module.this.context + tags = module.this.tags +} + +data "aws_caller_identity" "current" { + count = local.enabled ? 1 : 0 } module "queue_policy" { @@ -45,7 +64,7 @@ module "queue_policy" { merge( statement, { - resources = [module.sqs_queue.arn] + resources = [module.sqs.queue_arn] }, var.iam_policy_limit_to_current_account ? { conditions = concat(statement.conditions, [ @@ -67,16 +86,6 @@ module "queue_policy" { resource "aws_sqs_queue_policy" "sqs_queue_policy" { count = local.policy_enabled ? 1 : 0 - queue_url = module.sqs_queue.url + queue_url = module.sqs.queue_url policy = one(module.queue_policy[*].json) } - -resource "aws_sqs_queue_redrive_policy" "dead_letter_queue" { - count = local.redrive_policy_enabled != null ? 1 : 0 - - queue_url = module.sqs_queue.url - redrive_policy = jsonencode({ - deadLetterTargetArn = var.dead_letter_sqs_arn != null ? var.dead_letter_sqs_arn : one(module.dead_letter_sqs_remote_state[*].outputs.arn) - maxReceiveCount = var.dead_letter_max_receive_count - }) -} diff --git a/modules/sqs-queue/modules/terraform-aws-sqs-queue/context.tf b/modules/sqs-queue/modules/terraform-aws-sqs-queue/context.tf deleted file mode 100644 index 5e0ef8856..000000000 --- a/modules/sqs-queue/modules/terraform-aws-sqs-queue/context.tf +++ /dev/null @@ -1,279 +0,0 @@ -# -# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label -# All other instances of this file should be a copy of that one -# -# -# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf -# and then place it in your Terraform module to automatically get -# Cloud Posse's standard configuration inputs suitable for passing -# to Cloud Posse modules. -# -# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf -# -# Modules should access the whole context as `module.this.context` -# to get the input variables with nulls for defaults, -# for example `context = module.this.context`, -# and access individual variables as `module.this.`, -# with final values filled in. -# -# For example, when using defaults, `module.this.context.delimiter` -# will be null, and `module.this.delimiter` will be `-` (hyphen). -# - -module "this" { - source = "cloudposse/label/null" - version = "0.25.0" # requires Terraform >= 0.13.0 - - enabled = var.enabled - namespace = var.namespace - tenant = var.tenant - environment = var.environment - stage = var.stage - name = var.name - delimiter = var.delimiter - attributes = var.attributes - tags = var.tags - additional_tag_map = var.additional_tag_map - label_order = var.label_order - regex_replace_chars = var.regex_replace_chars - id_length_limit = var.id_length_limit - label_key_case = var.label_key_case - label_value_case = var.label_value_case - descriptor_formats = var.descriptor_formats - labels_as_tags = var.labels_as_tags - - context = var.context -} - -# Copy contents of cloudposse/terraform-null-label/variables.tf here - -variable "context" { - type = any - default = { - enabled = true - namespace = null - tenant = null - environment = null - stage = null - name = null - delimiter = null - attributes = [] - tags = {} - additional_tag_map = {} - regex_replace_chars = null - label_order = [] - id_length_limit = null - label_key_case = null - label_value_case = null - descriptor_formats = {} - # Note: we have to use [] instead of null for unset lists due to - # https://github.com/hashicorp/terraform/issues/28137 - # which was not fixed until Terraform 1.0.0, - # but we want the default to be all the labels in `label_order` - # and we want users to be able to prevent all tag generation - # by setting `labels_as_tags` to `[]`, so we need - # a different sentinel to indicate "default" - labels_as_tags = ["unset"] - } - description = <<-EOT - Single object for setting entire context at once. - See description of individual variables for details. - Leave string and numeric variables as `null` to use default value. - Individual variable settings (non-null) override settings in context object, - except for attributes, tags, and additional_tag_map, which are merged. - EOT - - validation { - condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) - error_message = "Allowed values: `lower`, `title`, `upper`." - } - - validation { - condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) - error_message = "Allowed values: `lower`, `title`, `upper`, `none`." - } -} - -variable "enabled" { - type = bool - default = null - description = "Set to false to prevent the module from creating any resources" -} - -variable "namespace" { - type = string - default = null - description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" -} - -variable "tenant" { - type = string - default = null - description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" -} - -variable "environment" { - type = string - default = null - description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" -} - -variable "stage" { - type = string - default = null - description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" -} - -variable "name" { - type = string - default = null - description = <<-EOT - ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. - This is the only ID element not also included as a `tag`. - The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. - EOT -} - -variable "delimiter" { - type = string - default = null - description = <<-EOT - Delimiter to be used between ID elements. - Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. - EOT -} - -variable "attributes" { - type = list(string) - default = [] - description = <<-EOT - ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, - in the order they appear in the list. New attributes are appended to the - end of the list. The elements of the list are joined by the `delimiter` - and treated as a single ID element. - EOT -} - -variable "labels_as_tags" { - type = set(string) - default = ["default"] - description = <<-EOT - Set of labels (ID elements) to include as tags in the `tags` output. - Default is to include all labels. - Tags with empty values will not be included in the `tags` output. - Set to `[]` to suppress all generated tags. - **Notes:** - The value of the `name` tag, if included, will be the `id`, not the `name`. - Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be - changed in later chained modules. Attempts to change it will be silently ignored. - EOT -} - -variable "tags" { - type = map(string) - default = {} - description = <<-EOT - Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). - Neither the tag keys nor the tag values will be modified by this module. - EOT -} - -variable "additional_tag_map" { - type = map(string) - default = {} - description = <<-EOT - Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. - This is for some rare cases where resources want additional configuration of tags - and therefore take a list of maps with tag key, value, and additional configuration. - EOT -} - -variable "label_order" { - type = list(string) - default = null - description = <<-EOT - The order in which the labels (ID elements) appear in the `id`. - Defaults to ["namespace", "environment", "stage", "name", "attributes"]. - You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. - EOT -} - -variable "regex_replace_chars" { - type = string - default = null - description = <<-EOT - Terraform regular expression (regex) string. - Characters matching the regex will be removed from the ID elements. - If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. - EOT -} - -variable "id_length_limit" { - type = number - default = null - description = <<-EOT - Limit `id` to this many characters (minimum 6). - Set to `0` for unlimited length. - Set to `null` for keep the existing setting, which defaults to `0`. - Does not affect `id_full`. - EOT - validation { - condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 - error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." - } -} - -variable "label_key_case" { - type = string - default = null - description = <<-EOT - Controls the letter case of the `tags` keys (label names) for tags generated by this module. - Does not affect keys of tags passed in via the `tags` input. - Possible values: `lower`, `title`, `upper`. - Default value: `title`. - EOT - - validation { - condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) - error_message = "Allowed values: `lower`, `title`, `upper`." - } -} - -variable "label_value_case" { - type = string - default = null - description = <<-EOT - Controls the letter case of ID elements (labels) as included in `id`, - set as tag values, and output by this module individually. - Does not affect values of tags passed in via the `tags` input. - Possible values: `lower`, `title`, `upper` and `none` (no transformation). - Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. - Default value: `lower`. - EOT - - validation { - condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) - error_message = "Allowed values: `lower`, `title`, `upper`, `none`." - } -} - -variable "descriptor_formats" { - type = any - default = {} - description = <<-EOT - Describe additional descriptors to be output in the `descriptors` output map. - Map of maps. Keys are names of descriptors. Values are maps of the form - `{ - format = string - labels = list(string) - }` - (Type is `any` so the map values can later be enhanced to provide additional options.) - `format` is a Terraform format string to be passed to the `format()` function. - `labels` is a list of labels, in order, to pass to `format()` function. - Label values will be normalized before being passed to `format()` so they will be - identical to how they appear in `id`. - Default is `{}` (`descriptors` output will be empty). - EOT -} - -#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/sqs-queue/modules/terraform-aws-sqs-queue/main.tf b/modules/sqs-queue/modules/terraform-aws-sqs-queue/main.tf deleted file mode 100644 index f49930c76..000000000 --- a/modules/sqs-queue/modules/terraform-aws-sqs-queue/main.tf +++ /dev/null @@ -1,24 +0,0 @@ -locals { - enabled = module.this.enabled -} - -resource "aws_sqs_queue" "default" { - count = local.enabled ? 1 : 0 - - name = var.fifo_queue ? "${module.this.id}.fifo" : module.this.id - visibility_timeout_seconds = var.visibility_timeout_seconds - message_retention_seconds = var.message_retention_seconds - max_message_size = var.max_message_size - delay_seconds = var.delay_seconds - receive_wait_time_seconds = var.receive_wait_time_seconds - policy = try(var.policy[0], null) - redrive_policy = try(var.redrive_policy[0], null) - fifo_queue = var.fifo_queue - fifo_throughput_limit = try(var.fifo_throughput_limit[0], null) - content_based_deduplication = var.content_based_deduplication - kms_master_key_id = try(var.kms_master_key_id[0], null) - kms_data_key_reuse_period_seconds = var.kms_data_key_reuse_period_seconds - deduplication_scope = try(var.deduplication_scope[0], null) - - tags = module.this.tags -} diff --git a/modules/sqs-queue/modules/terraform-aws-sqs-queue/outputs.tf b/modules/sqs-queue/modules/terraform-aws-sqs-queue/outputs.tf deleted file mode 100644 index 3aa19bae5..000000000 --- a/modules/sqs-queue/modules/terraform-aws-sqs-queue/outputs.tf +++ /dev/null @@ -1,19 +0,0 @@ -output "url" { - description = "The URL of the created Amazon SQS queue." - value = local.enabled ? aws_sqs_queue.default[0].url : null -} - -output "id" { - description = "The ID of the created Amazon SQS queue. Same as the URL." - value = local.enabled ? aws_sqs_queue.default[0].id : null -} - -output "name" { - description = "The name of the created Amazon SQS queue." - value = local.enabled ? module.this.id : null -} - -output "arn" { - description = "The ARN of the created Amazon SQS queue." - value = local.enabled ? aws_sqs_queue.default[0].arn : null -} diff --git a/modules/sqs-queue/modules/terraform-aws-sqs-queue/variables.tf b/modules/sqs-queue/modules/terraform-aws-sqs-queue/variables.tf deleted file mode 100644 index 2ba38d026..000000000 --- a/modules/sqs-queue/modules/terraform-aws-sqs-queue/variables.tf +++ /dev/null @@ -1,125 +0,0 @@ -variable "visibility_timeout_seconds" { - type = number - description = "The visibility timeout for the queue. An integer from 0 to 43200 (12 hours). The default for this attribute is 30. For more information about visibility timeout, see AWS docs." - default = 30 - validation { - condition = ( - var.visibility_timeout_seconds >= 0 && var.visibility_timeout_seconds <= 43200 - ) - error_message = "Var must be between 0 and 43200." - } -} - -variable "message_retention_seconds" { - type = number - description = "The number of seconds Amazon SQS retains a message. Integer representing seconds, from 60 (1 minute) to 1209600 (14 days). The default for this attribute is 345600 (4 days)." - default = 345600 - validation { - condition = ( - var.message_retention_seconds >= 60 && var.message_retention_seconds <= 1209600 - ) - error_message = "Var must be between 60 and 1209600." - } -} - -variable "max_message_size" { - type = number - description = "The limit of how many bytes a message can contain before Amazon SQS rejects it. An integer from 1024 bytes (1 KiB) up to 262144 bytes (256 KiB). The default for this attribute is 262144 (256 KiB)." - default = 262144 - validation { - condition = ( - var.max_message_size >= 1024 && var.max_message_size <= 262144 - ) - error_message = "Var must be between 1024 and 262144." - } -} - -variable "delay_seconds" { - type = number - description = "The time in seconds that the delivery of all messages in the queue will be delayed. An integer from 0 to 900 (15 minutes). The default for this attribute is 0 seconds." - default = 0 - validation { - condition = ( - var.delay_seconds >= 0 && var.delay_seconds <= 900 - ) - error_message = "Var must be between 0 and 900." - } -} - -variable "receive_wait_time_seconds" { - type = number - description = "The time for which a ReceiveMessage call will wait for a message to arrive (long polling) before returning. An integer from 0 to 20 (seconds). The default for this attribute is 0, meaning that the call will return immediately." - default = 0 - validation { - condition = ( - var.receive_wait_time_seconds >= 0 && var.receive_wait_time_seconds <= 20 - ) - error_message = "Var must be between 0 and 20." - } -} - -variable "policy" { - type = list(string) - description = "This is a list of 0 or 1. The JSON policy for the SQS queue. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy)." - default = [] -} - -variable "redrive_policy" { - type = list(string) - description = "This is a list of 0 or 1. The JSON policy to set up the Dead Letter Queue, see AWS docs. Note: when specifying maxReceiveCount, you must specify it as an integer (5), and not a string (\"5\")." - default = [] -} - -variable "fifo_queue" { - type = bool - description = "Boolean designating a FIFO queue. If not set, it defaults to false making it standard." - default = false -} - -variable "fifo_throughput_limit" { - type = list(string) - description = "This is a list of 0 or 1. Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group. Valid values are perQueue and perMessageGroupId. This can be specified if fifo_queue is true." - default = [] - validation { - condition = ( - length(var.fifo_throughput_limit) > 0 ? contains(["perQueue", "perMessageGroupId"], var.fifo_throughput_limit[0]) : true - ) - error_message = "Var must be one of \"perQueue\", \"perMessageGroupId\"." - } -} - -variable "content_based_deduplication" { - type = bool - description = "Enables content-based deduplication for FIFO queues. For more information, see the [related documentation](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html#FIFO-queues-exactly-once-processing)" - default = false -} - -variable "kms_master_key_id" { - type = list(string) - description = "This is a list of 0 or 1. The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see [Key Terms](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-server-side-encryption.html#sqs-sse-key-terms)." - default = ["alias/aws/sqs"] -} - -variable "kms_data_key_reuse_period_seconds" { - type = number - description = "The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. An integer representing seconds, between 60 seconds (1 minute) and 86,400 seconds (24 hours). The default is 300 (5 minutes)." - default = 300 - validation { - condition = ( - var.kms_data_key_reuse_period_seconds >= 60 && var.kms_data_key_reuse_period_seconds <= 86400 - ) - error_message = "Var must be between 60 and 86400." - } -} - -variable "deduplication_scope" { - type = list(string) - description = "This is a list of 0 or 1. Specifies whether message deduplication occurs at the message group or queue level. Valid values are messageGroup and queue. This can be specified if fifo_queue is true." - default = [] - validation { - condition = ( - length(var.deduplication_scope) > 0 ? contains(["messageGroup", "queue"], var.deduplication_scope[0]) : true - ) - error_message = "Var must be one of \"messageGroup\", \"queue\"." - } -} diff --git a/modules/sqs-queue/modules/terraform-aws-sqs-queue/versions.tf b/modules/sqs-queue/modules/terraform-aws-sqs-queue/versions.tf deleted file mode 100644 index f33ede77f..000000000 --- a/modules/sqs-queue/modules/terraform-aws-sqs-queue/versions.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = ">= 1.0.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 4.0" - } - } -} diff --git a/modules/sqs-queue/outputs.tf b/modules/sqs-queue/outputs.tf index 6126209db..ef290e84d 100644 --- a/modules/sqs-queue/outputs.tf +++ b/modules/sqs-queue/outputs.tf @@ -1,19 +1,4 @@ -output "url" { - description = "The URL of the created Amazon SQS queue." - value = module.sqs_queue.url -} - -output "id" { - description = "The ID of the created Amazon SQS queue. Same as the URL." - value = module.sqs_queue.id -} - -output "name" { - description = "The name of the created Amazon SQS queue." - value = module.sqs_queue.name -} - -output "arn" { - description = "The ARN of the created Amazon SQS queue" - value = module.sqs_queue.arn +output "sqs_queue" { + description = "The SQS queue." + value = module.sqs } diff --git a/modules/sqs-queue/remote-state.tf b/modules/sqs-queue/remote-state.tf deleted file mode 100644 index 0efbb43cc..000000000 --- a/modules/sqs-queue/remote-state.tf +++ /dev/null @@ -1,11 +0,0 @@ - -module "dead_letter_sqs_remote_state" { - count = var.dead_letter_sqs_component_name != null ? 1 : 0 - - source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.5.0" - - component = var.dead_letter_sqs_component_name - - context = module.this.context -} diff --git a/modules/sqs-queue/variables.tf b/modules/sqs-queue/variables.tf index a67b4580b..ce4f560e7 100644 --- a/modules/sqs-queue/variables.tf +++ b/modules/sqs-queue/variables.tf @@ -33,34 +33,94 @@ variable "receive_wait_time_seconds" { default = 0 } -variable "policy" { - type = list(string) - description = "The JSON policy for the SQS Queue. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy)." - default = [] +variable "dlq_enabled" { + type = bool + description = "Boolean designating whether the Dead Letter Queue should be created by this component." + default = false } -variable "redrive_policy" { - type = list(string) - description = "**DEPRECATED** This is deprecated and is left as an escape hatch, please use `dead_letter_sqs_component_name` or `dead_letter_sqs_arn` instead. The JSON policy to set up the Dead Letter Queue, see AWS docs. Note: when specifying maxReceiveCount, you must specify it as an integer (5), and not a string (\"5\")." - default = [] +variable "dlq_name_suffix" { + type = string + description = "The suffix of the Dead Letter Queue." + default = "dlq" } -variable "dead_letter_sqs_arn" { +variable "dlq_max_receive_count" { + type = number + description = "The number of times a message can be unsuccessfully dequeued before being moved to the Dead Letter Queue." + default = 5 +} + +variable "dlq_content_based_deduplication" { + description = "Enables content-based deduplication for FIFO queues" + type = bool + default = null +} + +variable "dlq_deduplication_scope" { + description = "Specifies whether message deduplication occurs at the message group or queue level" type = string - description = "The SQS url of the Dead Letter Queue. This is used to create the redrive policy." default = null } -variable "dead_letter_sqs_component_name" { +variable "dlq_delay_seconds" { + description = "The time in seconds that the delivery of all messages in the queue will be delayed. An integer from 0 to 900 (15 minutes)" + type = number + default = null +} + +variable "dlq_kms_data_key_reuse_period_seconds" { + description = "The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. An integer representing seconds, between 60 seconds (1 minute) and 86,400 seconds (24 hours)" + type = number + default = null +} + +variable "dlq_kms_master_key_id" { + description = "The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK" type = string - description = "The name of the component that will be looked up for the ARN and be used as the Dead Letter Queue." default = null } -variable "dead_letter_max_receive_count" { +variable "dlq_message_retention_seconds" { + description = "The number of seconds Amazon SQS retains a message. Integer representing seconds, from 60 (1 minute) to 1209600 (14 days)" type = number - description = "The number of times a message can be unsuccessfully dequeued before being moved to the Dead Letter Queue." - default = 5 + default = null +} + +variable "dlq_receive_wait_time_seconds" { + description = "The time for which a ReceiveMessage call will wait for a message to arrive (long polling) before returning. An integer from 0 to 20 (seconds)" + type = number + default = null +} + +variable "create_dlq_redrive_allow_policy" { + description = "Determines whether to create a redrive allow policy for the dead letter queue." + type = bool + default = true +} + +variable "dlq_redrive_allow_policy" { + description = "The JSON policy to set up the Dead Letter Queue redrive permission, see AWS docs." + type = any + default = {} +} + +variable "dlq_sqs_managed_sse_enabled" { + description = "Boolean to enable server-side encryption (SSE) of message content with SQS-owned encryption keys" + type = bool + default = true +} + +variable "dlq_visibility_timeout_seconds" { + description = "The visibility timeout for the queue. An integer from 0 to 43200 (12 hours)" + type = number + default = null +} + +variable "dlq_tags" { + description = "A mapping of additional tags to assign to the dead letter queue" + type = map(string) + default = {} } variable "fifo_queue" { @@ -70,9 +130,9 @@ variable "fifo_queue" { } variable "fifo_throughput_limit" { - type = list(string) + type = string description = "Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group. Valid values are perQueue and perMessageGroupId. This can be specified if fifo_queue is true." - default = [] + default = null } variable "content_based_deduplication" { @@ -82,9 +142,9 @@ variable "content_based_deduplication" { } variable "kms_master_key_id" { - type = list(string) + type = string description = "The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see Key Terms." - default = ["alias/aws/sqs"] + default = "alias/aws/sqs" } variable "kms_data_key_reuse_period_seconds" { @@ -93,15 +153,21 @@ variable "kms_data_key_reuse_period_seconds" { default = 300 } +variable "iam_policy_limit_to_current_account" { + type = bool + description = "Boolean designating whether the IAM policy should be limited to the current account." + default = true +} + variable "deduplication_scope" { - type = list(string) - description = "Specifies whether message deduplication occurs at the message group or queue level. Valid values are messageGroup and queue. This can be specified if fifo_queue is true." - default = [] + description = "Specifies whether message deduplication occurs at the message group or queue level" + type = string + default = null } -variable "iam_policy_limit_to_current_account" { +variable "sqs_managed_sse_enabled" { + description = "Boolean to enable server-side encryption (SSE) of message content with SQS-owned encryption keys" type = bool - description = "Boolean designating whether the IAM policy should be limited to the current account." default = true } From c019843dc1cb3481ad5340016b13394b0df7b998 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 22 May 2024 13:01:44 -0700 Subject: [PATCH 416/501] `sqs-queue` Update default KMS key to be null (#1043) --- modules/sqs-queue/README.md | 5 +++-- modules/sqs-queue/variables.tf | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index d8efdbfac..4c0f1b786 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -35,7 +35,8 @@ components: dlq_max_receive_count: 1 dlq_kms_data_key_reuse_period_seconds: 86400 # 1 day kms_data_key_reuse_period_seconds: 86400 # 1 day - sqs_managed_sse_enabled: true # SSE vs KMS + # kms_master_key_id: "alias/aws/sqs" # Use KMS # default null + sqs_managed_sse_enabled: true # SSE vs KMS (Priority goes to KMS) iam_policy_limit_to_current_account: true # default true iam_policy: - version: 2012-10-17 @@ -126,7 +127,7 @@ components: | [iam\_policy\_limit\_to\_current\_account](#input\_iam\_policy\_limit\_to\_current\_account) | Boolean designating whether the IAM policy should be limited to the current account. | `bool` | `true` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [kms\_data\_key\_reuse\_period\_seconds](#input\_kms\_data\_key\_reuse\_period\_seconds) | The length of time, in seconds, for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. An integer representing seconds, between 60 seconds (1 minute) and 86,400 seconds (24 hours). The default is 300 (5 minutes). | `number` | `300` | no | -| [kms\_master\_key\_id](#input\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see Key Terms. | `string` | `"alias/aws/sqs"` | no | +| [kms\_master\_key\_id](#input\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see Key Terms. | `string` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | diff --git a/modules/sqs-queue/variables.tf b/modules/sqs-queue/variables.tf index ce4f560e7..48f938e2e 100644 --- a/modules/sqs-queue/variables.tf +++ b/modules/sqs-queue/variables.tf @@ -144,7 +144,7 @@ variable "content_based_deduplication" { variable "kms_master_key_id" { type = string description = "The ID of an AWS-managed customer master key (CMK) for Amazon SQS or a custom CMK. For more information, see Key Terms." - default = "alias/aws/sqs" + default = null } variable "kms_data_key_reuse_period_seconds" { From 138fda915cb9d5e7d5696bdc003dc00f7fbecf35 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 22 May 2024 14:21:13 -0700 Subject: [PATCH 417/501] feat: `elasticache-redis` Disable Default VPC Ingress Rule (#1044) --- modules/elasticache-redis/README.md | 2 ++ modules/elasticache-redis/main.tf | 4 +--- modules/elasticache-redis/variables.tf | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md index 1f33e1a24..0088fa0e8 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -96,6 +96,7 @@ No resources. |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [allow\_all\_egress](#input\_allow\_all\_egress) | If `true`, the created security group will allow egress on all ports and protocols to all IP address.
If this is false and no egress rules are otherwise specified, then no egress will be allowed. | `bool` | `true` | no | +| [allow\_ingress\_from\_this\_vpc](#input\_allow\_ingress\_from\_this\_vpc) | If set to `true`, allow ingress from the VPC CIDR for this account | `bool` | `true` | no | | [allow\_ingress\_from\_vpc\_stages](#input\_allow\_ingress\_from\_vpc\_stages) | List of stages to pull VPC ingress cidr and add to security group | `list(string)` | `[]` | no | | [apply\_immediately](#input\_apply\_immediately) | Apply changes immediately | `bool` | n/a | yes | | [at\_rest\_encryption\_enabled](#input\_at\_rest\_encryption\_enabled) | Enable encryption at rest | `bool` | n/a | yes | @@ -118,6 +119,7 @@ No resources. | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [multi\_az\_enabled](#input\_multi\_az\_enabled) | Multi AZ (Automatic Failover must also be enabled. If Cluster Mode is enabled, Multi AZ is on by default, and this setting is ignored) | `bool` | `false` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [port](#input\_port) | Port number | `number` | n/a | yes | diff --git a/modules/elasticache-redis/main.tf b/modules/elasticache-redis/main.tf index 2e5f8ead9..620c06a70 100644 --- a/modules/elasticache-redis/main.tf +++ b/modules/elasticache-redis/main.tf @@ -3,10 +3,8 @@ locals { eks_security_group_enabled = local.enabled && var.eks_security_group_enabled - vpc_cidr = module.vpc.outputs.vpc_cidr - allowed_cidr_blocks = concat( - [local.vpc_cidr], + var.allow_ingress_from_this_vpc ? [module.vpc.outputs.vpc_cidr] : [], var.ingress_cidr_blocks, [ for k in keys(module.vpc_ingress) : diff --git a/modules/elasticache-redis/variables.tf b/modules/elasticache-redis/variables.tf index 6f9739ff3..3bf784a4a 100644 --- a/modules/elasticache-redis/variables.tf +++ b/modules/elasticache-redis/variables.tf @@ -75,6 +75,12 @@ variable "redis_clusters" { description = "Redis cluster configuration" } +variable "allow_ingress_from_this_vpc" { + type = bool + default = true + description = "If set to `true`, allow ingress from the VPC CIDR for this account" +} + variable "allow_ingress_from_vpc_stages" { type = list(string) default = [] From 3fb01a2578758ab489a41f5ee90f5b2631d30a71 Mon Sep 17 00:00:00 2001 From: nitrocode <7775707+nitrocode@users.noreply.github.com> Date: Fri, 24 May 2024 13:03:17 -0500 Subject: [PATCH 418/501] feat: allow overriding the dynamodb table name (#1016) Signed-off-by: nitrocode <7775707+nitrocode@users.noreply.github.com> Co-authored-by: Dan Miller --- modules/dynamodb/README.md | 3 ++- modules/dynamodb/main.tf | 3 ++- modules/dynamodb/variables.tf | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/dynamodb/README.md b/modules/dynamodb/README.md index 48c5c4d0b..8d8cd36ff 100644 --- a/modules/dynamodb/README.md +++ b/modules/dynamodb/README.md @@ -44,7 +44,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [dynamodb\_table](#module\_dynamodb\_table) | cloudposse/dynamodb/aws | 0.35.0 | +| [dynamodb\_table](#module\_dynamodb\_table) | cloudposse/dynamodb/aws | 0.36.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -97,6 +97,7 @@ No resources. | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [stream\_view\_type](#input\_stream\_view\_type) | When an item in the table is modified, what information is written to the stream | `string` | `""` | no | | [streams\_enabled](#input\_streams\_enabled) | Enable DynamoDB streams | `bool` | `false` | no | +| [table\_name](#input\_table\_name) | Table name. If provided, the bucket will be created with this name instead of generating the name from the context | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [ttl\_attribute](#input\_ttl\_attribute) | DynamoDB table TTL attribute | `string` | `""` | no | diff --git a/modules/dynamodb/main.tf b/modules/dynamodb/main.tf index 6bd309b44..979d66b2e 100644 --- a/modules/dynamodb/main.tf +++ b/modules/dynamodb/main.tf @@ -6,8 +6,9 @@ locals { module "dynamodb_table" { source = "cloudposse/dynamodb/aws" - version = "0.35.0" + version = "0.36.0" + table_name = var.table_name billing_mode = var.billing_mode replicas = var.replicas dynamodb_attributes = var.dynamodb_attributes diff --git a/modules/dynamodb/variables.tf b/modules/dynamodb/variables.tf index 764cc51d8..a0e7b593e 100644 --- a/modules/dynamodb/variables.tf +++ b/modules/dynamodb/variables.tf @@ -128,6 +128,12 @@ variable "autoscaler_tags" { description = "Additional resource tags for the autoscaler module" } +variable "table_name" { + type = string + default = null + description = "Table name. If provided, the bucket will be created with this name instead of generating the name from the context" +} + variable "dynamodb_attributes" { type = list(object({ name = string From af287c155909d7a20b3a507346090827edc64c85 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 24 May 2024 20:08:26 -0400 Subject: [PATCH 419/501] feat: add org compliance packs to aws-config (#1006) Co-authored-by: Andriy Knysh --- modules/aws-config/README.md | 183 +++++++----- modules/aws-config/main.tf | 30 +- .../modules/org-conformance-pack/README.md | 48 +++ .../modules/org-conformance-pack/context.tf | 279 ++++++++++++++++++ .../modules/org-conformance-pack/main.tf | 17 ++ .../modules/org-conformance-pack/outputs.tf | 4 + .../modules/org-conformance-pack/variables.tf | 10 + .../modules/org-conformance-pack/versions.tf | 15 + modules/aws-config/variables.tf | 16 + 9 files changed, 526 insertions(+), 76 deletions(-) create mode 100644 modules/aws-config/modules/org-conformance-pack/README.md create mode 100644 modules/aws-config/modules/org-conformance-pack/context.tf create mode 100644 modules/aws-config/modules/org-conformance-pack/main.tf create mode 100644 modules/aws-config/modules/org-conformance-pack/outputs.tf create mode 100644 modules/aws-config/modules/org-conformance-pack/variables.tf create mode 100644 modules/aws-config/modules/org-conformance-pack/versions.tf diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index 481850a1c..c8f35b94c 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -20,6 +20,26 @@ Some of the key features of AWS Config include: - Notifications and alerts: AWS Config can send notifications and alerts when changes are made to your AWS resources that could impact their compliance or security posture. +:::caution AWS Config Limitations + +You'll also want to be aware of some limitations with AWS Config: + +- The maximum number of AWS Config rules that can be evaluated in a single account is 1000. + - This can be mitigated by removing rules that are duplicated across packs. You'll have to manually search for these + duplicates. + - You can also look for rules that do not apply to any resources and remove those. You'll have to manually click + through rules in the AWS Config interface to see which rules are not being evaluated. + - If you end up still needing more than 1000 rules, one recommendation is to only run packs on a schedule with a + lambda that removes the pack after results are collected. If you had different schedule for each day of the week, + that would mean 7000 rules over the week. The aggregators would not be able to handle this, so you would need to + make sure to store them somewhere else (i.e. S3) so the findings are not lost. + - See the + [Audit Manager docs](https://aws.amazon.com/blogs/mt/integrate-across-the-three-lines-model-part-2-transform-aws-config-conformance-packs-into-aws-audit-manager-assessments/) + if you think you would like to convert conformance packs to custom Audit Manager assessments. +- The maximum number of AWS Config conformance packs that can be created in a single account is 50. + +::: + Overall, AWS Config provides you with a powerful toolset to help you monitor and manage the configurations of your AWS resources, ensuring that they remain compliant, secure, and properly configured over time. @@ -54,9 +74,26 @@ Before deploying this AWS Config component `config-bucket` and `cloudtrail-bucke ## Usage -**Stack Level**: Regional +**Stack Level**: Regional or Global + +This component has a `default_scope` variable for configuring if it will be an organization-wide or account-level +component by default. Note that this can be overridden by the `scope` variable in the `conformance_packs` items. + +:::info Using the account default_scope + +If default_scope == `account`, AWS Config is regional AWS service, so this component needs to be deployed to all +regions. If an individual `conformance_packs` item has `scope` set to `organization`, that particular pack will be +deployed to the organization level. + +::: -_**NOTE**: Since AWS Config is regional AWS service, this component needs to be deployed to all regions._ +:::info Using the organization default_scope + +If default_scope == `organization`, AWS Config is global unless overriden in the `conformance_packs` items. You will +need to update your org to allow the `config-multiaccountsetup.amazonaws.com` service access principal for this to work. +If you are using our `account` component, just add that principal to the `aws_service_access_principals` variable. + +::: At the AWS Organizational level, the Components designate an account to be the `central collection account` and a single region to be the `central collection region` so that compliance information can be aggregated into a central location. @@ -99,7 +136,7 @@ components: input_parameters: maxAccessKeyAge: "30" enabled: true - tags: {} + tags: { } ``` ## Deployment @@ -122,95 +159,97 @@ Apply aws-config to all stacks in all stages. atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} ``` - + ## Requirements -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 4.0 | -| [awsutils](#requirement\_awsutils) | >= 0.16.0 | +| Name | Version | +| ------------------------------------------------------------------------ | --------- | +| [terraform](#requirement_terraform) | >= 1.0.0 | +| [aws](#requirement_aws) | >= 4.0 | +| [awsutils](#requirement_awsutils) | >= 0.16.0 | ## Providers -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | >= 4.0 | +| Name | Version | +| ------------------------------------------------ | ------- | +| [aws](#provider_aws) | >= 4.0 | ## Modules -| Name | Source | Version | -|------|--------|---------| -| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [aws\_config](#module\_aws\_config) | cloudposse/config/aws | 1.1.0 | -| [aws\_config\_label](#module\_aws\_config\_label) | cloudposse/label/null | 0.25.0 | -| [aws\_team\_roles](#module\_aws\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [config\_bucket](#module\_config\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [conformance\_pack](#module\_conformance\_pack) | cloudposse/config/aws//modules/conformance-pack | 1.1.0 | -| [global\_collector\_region](#module\_global\_collector\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [this](#module\_this) | cloudposse/label/null | 0.25.0 | -| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | +| Name | Source | Version | +| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------- | +| [account_map](#module_account_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [aws_config](#module_aws_config) | cloudposse/config/aws | 1.1.0 | +| [aws_config_label](#module_aws_config_label) | cloudposse/label/null | 0.25.0 | +| [aws_team_roles](#module_aws_team_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [config_bucket](#module_config_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [conformance_pack](#module_conformance_pack) | cloudposse/config/aws//modules/conformance-pack | 1.1.0 | +| [global_collector_region](#module_global_collector_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam_roles](#module_iam_roles) | ../account-map/modules/iam-roles | n/a | +| [org_conformance_pack](#module_org_conformance_pack) | ./modules/org-conformance-pack | n/a | +| [this](#module_this) | cloudposse/label/null | 0.25.0 | +| [utils](#module_utils) | cloudposse/utils/aws | 1.3.0 | ## Resources -| Name | Type | -|------|------| +| Name | Type | +| -------------------------------------------------------------------------------------------------------------------------- | ----------- | | [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | -| [aws_partition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | -| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [aws_partition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [account\_map\_tenant](#input\_account\_map\_tenant) | (Optional) The tenant where the account\_map component required by remote-state is deployed. | `string` | `""` | no | -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [az\_abbreviation\_type](#input\_az\_abbreviation\_type) | AZ abbreviation type, `fixed` or `short` | `string` | `"fixed"` | no | -| [central\_resource\_collector\_account](#input\_central\_resource\_collector\_account) | The name of the account that is the centralized aggregation account. | `string` | n/a | yes | -| [config\_bucket\_env](#input\_config\_bucket\_env) | The environment of the AWS Config S3 Bucket | `string` | n/a | yes | -| [config\_bucket\_stage](#input\_config\_bucket\_stage) | The stage of the AWS Config S3 Bucket | `string` | n/a | yes | -| [config\_bucket\_tenant](#input\_config\_bucket\_tenant) | (Optional) The tenant of the AWS Config S3 Bucket | `string` | `""` | no | -| [conformance\_packs](#input\_conformance\_packs) | List of conformance packs. Each conformance pack is a map with the following keys: name, conformance\_pack, parameter\_overrides.

For example:
conformance\_packs = [
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level1"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level1.yaml"
parameter\_overrides = {
"AccessKeysRotatedParamMaxAccessKeyAge" = "45"
}
},
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level2"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml"
parameter\_overrides = {
"IamPasswordPolicyParamMaxPasswordAge" = "45"
}
}
]

Complete list of AWS Conformance Packs managed by AWSLabs can be found here:
https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs |
list(object({
name = string
conformance_pack = string
parameter_overrides = map(string)
}))
| `[]` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create\_iam\_role](#input\_create\_iam\_role) | Flag to indicate whether an IAM Role should be created to grant the proper permissions for AWS Config | `bool` | `false` | no | -| [delegated\_accounts](#input\_delegated\_accounts) | The account IDs of other accounts that will send their AWS Configuration or Security Hub data to this account | `set(string)` | `null` | no | -| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | -| [global\_resource\_collector\_region](#input\_global\_resource\_collector\_region) | The region that collects AWS Config data for global resources such as IAM | `string` | n/a | yes | -| [iam\_role\_arn](#input\_iam\_role\_arn) | The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the
AWS resources associated with the account. This is only used if create\_iam\_role is false.

If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create\_iam\_role to `false`.

See the AWS Docs for further information:
http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html | `string` | `null` | no | -| [iam\_roles\_environment\_name](#input\_iam\_roles\_environment\_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | -| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [managed\_rules](#input\_managed\_rules) | A list of AWS Managed Rules that should be enabled on the account.

See the following for a list of possible rules to enable:
https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html

Example:
managed_rules = {
access-keys-rotated = {
identifier = "ACCESS_KEYS_ROTATED"
description = "Checks whether the active access keys are rotated within the number of days specified in maxAccessKeyAge. The rule is NON_COMPLIANT if the access keys have not been rotated for more than maxAccessKeyAge number of days."
input_parameters = {
maxAccessKeyAge : "90"
}
enabled = true
tags = {}
}
}
|
map(object({
description = string
identifier = string
input_parameters = any
tags = map(string)
enabled = bool
}))
| `{}` | no | -| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | -| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (master) account | `string` | `"root"` | no | -| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| Name | Description | Type | Default | Required | +| --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | +| [account_map_tenant](#input_account_map_tenant) | (Optional) The tenant where the account_map component required by remote-state is deployed. | `string` | `""` | no | +| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [az_abbreviation_type](#input_az_abbreviation_type) | AZ abbreviation type, `fixed` or `short` | `string` | `"fixed"` | no | +| [central_resource_collector_account](#input_central_resource_collector_account) | The name of the account that is the centralized aggregation account. | `string` | n/a | yes | +| [config_bucket_env](#input_config_bucket_env) | The environment of the AWS Config S3 Bucket | `string` | n/a | yes | +| [config_bucket_stage](#input_config_bucket_stage) | The stage of the AWS Config S3 Bucket | `string` | n/a | yes | +| [config_bucket_tenant](#input_config_bucket_tenant) | (Optional) The tenant of the AWS Config S3 Bucket | `string` | `""` | no | +| [conformance_packs](#input_conformance_packs) | List of conformance packs. Each conformance pack is a map with the following keys: name, conformance_pack, parameter_overrides.

For example:
conformance_packs = [
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level1"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level1.yaml"
parameter\_overrides = {
"AccessKeysRotatedParamMaxAccessKeyAge" = "45"
}
},
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level2"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml"
parameter\_overrides = {
"IamPasswordPolicyParamMaxPasswordAge" = "45"
}
}
]

Complete list of AWS Conformance Packs managed by AWSLabs can be found here:
https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs |
list(object({
name = string
conformance_pack = string
parameter_overrides = map(string)
scope = optional(string, null)
}))
| `[]` | no | +| [context](#input_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create_iam_role](#input_create_iam_role) | Flag to indicate whether an IAM Role should be created to grant the proper permissions for AWS Config | `bool` | `false` | no | +| [default_scope](#input_default_scope) | The default scope of the conformance pack. Valid values are `account` and `organization`. | `string` | `"account"` | no | +| [delegated_accounts](#input_delegated_accounts) | The account IDs of other accounts that will send their AWS Configuration or Security Hub data to this account | `set(string)` | `null` | no | +| [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global_environment](#input_global_environment) | Global environment name | `string` | `"gbl"` | no | +| [global_resource_collector_region](#input_global_resource_collector_region) | The region that collects AWS Config data for global resources such as IAM | `string` | n/a | yes | +| [iam_role_arn](#input_iam_role_arn) | The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the
AWS resources associated with the account. This is only used if create_iam_role is false.

If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create_iam_role to `false`.

See the AWS Docs for further information:
http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html | `string` | `null` | no | +| [iam_roles_environment_name](#input_iam_roles_environment_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | +| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [managed_rules](#input_managed_rules) | A list of AWS Managed Rules that should be enabled on the account.

See the following for a list of possible rules to enable:
https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html

Example:
managed_rules = {
access-keys-rotated = {
identifier = "ACCESS_KEYS_ROTATED"
description = "Checks whether the active access keys are rotated within the number of days specified in maxAccessKeyAge. The rule is NON_COMPLIANT if the access keys have not been rotated for more than maxAccessKeyAge number of days."
input_parameters = {
maxAccessKeyAge : "90"
}
enabled = true
tags = {}
}
}
|
map(object({
description = string
identifier = string
input_parameters = any
tags = map(string)
enabled = bool
}))
| `{}` | no | +| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [privileged](#input_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | +| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input_region) | AWS Region | `string` | n/a | yes | +| [root_account_stage](#input_root_account_stage) | The stage name for the Organization root (master) account | `string` | `"root"` | no | +| [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | ## Outputs -| Name | Description | -|------|-------------| -| [aws\_config\_configuration\_recorder\_id](#output\_aws\_config\_configuration\_recorder\_id) | The ID of the AWS Config Recorder | -| [aws\_config\_iam\_role](#output\_aws\_config\_iam\_role) | The ARN of the IAM Role used for AWS Config | -| [storage\_bucket\_arn](#output\_storage\_bucket\_arn) | Storage Config bucket ARN | -| [storage\_bucket\_id](#output\_storage\_bucket\_id) | Storage Config bucket ID | +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| [aws_config_configuration_recorder_id](#output_aws_config_configuration_recorder_id) | The ID of the AWS Config Recorder | +| [aws_config_iam_role](#output_aws_config_iam_role) | The ARN of the IAM Role used for AWS Config | +| [storage_bucket_arn](#output_storage_bucket_arn) | Storage Config bucket ARN | +| [storage_bucket_id](#output_storage_bucket_id) | Storage Config bucket ID | + - ## References diff --git a/modules/aws-config/main.tf b/modules/aws-config/main.tf index 28b444184..b36912ada 100644 --- a/modules/aws-config/main.tf +++ b/modules/aws-config/main.tf @@ -40,15 +40,37 @@ module "utils" { context = module.this.context } +locals { + packs = [for pack in var.conformance_packs : merge(pack, { scope = coalesce(pack.scope, var.default_scope) })] + account_packs = { for pack in local.packs : pack.name => pack if pack.scope == "account" } + org_packs = { for pack in local.packs : pack.name => pack if pack.scope == "organization" } +} + module "conformance_pack" { source = "cloudposse/config/aws//modules/conformance-pack" version = "1.1.0" - count = local.enabled ? length(var.conformance_packs) : 0 + for_each = local.enabled ? local.account_packs : {} + + name = each.key + conformance_pack = each.value.conformance_pack + parameter_overrides = each.value.parameter_overrides + + depends_on = [ + module.aws_config + ] + + context = module.this.context +} + +module "org_conformance_pack" { + source = "./modules/org-conformance-pack" + + for_each = local.enabled ? local.org_packs : {} - name = var.conformance_packs[count.index].name - conformance_pack = var.conformance_packs[count.index].conformance_pack - parameter_overrides = var.conformance_packs[count.index].parameter_overrides + name = each.key + conformance_pack = each.value.conformance_pack + parameter_overrides = each.value.parameter_overrides depends_on = [ module.aws_config diff --git a/modules/aws-config/modules/org-conformance-pack/README.md b/modules/aws-config/modules/org-conformance-pack/README.md new file mode 100644 index 000000000..5e196d5ff --- /dev/null +++ b/modules/aws-config/modules/org-conformance-pack/README.md @@ -0,0 +1,48 @@ +# AWS Config Conformance Pack + +This module deploys a +[Conformance Pack](https://docs.aws.amazon.com/config/latest/developerguide/conformance-packs.html). A conformance pack +is a collection of AWS Config rules and remediation actions that can be easily deployed as a single entity in an account +and a Region or across an organization in AWS Organizations. Conformance packs are created by authoring a YAML template +that contains the list of AWS Config managed or custom rules and remediation actions. + +The Conformance Pack cannot be deployed until AWS Config is deployed, which can be deployed using the +[aws-config](../../) component. + +## Usage + +First, make sure your root `account` allows the service access principal `config-multiaccountsetup.amazonaws.com` to +update child organizations. You can see the docs on the account module here: +[aws_service_access_principals](https://docs.cloudposse.com/components/library/aws/account/#input_aws_service_access_principals) + +Then you have two options: + +- Set the `default_scope` of the parent `aws-config` component to be `organization` (can be overridden by the `scope` of + each `conformance_packs` item) +- Set the `scope` of the `conformance_packs` item to be `organization` + +An example YAML stack config for Atmos follows. Note, that both options are shown for demonstration purposes. In +practice you should only have one `aws-config` per account: + +```yaml +components: + terraform: + account: + vars: + aws_service_access_principals: + - config-multiaccountsetup.amazonaws.com + + aws-config/cis/level-1: + vars: + conformance_packs: + - name: Operational-Best-Practices-for-CIS-AWS-v1.4-Level1 + conformance_pack: https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level1.yaml + scope: organization + + aws-config/cis/level-2: + vars: + default_scope: organization + conformance_packs: + - name: Operational-Best-Practices-for-CIS-AWS-v1.4-Level2 + conformance_pack: https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml +``` diff --git a/modules/aws-config/modules/org-conformance-pack/context.tf b/modules/aws-config/modules/org-conformance-pack/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/aws-config/modules/org-conformance-pack/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/aws-config/modules/org-conformance-pack/main.tf b/modules/aws-config/modules/org-conformance-pack/main.tf new file mode 100644 index 000000000..b2c021536 --- /dev/null +++ b/modules/aws-config/modules/org-conformance-pack/main.tf @@ -0,0 +1,17 @@ +resource "aws_config_organization_conformance_pack" "default" { + name = module.this.name + + dynamic "input_parameter" { + for_each = var.parameter_overrides + content { + parameter_name = input_parameter.key + parameter_value = input_parameter.value + } + } + + template_body = data.http.conformance_pack.body +} + +data "http" "conformance_pack" { + url = var.conformance_pack +} diff --git a/modules/aws-config/modules/org-conformance-pack/outputs.tf b/modules/aws-config/modules/org-conformance-pack/outputs.tf new file mode 100644 index 000000000..f3b7cef11 --- /dev/null +++ b/modules/aws-config/modules/org-conformance-pack/outputs.tf @@ -0,0 +1,4 @@ +output "arn" { + value = aws_config_organization_conformance_pack.default.arn + description = "ARN for the AWS Config Organization Conformance Pack" +} diff --git a/modules/aws-config/modules/org-conformance-pack/variables.tf b/modules/aws-config/modules/org-conformance-pack/variables.tf new file mode 100644 index 000000000..cb92dbf5c --- /dev/null +++ b/modules/aws-config/modules/org-conformance-pack/variables.tf @@ -0,0 +1,10 @@ +variable "conformance_pack" { + type = string + description = "The URL to a Conformance Pack" +} + +variable "parameter_overrides" { + type = map(any) + description = "A map of parameters names to values to override from the template" + default = {} +} diff --git a/modules/aws-config/modules/org-conformance-pack/versions.tf b/modules/aws-config/modules/org-conformance-pack/versions.tf new file mode 100644 index 000000000..cff384723 --- /dev/null +++ b/modules/aws-config/modules/org-conformance-pack/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + + http = { + source = "hashicorp/http" + version = ">= 2.1.0" + } + } +} diff --git a/modules/aws-config/variables.tf b/modules/aws-config/variables.tf index cf8d17c8d..367ddc360 100644 --- a/modules/aws-config/variables.tf +++ b/modules/aws-config/variables.tf @@ -108,8 +108,14 @@ variable "conformance_packs" { name = string conformance_pack = string parameter_overrides = map(string) + scope = optional(string, null) })) default = [] + validation { + # verify scope is valid + condition = alltrue([for conformance_pack in var.conformance_packs : conformance_pack.scope == null || conformance_pack.scope == "account" || conformance_pack.scope == "organization"]) + error_message = "The scope must be either `account` or `organization`." + } } variable "delegated_accounts" { @@ -155,3 +161,13 @@ variable "managed_rules" { })) default = {} } + +variable "default_scope" { + type = string + description = "The default scope of the conformance pack. Valid values are `account` and `organization`." + default = "account" + validation { + condition = var.default_scope == "account" || var.default_scope == "organization" + error_message = "The scope must be either `account` or `organization`." + } +} From c289dac597912c40371910e390a4abe90f9cc724 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Sat, 25 May 2024 02:04:27 -0400 Subject: [PATCH 420/501] chore: karpenter v1beta upgrade (#1039) --- .../eks/karpenter-provisioner/README.md | 9 + .../eks/karpenter-provisioner/context.tf | 0 .../eks/karpenter-provisioner/main.tf | 0 .../eks/karpenter-provisioner/outputs.tf | 0 .../karpenter-provisioner/provider-helm.tf | 0 .../eks/karpenter-provisioner/providers.tf | 0 .../eks/karpenter-provisioner/remote-state.tf | 0 .../eks/karpenter-provisioner/variables.tf | 0 .../eks/karpenter-provisioner/versions.tf | 0 deprecated/eks/karpenter/CHANGELOG.md | 85 ++++ deprecated/eks/karpenter/README.md | 467 ++++++++++++++++++ deprecated/eks/karpenter/context.tf | 279 +++++++++++ .../eks/karpenter/interruption_handler.tf | 99 ++++ .../eks/karpenter/karpenter-crd-upgrade | 24 + deprecated/eks/karpenter/main.tf | 236 +++++++++ deprecated/eks/karpenter/outputs.tf | 9 + deprecated/eks/karpenter/provider-helm.tf | 201 ++++++++ deprecated/eks/karpenter/providers.tf | 19 + deprecated/eks/karpenter/remote-state.tf | 8 + deprecated/eks/karpenter/variables.tf | 134 +++++ deprecated/eks/karpenter/versions.tf | 18 + modules/eks/cluster/README.md | 1 - modules/eks/cluster/main.tf | 1 - modules/eks/cluster/remote-state.tf | 18 - modules/eks/cluster/variables.tf | 7 - modules/eks/karpenter-node-pool/README.md | 232 +++++++++ modules/eks/karpenter-node-pool/context.tf | 279 +++++++++++ .../eks/karpenter-node-pool/ec2-node-class.tf | 53 ++ modules/eks/karpenter-node-pool/main.tf | 70 +++ modules/eks/karpenter-node-pool/outputs.tf | 9 + .../eks/karpenter-node-pool/provider-helm.tf | 201 ++++++++ modules/eks/karpenter-node-pool/providers.tf | 29 ++ .../eks/karpenter-node-pool/remote-state.tf | 24 + modules/eks/karpenter-node-pool/variables.tf | 124 +++++ modules/eks/karpenter-node-pool/versions.tf | 18 + modules/eks/karpenter/CHANGELOG.md | 29 ++ modules/eks/karpenter/README.md | 318 ++++++------ .../karpenter/controller-policy-v1alpha.tf | 67 +++ modules/eks/karpenter/controller-policy.tf | 298 +++++++++++ .../docs/v1alpha-to-v1beta-migration.md | 209 ++++++++ modules/eks/karpenter/interruption_handler.tf | 10 +- modules/eks/karpenter/karpenter-crd-upgrade | 12 +- modules/eks/karpenter/main.tf | 199 ++------ modules/eks/karpenter/outputs.tf | 5 - modules/eks/karpenter/provider-helm.tf | 69 +-- modules/eks/karpenter/remote-state.tf | 8 + modules/eks/karpenter/variables.tf | 59 ++- modules/eks/karpenter/versions.tf | 2 +- 48 files changed, 3497 insertions(+), 442 deletions(-) rename {modules => deprecated}/eks/karpenter-provisioner/README.md (98%) rename {modules => deprecated}/eks/karpenter-provisioner/context.tf (100%) rename {modules => deprecated}/eks/karpenter-provisioner/main.tf (100%) rename {modules => deprecated}/eks/karpenter-provisioner/outputs.tf (100%) rename {modules => deprecated}/eks/karpenter-provisioner/provider-helm.tf (100%) rename {modules => deprecated}/eks/karpenter-provisioner/providers.tf (100%) rename {modules => deprecated}/eks/karpenter-provisioner/remote-state.tf (100%) rename {modules => deprecated}/eks/karpenter-provisioner/variables.tf (100%) rename {modules => deprecated}/eks/karpenter-provisioner/versions.tf (100%) create mode 100644 deprecated/eks/karpenter/CHANGELOG.md create mode 100644 deprecated/eks/karpenter/README.md create mode 100644 deprecated/eks/karpenter/context.tf create mode 100644 deprecated/eks/karpenter/interruption_handler.tf create mode 100755 deprecated/eks/karpenter/karpenter-crd-upgrade create mode 100644 deprecated/eks/karpenter/main.tf create mode 100644 deprecated/eks/karpenter/outputs.tf create mode 100644 deprecated/eks/karpenter/provider-helm.tf create mode 100644 deprecated/eks/karpenter/providers.tf create mode 100644 deprecated/eks/karpenter/remote-state.tf create mode 100644 deprecated/eks/karpenter/variables.tf create mode 100644 deprecated/eks/karpenter/versions.tf create mode 100644 modules/eks/karpenter-node-pool/README.md create mode 100644 modules/eks/karpenter-node-pool/context.tf create mode 100644 modules/eks/karpenter-node-pool/ec2-node-class.tf create mode 100644 modules/eks/karpenter-node-pool/main.tf create mode 100644 modules/eks/karpenter-node-pool/outputs.tf create mode 100644 modules/eks/karpenter-node-pool/provider-helm.tf create mode 100644 modules/eks/karpenter-node-pool/providers.tf create mode 100644 modules/eks/karpenter-node-pool/remote-state.tf create mode 100644 modules/eks/karpenter-node-pool/variables.tf create mode 100644 modules/eks/karpenter-node-pool/versions.tf create mode 100644 modules/eks/karpenter/controller-policy-v1alpha.tf create mode 100644 modules/eks/karpenter/controller-policy.tf create mode 100644 modules/eks/karpenter/docs/v1alpha-to-v1beta-migration.md diff --git a/modules/eks/karpenter-provisioner/README.md b/deprecated/eks/karpenter-provisioner/README.md similarity index 98% rename from modules/eks/karpenter-provisioner/README.md rename to deprecated/eks/karpenter-provisioner/README.md index 3003bb610..9f0ce2010 100644 --- a/modules/eks/karpenter-provisioner/README.md +++ b/deprecated/eks/karpenter-provisioner/README.md @@ -1,5 +1,14 @@ # Component: `eks/karpenter-provisioner` +:::warning This component is DEPRECATED + +With v1beta1 of Karpenter, the `provisioner` component is deprecated. +Please use the `eks/karpenter-node-group` component instead. + +For more details, see the [Karpenter v1beta1 release notes](/modules/eks/karpenter/CHANGELOG.md). + +::: + This component deploys [Karpenter provisioners](https://karpenter.sh/v0.18.0/aws/provisioning) on an EKS cluster. ## Usage diff --git a/modules/eks/karpenter-provisioner/context.tf b/deprecated/eks/karpenter-provisioner/context.tf similarity index 100% rename from modules/eks/karpenter-provisioner/context.tf rename to deprecated/eks/karpenter-provisioner/context.tf diff --git a/modules/eks/karpenter-provisioner/main.tf b/deprecated/eks/karpenter-provisioner/main.tf similarity index 100% rename from modules/eks/karpenter-provisioner/main.tf rename to deprecated/eks/karpenter-provisioner/main.tf diff --git a/modules/eks/karpenter-provisioner/outputs.tf b/deprecated/eks/karpenter-provisioner/outputs.tf similarity index 100% rename from modules/eks/karpenter-provisioner/outputs.tf rename to deprecated/eks/karpenter-provisioner/outputs.tf diff --git a/modules/eks/karpenter-provisioner/provider-helm.tf b/deprecated/eks/karpenter-provisioner/provider-helm.tf similarity index 100% rename from modules/eks/karpenter-provisioner/provider-helm.tf rename to deprecated/eks/karpenter-provisioner/provider-helm.tf diff --git a/modules/eks/karpenter-provisioner/providers.tf b/deprecated/eks/karpenter-provisioner/providers.tf similarity index 100% rename from modules/eks/karpenter-provisioner/providers.tf rename to deprecated/eks/karpenter-provisioner/providers.tf diff --git a/modules/eks/karpenter-provisioner/remote-state.tf b/deprecated/eks/karpenter-provisioner/remote-state.tf similarity index 100% rename from modules/eks/karpenter-provisioner/remote-state.tf rename to deprecated/eks/karpenter-provisioner/remote-state.tf diff --git a/modules/eks/karpenter-provisioner/variables.tf b/deprecated/eks/karpenter-provisioner/variables.tf similarity index 100% rename from modules/eks/karpenter-provisioner/variables.tf rename to deprecated/eks/karpenter-provisioner/variables.tf diff --git a/modules/eks/karpenter-provisioner/versions.tf b/deprecated/eks/karpenter-provisioner/versions.tf similarity index 100% rename from modules/eks/karpenter-provisioner/versions.tf rename to deprecated/eks/karpenter-provisioner/versions.tf diff --git a/deprecated/eks/karpenter/CHANGELOG.md b/deprecated/eks/karpenter/CHANGELOG.md new file mode 100644 index 000000000..f3ff1cc20 --- /dev/null +++ b/deprecated/eks/karpenter/CHANGELOG.md @@ -0,0 +1,85 @@ +## Version 1.348.0 + +Components PR [#868](https://github.com/cloudposse/terraform-aws-components/pull/868) + +The `karpenter-crd` helm chart can now be installed alongside the `karpenter` helm chart to automatically manage the +lifecycle of Karpenter CRDs. However since this chart must be installed before the `karpenter` helm chart, the +Kubernetes namespace must be available before either chart is deployed. Furthermore, this namespace should persist +whether or not the `karpenter-crd` chart is deployed, so it should not be installed with that given `helm-release` +resource. Therefore, we've moved namespace creation to a separate resource that runs before both charts. Terraform will +handle that namespace state migration with the `moved` block. + +There are several scenarios that may or may not require additional steps. Please review the following scenarios and +follow the steps for your given requirements. + +### Upgrading an existing `eks/karpenter` deployment without changes + +If you currently have `eks/karpenter` deployed to an EKS cluster and have upgraded to this version of the component, no +changes are required. `var.crd_chart_enabled` will default to `false`. + +### Upgrading an existing `eks/karpenter` deployment and deploying the `karpenter-crd` chart + +If you currently have `eks/karpenter` deployed to an EKS cluster, have upgraded to this version of the component, do not +currently have the `karpenter-crd` chart installed, and want to now deploy the `karpenter-crd` helm chart, a few +additional steps are required! + +First, set `var.crd_chart_enabled` to `true`. + +Next, update the installed Karpenter CRDs in order for Helm to automatically take over their management when the +`karpenter-crd` chart is deployed. We have included a script to run that upgrade. Run the `./karpenter-crd-upgrade` +script or run the following commands on the given cluster before deploying the chart. Please note that this script or +commands will only need to be run on first use of the CRD chart. + +Before running the script, ensure that the `kubectl` context is set to the cluster where the `karpenter` helm chart is +deployed. In Geodesic, you can usually do this with the `set-cluster` command, though your configuration may vary. + +```bash +set-cluster -- terraform +``` + +Then run the script or commands: + +```bash +kubectl label crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh app.kubernetes.io/managed-by=Helm --overwrite +kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-name=karpenter-crd --overwrite +kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-namespace=karpenter --overwrite +``` + +:::info + +Previously the `karpenter-crd-upgrade` script included deploying the `karpenter-crd` chart. Now that this chart is moved +to Terraform, that helm deployment is no longer necessary. + +For reference, the `karpenter-crd` chart can be installed with helm with the following: + +```bash +helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version "$VERSION" --namespace karpenter +``` + +::: + +Now that the CRDs are upgraded, the component is ready to be applied. Apply the `eks/karpenter` component and then apply +`eks/karpenter-provisioner`. + +#### Note for upgrading Karpenter from before v0.27.3 to v0.27.3 or later + +If you are upgrading Karpenter from before v0.27.3 to v0.27.3 or later, you may need to run the following command to +remove an obsolete webhook: + +```bash +kubectl delete mutatingwebhookconfigurations defaulting.webhook.karpenter.sh +``` + +See [the Karpenter upgrade guide](https://karpenter.sh/v0.32/upgrading/upgrade-guide/#upgrading-to-v0273) for more +details. + +### Upgrading an existing `eks/karpenter` deployment where the `karpenter-crd` chart is already deployed + +If you currently have `eks/karpenter` deployed to an EKS cluster, have upgraded to this version of the component, and +already have the `karpenter-crd` chart installed, simply set `var.crd_chart_enabled` to `true` and redeploy Terraform to +have Terraform manage the helm release for `karpenter-crd`. + +### Net new deployments + +If you are initially deploying `eks/karpenter`, no changes are required, but we recommend installing the CRD chart. Set +`var.crd_chart_enabled` to `true` and continue with deployment. diff --git a/deprecated/eks/karpenter/README.md b/deprecated/eks/karpenter/README.md new file mode 100644 index 000000000..1ad01d35b --- /dev/null +++ b/deprecated/eks/karpenter/README.md @@ -0,0 +1,467 @@ +# Component: `eks/karpenter` + +This component provisions [Karpenter](https://karpenter.sh) on an EKS cluster. It requires at least version 0.19.0 of +Karpenter, though you are encouraged to use the latest version. + +## Usage + +**Stack Level**: Regional + +These instructions assume you are provisioning 2 EKS clusters in the same account and region, named "blue" and "green", +and alternating between them. If you are only using a single cluster, you can ignore the "blue" and "green" references +and remove the `metadata` block from the `karpenter` module. + +```yaml +components: + terraform: + # Base component of all `karpenter` components + eks/karpenter: + metadata: + type: abstract + vars: + enabled: true + eks_component_name: "eks/cluster" + name: "karpenter" + # https://github.com/aws/karpenter/tree/main/charts/karpenter + chart_repository: "oci://public.ecr.aws/karpenter" + chart: "karpenter" + chart_version: "v0.31.0" + create_namespace: true + kubernetes_namespace: "karpenter" + resources: + limits: + cpu: "300m" + memory: "1Gi" + requests: + cpu: "100m" + memory: "512Mi" + cleanup_on_fail: true + atomic: true + wait: true + rbac_enabled: true + # "karpenter-crd" can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs + crd_chart_enabled: true + crd_chart: "karpenter-crd" + # Set `legacy_create_karpenter_instance_profile` to `false` to allow the `eks/cluster` component + # to manage the instance profile for the nodes launched by Karpenter (recommended for all new clusters). + legacy_create_karpenter_instance_profile: false + # Enable interruption handling to deploy a SQS queue and a set of Event Bridge rules to handle interruption with Karpenter. + interruption_handler_enabled: true + + # Provision `karpenter` component on the blue EKS cluster + eks/karpenter-blue: + metadata: + component: eks/karpenter + inherits: + - eks/karpenter + vars: + eks_component_name: eks/cluster-blue +``` + +## Provision Karpenter on EKS cluster + +Here we describe how to provision Karpenter on an EKS cluster. We will be using the `plat-ue2-dev` stack as an example. + +### Provision Service-Linked Roles for EC2 Spot and EC2 Spot Fleet + +**Note:** If you want to use EC2 Spot for the instances launched by Karpenter, you may need to provision the following +Service-Linked Role for EC2 Spot: + +- Service-Linked Role for EC2 Spot + +This is only necessary if this is the first time you're using EC2 Spot in the account. Since this is a one-time +operation, we recommend you do this manually via the AWS CLI: + +```bash +aws --profile --gbl--admin iam create-service-linked-role --aws-service-name spot.amazonaws.com +``` + +Note that if the Service-Linked Roles already exist in the AWS account (if you used EC2 Spot or Spot Fleet before), and +you try to provision them again, you will see the following errors: + +```text +An error occurred (InvalidInput) when calling the CreateServiceLinkedRole operation: +Service role name AWSServiceRoleForEC2Spot has been taken in this account, please try a different suffix +``` + +For more details, see: + +- https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html +- https://docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html + +The process of provisioning Karpenter on an EKS cluster consists of 3 steps. + +### 1. Provision EKS Fargate Profile for Karpenter and IAM Role for Nodes Launched by Karpenter + +EKS Fargate Profile for Karpenter and IAM Role for Nodes launched by Karpenter are provisioned by the `eks/cluster` +component: + +```yaml +components: + terraform: + eks/cluster-blue: + metadata: + component: eks/cluster + inherits: + - eks/cluster + vars: + attributes: + - blue + eks_component_name: eks/cluster-blue + node_groups: + main: + instance_types: + - t3.medium + max_group_size: 3 + min_group_size: 1 + fargate_profiles: + karpenter: + kubernetes_namespace: karpenter + kubernetes_labels: null + karpenter_iam_role_enabled: true +``` + +**Notes**: + +- Fargate Profile role ARNs need to be added to the `aws-auth` ConfigMap to allow the Fargate Profile nodes to join the + EKS cluster (this is done by EKS) +- Karpenter IAM role ARN needs to be added to the `aws-auth` ConfigMap to allow the nodes launched by Karpenter to join + the EKS cluster (this is done by the `eks/cluster` component) + +We use EKS Fargate Profile for Karpenter because It is recommended to run Karpenter on an EKS Fargate Profile. + +```text +Karpenter is installed using a Helm chart. The Helm chart installs the Karpenter controller and +a webhook pod as a Deployment that needs to run before the controller can be used for scaling your cluster. +We recommend a minimum of one small node group with at least one worker node. + +As an alternative, you can run these pods on EKS Fargate by creating a Fargate profile for the +karpenter namespace. Doing so will cause all pods deployed into this namespace to run on EKS Fargate. +Do not run Karpenter on a node that is managed by Karpenter. +``` + +See +[Run Karpenter Controller on EKS Fargate](https://aws.github.io/aws-eks-best-practices/karpenter/#run-the-karpenter-controller-on-eks-fargate-or-on-a-worker-node-that-belongs-to-a-node-group) +for more details. + +We provision IAM Role for Nodes launched by Karpenter because they must run with an Instance Profile that grants +permissions necessary to run containers and configure networking. + +We define the IAM role for the Instance Profile in `components/terraform/eks/cluster/karpenter.tf`. + +Note that we provision the EC2 Instance Profile for the Karpenter IAM role in the `components/terraform/eks/karpenter` +component (see the next step). + +Run the following commands to provision the EKS Fargate Profile for Karpenter and the IAM role for instances launched by +Karpenter on the blue EKS cluster and add the role ARNs to the `aws-auth` ConfigMap: + +```bash +atmos terraform plan eks/cluster-blue -s plat-ue2-dev +atmos terraform apply eks/cluster-blue -s plat-ue2-dev +``` + +For more details, refer to: + +- https://karpenter.sh/v0.18.0/getting-started/getting-started-with-terraform +- https://karpenter.sh/v0.18.0/getting-started/getting-started-with-eksctl + +### 2. Provision `karpenter` component + +In this step, we provision the `components/terraform/eks/karpenter` component, which deploys the following resources: + +- EC2 Instance Profile for the nodes launched by Karpenter (note that the IAM role for the Instance Profile is + provisioned in the previous step in the `eks/cluster` component) +- Karpenter Kubernetes controller using the Karpenter Helm Chart and the `helm_release` Terraform resource +- EKS IAM role for Kubernetes Service Account for the Karpenter controller (with all the required permissions) + +Run the following commands to provision the Karpenter component on the blue EKS cluster: + +```bash +atmos terraform plan eks/karpenter-blue -s plat-ue2-dev +atmos terraform apply eks/karpenter-blue -s plat-ue2-dev +``` + +Note that the stack config for the blue Karpenter component is defined in `stacks/catalog/eks/clusters/blue.yaml`. + +```yaml +eks/karpenter-blue: + metadata: + component: eks/karpenter + inherits: + - eks/karpenter + vars: + eks_component_name: eks/cluster-blue +``` + +### 3. Provision `karpenter-provisioner` component + +In this step, we provision the `components/terraform/eks/karpenter-provisioner` component, which deploys Karpenter +[Provisioners](https://karpenter.sh/v0.18.0/aws/provisioning) using the `kubernetes_manifest` resource. + +**NOTE:** We deploy the provisioners in a separate step as a separate component since it uses `kind: Provisioner` CRD +which itself is created by the `karpenter` component in the previous step. + +Run the following commands to deploy the Karpenter provisioners on the blue EKS cluster: + +```bash +atmos terraform plan eks/karpenter-provisioner-blue -s plat-ue2-dev +atmos terraform apply eks/karpenter-provisioner-blue -s plat-ue2-dev +``` + +Note that the stack config for the blue Karpenter provisioner component is defined in +`stacks/catalog/eks/clusters/blue.yaml`. + +```yaml +eks/karpenter-provisioner-blue: + metadata: + component: eks/karpenter-provisioner + inherits: + - eks/karpenter-provisioner + vars: + attributes: + - blue + eks_component_name: eks/cluster-blue +``` + +You can override the default values from the `eks/karpenter-provisioner` base component. + +For your cluster, you will need to review the following configurations for the Karpenter provisioners and update it +according to your requirements: + +- [requirements](https://karpenter.sh/v0.18.0/provisioner/#specrequirements): + + ```yaml + requirements: + - key: "karpenter.sh/capacity-type" + operator: "In" + values: + - "on-demand" + - "spot" + - key: "node.kubernetes.io/instance-type" + operator: "In" + values: + - "m5.xlarge" + - "m5.large" + - "m5.medium" + - "c5.xlarge" + - "c5.large" + - "c5.medium" + - key: "kubernetes.io/arch" + operator: "In" + values: + - "amd64" + ``` + +- `taints`, `startup_taints`, `ami_family` + +- Resource limits/requests for the Karpenter controller itself: + + ```yaml + resources: + limits: + cpu: "300m" + memory: "1Gi" + requests: + cpu: "100m" + memory: "512Mi" + ``` + +- Total CPU and memory limits for all pods running on the EC2 instances launched by Karpenter: + + ```yaml + total_cpu_limit: "1k" + total_memory_limit: "1000Gi" + ``` + +- Config to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the + value to `null` (never scales down if not set): + + ```yaml + ttl_seconds_after_empty: 30 + ``` + +- Config to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` + (never expires if not set): + + ```yaml + ttl_seconds_until_expired: 2592000 + ``` + +## Node Interruption + +Karpenter also supports listening for and responding to Node Interruption events. If interruption handling is enabled, +Karpenter will watch for upcoming involuntary interruption events that would cause disruption to your workloads. These +interruption events include: + +- Spot Interruption Warnings +- Scheduled Change Health Events (Maintenance Events) +- Instance Terminating Events +- Instance Stopping Events + +:::info + +The Node Interruption Handler is not the same as the Node Termination Handler. The latter is always enabled and cleanly +shuts down the node in 2 minutes in response to a Node Termination event. The former gets advance notice that a node +will soon be terminated, so it can have 5-10 minutes to shut down a node. + +::: + +For more details, see refer to the [Karpenter docs](https://karpenter.sh/v0.32/concepts/disruption/#interruption) and +[FAQ](https://karpenter.sh/v0.32/faq/#interruption-handling) + +To enable Node Interruption handling, set `var.interruption_handler_enabled` to `true`. This will create an SQS queue +and a set of Event Bridge rules to deliver interruption events to Karpenter. + +## Custom Resource Definition (CRD) Management + +Karpenter ships with a few Custom Resource Definitions (CRDs). In earlier versions of this component, when installing a +new version of the `karpenter` helm chart, CRDs were not be upgraded at the same time, requiring manual steps to upgrade +CRDs after deploying the latest chart. However Karpenter now supports an additional, independent helm chart for CRD +management. This helm chart, `karpenter-crd`, can be installed alongside the `karpenter` helm chart to automatically +manage the lifecycle of these CRDs. + +To deploy the `karpenter-crd` helm chart, set `var.crd_chart_enabled` to `true`. (Installing the `karpenter-crd` chart +is recommended. `var.crd_chart_enabled` defaults to `false` to preserve backward compatibility with older versions of +this component.) + +## Troubleshooting + +For Karpenter issues, checkout the [Karpenter Troubleshooting Guide](https://karpenter.sh/docs/troubleshooting/) + +### References + +For more details, refer to: + +- https://karpenter.sh/v0.28.0/provisioner/#specrequirements +- https://karpenter.sh/v0.28.0/aws/provisioning +- https://aws.github.io/aws-eks-best-practices/karpenter/#creating-provisioners +- https://aws.github.io/aws-eks-best-practices/karpenter +- https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [karpenter](#module\_karpenter) | cloudposse/helm-release/aws | 0.10.1 | +| [karpenter\_crd](#module\_karpenter\_crd) | cloudposse/helm-release/aws | 0.10.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_event_rule.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_iam_instance_profile.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_sqs_queue.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource | +| [aws_sqs_queue_policy.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | +| [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_iam_policy_document.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended | `string` | n/a | yes | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history) | `string` | `null` | no | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart | `string` | n/a | yes | +| [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values | `any` | `{}` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed | `string` | `null` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [crd\_chart](#input\_crd\_chart) | The name of the Karpenter CRD chart to be installed, if `var.crd_chart_enabled` is set to `true`. | `string` | `"karpenter-crd"` | no | +| [crd\_chart\_enabled](#input\_crd\_chart\_enabled) | `karpenter-crd` can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs. Set to `true` to install this CRD helm chart before the primary karpenter chart. | `bool` | `false` | no | +| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false` | `bool` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [interruption\_handler\_enabled](#input\_interruption\_handler\_enabled) | If `true`, deploy a SQS queue and Event Bridge rules to enable interruption handling by Karpenter.

https://karpenter.sh/v0.27.5/concepts/deprovisioning/#interruption | `bool` | `false` | no | +| [interruption\_queue\_message\_retention](#input\_interruption\_queue\_message\_retention) | The message retention in seconds for the interruption handler SQS queue. | `number` | `300` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [legacy\_create\_karpenter\_instance\_profile](#input\_legacy\_create\_karpenter\_instance\_profile) | When `true` (the default), this component creates an IAM Instance Profile
for nodes launched by Karpenter, to preserve the legacy behavior.
Set to `false` to disable creation of the IAM Instance Profile, which
avoids conflict with having `eks/cluster` create it.
Use in conjunction with `eks/cluster` component `legacy_do_not_create_karpenter_instance_profile`,
which see for further details. | `bool` | `true` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [rbac\_enabled](#input\_rbac\_enabled) | Enable/disable RBAC | `bool` | `true` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [resources](#input\_resources) | The CPU and memory of the deployment's limits and requests |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true` | `bool` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [instance\_profile](#output\_instance\_profile) | Provisioned EC2 Instance Profile for nodes launched by Karpenter | +| [metadata](#output\_metadata) | Block status of the deployed release | + + + +## References + +- https://karpenter.sh +- https://aws.github.io/aws-eks-best-practices/karpenter +- https://karpenter.sh/v0.18.0/getting-started/getting-started-with-terraform +- https://aws.amazon.com/blogs/aws/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler +- https://github.com/aws/karpenter +- https://www.eksworkshop.com/beginner/085_scaling_karpenter +- https://ec2spotworkshops.com/karpenter.html +- https://www.eksworkshop.com/beginner/085_scaling_karpenter/install_karpenter +- https://karpenter.sh/v0.18.0/development-guide +- https://karpenter.sh/v0.18.0/aws/provisioning +- https://docs.aws.amazon.com/eks/latest/userguide/pod-execution-role.html +- https://aws.amazon.com/premiumsupport/knowledge-center/fargate-troubleshoot-profile-creation +- https://learn.hashicorp.com/tutorials/terraform/kubernetes-crd-faas +- https://github.com/hashicorp/terraform-provider-kubernetes/issues/1545 +- https://issuemode.com/issues/hashicorp/terraform-provider-kubernetes-alpha/4840198 +- https://bytemeta.vip/repo/hashicorp/terraform-provider-kubernetes/issues/1442 +- https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html + +[](https://cpco.io/component) diff --git a/deprecated/eks/karpenter/context.tf b/deprecated/eks/karpenter/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/deprecated/eks/karpenter/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/deprecated/eks/karpenter/interruption_handler.tf b/deprecated/eks/karpenter/interruption_handler.tf new file mode 100644 index 000000000..558ee7de1 --- /dev/null +++ b/deprecated/eks/karpenter/interruption_handler.tf @@ -0,0 +1,99 @@ +locals { + interruption_handler_enabled = local.enabled && var.interruption_handler_enabled + interruption_handler_queue_name = module.this.id + + dns_suffix = join("", data.aws_partition.current[*].dns_suffix) + + events = { + health_event = { + name = "HealthEvent" + description = "Karpenter interrupt - AWS health event" + event_pattern = { + source = ["aws.health"] + detail-type = ["AWS Health Event"] + } + } + spot_interupt = { + name = "SpotInterrupt" + description = "Karpenter interrupt - EC2 spot instance interruption warning" + event_pattern = { + source = ["aws.ec2"] + detail-type = ["EC2 Spot Instance Interruption Warning"] + } + } + instance_rebalance = { + name = "InstanceRebalance" + description = "Karpenter interrupt - EC2 instance rebalance recommendation" + event_pattern = { + source = ["aws.ec2"] + detail-type = ["EC2 Instance Rebalance Recommendation"] + } + } + instance_state_change = { + name = "InstanceStateChange" + description = "Karpenter interrupt - EC2 instance state-change notification" + event_pattern = { + source = ["aws.ec2"] + detail-type = ["EC2 Instance State-change Notification"] + } + } + } +} + +data "aws_partition" "current" { + count = local.interruption_handler_enabled ? 1 : 0 +} + +resource "aws_sqs_queue" "interruption_handler" { + count = local.interruption_handler_enabled ? 1 : 0 + + name = local.interruption_handler_queue_name + message_retention_seconds = var.interruption_queue_message_retention + sqs_managed_sse_enabled = true + + tags = module.this.tags +} + +data "aws_iam_policy_document" "interruption_handler" { + count = local.interruption_handler_enabled ? 1 : 0 + + statement { + sid = "SqsWrite" + actions = ["sqs:SendMessage"] + resources = [aws_sqs_queue.interruption_handler[0].arn] + + principals { + type = "Service" + identifiers = [ + "events.${local.dns_suffix}", + "sqs.${local.dns_suffix}", + ] + } + + } +} + +resource "aws_sqs_queue_policy" "interruption_handler" { + count = local.interruption_handler_enabled ? 1 : 0 + + queue_url = aws_sqs_queue.interruption_handler[0].url + policy = data.aws_iam_policy_document.interruption_handler[0].json +} + +resource "aws_cloudwatch_event_rule" "interruption_handler" { + for_each = { for k, v in local.events : k => v if local.interruption_handler_enabled } + + name = "${module.this.id}-${each.value.name}" + description = each.value.description + event_pattern = jsonencode(each.value.event_pattern) + + tags = module.this.tags +} + +resource "aws_cloudwatch_event_target" "interruption_handler" { + for_each = { for k, v in local.events : k => v if local.interruption_handler_enabled } + + rule = aws_cloudwatch_event_rule.interruption_handler[each.key].name + target_id = "KarpenterInterruptionQueueTarget" + arn = aws_sqs_queue.interruption_handler[0].arn +} diff --git a/deprecated/eks/karpenter/karpenter-crd-upgrade b/deprecated/eks/karpenter/karpenter-crd-upgrade new file mode 100755 index 000000000..e6274deb3 --- /dev/null +++ b/deprecated/eks/karpenter/karpenter-crd-upgrade @@ -0,0 +1,24 @@ +#!/bin/bash + +function usage() { + cat >&2 <<'EOF' +./karpenter-crd-upgrade + +Use this script to prepare a cluster for karpenter-crd helm chart support by upgrading Karpenter CRDs. + +EOF +} + +function upgrade() { + set -x + + kubectl label crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh app.kubernetes.io/managed-by=Helm --overwrite + kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-name=karpenter-crd --overwrite + kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-namespace=karpenter --overwrite +} + +if (($# == 0)); then + upgrade +else + usage +fi diff --git a/deprecated/eks/karpenter/main.tf b/deprecated/eks/karpenter/main.tf new file mode 100644 index 000000000..1ebf263c4 --- /dev/null +++ b/deprecated/eks/karpenter/main.tf @@ -0,0 +1,236 @@ +# https://aws.amazon.com/blogs/aws/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler/ +# https://karpenter.sh/ +# https://karpenter.sh/v0.10.1/getting-started/getting-started-with-terraform/ +# https://karpenter.sh/v0.10.1/getting-started/getting-started-with-eksctl/ +# https://www.eksworkshop.com/beginner/085_scaling_karpenter/ +# https://karpenter.sh/v0.10.1/aws/provisioning/ +# https://www.eksworkshop.com/beginner/085_scaling_karpenter/setup_the_environment/ +# https://ec2spotworkshops.com/karpenter.html +# https://catalog.us-east-1.prod.workshops.aws/workshops/76a5dd80-3249-4101-8726-9be3eeee09b2/en-US/autoscaling/karpenter + +locals { + enabled = module.this.enabled + + eks_cluster_identity_oidc_issuer = try(module.eks.outputs.eks_cluster_identity_oidc_issuer, "") + karpenter_iam_role_name = try(module.eks.outputs.karpenter_iam_role_name, "") + + karpenter_instance_profile_enabled = local.enabled && var.legacy_create_karpenter_instance_profile && length(local.karpenter_iam_role_name) > 0 +} + +resource "aws_iam_instance_profile" "default" { + count = local.karpenter_instance_profile_enabled ? 1 : 0 + + name = local.karpenter_iam_role_name + role = local.karpenter_iam_role_name + tags = module.this.tags +} + +# See CHANGELOG for PR #868: +# https://github.com/cloudposse/terraform-aws-components/pull/868 +# +# Namespace was moved from the karpenter module to an independent resource in order to be +# shared between both the karpenter and karpenter-crd modules. +moved { + from = module.karpenter.kubernetes_namespace.default[0] + to = kubernetes_namespace.default[0] +} + +resource "kubernetes_namespace" "default" { + count = local.enabled && var.create_namespace ? 1 : 0 + + metadata { + name = var.kubernetes_namespace + annotations = {} + labels = merge(module.this.tags, { name = var.kubernetes_namespace }) + } +} + +# Deploy karpenter-crd helm chart +# "karpenter-crd" can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs +module "karpenter_crd" { + enabled = local.enabled && var.crd_chart_enabled + + source = "cloudposse/helm-release/aws" + version = "0.10.1" + + name = var.crd_chart + chart = var.crd_chart + repository = var.chart_repository + description = var.chart_description + chart_version = var.chart_version + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + create_namespace_with_kubernetes = false # Namespace is created with kubernetes_namespace resources to be shared between charts + kubernetes_namespace = join("", kubernetes_namespace.default[*].id) + kubernetes_namespace_labels = merge(module.this.tags, { name = join("", kubernetes_namespace.default[*].id) }) + + eks_cluster_oidc_issuer_url = coalesce(replace(local.eks_cluster_identity_oidc_issuer, "https://", ""), "deleted") + + values = compact([ + # standard k8s object settings + yamlencode({ + fullnameOverride = module.this.name + resources = var.resources + rbac = { + create = var.rbac_enabled + } + }), + ]) + + context = module.this.context + + depends_on = [ + kubernetes_namespace.default + ] +} + +# Deploy Karpenter helm chart +module "karpenter" { + source = "cloudposse/helm-release/aws" + version = "0.10.1" + + chart = var.chart + repository = var.chart_repository + description = var.chart_description + chart_version = var.chart_version + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + create_namespace_with_kubernetes = false # Namespace is created with kubernetes_namespace resources to be shared between charts + kubernetes_namespace = join("", kubernetes_namespace.default[*].id) + kubernetes_namespace_labels = merge(module.this.tags, { name = join("", kubernetes_namespace.default[*].id) }) + + eks_cluster_oidc_issuer_url = coalesce(replace(local.eks_cluster_identity_oidc_issuer, "https://", ""), "deleted") + + service_account_name = module.this.name + service_account_namespace = join("", kubernetes_namespace.default[*].id) + + iam_role_enabled = true + + # https://karpenter.sh/v0.6.1/getting-started/cloudformation.yaml + # https://karpenter.sh/v0.10.1/getting-started/getting-started-with-terraform + # https://github.com/aws/karpenter/issues/2649 + # Apparently the source of truth for the best IAM policy is the `data.aws_iam_policy_document.karpenter_controller` in + # https://github.com/terraform-aws-modules/terraform-aws-iam/blob/master/modules/iam-role-for-service-accounts-eks/policies.tf + iam_policy = [{ + statements = concat([ + { + sid = "KarpenterController" + effect = "Allow" + resources = ["*"] + + actions = [ + # https://github.com/terraform-aws-modules/terraform-aws-iam/blob/99c69ad54d985f67acf211885aa214a3a6cc931c/modules/iam-role-for-service-accounts-eks/policies.tf#L511-L581 + # The reference policy is broken up into multiple statements with different resource restrictions based on tags. + # This list has breaks where statements are separated in the reference policy for easier comparison and maintenance. + "ec2:CreateLaunchTemplate", + "ec2:CreateFleet", + "ec2:CreateTags", + "ec2:DescribeLaunchTemplates", + "ec2:DescribeImages", + "ec2:DescribeInstances", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceTypeOfferings", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeSpotPriceHistory", + "pricing:GetProducts", + + "ec2:TerminateInstances", + "ec2:DeleteLaunchTemplate", + + "ec2:RunInstances", + + "iam:PassRole", + ] + }, + { + sid = "KarpenterControllerSSM" + effect = "Allow" + # Allow Karpenter to read AMI IDs from SSM + actions = ["ssm:GetParameter"] + resources = ["arn:aws:ssm:*:*:parameter/aws/service/*"] + }, + { + sid = "KarpenterControllerClusterAccess" + effect = "Allow" + actions = [ + "eks:DescribeCluster" + ] + resources = [ + module.eks.outputs.eks_cluster_arn + ] + } + ], + local.interruption_handler_enabled ? [ + { + sid = "KarpenterInterruptionHandlerAccess" + effect = "Allow" + actions = [ + "sqs:DeleteMessage", + "sqs:GetQueueUrl", + "sqs:GetQueueAttributes", + "sqs:ReceiveMessage", + ] + resources = [ + one(aws_sqs_queue.interruption_handler[*].arn) + ] + } + ] : [] + ) + }] + + + values = compact([ + # standard k8s object settings + yamlencode({ + fullnameOverride = module.this.name + serviceAccount = { + name = module.this.name + } + controller = { + resources = var.resources + } + rbac = { + create = var.rbac_enabled + } + }), + # karpenter-specific values + yamlencode({ + settings = { + # This configuration of settings requires Karpenter chart v0.19.0 or later + aws = { + defaultInstanceProfile = local.karpenter_iam_role_name # instance profile name === role name + clusterName = local.eks_cluster_id + # clusterEndpoint not needed as of v0.25.0 + clusterEndpoint = local.eks_cluster_endpoint + tags = module.this.tags + } + } + }), + yamlencode( + local.interruption_handler_enabled ? { + settings = { + aws = { + interruptionQueueName = local.interruption_handler_queue_name + } + } + } : {}), + # additional values + yamlencode(var.chart_values) + ]) + + context = module.this.context + + depends_on = [ + aws_iam_instance_profile.default, + module.karpenter_crd, + kubernetes_namespace.default + ] +} diff --git a/deprecated/eks/karpenter/outputs.tf b/deprecated/eks/karpenter/outputs.tf new file mode 100644 index 000000000..830bd12aa --- /dev/null +++ b/deprecated/eks/karpenter/outputs.tf @@ -0,0 +1,9 @@ +output "metadata" { + value = module.karpenter.metadata + description = "Block status of the deployed release" +} + +output "instance_profile" { + value = aws_iam_instance_profile.default + description = "Provisioned EC2 Instance Profile for nodes launched by Karpenter" +} diff --git a/deprecated/eks/karpenter/provider-helm.tf b/deprecated/eks/karpenter/provider-helm.tf new file mode 100644 index 000000000..91cc7f6d4 --- /dev/null +++ b/deprecated/eks/karpenter/provider-helm.tf @@ -0,0 +1,201 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false +} + +variable "kubeconfig_context" { + type = string + default = "" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT + nullable = false +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT + nullable = false +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" + nullable = false +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" + nullable = false +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = local.cluster_ca_certificate + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = local.cluster_ca_certificate + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/deprecated/eks/karpenter/providers.tf b/deprecated/eks/karpenter/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/deprecated/eks/karpenter/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/deprecated/eks/karpenter/remote-state.tf b/deprecated/eks/karpenter/remote-state.tf new file mode 100644 index 000000000..c1ec8226d --- /dev/null +++ b/deprecated/eks/karpenter/remote-state.tf @@ -0,0 +1,8 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + context = module.this.context +} diff --git a/deprecated/eks/karpenter/variables.tf b/deprecated/eks/karpenter/variables.tf new file mode 100644 index 000000000..9b84ba3b4 --- /dev/null +++ b/deprecated/eks/karpenter/variables.tf @@ -0,0 +1,134 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "chart_description" { + type = string + description = "Set release description attribute (visible in the history)" + default = null +} + +variable "chart" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended" +} + +variable "chart_repository" { + type = string + description = "Repository URL where to locate the requested chart" +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed" + default = null +} + +variable "crd_chart_enabled" { + type = bool + description = "`karpenter-crd` can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs. Set to `true` to install this CRD helm chart before the primary karpenter chart." + default = false +} + +variable "crd_chart" { + type = string + description = "The name of the Karpenter CRD chart to be installed, if `var.crd_chart_enabled` is set to `true`." + default = "karpenter-crd" +} + +variable "resources" { + type = object({ + limits = object({ + cpu = string + memory = string + }) + requests = object({ + cpu = string + memory = string + }) + }) + description = "The CPU and memory of the deployment's limits and requests" +} + +variable "create_namespace" { + type = bool + description = "Create the namespace if it does not yet exist. Defaults to `false`" + default = null +} + +variable "kubernetes_namespace" { + type = string + description = "The namespace to install the release into" +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = null +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails" + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used" + default = true +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`" + default = null +} + +variable "chart_values" { + type = any + description = "Additional values to yamlencode as `helm_release` values" + default = {} +} + +variable "rbac_enabled" { + type = bool + description = "Enable/disable RBAC" + default = true +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "interruption_handler_enabled" { + type = bool + default = false + description = < [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [deploy\_addons\_to\_fargate](#input\_deploy\_addons\_to\_fargate) | Set to `true` (not recommended) to deploy addons to Fargate instead of initial node pool | `bool` | `false` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [enabled\_cluster\_log\_types](#input\_enabled\_cluster\_log\_types) | A list of the desired control plane logging to enable. For more information, see https://docs.aws.amazon.com/en_us/eks/latest/userguide/control-plane-logs.html. Possible values [`api`, `audit`, `authenticator`, `controllerManager`, `scheduler`] | `list(string)` | `[]` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index b86aba520..80cbdc86c 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -1,6 +1,5 @@ locals { enabled = module.this.enabled - eks_outputs = module.eks.outputs vpc_outputs = module.vpc.outputs attributes = flatten(concat(module.this.attributes, [var.color])) diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf index 772c7caf5..0ad0cd62d 100644 --- a/modules/eks/cluster/remote-state.tf +++ b/modules/eks/cluster/remote-state.tf @@ -46,21 +46,3 @@ module "vpc_ingress" { context = module.this.context } -# Yes, this is self-referential. -# It obtains the previous state of the cluster so that we can add -# to it rather than overwrite it (specifically the aws-auth configMap) -module "eks" { - source = "cloudposse/stack-config/yaml//modules/remote-state" - version = "1.5.0" - - component = var.eks_component_name - - defaults = { - eks_managed_node_workers_role_arns = [] - fargate_profile_role_arns = [] - fargate_profile_role_names = [] - eks_cluster_identity_oidc_issuer = "" - } - - context = module.this.context -} diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 232fe782e..3a7aadd02 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -433,13 +433,6 @@ variable "allow_ingress_from_vpc_accounts" { nullable = false } -variable "eks_component_name" { - type = string - description = "The name of the eks component" - default = "eks/cluster" - nullable = false -} - variable "vpc_component_name" { type = string description = "The name of the vpc component" diff --git a/modules/eks/karpenter-node-pool/README.md b/modules/eks/karpenter-node-pool/README.md new file mode 100644 index 000000000..e972c2299 --- /dev/null +++ b/modules/eks/karpenter-node-pool/README.md @@ -0,0 +1,232 @@ +# Component: `eks/karpenter-node-pool` + +This component deploys [Karpenter NodePools](https://karpenter.sh/docs/concepts/nodepools/) to an EKS cluster. + +Karpenter is still in v0 and rapidly evolving. At this time, this component only supports a subset of the features +available in Karpenter. Support could be added for additional features as needed. + +Not supported: + +- Elements of NodePool: + - [`template.spec.kubelet`](https://karpenter.sh/docs/concepts/nodepools/#spectemplatespeckubelet) + - [`limits`](https://karpenter.sh/docs/concepts/nodepools/#limits) currently only supports `cpu` and `memory`. Other + limits such as `nvidia.com/gpu` are not supported. +- Elements of NodeClass: + - `subnetSelectorTerms`. This component only supports selecting all public or all private subnets of the referenced + EKS cluster. + - `securityGroupSelectorTerms`. This component only supports selecting the security group of the referenced EKS + cluster. + - `amiSelectorTerms`. Such terms override the `amiFamily` setting, which is the only AMI selection supported by this + component. + - `instanceStorePolicy` + - `userData` + - `detailedMonitoring` + - `associatePublicIPAddress` + +## Usage + +**Stack Level**: Regional + +If provisioning more than one NodePool, it is +[best practice](https://aws.github.io/aws-eks-best-practices/karpenter/#creating-nodepools) to create NodePools that are +mutually exclusive or weighted. + +```yaml +components: + terraform: + eks/karpenter-node-pool: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + eks_component_name: eks/cluster + name: "karpenter-node-pool" + # https://karpenter.sh/v0.36.0/docs/concepts/nodepools/ + node_pools: + default: + name: default + # Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets + private_subnets_enabled: true + disruption: + consolidation_policy: WhenUnderutilized + consolidate_after: 1h + max_instance_lifetime: 336h + budgets: + # This budget allows 0 disruptions during business hours (from 9am to 5pm) on weekdays + - schedule: "0 9 * * mon-fri" + duration: 8h + nodes: "0" + # The total cpu of the cluster. Maps to spec.limits.cpu in the Karpenter NodeClass + total_cpu_limit: "100" + # The total memory of the cluster. Maps to spec.limits.memory in the Karpenter NodeClass + total_memory_limit: "1000Gi" + # The weight of the node pool. See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools + weight: 50 + # Taints to apply to the nodes in the node pool. See https://karpenter.sh/docs/concepts/nodeclasses/#spectaints + taints: + - key: "node.kubernetes.io/unreachable" + effect: "NoExecute" + value: "true" + # Taints to apply to the nodes in the node pool at startup. See https://karpenter.sh/docs/concepts/nodeclasses/#specstartuptaints + startup_taints: + - key: "node.kubernetes.io/unreachable" + effect: "NoExecute" + value: "true" + # Metadata options for the node pool. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions + metadata_options: + httpEndpoint: "enabled" # allows the node to call the AWS metadata service + httpProtocolIPv6: "disabled" + httpPutResponseHopLimit: 2 + httpTokens: "required" + # The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM) + # Bottlerocket, AL2, Ubuntu + # https://karpenter.sh/v0.18.0/aws/provisioning/#amazon-machine-image-ami-family + ami_family: AL2 + # Karpenter provisioner block device mappings. + block_device_mappings: + - deviceName: /dev/xvda + ebs: + volumeSize: 200Gi + volumeType: gp3 + encrypted: true + deleteOnTermination: true + # Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on + # Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, + # and capacity type (such as AWS spot or on-demand). + # See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details + requirements: + - key: "karpenter.sh/capacity-type" + operator: "In" + values: + - "on-demand" + - "spot" + - key: "node.kubernetes.io/instance-type" + operator: "In" + # See https://aws.amazon.com/ec2/instance-explorer/ and https://aws.amazon.com/ec2/instance-types/ + # Values limited by DenyEC2InstancesWithoutEncryptionInTransit service control policy + # See https://github.com/cloudposse/terraform-aws-service-control-policies/blob/master/catalog/ec2-policies.yaml + # Karpenter recommends allowing at least 20 instance types to ensure availability. + values: + - "c5n.2xlarge" + - "c5n.xlarge" + - "c5n.large" + - "c6i.2xlarge" + - "c6i.xlarge" + - "c6i.large" + - "m5n.2xlarge" + - "m5n.xlarge" + - "m5n.large" + - "m5zn.2xlarge" + - "m5zn.xlarge" + - "m5zn.large" + - "m6i.2xlarge" + - "m6i.xlarge" + - "m6i.large" + - "r5n.2xlarge" + - "r5n.xlarge" + - "r5n.large" + - "r6i.2xlarge" + - "r6i.xlarge" + - "r6i.large" + - key: "kubernetes.io/arch" + operator: "In" + values: + - "amd64" +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_manifest.ec2_node_class](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_manifest.node_pool](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s|m|h).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, s|m|h).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
}))
| n/a | yes | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [ec2\_node\_classes](#output\_ec2\_node\_classes) | Deployed Karpenter EC2NodeClass | +| [node\_pools](#output\_node\_pools) | Deployed Karpenter NodePool | + + + +## References + +- https://karpenter.sh +- https://aws.github.io/aws-eks-best-practices/karpenter +- https://karpenter.sh/docs/concepts/nodepools +- https://aws.amazon.com/blogs/aws/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler +- https://github.com/aws/karpenter +- https://ec2spotworkshops.com/karpenter.html +- https://www.eksworkshop.com/docs/autoscaling/compute/karpenter/ + +[](https://cpco.io/component) diff --git a/modules/eks/karpenter-node-pool/context.tf b/modules/eks/karpenter-node-pool/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/karpenter-node-pool/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/karpenter-node-pool/ec2-node-class.tf b/modules/eks/karpenter-node-pool/ec2-node-class.tf new file mode 100644 index 000000000..7be308615 --- /dev/null +++ b/modules/eks/karpenter-node-pool/ec2-node-class.tf @@ -0,0 +1,53 @@ +# This provisions the EC2NodeClass for the NodePool. +# https://karpenter.sh/docs/concepts/nodeclasses/ +# +# We keep it separate from the NodePool creation, +# even though there is a 1-to-1 mapping between the two, +# to make it a little easier to compare the implementation here +# with the Karpenter documentation, and to track changes as +# Karpenter evolves. +# + + +locals { + # If you include a field but set it to null, the field will be omitted from the Kubernetes resource, + # but the Kubernetes provider will still try to include it with a null value, + # which will cause perpetual diff in the Terraform plan. + # We strip out the null values from block_device_mappings here, because it is too complicated to do inline. + node_block_device_mappings = { for pk, pv in local.node_pools : pk => [ + for i, map in pv.block_device_mappings : merge({ + for dk, dv in map : dk => dv if dk != "ebs" && dv != null + }, try(length(map.ebs), 0) == 0 ? {} : { ebs = { for ek, ev in map.ebs : ek => ev if ev != null } }) + ] + } +} + +# https://karpenter.sh/docs/concepts/nodeclasses/ +resource "kubernetes_manifest" "ec2_node_class" { + for_each = local.node_pools + + manifest = { + apiVersion = "karpenter.k8s.aws/v1beta1" + kind = "EC2NodeClass" + metadata = { + name = coalesce(each.value.name, each.key) + } + spec = merge({ + role = module.eks.outputs.karpenter_iam_role_name + subnetSelectorTerms = [for id in(each.value.private_subnets_enabled ? local.private_subnet_ids : local.public_subnet_ids) : { + id = id + }] + securityGroupSelectorTerms = [{ + tags = { + "aws:eks:cluster-name" = local.eks_cluster_id + } + }] + # https://karpenter.sh/v0.18.0/aws/provisioning/#amazon-machine-image-ami-family + amiFamily = each.value.ami_family + metadataOptions = each.value.metadata_options + tags = module.this.tags + }, try(length(local.node_block_device_mappings[each.key]), 0) == 0 ? {} : { + blockDeviceMappings = local.node_block_device_mappings[each.key] + }) + } +} diff --git a/modules/eks/karpenter-node-pool/main.tf b/modules/eks/karpenter-node-pool/main.tf new file mode 100644 index 000000000..a393c4d43 --- /dev/null +++ b/modules/eks/karpenter-node-pool/main.tf @@ -0,0 +1,70 @@ +# Create Provisioning Configuration +# https://karpenter.sh/docs/concepts/ + +locals { + enabled = module.this.enabled + + private_subnet_ids = module.vpc.outputs.private_subnet_ids + public_subnet_ids = module.vpc.outputs.public_subnet_ids + + node_pools = { for k, v in var.node_pools : k => v if local.enabled } +} + +# https://karpenter.sh/docs/concepts/nodepools/ + +resource "kubernetes_manifest" "node_pool" { + for_each = local.node_pools + + manifest = { + apiVersion = "karpenter.sh/v1beta1" + kind = "NodePool" + metadata = { + name = coalesce(each.value.name, each.key) + } + spec = { + limits = { + cpu = each.value.total_cpu_limit + memory = each.value.total_memory_limit + } + weight = each.value.weight + disruption = merge({ + consolidationPolicy = each.value.disruption.consolidation_policy + expireAfter = each.value.disruption.max_instance_lifetime + }, + each.value.disruption.consolidate_after == null ? {} : { + consolidateAfter = each.value.disruption.consolidate_after + }, + length(each.value.disruption.budgets) == 0 ? {} : { + budgets = each.value.disruption.budgets + } + ) + template = { + spec = merge({ + nodeClassRef = { + apiVersion = "karpenter.k8s.aws/v1beta1" + kind = "EC2NodeClass" + name = coalesce(each.value.name, each.key) + } + }, + try(length(each.value.requirements), 0) == 0 ? {} : { + requirements = [for r in each.value.requirements : merge({ + key = r.key + operator = r.operator + }, + try(length(r.values), 0) == 0 ? {} : { + values = r.values + })] + }, + try(length(each.value.taints), 0) == 0 ? {} : { + taints = each.value.taints + }, + try(length(each.value.startup_taints), 0) == 0 ? {} : { + startupTaints = each.value.startup_taints + } + ) + } + } + } + + depends_on = [kubernetes_manifest.ec2_node_class] +} diff --git a/modules/eks/karpenter-node-pool/outputs.tf b/modules/eks/karpenter-node-pool/outputs.tf new file mode 100644 index 000000000..507f516cc --- /dev/null +++ b/modules/eks/karpenter-node-pool/outputs.tf @@ -0,0 +1,9 @@ +output "node_pools" { + value = kubernetes_manifest.node_pool + description = "Deployed Karpenter NodePool" +} + +output "ec2_node_classes" { + value = kubernetes_manifest.ec2_node_class + description = "Deployed Karpenter EC2NodeClass" +} diff --git a/modules/eks/karpenter-node-pool/provider-helm.tf b/modules/eks/karpenter-node-pool/provider-helm.tf new file mode 100644 index 000000000..91cc7f6d4 --- /dev/null +++ b/modules/eks/karpenter-node-pool/provider-helm.tf @@ -0,0 +1,201 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false +} + +variable "kubeconfig_context" { + type = string + default = "" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT + nullable = false +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT + nullable = false +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" + nullable = false +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" + nullable = false +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = local.cluster_ca_certificate + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = local.cluster_ca_certificate + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/karpenter-node-pool/providers.tf b/modules/eks/karpenter-node-pool/providers.tf new file mode 100644 index 000000000..c2419aabb --- /dev/null +++ b/modules/eks/karpenter-node-pool/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/eks/karpenter-node-pool/remote-state.tf b/modules/eks/karpenter-node-pool/remote-state.tf new file mode 100644 index 000000000..ffca1d833 --- /dev/null +++ b/modules/eks/karpenter-node-pool/remote-state.tf @@ -0,0 +1,24 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + defaults = { + eks_cluster_id = "deleted" + eks_cluster_arn = "deleted" + eks_cluster_identity_oidc_issuer = "deleted" + karpenter_node_role_arn = "deleted" + } + + context = module.this.context +} + +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "vpc" + + context = module.this.context +} diff --git a/modules/eks/karpenter-node-pool/variables.tf b/modules/eks/karpenter-node-pool/variables.tf new file mode 100644 index 000000000..d768a0131 --- /dev/null +++ b/modules/eks/karpenter-node-pool/variables.tf @@ -0,0 +1,124 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "node_pools" { + type = map(object({ + # The name of the Karpenter provisioner. The map key is used if this is not set. + name = optional(string) + # Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets. + private_subnets_enabled = bool + # The Disruption spec controls how Karpenter scales down the node group. + # See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details + disruption = optional(object({ + # Describes which types of Nodes Karpenter should consider for consolidation. + # If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or + # replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost. + # If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods. + consolidation_policy = optional(string, "WhenUnderutilized") + + # The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s|m|h). + # This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'. + # You can choose to disable consolidation entirely by setting the string value 'Never' here. + # Earlier versions of Karpenter called this field `ttl_seconds_after_empty`. + consolidate_after = optional(string) + + # The amount of time a Node can live on the cluster before being removed (`go` duration string, s|m|h). + # You can choose to disable expiration entirely by setting the string value 'Never' here. + # This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days). + # Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`, + # but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups. + max_instance_lifetime = optional(string, "336h") + + # Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once. + # See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details. + # A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up. + # If there are multiple active budgets, Karpenter uses the most restrictive value. + # If left undefined, this will default to one budget with a value of nodes: 10%. + # Note that budgets do not prevent or limit involuntary terminations. + # Example: + # On Weekdays during business hours, don't do any deprovisioning. + # budgets = { + # schedule = "0 9 * * mon-fri" + # duration = 8h + # nodes = "0" + # } + budgets = optional(list(object({ + # The schedule specifies when a budget begins being active, using extended cronjob syntax. + # See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details. + # Timezones are not supported. This field is required if Duration is set. + schedule = optional(string) + # Duration determines how long a Budget is active after each Scheduled start. + # If omitted, the budget is always active. This is required if Schedule is set. + # Must be a whole number of minutes and hours, as cron does not work in seconds, + # but since Go's `duration.String()` always adds a "0s" at the end, that is allowed. + duration = optional(string) + # The percentage or number of nodes that Karpenter can scale down during the budget. + nodes = string + })), []) + }), {}) + # Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter + total_cpu_limit = string + # Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter + total_memory_limit = string + # Set a weight for this node pool. + # See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools + weight = optional(number, 50) + # Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details + taints = optional(list(object({ + key = string + effect = string + value = string + }))) + startup_taints = optional(list(object({ + key = string + effect = string + value = string + }))) + # Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details + metadata_options = optional(object({ + httpEndpoint = optional(string, "enabled") + httpProtocolIPv6 = optional(string, "disabled") + httpPutResponseHopLimit = optional(number, 2) + # httpTokens can be either "required" or "optional" + httpTokens = optional(string, "required") + }), {}) + # The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM) + ami_family = string + # Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes. + # Karpenter uses default block device mappings for the AMI Family specified. + # For example, the Bottlerocket AMI Family defaults with two block device mappings, + # and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored. + # Most other AMIs only have one device mapping at `/dev/xvda`. + # See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details + block_device_mappings = list(object({ + deviceName = string + ebs = optional(object({ + volumeSize = string + volumeType = string + deleteOnTermination = optional(bool, true) + encrypted = optional(bool, true) + iops = optional(number) + kmsKeyID = optional(string, "alias/aws/ebs") + snapshotID = optional(string) + throughput = optional(number) + })) + })) + # Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details + requirements = list(object({ + key = string + operator = string + # Operators like "Exists" and "DoesNotExist" do not require a value + values = optional(list(string)) + })) + })) + description = "Configuration for node pools. See code for details." + nullable = false +} diff --git a/modules/eks/karpenter-node-pool/versions.tf b/modules/eks/karpenter-node-pool/versions.tf new file mode 100644 index 000000000..b58e8e98f --- /dev/null +++ b/modules/eks/karpenter-node-pool/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1, != 2.21.0" + } + } +} diff --git a/modules/eks/karpenter/CHANGELOG.md b/modules/eks/karpenter/CHANGELOG.md index f3ff1cc20..72f3ee74c 100644 --- a/modules/eks/karpenter/CHANGELOG.md +++ b/modules/eks/karpenter/CHANGELOG.md @@ -1,3 +1,32 @@ +## Version 1.445.0 + +Components PR #1039 + +:::warning Major Breaking Changes + +Karpenter at version v0.33.0 transitioned from the `v1alpha` API to the `v1beta` API with many breaking changes. This +component (`eks/karpenter`) changed as well, dropping support for the `v1alpha` API and adding support for the `v1beta` +API. At the same time, the corresponding `eks/karpenter-provisioner` component was replaced with the +`eks/karpenter-node-pool` component. The old components remain available under the +[`deprecated/`](https://github.com/cloudposse/terraform-aws-components/tree/main/deprecated) directory. + +::: + +The full list of changes in Karpenter is too extensive to repeat here. See the +[Karpenter v1beta Migration Guide](https://karpenter.sh/v0.32/upgrading/v1beta1-migration/) and the +[Karpenter Upgrade Guide](https://karpenter.sh/docs/upgrading/upgrade-guide/) for details. + +While a zero-downtime upgrade is possible, it is very complex and tedious and Cloud Posse does not support it at this +time. Instead, we recommend you delete your existing Karpenter Provisioner (`karpenter-provisioner`) and Controller +(`karpenter`) deployments, which will scale your cluster to zero and leave all your pods suspended, and then deploy the +new components, which will resume your pods. + +Full details of the recommended migration process for these components can be found in the +[Migration Guide](./docs/v1alpha-to-v1beta-migration.md). + +If you require a zero-downtime upgrade, please contact +[Cloud Posse professional services](https://cloudposse.com/services/) for assistance. + ## Version 1.348.0 Components PR [#868](https://github.com/cloudposse/terraform-aws-components/pull/868) diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 1ad01d35b..64cbdafda 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -1,6 +1,6 @@ # Component: `eks/karpenter` -This component provisions [Karpenter](https://karpenter.sh) on an EKS cluster. It requires at least version 0.19.0 of +This component provisions [Karpenter](https://karpenter.sh) on an EKS cluster. It requires at least version 0.32.0 of Karpenter, though you are encouraged to use the latest version. ## Usage @@ -25,9 +25,10 @@ components: # https://github.com/aws/karpenter/tree/main/charts/karpenter chart_repository: "oci://public.ecr.aws/karpenter" chart: "karpenter" - chart_version: "v0.31.0" - create_namespace: true - kubernetes_namespace: "karpenter" + chart_version: "v0.36.0" + # Enable Karpenter to get advance notice of spot instances being terminated + # See https://karpenter.sh/docs/concepts/#interruption + interruption_handler_enabled: true resources: limits: cpu: "300m" @@ -42,20 +43,20 @@ components: # "karpenter-crd" can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs crd_chart_enabled: true crd_chart: "karpenter-crd" - # Set `legacy_create_karpenter_instance_profile` to `false` to allow the `eks/cluster` component - # to manage the instance profile for the nodes launched by Karpenter (recommended for all new clusters). - legacy_create_karpenter_instance_profile: false - # Enable interruption handling to deploy a SQS queue and a set of Event Bridge rules to handle interruption with Karpenter. - interruption_handler_enabled: true - - # Provision `karpenter` component on the blue EKS cluster - eks/karpenter-blue: - metadata: - component: eks/karpenter - inherits: - - eks/karpenter - vars: - eks_component_name: eks/cluster-blue + # replicas set the number of Karpenter controller replicas to run + replicas: 2 + # "settings" controls a subset of the settings for the Karpenter controller regarding batch idle and max duration. + # you can read more about these settings here: https://karpenter.sh/docs/reference/settings/ + settings: + batch_idle_duration: "1s" + batch_max_duration: "10s" + # The logging settings for the Karpenter controller + logging: + enabled: true + level: + controller: "info" + global: "info" + webhook: "error" ``` ## Provision Karpenter on EKS cluster @@ -91,10 +92,17 @@ For more details, see: The process of provisioning Karpenter on an EKS cluster consists of 3 steps. -### 1. Provision EKS Fargate Profile for Karpenter and IAM Role for Nodes Launched by Karpenter +### 1. Provision EKS IAM Role for Nodes Launched by Karpenter -EKS Fargate Profile for Karpenter and IAM Role for Nodes launched by Karpenter are provisioned by the `eks/cluster` -component: +:::note VPC assumptions being made + +We assume you've already created a VPC using our [VPC component](/modules/vpc) and have private subnets already set up. +The Karpenter node pools will be launched in the private subnets. + +::: + +EKS IAM Role for Nodes launched by Karpenter are provisioned by the `eks/cluster` component. (EKS can also provision a +Fargate Profile for Karpenter, but deploying Karpenter to Fargate is not recommended.): ```yaml components: @@ -105,55 +113,37 @@ components: inherits: - eks/cluster vars: - attributes: - - blue - eks_component_name: eks/cluster-blue - node_groups: - main: - instance_types: - - t3.medium - max_group_size: 3 - min_group_size: 1 - fargate_profiles: - karpenter: - kubernetes_namespace: karpenter - kubernetes_labels: null karpenter_iam_role_enabled: true ``` -**Notes**: +:::note Authorization -- Fargate Profile role ARNs need to be added to the `aws-auth` ConfigMap to allow the Fargate Profile nodes to join the - EKS cluster (this is done by EKS) -- Karpenter IAM role ARN needs to be added to the `aws-auth` ConfigMap to allow the nodes launched by Karpenter to join - the EKS cluster (this is done by the `eks/cluster` component) +- The AWS Auth API for EKS is used to authorize the Karpenter controller to interact with the EKS cluster. -We use EKS Fargate Profile for Karpenter because It is recommended to run Karpenter on an EKS Fargate Profile. +::: -```text -Karpenter is installed using a Helm chart. The Helm chart installs the Karpenter controller and -a webhook pod as a Deployment that needs to run before the controller can be used for scaling your cluster. -We recommend a minimum of one small node group with at least one worker node. +Karpenter is installed using a Helm chart. The Helm chart installs the Karpenter controller and a webhook pod as a +Deployment that needs to run before the controller can be used for scaling your cluster. We recommend a minimum of one +small node group with at least one worker node. -As an alternative, you can run these pods on EKS Fargate by creating a Fargate profile for the -karpenter namespace. Doing so will cause all pods deployed into this namespace to run on EKS Fargate. -Do not run Karpenter on a node that is managed by Karpenter. -``` +As an alternative, you can run these pods on EKS Fargate by creating a Fargate profile for the karpenter namespace. +Doing so will cause all pods deployed into this namespace to run on EKS Fargate. Do not run Karpenter on a node that is +managed by Karpenter. See -[Run Karpenter Controller on EKS Fargate](https://aws.github.io/aws-eks-best-practices/karpenter/#run-the-karpenter-controller-on-eks-fargate-or-on-a-worker-node-that-belongs-to-a-node-group) +[Run Karpenter Controller...](https://aws.github.io/aws-eks-best-practices/karpenter/#run-the-karpenter-controller-on-eks-fargate-or-on-a-worker-node-that-belongs-to-a-node-group) for more details. We provision IAM Role for Nodes launched by Karpenter because they must run with an Instance Profile that grants permissions necessary to run containers and configure networking. -We define the IAM role for the Instance Profile in `components/terraform/eks/cluster/karpenter.tf`. +We define the IAM role for the Instance Profile in `components/terraform/eks/cluster/controller-policy.tf`. Note that we provision the EC2 Instance Profile for the Karpenter IAM role in the `components/terraform/eks/karpenter` component (see the next step). -Run the following commands to provision the EKS Fargate Profile for Karpenter and the IAM role for instances launched by -Karpenter on the blue EKS cluster and add the role ARNs to the `aws-auth` ConfigMap: +Run the following commands to provision the EKS Instance Profile for Karpenter and the IAM role for instances launched +by Karpenter on the blue EKS cluster and add the role ARNs to the EKS Auth API: ```bash atmos terraform plan eks/cluster-blue -s plat-ue2-dev @@ -162,26 +152,19 @@ atmos terraform apply eks/cluster-blue -s plat-ue2-dev For more details, refer to: -- https://karpenter.sh/v0.18.0/getting-started/getting-started-with-terraform -- https://karpenter.sh/v0.18.0/getting-started/getting-started-with-eksctl +- [Getting started with Terraform](https://aws-ia.github.io/terraform-aws-eks-blueprints/getting-started/) +- [Getting started with `eksctl`](https://karpenter.sh/docs/getting-started/getting-started-with-karpenter/) ### 2. Provision `karpenter` component In this step, we provision the `components/terraform/eks/karpenter` component, which deploys the following resources: -- EC2 Instance Profile for the nodes launched by Karpenter (note that the IAM role for the Instance Profile is - provisioned in the previous step in the `eks/cluster` component) +- Karpenter CustomerResourceDefinitions (CRDs) using the Karpenter CRD Chart and the `helm_release` Terraform resource - Karpenter Kubernetes controller using the Karpenter Helm Chart and the `helm_release` Terraform resource - EKS IAM role for Kubernetes Service Account for the Karpenter controller (with all the required permissions) +- An SQS Queue and Event Bridge rules for handling Node Interruption events (i.e. Spot) -Run the following commands to provision the Karpenter component on the blue EKS cluster: - -```bash -atmos terraform plan eks/karpenter-blue -s plat-ue2-dev -atmos terraform apply eks/karpenter-blue -s plat-ue2-dev -``` - -Note that the stack config for the blue Karpenter component is defined in `stacks/catalog/eks/clusters/blue.yaml`. +Create a stack config for the blue Karpenter component in `stacks/catalog/eks/clusters/blue.yaml`: ```yaml eks/karpenter-blue: @@ -193,99 +176,105 @@ eks/karpenter-blue: eks_component_name: eks/cluster-blue ``` -### 3. Provision `karpenter-provisioner` component +Run the following commands to provision the Karpenter component on the blue EKS cluster: -In this step, we provision the `components/terraform/eks/karpenter-provisioner` component, which deploys Karpenter -[Provisioners](https://karpenter.sh/v0.18.0/aws/provisioning) using the `kubernetes_manifest` resource. +```bash +atmos terraform plan eks/karpenter-blue -s plat-ue2-dev +atmos terraform apply eks/karpenter-blue -s plat-ue2-dev +``` -**NOTE:** We deploy the provisioners in a separate step as a separate component since it uses `kind: Provisioner` CRD -which itself is created by the `karpenter` component in the previous step. +### 3. Provision `karpenter-node-pool` component -Run the following commands to deploy the Karpenter provisioners on the blue EKS cluster: +In this step, we provision the `components/terraform/eks/karpenter-node-pool` component, which deploys Karpenter +[NodePools](https://karpenter.sh/v0.36/getting-started/getting-started-with-karpenter/#5-create-nodepool) using the +`kubernetes_manifest` resource. -```bash -atmos terraform plan eks/karpenter-provisioner-blue -s plat-ue2-dev -atmos terraform apply eks/karpenter-provisioner-blue -s plat-ue2-dev +:::note Why use a separate component for NodePools? + +We create the NodePools as a separate component since the CRDs for the NodePools are created by the Karpenter component. +This helps manage dependencies. + +::: + +First, create an abstract component for the `eks/karpenter-node-pool` component: + +```yaml +components: + terraform: + eks/karpenter-node-pool: + metadata: + type: abstract + vars: + enabled: true + # Disabling Manifest Experiment disables stored metadata with Terraform state + # Otherwise, the state will show changes on all plans + helm_manifest_experiment_enabled: false + node_pools: + default: + # Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets + private_subnets_enabled: true + # You can use disruption to set the maximum instance lifetime for the EC2 instances launched by Karpenter. + # You can also configure how fast or slow Karpenter should add/remove nodes. + # See more: https://karpenter.sh/v0.36/concepts/disruption/ + disruption: + max_instance_lifetime: "336h" # 14 days + # Taints can be used to prevent pods without the right tolerations from running on this node pool. + # See more: https://karpenter.sh/v0.36/concepts/nodepools/#taints + taints: [] + total_cpu_limit: "1k" + # Karpenter node pool total memory limit for all pods running on the EC2 instances launched by Karpenter + total_memory_limit: "1200Gi" + # Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on + # Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, + # and capacity type (such as AWS spot or on-demand). + # See https://karpenter.sh/v0.36/concepts/nodepools/#spectemplatespecrequirements for more details + requirements: + - key: "karpenter.sh/capacity-type" + operator: "In" + # See https://karpenter.sh/docs/concepts/nodepools/#capacity-type + # Allow fallback to on-demand instances when spot instances are unavailable + # By default, Karpenter uses the "price-capacity-optimized" allocation strategy + # https://aws.amazon.com/blogs/compute/introducing-price-capacity-optimized-allocation-strategy-for-ec2-spot-instances/ + # It is currently not configurable, but that may change in the future. + # See https://github.com/aws/karpenter-provider-aws/issues/1240 + values: + - "on-demand" + - "spot" + - key: "kubernetes.io/os" + operator: "In" + values: + - "linux" + - key: "kubernetes.io/arch" + operator: "In" + values: + - "amd64" + # The following two requirements pick instances such as c3 or m5 + - key: karpenter.k8s.aws/instance-category + operator: In + values: ["c", "m", "r"] + - key: karpenter.k8s.aws/instance-generation + operator: Gt + values: ["2"] ``` -Note that the stack config for the blue Karpenter provisioner component is defined in -`stacks/catalog/eks/clusters/blue.yaml`. +Now, create the stack config for the blue Karpenter NodePool component in `stacks/catalog/eks/clusters/blue.yaml`: ```yaml -eks/karpenter-provisioner-blue: +eks/karpenter-node-pool/blue: metadata: - component: eks/karpenter-provisioner + component: eks/karpenter-node-pool inherits: - - eks/karpenter-provisioner + - eks/karpenter-node-pool vars: - attributes: - - blue eks_component_name: eks/cluster-blue ``` -You can override the default values from the `eks/karpenter-provisioner` base component. - -For your cluster, you will need to review the following configurations for the Karpenter provisioners and update it -according to your requirements: - -- [requirements](https://karpenter.sh/v0.18.0/provisioner/#specrequirements): - - ```yaml - requirements: - - key: "karpenter.sh/capacity-type" - operator: "In" - values: - - "on-demand" - - "spot" - - key: "node.kubernetes.io/instance-type" - operator: "In" - values: - - "m5.xlarge" - - "m5.large" - - "m5.medium" - - "c5.xlarge" - - "c5.large" - - "c5.medium" - - key: "kubernetes.io/arch" - operator: "In" - values: - - "amd64" - ``` - -- `taints`, `startup_taints`, `ami_family` - -- Resource limits/requests for the Karpenter controller itself: - - ```yaml - resources: - limits: - cpu: "300m" - memory: "1Gi" - requests: - cpu: "100m" - memory: "512Mi" - ``` - -- Total CPU and memory limits for all pods running on the EC2 instances launched by Karpenter: - - ```yaml - total_cpu_limit: "1k" - total_memory_limit: "1000Gi" - ``` - -- Config to terminate empty nodes after the specified number of seconds. This behavior can be disabled by setting the - value to `null` (never scales down if not set): - - ```yaml - ttl_seconds_after_empty: 30 - ``` - -- Config to terminate nodes when a maximum age is reached. This behavior can be disabled by setting the value to `null` - (never expires if not set): - - ```yaml - ttl_seconds_until_expired: 2592000 - ``` +Finally, run the following commands to deploy the Karpenter NodePools on the blue EKS cluster: + +```bash +atmos terraform plan eks/karpenter-node-pool/blue -s plat-ue2-dev +atmos terraform apply eks/karpenter-node-pool/blue -s plat-ue2-dev +``` ## Node Interruption @@ -298,7 +287,7 @@ interruption events include: - Instance Terminating Events - Instance Stopping Events -:::info +:::info Interruption Handler vs. Termination Handler The Node Interruption Handler is not the same as the Node Termination Handler. The latter is always enabled and cleanly shuts down the node in 2 minutes in response to a Node Termination event. The former gets advance notice that a node @@ -330,13 +319,15 @@ For Karpenter issues, checkout the [Karpenter Troubleshooting Guide](https://kar ### References -For more details, refer to: +For more details on the CRDs, see: + +- https://karpenter.sh/v0.36/getting-started/getting-started-with-karpenter/#5-create-nodepool +- https://karpenter.sh/v0.36/concepts/disruption/#interruption +- https://karpenter.sh/v0.36/concepts/nodepools/#taints +- https://karpenter.sh/v0.36/concepts/nodepools/#spectemplatespecrequirements -- https://karpenter.sh/v0.28.0/provisioner/#specrequirements -- https://karpenter.sh/v0.28.0/aws/provisioning -- https://aws.github.io/aws-eks-best-practices/karpenter/#creating-provisioners +- https://karpenter.sh/v0.36/getting-started/getting-started-with-karpenter/ - https://aws.github.io/aws-eks-best-practices/karpenter -- https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html @@ -344,7 +335,7 @@ For more details, refer to: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | | [aws](#requirement\_aws) | >= 4.9.0 | | [helm](#requirement\_helm) | >= 2.0 | | [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | @@ -354,7 +345,6 @@ For more details, refer to: | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Modules @@ -372,10 +362,8 @@ For more details, refer to: |------|------| | [aws_cloudwatch_event_rule.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | | [aws_cloudwatch_event_target.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | -| [aws_iam_instance_profile.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | | [aws_sqs_queue.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource | | [aws_sqs_queue_policy.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | -| [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | | [aws_iam_policy_document.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | @@ -396,7 +384,6 @@ For more details, refer to: | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [crd\_chart](#input\_crd\_chart) | The name of the Karpenter CRD chart to be installed, if `var.crd_chart_enabled` is set to `true`. | `string` | `"karpenter-crd"` | no | | [crd\_chart\_enabled](#input\_crd\_chart\_enabled) | `karpenter-crd` can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs. Set to `true` to install this CRD helm chart before the primary karpenter chart. | `bool` | `false` | no | -| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false` | `bool` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | @@ -404,7 +391,7 @@ For more details, refer to: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [interruption\_handler\_enabled](#input\_interruption\_handler\_enabled) | If `true`, deploy a SQS queue and Event Bridge rules to enable interruption handling by Karpenter.

https://karpenter.sh/v0.27.5/concepts/deprovisioning/#interruption | `bool` | `false` | no | +| [interruption\_handler\_enabled](#input\_interruption\_handler\_enabled) | If `true`, deploy a SQS queue and Event Bridge rules to enable interruption handling by Karpenter.
https://karpenter.sh/docs/concepts/disruption/#interruption | `bool` | `true` | no | | [interruption\_queue\_message\_retention](#input\_interruption\_queue\_message\_retention) | The message retention in seconds for the interruption handler SQS queue. | `number` | `300` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | @@ -412,23 +399,23 @@ For more details, refer to: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | -| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | -| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into | `string` | n/a | yes | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | | [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [legacy\_create\_karpenter\_instance\_profile](#input\_legacy\_create\_karpenter\_instance\_profile) | When `true` (the default), this component creates an IAM Instance Profile
for nodes launched by Karpenter, to preserve the legacy behavior.
Set to `false` to disable creation of the IAM Instance Profile, which
avoids conflict with having `eks/cluster` create it.
Use in conjunction with `eks/cluster` component `legacy_do_not_create_karpenter_instance_profile`,
which see for further details. | `bool` | `true` | no | +| [logging](#input\_logging) | A subset of the logging settings for the Karpenter controller |
object({
enabled = optional(bool, true)
level = optional(object({
controller = optional(string, "info")
global = optional(string, "info")
webhook = optional(string, "error")
}), {})
})
| `{}` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [rbac\_enabled](#input\_rbac\_enabled) | Enable/disable RBAC | `bool` | `true` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [replicas](#input\_replicas) | The number of Karpenter controller replicas to run | `number` | `2` | no | | [resources](#input\_resources) | The CPU and memory of the deployment's limits and requests |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | +| [settings](#input\_settings) | A subset of the settings for the Karpenter controller.
Some settings are implicitly set by this component, such as `clusterName` and
`interruptionQueue`. All settings can be overridden by providing a `settings`
section in the `chart_values` variable. The settings provided here are the ones
mostly likely to be set to other than default values, and are provided here for convenience. |
object({
batch_idle_duration = optional(string, "1s")
batch_max_duration = optional(string, "10s")
})
| `{}` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | @@ -439,29 +426,20 @@ For more details, refer to: | Name | Description | |------|-------------| -| [instance\_profile](#output\_instance\_profile) | Provisioned EC2 Instance Profile for nodes launched by Karpenter | | [metadata](#output\_metadata) | Block status of the deployed release | -## References +## Related reading - https://karpenter.sh -- https://aws.github.io/aws-eks-best-practices/karpenter -- https://karpenter.sh/v0.18.0/getting-started/getting-started-with-terraform - https://aws.amazon.com/blogs/aws/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler - https://github.com/aws/karpenter -- https://www.eksworkshop.com/beginner/085_scaling_karpenter - https://ec2spotworkshops.com/karpenter.html -- https://www.eksworkshop.com/beginner/085_scaling_karpenter/install_karpenter -- https://karpenter.sh/v0.18.0/development-guide -- https://karpenter.sh/v0.18.0/aws/provisioning +- https://www.eksworkshop.com/docs/autoscaling/compute/karpenter/ - https://docs.aws.amazon.com/eks/latest/userguide/pod-execution-role.html - https://aws.amazon.com/premiumsupport/knowledge-center/fargate-troubleshoot-profile-creation - https://learn.hashicorp.com/tutorials/terraform/kubernetes-crd-faas -- https://github.com/hashicorp/terraform-provider-kubernetes/issues/1545 -- https://issuemode.com/issues/hashicorp/terraform-provider-kubernetes-alpha/4840198 -- https://bytemeta.vip/repo/hashicorp/terraform-provider-kubernetes/issues/1442 - https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html [](https://cpco.io/component) diff --git a/modules/eks/karpenter/controller-policy-v1alpha.tf b/modules/eks/karpenter/controller-policy-v1alpha.tf new file mode 100644 index 000000000..0c4d010d8 --- /dev/null +++ b/modules/eks/karpenter/controller-policy-v1alpha.tf @@ -0,0 +1,67 @@ +##### +# The primary and current (v1beta API) controller policy is in the controller-policy.tf file. +# +# However, if you have workloads that were deployed under the v1alpha API, you need to also +# apply this controller-policy-v1alpha.tf policy to the Karpenter controller to give it permission +# to manage (an in particular, delete) those workloads, and give it permission to manage the +# EC2 Instance Profile possibly created by the EKS cluster component. +# +# This policy is not needed for workloads deployed under the v1beta API with the +# EC2 Instance Profile created by the Karpenter controller. +# +# This allows it to terminate instances and delete launch templates that are tagged with the +# v1alpha API tag "karpenter.sh/provisioner-name" and to manage the EC2 Instance Profile +# created by the EKS cluster component. +# +# WARNING: it is important that the SID values do not conflict with the SID values in the +# controller-policy.tf file, otherwise they will be overwritten. +# + +locals { + controller_policy_v1alpha_json = <<-EndOfPolicy + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowScopedDeletionV1alpha", + "Effect": "Allow", + "Resource": [ + "arn:${local.aws_partition}:ec2:${var.region}:*:instance/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:launch-template/*" + ], + "Action": [ + "ec2:TerminateInstances", + "ec2:DeleteLaunchTemplate" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned" + }, + "StringLike": { + "aws:ResourceTag/karpenter.sh/provisioner-name": "*" + } + } + }, + { + "Sid": "AllowScopedInstanceProfileActionsV1alpha", + "Effect": "Allow", + "Resource": "*", + "Action": [ + "iam:AddRoleToInstanceProfile", + "iam:RemoveRoleFromInstanceProfile", + "iam:DeleteInstanceProfile" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned", + "aws:ResourceTag/topology.kubernetes.io/region": "${var.region}" + }, + "ArnEquals": { + "ec2:InstanceProfile": "${replace(local.karpenter_node_role_arn, "role", "instance-profile")}" + } + } + } + ] + } + EndOfPolicy +} diff --git a/modules/eks/karpenter/controller-policy.tf b/modules/eks/karpenter/controller-policy.tf new file mode 100644 index 000000000..6e234378b --- /dev/null +++ b/modules/eks/karpenter/controller-policy.tf @@ -0,0 +1,298 @@ +# Unfortunately, Karpenter does not provide the Karpenter controller IAM policy in JSON directly: +# https://github.com/aws/karpenter/issues/2649 +# +# You can get it from the `data.aws_iam_policy_document.karpenter_controller` in +# https://github.com/terraform-aws-modules/terraform-aws-iam/blob/master/modules/iam-role-for-service-accounts-eks/policies.tf +# but that is not guaranteed to be up-to-date. +# +# Instead, we download the official source of truth, the CloudFormation template, and extract the IAM policy from it. +# +# The policy is not guaranteed to be stable from version to version. +# However, it seems stable enough, and we will leave for later the task of supporting multiple versions. +# +# To get the policy for a given Karpenter version >= 0.32.0, run: +# +# KARPENTER_VERSION= +# curl -O -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml +# +# Then open the downloaded cloudformation.yaml file and look for this resource (there may be other lines in between): +# +# KarpenterControllerPolicy: +# Type: AWS::IAM::ManagedPolicy +# Properties: +# PolicyDocument: !Sub | +# +# After which should be the IAM policy document in JSON format, with +# CloudFormation substitutions like +# +# "Resource": "arn:${local.aws_partition}:eks:${var.region}:${AWS::AccountId}:cluster/${local.eks_cluster_id}" +# +# NOTE: As a special case, the above multiple substitutions which create the ARN for the EKS cluster +# should be replaced with a single substitution, `${local.eks_cluster_arn}` to avoid neeeding to +# look up the account ID and because it is more robust. +# +# Review the existing HEREDOC below to find conditionals such as: +# %{if local.interruption_handler_enabled } +# and figure out how you want to re-incorporate them into the new policy, if needed. +# +# Paste the new policy into the HEREDOC below, then replace the CloudFormation substitutions with Terraform substitutions, +# e.g. ${var.region} -> ${var.region} +# +# and restore the conditionals. +# + +locals { + controller_policy_json = <<-EndOfPolicy + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowScopedEC2InstanceAccessActions", + "Effect": "Allow", + "Resource": [ + "arn:${local.aws_partition}:ec2:${var.region}::image/*", + "arn:${local.aws_partition}:ec2:${var.region}::snapshot/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:security-group/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:subnet/*" + ], + "Action": [ + "ec2:RunInstances", + "ec2:CreateFleet" + ] + }, + { + "Sid": "AllowScopedEC2LaunchTemplateAccessActions", + "Effect": "Allow", + "Resource": "arn:${local.aws_partition}:ec2:${var.region}:*:launch-template/*", + "Action": [ + "ec2:RunInstances", + "ec2:CreateFleet" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned" + }, + "StringLike": { + "aws:ResourceTag/karpenter.sh/nodepool": "*" + } + } + }, + { + "Sid": "AllowScopedEC2InstanceActionsWithTags", + "Effect": "Allow", + "Resource": [ + "arn:${local.aws_partition}:ec2:${var.region}:*:fleet/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:instance/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:volume/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:network-interface/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:launch-template/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:spot-instances-request/*" + ], + "Action": [ + "ec2:RunInstances", + "ec2:CreateFleet", + "ec2:CreateLaunchTemplate" + ], + "Condition": { + "StringEquals": { + "aws:RequestTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned" + }, + "StringLike": { + "aws:RequestTag/karpenter.sh/nodepool": "*" + } + } + }, + { + "Sid": "AllowScopedResourceCreationTagging", + "Effect": "Allow", + "Resource": [ + "arn:${local.aws_partition}:ec2:${var.region}:*:fleet/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:instance/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:volume/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:network-interface/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:launch-template/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:spot-instances-request/*" + ], + "Action": "ec2:CreateTags", + "Condition": { + "StringEquals": { + "aws:RequestTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned", + "ec2:CreateAction": [ + "RunInstances", + "CreateFleet", + "CreateLaunchTemplate" + ] + }, + "StringLike": { + "aws:RequestTag/karpenter.sh/nodepool": "*" + } + } + }, + { + "Sid": "AllowScopedResourceTagging", + "Effect": "Allow", + "Resource": "arn:${local.aws_partition}:ec2:${var.region}:*:instance/*", + "Action": "ec2:CreateTags", + "Condition": { + "StringEquals": { + "aws:ResourceTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned" + }, + "StringLike": { + "aws:ResourceTag/karpenter.sh/nodepool": "*" + }, + "ForAllValues:StringEquals": { + "aws:TagKeys": [ + "karpenter.sh/nodeclaim", + "Name" + ] + } + } + }, + { + "Sid": "AllowScopedDeletion", + "Effect": "Allow", + "Resource": [ + "arn:${local.aws_partition}:ec2:${var.region}:*:instance/*", + "arn:${local.aws_partition}:ec2:${var.region}:*:launch-template/*" + ], + "Action": [ + "ec2:TerminateInstances", + "ec2:DeleteLaunchTemplate" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned" + }, + "StringLike": { + "aws:ResourceTag/karpenter.sh/nodepool": "*" + } + } + }, + { + "Sid": "AllowRegionalReadActions", + "Effect": "Allow", + "Resource": "*", + "Action": [ + "ec2:DescribeAvailabilityZones", + "ec2:DescribeImages", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypeOfferings", + "ec2:DescribeInstanceTypes", + "ec2:DescribeLaunchTemplates", + "ec2:DescribeSecurityGroups", + "ec2:DescribeSpotPriceHistory", + "ec2:DescribeSubnets" + ], + "Condition": { + "StringEquals": { + "aws:RequestedRegion": "${var.region}" + } + } + }, + { + "Sid": "AllowSSMReadActions", + "Effect": "Allow", + "Resource": "arn:${local.aws_partition}:ssm:${var.region}::parameter/aws/service/*", + "Action": "ssm:GetParameter" + }, + { + "Sid": "AllowPricingReadActions", + "Effect": "Allow", + "Resource": "*", + "Action": "pricing:GetProducts" + }, + %{if local.interruption_handler_enabled} + { + "Sid": "AllowInterruptionQueueActions", + "Effect": "Allow", + "Resource": "${local.interruption_handler_queue_arn}", + "Action": [ + "sqs:DeleteMessage", + "sqs:GetQueueUrl", + "sqs:ReceiveMessage" + ] + }, + %{endif} + { + "Sid": "AllowPassingInstanceRole", + "Effect": "Allow", + "Resource": "${local.karpenter_node_role_arn}", + "Action": "iam:PassRole", + "Condition": { + "StringEquals": { + "iam:PassedToService": "ec2.amazonaws.com" + } + } + }, + { + "Sid": "AllowScopedInstanceProfileCreationActions", + "Effect": "Allow", + "Resource": "*", + "Action": [ + "iam:CreateInstanceProfile" + ], + "Condition": { + "StringEquals": { + "aws:RequestTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned", + "aws:RequestTag/topology.kubernetes.io/region": "${var.region}" + }, + "StringLike": { + "aws:RequestTag/karpenter.k8s.aws/ec2nodeclass": "*" + } + } + }, + { + "Sid": "AllowScopedInstanceProfileTagActions", + "Effect": "Allow", + "Resource": "*", + "Action": [ + "iam:TagInstanceProfile" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned", + "aws:ResourceTag/topology.kubernetes.io/region": "${var.region}", + "aws:RequestTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned", + "aws:RequestTag/topology.kubernetes.io/region": "${var.region}" + }, + "StringLike": { + "aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass": "*", + "aws:RequestTag/karpenter.k8s.aws/ec2nodeclass": "*" + } + } + }, + { + "Sid": "AllowScopedInstanceProfileActions", + "Effect": "Allow", + "Resource": "*", + "Action": [ + "iam:AddRoleToInstanceProfile", + "iam:RemoveRoleFromInstanceProfile", + "iam:DeleteInstanceProfile" + ], + "Condition": { + "StringEquals": { + "aws:ResourceTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned", + "aws:ResourceTag/topology.kubernetes.io/region": "${var.region}" + }, + "StringLike": { + "aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass": "*" + } + } + }, + { + "Sid": "AllowInstanceProfileReadActions", + "Effect": "Allow", + "Resource": "*", + "Action": "iam:GetInstanceProfile" + }, + { + "Sid": "AllowAPIServerEndpointDiscovery", + "Effect": "Allow", + "Resource": "${local.eks_cluster_arn}", + "Action": "eks:DescribeCluster" + } + ] + } + EndOfPolicy +} diff --git a/modules/eks/karpenter/docs/v1alpha-to-v1beta-migration.md b/modules/eks/karpenter/docs/v1alpha-to-v1beta-migration.md new file mode 100644 index 000000000..fb73b326a --- /dev/null +++ b/modules/eks/karpenter/docs/v1alpha-to-v1beta-migration.md @@ -0,0 +1,209 @@ +# Migration Guide + +## Prepare to Upgrade Karpenter API version + +Before you begin upgrading from Karpenter `v1alpha5` to `v1beta1` APIs, you should get your applications ready for the +changes and validate that your existing configuration has been applied to all your Karpenter instances. You may also +want to upgrade to the latest `v1alpha5` version of Karpenter (0.31.4 as of this writing) to ensure you haven't missed +any changes. + +### Validate your existing Karpenter deployments + +In order to preserve some kind of ability to rollback, you should validate your existing Karpenter deployments are in a +good state by planning them and either verifying they have no changes, or fixing them or deploying the changes. Then +freeze this configuration so that you can roll back to it if needed. + +### Make all your changes to related components + +Make all the changes to related components that are required to support the new version of Karpenter. This mainly +involves updating annotations and tolerations in your workloads to match the new Karpenter annotations and taints. Keep +the existing annotations and tolerations in place, so that your workloads will work with both versions. + +A lot of labels, tags, and annotations have changed in the new version of Karpenter. You should review the +[Karpenter v1beta1 migration guide](https://karpenter.sh/v0.32/upgrading/v1beta1-migration/) and roll out the changes to +your workloads before upgrading Karpenter. Where possible, you should roll out the changes in such a way that they work +with both the old and new versions of Karpenter. For example, instead of replacing the old annotations with the new +annotations, you should add the new annotations in addition to the old annotations, and remove the old annotations +later. + +Here are some highlights of the changes, but you should review the full +[Karpenter v1beta1 migration guide](https://karpenter.sh/v0.32/upgrading/v1beta1-migration/) for all the changes: + +- Annotations `karpenter.sh/do-not-consolidate` and `karpenter.sh/do-not-evict` have been replaced with + `karpenter.sh/do-not-disrupt: "true"` +- Nodes spawned by the `v1beta1` resource will use the taint `karpenter.sh/disruption:NoSchedule=disrupting` instead of + `node.kubernetes.io/unschedulable` so you may need to adjust pod tolerations +- The following deprecated node labels have been removed in favor of their more modern equivalents. These need to be + changed in your workloads where they are used for topology constraints, affinities, etc. and they also need to be + changed in your NodePool (formerly Provisioner) requirements: + - `failure-domain.beta.kubernetes.io/zone` -> `topology.kubernetes.io/zone` + - `failure-domain.beta.kubernetes.io/region` -> `topology.kubernetes.io/region` + - `beta.kubernetes.io/arch` -> `kubernetes.io/arch` + - `beta.kubernetes.io/os` -> `kubernetes.io/os` + - `beta.kubernetes.io/instance-type` -> `node.kubernetes.io/instance-type` + +Deploy all these changes. + +### Deploy a managed node group, if you haven't already + +Karpenter now recommends deploying to it into a managed node group rather than via Fargate. In part, this is because +Karpenter also strongly recommends it be deployed to the `kube-system` namespace, and deploying the `kube-system` +namespace to Fargate is inefficient at best. This component no longer supports deploying Karpenter to any namespace +other than `kube-system`, so if you had been deploying it to Fargate, you probably want to provision a minimal managed +node group to run the `kube-system` namespace, and it will also host Karpenter as well. + +## Migration, the Long Way + +It is possible to upgrade Karpenter step-by-step, but it is a long process. Here are the basic steps to get you to +v0.36.0 (there may be more for later versions): + +- Upgrade to v0.31.4 (or later v0.31.x if available), fixing any upgrade issues +- Upgrade to v0.32.9, moving Karpenter to the `kube-system` namespace, which will require some manual intervention when + applying the Helm chart +- Deploy all new Karpenter `v1beta1` resources that mirror your `v1alpha5` resources, and make all the other changes + listed in the [v1beta1 migration guide](https://karpenter.sh/v0.32/upgrading/v1beta1-migration/) such as (not a + complete list): + - Annotations `karpenter.sh/do-not-consolidate` and `karpenter.sh/do-not-evict` have been replaced with + `karpenter.sh/do-not-disrupt: "true"` + - Karpenter-generated tag keys have changed, so you may need to adjust your IAM Policies if you are using + Attribute-Based Access Control. + - The `karpenter-global-settings` ConfigMap has been replaced with settings via Environment Variables and CLI flags + - Default log encoding changed from console to JSON, so if your log processing cannot handle JSON logs, you should + probably change your log processing rather than sticking with the deprecated console encoding + - Prometheus metrics are now served on port 8001. You may need to adjust scraper configurations, and you may need to + override this port setting if it would otherwise cause a conflict. +- Delete all old Karpenter `v1alpha5` resources +- Review the [Karpenter upgrade guide](https://karpenter.sh/docs/upgrading/upgrade-guide/) and make additional changes + to reflect your preferences regarding new features and changes in behavior, such as (not a complete list): + - Availability of Node Pool Disruption Budgets + - Incompatibility with Ubuntu 22.04 EKS AMI + - Changes to names of Kubernetes labels Karpenter uses + - Changes to tags Karpenter uses + - Recommendation to move Karpenter from `karpenter` namespace to `kube-system` + - Deciding on if you want drift detection enabled + - Changes to logging configuration + - Changes to how Selectors, e.g. for Subnets, are configured + - Karpenter now uses a podSecurityContext to configure the `fsgroup` for pod volumes (to `65536`), which can affect + sidecars +- Upgrade to the latest version of Karpenter + +This multistep process is particularly difficult to organize and execute using Terraform and Helm because of the +changing resource types and configuration required to support both `v1alpha5` and `v1beta1` resources at the same time. +Therefore, this component does not support this path, and this document does not describe it in any greater detail. + +## Migration, the Shorter Way + +The shortest way is to delete all Karpenter resources, completely deleting the Cloud Posse `eks/karpenter` and +`eks/karpenter-provisioner` components, and then upgrading the components to the latest version and redeploying them. + +The shorter (but not shortest) way is to abandon the old configuration and code in place, taking advantage of the fact +that `eks/karpenter-provisioner` has been replaced with `eks/karpenter-node-pool`. That path is what the rest of this +document describes. + +### Disable automatic deployments + +If you are using some kind of automatic deployment, such as Spacelift, disable it for the `karpenter` and +`karpenter-provisioner` stacks. This is because we will roll out breaking changes, and want to sequence the operations +manually. If using Spacelift, you can disable it by setting `workspace_enabled: false`, but remember, you must check in +the changes and merge them to your default branch in order for them to take effect. + +### Copy existing configuration to new names + +The `eks/karpenter-provisioner` component has been replaced with the `eks/karpenter-node-pool` component. You should +copy your existing `karpenter-provisioner` stacks to `karpenter-node-pool` stacks, adjusting the component name and +adding it to the import list wherever `karpenter-provisioner` was imported. + +For the moment, we will leave the old `karpenter-provisioner` component and stacks in place. + +### Revise your copied `karpenter-node-pool` stacks + +Terminology has changed and some settings have been moved in the new version. See the +[Karpenter v1beta1 Migration Guide](https://karpenter.sh/v0.32/upgrading/v1beta1-migration/) for details. + +For the most part you can just use the copied settings from the old version of this component directly in new version, +but there are some changes. + +As you have seen, "provisioner" has been renamed "node_pool". So you will need to make some changes to your new +`karpenter-node-pool` stacks. + +Specifically, `provisioner` input has been renamed `node_pools`. Within that input: + +- The `consolidation` input, which used to be a single boolean, has been replaced with the full `disruption` element of + the NodePool. +- The old `ttl_seconds_after_empty` is now `disruption.consolidate_after`. +- The old `ttl_seconds_until_expired` is now `disruption.max_instance_lifetime` to align with the EC2 Auto Scaling Group + terminology, although Karpenter calles it `expiresAfter`. +- `spec.template.spec.kubelet` settings are not yet supported by this component. +- `settings.aws.enablePodENI` and `settings.aws.enableENILimitedPodDensity`, which you may have previously set via + `chart_values`, have been dropped by Karpenter. +- Many other chart values you may be been setting by `chart_values` have been moved. See + [Karpenter v1beta1 Migration Guide](https://karpenter.sh/v0.32/upgrading/v1beta1-migration/#helm-values) for details. + +### Revise your `karpenter` stacks + +The `karpenter` stack probably requires only a few changes. In general, if you had been setting anything via +`chart_values`, you probably should just delete those settings. If the component doesn't support the setting, it is +likely that Karpenter no longer supports it, or the way it is configured vai the chart has changed. + +For examples, `AWS_ENI_LIMITED_POD_DENSITY` is no longer supported by Karpenter, and `replicas` is now a setting of the +component, and does not need to be set via `chart_values`. + +- Update the chart version. Find the latest version by looking inside the + [Chart.yaml](https://github.com/aws/karpenter-provider-aws/blob/main/charts/karpenter/Chart.yaml) file in the + Karpenter Helm chart repository, on the main branch. Use the value set as `version` (not `appVersion`, if different) + in that file. + +- Karpenter is now always deployed to the `kube-system` namespace. Any Kubernetes namespace configuration inputs have + been removed. Remove these lines from your configuration: + + ```yaml + create_namespace: true + kubernetes_namespace: "karpenter" + ``` + +- The number of replicas can now be set via the `replicas` input. That said, there is little reason to change this from + the default of 2. Only one controller is active at a time, and the other one is a standby. There is no load sharing or + other reason to have more than 2 replicas in most cases. + +- The lifecycle settings `consolidation`, `ttl_seconds_after_empty` and `ttl_seconds_until_expired` have been moved to + the `disruption` input. Unfortunately, the documentation for the Karpetner Disruption spec is lacking, so read the + comments in the code for the `disruption` input for details. The short story is: + + - `consolidation` is now enabled by default. To disable it, set `disruption.consolidate_after` to `"Never"`. + - If you previously set `ttl_seconds_after_empty`, move that setting to the `disruption.consolidate_after` attribute, + and set `disruption.consolidation_policy` to `"WhenEmpty"`. + - If you previously set `ttl_seconds_until_expired`, move that setting to the `disruption.max_instance_lifetime` + attribute. If you previously left it unset, you can keep the previous behavior by setting it to "Never". The new + default it to expire instances after 336 hours (14 days). + - The disruption setting can optionally take a list of `budget` settings. See the + [Disruption Budgets documentation](https://karpenter.sh/docs/concepts/disruption/#disruption-budgets) for details on + what this is. It is **not** the same as a Pod disruption budget, which tries to put limits on the number of + instances of a pod that are running at once. Instead, it is a limitation on how quickly Karpenter will remove + instances. + +- The [interruption handler](https://karpenter.sh/docs/concepts/disruption/#interruption) is now enabled by default. If + you had disabled it, you may want to reconsider. It is a key feature of Karpenter that allows it to automatically + handle interruptions and reschedule pods on other nodes gracefully given the advance notice provided by AWS of + involuntary interruption events. + +- The `legacy_create_karpenter_instance_profile` has been removed. Previously, this component would create an instance + profile for the Karpenter nodes. This flag disabled that behavior in favor of having the EKS cluster create the + instance profile, because the Terraform code could not handle certain edge cases. Now Karpenter itself creates the + instance profile and handles the edge cases, so the flag is no longer needed. + + As a side note: if you are using the `eks/cluster` component, you can remove any + `legacy_do_not_create_karpenter_instance_profile` configuration from it after finishing the migration to the new + Karpenter APIs. + +- Logging configuration has changed. The component has a single `logging` input object that defaults to enabled at the + "info" level for the controller. If you were configuring logging via `chart_values`, we recommend you remove that + configuration and use the new input object. However, if the input object is not sufficient for your needs, you can use + new chart values to configure the logging level and format, but be aware the new chart inputs controlling logging are + significantly different from the old ones. + +- You may want to take advantage of the new `batch_idle_duration` and `batch_max_duration` settings, set as attributes + of the `settings` input. These settings allow you to control how long Karpenter waits for more pods to be deployed + before launching a new instance. This is useful if you have many pods to deploy in response to a single event, such as + when launching multiple CI jobs to handle a new release. Karpenter can then launch a single instance to handle them + all, rather than launching a new instance for each pod. See the + [batching parameters](https://karpenter.sh/docs/reference/settings/#batching-parameters) documentation for details. diff --git a/modules/eks/karpenter/interruption_handler.tf b/modules/eks/karpenter/interruption_handler.tf index 558ee7de1..56a3334f7 100644 --- a/modules/eks/karpenter/interruption_handler.tf +++ b/modules/eks/karpenter/interruption_handler.tf @@ -1,6 +1,11 @@ +# These event definitions, queue policies, and SQS queue definition +# come from the Karpenter CloudFormation template. +# See comments in `controller-policy.tf` for more information. + locals { interruption_handler_enabled = local.enabled && var.interruption_handler_enabled interruption_handler_queue_name = module.this.id + interruption_handler_queue_arn = one(aws_sqs_queue.interruption_handler[*].arn) dns_suffix = join("", data.aws_partition.current[*].dns_suffix) @@ -40,10 +45,6 @@ locals { } } -data "aws_partition" "current" { - count = local.interruption_handler_enabled ? 1 : 0 -} - resource "aws_sqs_queue" "interruption_handler" { count = local.interruption_handler_enabled ? 1 : 0 @@ -69,7 +70,6 @@ data "aws_iam_policy_document" "interruption_handler" { "sqs.${local.dns_suffix}", ] } - } } diff --git a/modules/eks/karpenter/karpenter-crd-upgrade b/modules/eks/karpenter/karpenter-crd-upgrade index e6274deb3..a3e3ce05c 100755 --- a/modules/eks/karpenter/karpenter-crd-upgrade +++ b/modules/eks/karpenter/karpenter-crd-upgrade @@ -2,23 +2,27 @@ function usage() { cat >&2 <<'EOF' -./karpenter-crd-upgrade +./karpenter-crd-upgrade -Use this script to prepare a cluster for karpenter-crd helm chart support by upgrading Karpenter CRDs. +Use this script to upgrade the Karpenter CRDs by installing or upgrading the karpenter-crd helm chart. EOF } function upgrade() { + VERSION="${1}" + [[ $VERSION =~ ^v ]] || VERSION="v${VERSION}" + set -x kubectl label crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh app.kubernetes.io/managed-by=Helm --overwrite kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-name=karpenter-crd --overwrite kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-namespace=karpenter --overwrite + helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version "$VERSION" --namespace karpenter } if (($# == 0)); then - upgrade -else usage +else + upgrade $1 fi diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index 1ebf263c4..a038b645b 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -1,49 +1,29 @@ # https://aws.amazon.com/blogs/aws/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler/ # https://karpenter.sh/ -# https://karpenter.sh/v0.10.1/getting-started/getting-started-with-terraform/ -# https://karpenter.sh/v0.10.1/getting-started/getting-started-with-eksctl/ -# https://www.eksworkshop.com/beginner/085_scaling_karpenter/ -# https://karpenter.sh/v0.10.1/aws/provisioning/ -# https://www.eksworkshop.com/beginner/085_scaling_karpenter/setup_the_environment/ -# https://ec2spotworkshops.com/karpenter.html -# https://catalog.us-east-1.prod.workshops.aws/workshops/76a5dd80-3249-4101-8726-9be3eeee09b2/en-US/autoscaling/karpenter locals { enabled = module.this.enabled - eks_cluster_identity_oidc_issuer = try(module.eks.outputs.eks_cluster_identity_oidc_issuer, "") - karpenter_iam_role_name = try(module.eks.outputs.karpenter_iam_role_name, "") + # We need aws_partition to be non-null even when this module is disabled, because it is used in a string template + aws_partition = coalesce(one(data.aws_partition.current[*].partition), "aws") - karpenter_instance_profile_enabled = local.enabled && var.legacy_create_karpenter_instance_profile && length(local.karpenter_iam_role_name) > 0 -} + # eks_cluster_id is defined in provider-helm.tf + # eks_cluster_id = module.eks.outputs.eks_cluster_id + eks_cluster_arn = module.eks.outputs.eks_cluster_arn + eks_cluster_identity_oidc_issuer = module.eks.outputs.eks_cluster_identity_oidc_issuer -resource "aws_iam_instance_profile" "default" { - count = local.karpenter_instance_profile_enabled ? 1 : 0 + karpenter_node_role_arn = module.eks.outputs.karpenter_iam_role_arn - name = local.karpenter_iam_role_name - role = local.karpenter_iam_role_name - tags = module.this.tags + # Prior to Karpenter v0.32.0 (the v1Alpha APIs), Karpenter recommended using a dedicated namespace for Karpenter resources. + # Starting with Karpenter v0.32.0, Karpenter recommends installing Karpenter resources in the kube-system namespace. + # https://karpenter.sh/docs/getting-started/getting-started-with-karpenter/#preventing-apiserver-request-throttling + kubernetes_namespace = "kube-system" } -# See CHANGELOG for PR #868: -# https://github.com/cloudposse/terraform-aws-components/pull/868 -# -# Namespace was moved from the karpenter module to an independent resource in order to be -# shared between both the karpenter and karpenter-crd modules. -moved { - from = module.karpenter.kubernetes_namespace.default[0] - to = kubernetes_namespace.default[0] +data "aws_partition" "current" { + count = local.enabled ? 1 : 0 } -resource "kubernetes_namespace" "default" { - count = local.enabled && var.create_namespace ? 1 : 0 - - metadata { - name = var.kubernetes_namespace - annotations = {} - labels = merge(module.this.tags, { name = var.kubernetes_namespace }) - } -} # Deploy karpenter-crd helm chart # "karpenter-crd" can be installed as an independent helm chart to manage the lifecycle of Karpenter CRDs @@ -63,28 +43,12 @@ module "karpenter_crd" { cleanup_on_fail = var.cleanup_on_fail timeout = var.timeout - create_namespace_with_kubernetes = false # Namespace is created with kubernetes_namespace resources to be shared between charts - kubernetes_namespace = join("", kubernetes_namespace.default[*].id) - kubernetes_namespace_labels = merge(module.this.tags, { name = join("", kubernetes_namespace.default[*].id) }) + create_namespace_with_kubernetes = false # Namespace is created by EKS/Kubernetes by default + kubernetes_namespace = local.kubernetes_namespace eks_cluster_oidc_issuer_url = coalesce(replace(local.eks_cluster_identity_oidc_issuer, "https://", ""), "deleted") - values = compact([ - # standard k8s object settings - yamlencode({ - fullnameOverride = module.this.name - resources = var.resources - rbac = { - create = var.rbac_enabled - } - }), - ]) - context = module.this.context - - depends_on = [ - kubernetes_namespace.default - ] } # Deploy Karpenter helm chart @@ -102,93 +66,20 @@ module "karpenter" { timeout = var.timeout create_namespace_with_kubernetes = false # Namespace is created with kubernetes_namespace resources to be shared between charts - kubernetes_namespace = join("", kubernetes_namespace.default[*].id) - kubernetes_namespace_labels = merge(module.this.tags, { name = join("", kubernetes_namespace.default[*].id) }) + kubernetes_namespace = local.kubernetes_namespace eks_cluster_oidc_issuer_url = coalesce(replace(local.eks_cluster_identity_oidc_issuer, "https://", ""), "deleted") service_account_name = module.this.name - service_account_namespace = join("", kubernetes_namespace.default[*].id) - - iam_role_enabled = true - - # https://karpenter.sh/v0.6.1/getting-started/cloudformation.yaml - # https://karpenter.sh/v0.10.1/getting-started/getting-started-with-terraform - # https://github.com/aws/karpenter/issues/2649 - # Apparently the source of truth for the best IAM policy is the `data.aws_iam_policy_document.karpenter_controller` in - # https://github.com/terraform-aws-modules/terraform-aws-iam/blob/master/modules/iam-role-for-service-accounts-eks/policies.tf - iam_policy = [{ - statements = concat([ - { - sid = "KarpenterController" - effect = "Allow" - resources = ["*"] - - actions = [ - # https://github.com/terraform-aws-modules/terraform-aws-iam/blob/99c69ad54d985f67acf211885aa214a3a6cc931c/modules/iam-role-for-service-accounts-eks/policies.tf#L511-L581 - # The reference policy is broken up into multiple statements with different resource restrictions based on tags. - # This list has breaks where statements are separated in the reference policy for easier comparison and maintenance. - "ec2:CreateLaunchTemplate", - "ec2:CreateFleet", - "ec2:CreateTags", - "ec2:DescribeLaunchTemplates", - "ec2:DescribeImages", - "ec2:DescribeInstances", - "ec2:DescribeSecurityGroups", - "ec2:DescribeSubnets", - "ec2:DescribeInstanceTypes", - "ec2:DescribeInstanceTypeOfferings", - "ec2:DescribeAvailabilityZones", - "ec2:DescribeSpotPriceHistory", - "pricing:GetProducts", - - "ec2:TerminateInstances", - "ec2:DeleteLaunchTemplate", - - "ec2:RunInstances", - - "iam:PassRole", - ] - }, - { - sid = "KarpenterControllerSSM" - effect = "Allow" - # Allow Karpenter to read AMI IDs from SSM - actions = ["ssm:GetParameter"] - resources = ["arn:aws:ssm:*:*:parameter/aws/service/*"] - }, - { - sid = "KarpenterControllerClusterAccess" - effect = "Allow" - actions = [ - "eks:DescribeCluster" - ] - resources = [ - module.eks.outputs.eks_cluster_arn - ] - } - ], - local.interruption_handler_enabled ? [ - { - sid = "KarpenterInterruptionHandlerAccess" - effect = "Allow" - actions = [ - "sqs:DeleteMessage", - "sqs:GetQueueUrl", - "sqs:GetQueueAttributes", - "sqs:ReceiveMessage", - ] - resources = [ - one(aws_sqs_queue.interruption_handler[*].arn) - ] - } - ] : [] - ) - }] + service_account_namespace = local.kubernetes_namespace + # Defaults to true, but set it here so it can be disabled when switching to Pod Identities + service_account_role_arn_annotation_enabled = true + + iam_role_enabled = true + iam_source_policy_documents = [local.controller_policy_v1alpha_json, local.controller_policy_json] values = compact([ - # standard k8s object settings yamlencode({ fullnameOverride = module.this.name serviceAccount = { @@ -197,31 +88,29 @@ module "karpenter" { controller = { resources = var.resources } - rbac = { - create = var.rbac_enabled - } + replicas = var.replicas }), - # karpenter-specific values + # karpenter-specific values yamlencode({ - settings = { - # This configuration of settings requires Karpenter chart v0.19.0 or later - aws = { - defaultInstanceProfile = local.karpenter_iam_role_name # instance profile name === role name - clusterName = local.eks_cluster_id - # clusterEndpoint not needed as of v0.25.0 - clusterEndpoint = local.eks_cluster_endpoint - tags = module.this.tags + logConfig = { + enabled = var.logging.enabled + logLevel = { + controller = var.logging.level.controller + global = var.logging.level.global + webhook = var.logging.level.webhook } } - }), - yamlencode( - local.interruption_handler_enabled ? { - settings = { - aws = { - interruptionQueueName = local.interruption_handler_queue_name - } - } - } : {}), + settings = merge({ + batchIdleDuration = var.settings.batch_idle_duration + batchMaxDuration = var.settings.batch_max_duration + clusterName = local.eks_cluster_id + }, + local.interruption_handler_enabled ? { + interruptionQueue = local.interruption_handler_queue_name + } : {} + ) + } + ), # additional values yamlencode(var.chart_values) ]) @@ -229,8 +118,10 @@ module "karpenter" { context = module.this.context depends_on = [ - aws_iam_instance_profile.default, module.karpenter_crd, - kubernetes_namespace.default + aws_cloudwatch_event_rule.interruption_handler, + aws_cloudwatch_event_target.interruption_handler, + aws_sqs_queue.interruption_handler, + aws_sqs_queue_policy.interruption_handler, ] } diff --git a/modules/eks/karpenter/outputs.tf b/modules/eks/karpenter/outputs.tf index 830bd12aa..ac2640c71 100644 --- a/modules/eks/karpenter/outputs.tf +++ b/modules/eks/karpenter/outputs.tf @@ -2,8 +2,3 @@ output "metadata" { value = module.karpenter.metadata description = "Block status of the deployed release" } - -output "instance_profile" { - value = aws_iam_instance_profile.default - description = "Provisioned EC2 Instance Profile for nodes launched by Karpenter" -} diff --git a/modules/eks/karpenter/provider-helm.tf b/modules/eks/karpenter/provider-helm.tf index 91cc7f6d4..64459d4f4 100644 --- a/modules/eks/karpenter/provider-helm.tf +++ b/modules/eks/karpenter/provider-helm.tf @@ -21,35 +21,18 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" - nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" - nullable = false } variable "kubeconfig_context" { type = string default = "" - description = <<-EOT - Context to choose from the Kubernetes config file. - If supplied, `kubeconfig_context_format` will be ignored. - EOT - nullable = false -} - -variable "kubeconfig_context_format" { - type = string - default = "" - description = <<-EOT - A format string to use for creating the `kubectl` context name when - `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. - Must include a single `%s` which will be replaced with the cluster name. - EOT - nullable = false + description = "Context to choose from the Kubernetes kube config file" } variable "kube_data_auth_enabled" { @@ -59,7 +42,6 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT - nullable = false } variable "kube_exec_auth_enabled" { @@ -69,62 +51,48 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT - nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" - nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" - nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" - nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" - nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" - nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" - nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( - length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( - length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" - ) - ) - - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -139,11 +107,10 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) - cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -154,16 +121,15 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = local.cluster_ca_certificate + cluster_ca_certificate = base64decode(local.certificate_authority_data) token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication - # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` - # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. - config_path = local.kubeconfig_file - config_context = local.kubeconfig_context + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -180,16 +146,15 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = local.cluster_ca_certificate + cluster_ca_certificate = base64decode(local.certificate_authority_data) token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication - # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` - # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. - config_path = local.kubeconfig_file - config_context = local.kubeconfig_context + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" diff --git a/modules/eks/karpenter/remote-state.tf b/modules/eks/karpenter/remote-state.tf index c1ec8226d..723da0a44 100644 --- a/modules/eks/karpenter/remote-state.tf +++ b/modules/eks/karpenter/remote-state.tf @@ -5,4 +5,12 @@ module "eks" { component = var.eks_component_name context = module.this.context + + # Attempt to allow this component to be deleted from Terraform state even after the EKS cluster has been deleted + defaults = { + eks_cluster_id = "deleted" + eks_cluster_arn = "deleted" + eks_cluster_identity_oidc_issuer = "deleted" + karpenter_node_role_arn = "deleted" + } } diff --git a/modules/eks/karpenter/variables.tf b/modules/eks/karpenter/variables.tf index 9b84ba3b4..0c1117fa0 100644 --- a/modules/eks/karpenter/variables.tf +++ b/modules/eks/karpenter/variables.tf @@ -51,17 +51,6 @@ variable "resources" { description = "The CPU and memory of the deployment's limits and requests" } -variable "create_namespace" { - type = bool - description = "Create the namespace if it does not yet exist. Defaults to `false`" - default = null -} - -variable "kubernetes_namespace" { - type = string - description = "The namespace to install the release into" -} - variable "timeout" { type = number description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" @@ -106,11 +95,10 @@ variable "eks_component_name" { variable "interruption_handler_enabled" { type = bool - default = false + default = true description = < Date: Tue, 28 May 2024 15:33:44 -0400 Subject: [PATCH 421/501] chore: remove custom webhook from philips runners (#1049) --- modules/philips-labs-github-runners/README.md | 4 +- modules/philips-labs-github-runners/main.tf | 10 +- .../modules/README.md | 26 ----- .../modules/webhook-github-app/README.md | 44 --------- .../webhook-github-app/bin/update-app.sh | 95 ------------------- .../modules/webhook-github-app/main.tf | 13 --- .../modules/webhook-github-app/variables.tf | 13 --- .../modules/webhook-github-app/versions.tf | 10 -- 8 files changed, 6 insertions(+), 209 deletions(-) delete mode 100644 modules/philips-labs-github-runners/modules/README.md delete mode 100644 modules/philips-labs-github-runners/modules/webhook-github-app/README.md delete mode 100755 modules/philips-labs-github-runners/modules/webhook-github-app/bin/update-app.sh delete mode 100644 modules/philips-labs-github-runners/modules/webhook-github-app/main.tf delete mode 100644 modules/philips-labs-github-runners/modules/webhook-github-app/variables.tf delete mode 100644 modules/philips-labs-github-runners/modules/webhook-github-app/versions.tf diff --git a/modules/philips-labs-github-runners/README.md b/modules/philips-labs-github-runners/README.md index cbb0664a2..a1c4e7bce 100644 --- a/modules/philips-labs-github-runners/README.md +++ b/modules/philips-labs-github-runners/README.md @@ -93,13 +93,13 @@ the component, and available via the `webhook` output under `endpoint`. | Name | Source | Version | |------|--------|---------| -| [github\_runner](#module\_github\_runner) | philips-labs/github-runner/aws | 5.4.0 | +| [github\_runner](#module\_github\_runner) | philips-labs/github-runner/aws | 5.4.2 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [module\_artifact](#module\_module\_artifact) | cloudposse/module-artifact/external | 0.8.0 | | [store\_read](#module\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.11.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [webhook\_github\_app](#module\_webhook\_github\_app) | ./modules/webhook-github-app | n/a | +| [webhook\_github\_app](#module\_webhook\_github\_app) | philips-labs/github-runner/aws//modules/webhook-github-app | 5.4.2 | ## Resources diff --git a/modules/philips-labs-github-runners/main.tf b/modules/philips-labs-github-runners/main.tf index 2818f5fd4..1e76d2ff1 100644 --- a/modules/philips-labs-github-runners/main.tf +++ b/modules/philips-labs-github-runners/main.tf @@ -54,7 +54,7 @@ module "github_runner" { count = local.enabled ? 1 : 0 source = "philips-labs/github-runner/aws" - version = "5.4.0" + version = "5.4.2" depends_on = [module.module_artifact] @@ -95,11 +95,9 @@ module "github_runner" { } module "webhook_github_app" { - count = local.enabled && var.enable_update_github_app_webhook ? 1 : 0 - ## See README.md for more info on why we use this source instead of: - # source = "philips-labs/github-runner/aws//modules/webhook-github-app" - # version = "5.4.0" - source = "./modules/webhook-github-app" + count = local.enabled && var.enable_update_github_app_webhook ? 1 : 0 + source = "philips-labs/github-runner/aws//modules/webhook-github-app" + version = "5.4.2" depends_on = [module.github_runner] diff --git a/modules/philips-labs-github-runners/modules/README.md b/modules/philips-labs-github-runners/modules/README.md deleted file mode 100644 index bade162a5..000000000 --- a/modules/philips-labs-github-runners/modules/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Modules - -## `webhook-github-app` - -This is a fork of https://github.com/philips-labs/terraform-aws-github-runner/tree/main/modules/webhook-github-app. - -We customized it until this PR is resolved as it does not update the github app webhook until this is merged. - -- https://github.com/philips-labs/terraform-aws-github-runner/pull/3625 - -This module also requires an environment variable - -- `GH_TOKEN` - a github token be set - -This module also requires the `gh` cli to be installed. Your Dockerfile can be updated to include the following to -install it: - -```dockerfile -ARG GH_CLI_VERSION=2.39.1 -# ... -ARG GH_CLI_VERSION -RUN apt-get update && apt-get install -y --allow-downgrades \ - gh="${GH_CLI_VERSION}-*" -``` - -You can disable this module with `enable_update_github_app_webhook` set to `false`. This means you must manually diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/README.md b/modules/philips-labs-github-runners/modules/webhook-github-app/README.md deleted file mode 100644 index b6125e471..000000000 --- a/modules/philips-labs-github-runners/modules/webhook-github-app/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Module - Update GitHub App Webhook - -> This module is using the local executor to run a bash script. - -This module updates the GitHub App webhook with the endpoint and secret and can be changed with the root module. See the -examples for usages. - - - -## Requirements - -| Name | Version | -| ------------------------------------------------------------------------ | -------- | -| [terraform](#requirement_terraform) | >= 1.3.0 | -| [null](#requirement_null) | ~> 3 | - -## Providers - -| Name | Version | -| --------------------------------------------------- | ------- | -| [null](#provider_null) | ~> 3 | - -## Modules - -No modules. - -## Resources - -| Name | Type | -| ----------------------------------------------------------------------------------------------------------------- | -------- | -| [null_resource.update_app](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------- | :------: | -| [github_app](#input_github_app) | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). |
object({
key_base64 = string
id = string
webhook_secret = string
})
| n/a | yes | -| [webhook_endpoint](#input_webhook_endpoint) | The endpoint to use for the webhook, defaults to the endpoint of the runners module. | `string` | n/a | yes | - -## Outputs - -No outputs. - - diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/bin/update-app.sh b/modules/philips-labs-github-runners/modules/webhook-github-app/bin/update-app.sh deleted file mode 100755 index 15672ded5..000000000 --- a/modules/philips-labs-github-runners/modules/webhook-github-app/bin/update-app.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -set -e - -### CHECKS ### - -function testCommand() { - if ! command -v $1 &> /dev/null - then - echo "$1 could not be found" - exit - fi -} - -testCommand gh - -# create usages function usages mesaages. APP_ID and APP_PRIVATE_KEY_PATH are required as parameter or environment variable -usages() { - echo "Description: Update the GitHub App webhook configuration with terraform output for the webhook output of the module." >&2 - echo " " >&2 - echo "Usage: $0" >&2 - echo "Usage: $0 [-h]" >&2 - echo " Use environment variables" >&2 - echo " -a APP_ID GitHub App ID" >&2 - echo " -k APP_PRIVATE_KEY_BASE64 Base64 encoded private key of the GitHub App" >&2 - echo " -f APP APP_PRIVATE_KEY_FILE Path to the private key of the GitHub App" >&2 - echo " -e WEBHOOK_ENDPOINT Webhook endpoint" >&2 - echo " -s WEBHOOK_SECRET Webhook secret" >&2 - echo " -h Show this help message" >&2 - exit 1 -} - -# hadd h flag to show help -while getopts a:f:k:s:e:h flag -do - case "${flag}" in - a) APP_ID=${OPTARG};; - f) APP_PRIVATE_KEY_FILE=${OPTARG};; - k) APP_PRIVATE_KEY_BASE64=${OPTARG};; - e) WEBHOOK_ENDPOINT=${OPTARG};; - s) WEBHOOK_SECRET=${OPTARG};; - h) usages ;; - esac -done - -if [ -z "$APP_ID" ]; then - echo "APP_ID must be set" - usages -fi - -# check one of variables APP_PRIVATE_KEY_PATH or APP_PRIVATE_KEY are set -if [ -z "$APP_PRIVATE_KEY_BASE64" ] && [ -z "$APP_PRIVATE_KEY_FILE" ]; then - echo "APP_PRIVATE_KEY_BASE64 or APP_PRIVATE_KEY_FILE must be set" - usages -fi - -### Terraform outputs ### - -if [ -z "$WEBHOOK_ENDPOINT" ]; then - testCommand terraform - WEBHOOK_ENDPOINT=$(terraform output --raw webhook_endpoint) -fi - -if [ -z "$WEBHOOK_SECRET" ]; then - testCommand terraform - WEBHOOK_SECRET=$(terraform output --raw webhook_secret) -fi - -### CREATE JWT TOKEN ### - -# Generate the JWT header and payload -HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '\n') -PAYLOAD=$(echo -n "{\"iat\":$(date +%s),\"exp\":$(( $(date +%s) + 600 )),\"iss\":$APP_ID}" | base64 | tr -d '\n') - -# Generate the signature -if [ -z "$APP_PRIVATE_KEY_BASE64" ]; then - APP_PRIVATE_KEY_BASE64=$(cat $APP_PRIVATE_KEY_FILE | base64 | tr -d '\n') -fi - -SIGNATURE=$(echo -n "$HEADER.$PAYLOAD" | openssl dgst -sha256 -sign <(echo "$APP_PRIVATE_KEY_BASE64" | base64 -d) | base64 | tr -d '\n') - -JWT_TOKEN="$HEADER.$PAYLOAD.$SIGNATURE" - - -### UPDATE WEBHOOK ### - -gh api \ - --method PATCH \ - -H "Authorization: Bearer ${JWT_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /app/hook/config \ - -f content_type='json' \ - -f insecure_ssl='0' \ - -f secret=${WEBHOOK_SECRET} \ - -f url=${WEBHOOK_ENDPOINT} diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/main.tf b/modules/philips-labs-github-runners/modules/webhook-github-app/main.tf deleted file mode 100644 index 84cce50fa..000000000 --- a/modules/philips-labs-github-runners/modules/webhook-github-app/main.tf +++ /dev/null @@ -1,13 +0,0 @@ -resource "null_resource" "update_app" { - triggers = { - webhook_endpoint = var.webhook_endpoint - webhook_secret = var.github_app.webhook_secret - always_run = timestamp() - } - - provisioner "local-exec" { - interpreter = ["bash", "-c"] - command = "${path.module}/bin/update-app.sh -e ${var.webhook_endpoint} -s ${var.github_app.webhook_secret} -a ${var.github_app.id} -k ${var.github_app.key_base64}" - on_failure = fail - } -} diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/variables.tf b/modules/philips-labs-github-runners/modules/webhook-github-app/variables.tf deleted file mode 100644 index 69d404b7b..000000000 --- a/modules/philips-labs-github-runners/modules/webhook-github-app/variables.tf +++ /dev/null @@ -1,13 +0,0 @@ -variable "github_app" { - description = "GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`)." - type = object({ - key_base64 = string - id = string - webhook_secret = string - }) -} - -variable "webhook_endpoint" { - description = "The endpoint to use for the webhook, defaults to the endpoint of the runners module." - type = string -} diff --git a/modules/philips-labs-github-runners/modules/webhook-github-app/versions.tf b/modules/philips-labs-github-runners/modules/webhook-github-app/versions.tf deleted file mode 100644 index e0632ba7d..000000000 --- a/modules/philips-labs-github-runners/modules/webhook-github-app/versions.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = ">= 1.3.0" - - required_providers { - null = { - source = "hashicorp/null" - version = "~> 3" - } - } -} From d86ee82c3a5c41f5f631c4e08db51bfb910147a6 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 29 May 2024 18:42:07 +0200 Subject: [PATCH 422/501] Update release workflow (#1050) --- .github/workflows/release-published.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-published.yml b/.github/workflows/release-published.yml index 1b0aaca73..25c362459 100644 --- a/.github/workflows/release-published.yml +++ b/.github/workflows/release-published.yml @@ -5,7 +5,10 @@ on: types: - published -permissions: {} +permissions: + id-token: write + contents: write + pull-requests: write jobs: terraform-module: From 6317a82f412ba56b5a0221c02af52a14b84fa6e9 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 29 May 2024 10:28:07 -0700 Subject: [PATCH 423/501] Extra Dynamodb outputs (#1051) --- modules/dynamodb/README.md | 2 ++ modules/dynamodb/outputs.tf | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/modules/dynamodb/README.md b/modules/dynamodb/README.md index 8d8cd36ff..efef2584b 100644 --- a/modules/dynamodb/README.md +++ b/modules/dynamodb/README.md @@ -108,7 +108,9 @@ No resources. | Name | Description | |------|-------------| | [global\_secondary\_index\_names](#output\_global\_secondary\_index\_names) | DynamoDB global secondary index names | +| [hash\_key](#output\_hash\_key) | DynamoDB table hash key | | [local\_secondary\_index\_names](#output\_local\_secondary\_index\_names) | DynamoDB local secondary index names | +| [range\_key](#output\_range\_key) | DynamoDB table range key | | [table\_arn](#output\_table\_arn) | DynamoDB table ARN | | [table\_id](#output\_table\_id) | DynamoDB table ID | | [table\_name](#output\_table\_name) | DynamoDB table name | diff --git a/modules/dynamodb/outputs.tf b/modules/dynamodb/outputs.tf index 2423a2250..1126615db 100644 --- a/modules/dynamodb/outputs.tf +++ b/modules/dynamodb/outputs.tf @@ -32,3 +32,13 @@ output "table_stream_label" { value = module.dynamodb_table.table_stream_label description = "DynamoDB table stream label" } + +output "hash_key" { + value = var.hash_key + description = "DynamoDB table hash key" +} + +output "range_key" { + value = var.range_key + description = "DynamoDB table range key" +} From eed0992b8842bf6a4f9b2abd3fe8d85da76ce4a9 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 29 May 2024 17:04:08 -0700 Subject: [PATCH 424/501] fix(`rds`): Corrected SSM Paths for Non Existent `var.name` (#1052) --- modules/rds/README.md | 1 + modules/rds/outputs.tf | 5 +++++ modules/rds/systems-manager.tf | 11 ++++++----- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/rds/README.md b/modules/rds/README.md index 438b806c1..69341a92b 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -231,6 +231,7 @@ Example - I want a new instance `rds-example-new` to be provisioned from a snaps | Name | Description | |------|-------------| | [exports](#output\_exports) | Map of exports for use in deployment configuration templates | +| [kms\_key\_alias](#output\_kms\_key\_alias) | The KMS key alias | | [psql\_helper](#output\_psql\_helper) | A helper output to use with psql for connecting to this RDS instance. | | [rds\_address](#output\_rds\_address) | Address of the instance | | [rds\_arn](#output\_rds\_arn) | ARN of the instance | diff --git a/modules/rds/outputs.tf b/modules/rds/outputs.tf index d5c29821f..72e21bb88 100644 --- a/modules/rds/outputs.tf +++ b/modules/rds/outputs.tf @@ -83,3 +83,8 @@ output "psql_helper" { value = local.psql_access_enabled ? local.psql_message : "" description = "A helper output to use with psql for connecting to this RDS instance." } + +output "kms_key_alias" { + value = module.kms_key_rds.alias_name + description = "The KMS key alias" +} diff --git a/modules/rds/systems-manager.tf b/modules/rds/systems-manager.tf index 3dd7344f6..0ba6b8bbe 100644 --- a/modules/rds/systems-manager.tf +++ b/modules/rds/systems-manager.tf @@ -49,13 +49,14 @@ variable "ssm_key_port" { locals { ssm_enabled = local.enabled && var.ssm_enabled - rds_database_password_path = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_password) + ssm_name_path = join("-", compact(concat([var.name], var.attributes))) + rds_database_password_path = format(var.ssm_key_format, var.ssm_key_prefix, local.ssm_name_path, var.ssm_key_password) } resource "aws_ssm_parameter" "rds_database_user" { count = local.ssm_enabled ? 1 : 0 - name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_user) + name = format(var.ssm_key_format, var.ssm_key_prefix, local.ssm_name_path, var.ssm_key_user) value = local.database_user description = "RDS DB user" type = "String" @@ -76,7 +77,7 @@ resource "aws_ssm_parameter" "rds_database_password" { resource "aws_ssm_parameter" "rds_database_hostname" { count = local.ssm_enabled ? 1 : 0 - name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_hostname) + name = format(var.ssm_key_format, var.ssm_key_prefix, local.ssm_name_path, var.ssm_key_hostname) value = module.rds_instance.hostname == "" ? module.rds_instance.instance_address : module.rds_instance.hostname description = "RDS DB hostname" type = "String" @@ -86,7 +87,7 @@ resource "aws_ssm_parameter" "rds_database_hostname" { resource "aws_ssm_parameter" "rds_database_port" { count = local.ssm_enabled ? 1 : 0 - name = format(var.ssm_key_format, var.ssm_key_prefix, var.name, var.ssm_key_port) + name = format(var.ssm_key_format, var.ssm_key_prefix, local.ssm_name_path, var.ssm_key_port) value = var.database_port description = "RDS DB port" type = "String" @@ -94,6 +95,6 @@ resource "aws_ssm_parameter" "rds_database_port" { } output "rds_database_ssm_key_prefix" { - value = local.ssm_enabled ? format(var.ssm_key_format, var.ssm_key_prefix, var.name, "") : null + value = local.ssm_enabled ? format(var.ssm_key_format, var.ssm_key_prefix, local.ssm_name_path, "") : null description = "SSM prefix" } From af4edcd6e6a80ae37e98afc11e1676d3c564d002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?readme-action=20=F0=9F=93=96?= Date: Thu, 30 May 2024 03:02:51 +0000 Subject: [PATCH 425/501] chore: update README.md --- README.md | 122 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index f604c5195..79aedb85c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ + + -Project Banner
+Project Banner

Latest ReleaseLast UpdateSlack Community

@@ -29,16 +31,16 @@ This is a collection of reusable [AWS Terraform components](https://atmos.tools/ They work really well with [Atmos](https://atmos.tools), our open-source tool for managing infrastructure as code with Terraform. ---- -> [!NOTE] -> This project is part of Cloud Posse's comprehensive ["SweetOps"](https://cpco.io/homepage?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=) approach towards DevOps. ->
Learn More -> -> It's 100% Open Source and licensed under the [APACHE2](LICENSE). +> [!TIP] +> #### 👽 Use Atmos with Terraform +> Cloud Posse uses [`atmos`](https://atmos.tools) to easily orchestrate multiple environments using Terraform.
+> Works with [Github Actions](https://atmos.tools/integrations/github-actions/), [Atlantis](https://atmos.tools/integrations/atlantis), or [Spacelift](https://atmos.tools/integrations/spacelift). > ->
- - +>
+> Watch demo of using Atmos with Terraform +>
+> Example of running atmos to manage infrastructure from our Quick Start tutorial. +> ## Introduction @@ -59,9 +61,8 @@ with Terraform that can be used with [Atmos](https://atmos.tools). -## Usage - +## Usage Please take a look at each [component's README](https://docs.cloudposse.com/components/) for specific usage. @@ -153,6 +154,14 @@ make rebuild-docs > > We intend to eventually delete, but are leaving them for now in the repo. +> [!IMPORTANT] +> In Cloud Posse's examples, we avoid pinning modules to specific versions to prevent discrepancies between the documentation +> and the latest released versions. However, for your own projects, we strongly advise pinning each module to the exact version +> you're using. This practice ensures the stability of your infrastructure. Additionally, we recommend implementing a systematic +> approach for updating versions to avoid unexpected changes. + + + @@ -190,23 +199,60 @@ For additional context, refer to some of these links. - [Reference Architectures](https://cloudposse.com/) - Launch effortlessly with our turnkey reference architectures, built either by your team or ours. + +> [!TIP] +> #### Use Terraform Reference Architectures for AWS +> +> Use Cloud Posse's ready-to-go [terraform architecture blueprints](https://cloudposse.com/reference-architecture/) for AWS to get up and running quickly. +> +> ✅ We build it together with your team.
+> ✅ Your team owns everything.
+> ✅ 100% Open Source and backed by fanatical support.
+> +> Request Quote +>
📚 Learn More +> +>
+> +> Cloud Posse is the leading [**DevOps Accelerator**](https://cpco.io/commercial-support?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=commercial_support) for funded startups and enterprises. +> +> *Your team can operate like a pro today.* +> +> Ensure that your team succeeds by using Cloud Posse's proven process and turnkey blueprints. Plus, we stick around until you succeed. +> #### Day-0: Your Foundation for Success +> - **Reference Architecture.** You'll get everything you need from the ground up built using 100% infrastructure as code. +> - **Deployment Strategy.** Adopt a proven deployment strategy with GitHub Actions, enabling automated, repeatable, and reliable software releases. +> - **Site Reliability Engineering.** Gain total visibility into your applications and services with Datadog, ensuring high availability and performance. +> - **Security Baseline.** Establish a secure environment from the start, with built-in governance, accountability, and comprehensive audit logs, safeguarding your operations. +> - **GitOps.** Empower your team to manage infrastructure changes confidently and efficiently through Pull Requests, leveraging the full power of GitHub Actions. +> +> Request Quote +> +> #### Day-2: Your Operational Mastery +> - **Training.** Equip your team with the knowledge and skills to confidently manage the infrastructure, ensuring long-term success and self-sufficiency. +> - **Support.** Benefit from a seamless communication over Slack with our experts, ensuring you have the support you need, whenever you need it. +> - **Troubleshooting.** Access expert assistance to quickly resolve any operational challenges, minimizing downtime and maintaining business continuity. +> - **Code Reviews.** Enhance your team’s code quality with our expert feedback, fostering continuous improvement and collaboration. +> - **Bug Fixes.** Rely on our team to troubleshoot and resolve any issues, ensuring your systems run smoothly. +> - **Migration Assistance.** Accelerate your migration process with our dedicated support, minimizing disruption and speeding up time-to-value. +> - **Customer Workshops.** Engage with our team in weekly workshops, gaining insights and strategies to continuously improve and innovate. +> +> Request Quote +>
+ ## ✨ Contributing This project is under active development, and we encourage contributions from our community. + + + Many thanks to our outstanding contributors: -### 🐛 Bug Reports & Feature Requests - -Please use the [issue tracker](https://github.com/cloudposse/terraform-aws-components/issues) to report any bugs or file feature requests. - -### 💻 Developing - -If you are interested in being a contributor and want to get involved in developing this project or help out with Cloud Posse's other projects, we would love to hear from you! -Hit us up in [Slack](https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=slack), in the `#cloudposse` channel. +For 🐛 bug reports & feature requests, please use the [issue tracker](https://github.com/cloudposse/terraform-aws-components/issues). In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. 1. Review our [Code of Conduct](https://github.com/cloudposse/terraform-aws-components/?tab=coc-ov-file#code-of-conduct) and [Contributor Guidelines](https://github.com/cloudposse/.github/blob/main/CONTRIBUTING.md). @@ -231,38 +277,6 @@ Dropped straight into your Inbox every week — and usually a 5-minute read. [Join us every Wednesday via Zoom](https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=office_hours) for your weekly dose of insider DevOps trends, AWS news and Terraform insights, all sourced from our SweetOps community, plus a _live Q&A_ that you can’t find anywhere else. It's **FREE** for everyone! - -## About - -This project is maintained by Cloud Posse, LLC. - - -We are a [**DevOps Accelerator**](https://cpco.io/commercial-support?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/terraform-aws-components&utm_content=commercial_support) for funded startups and enterprises. -Use our ready-to-go terraform architecture blueprints for AWS to get up and running quickly. -We build it with you. You own everything. Your team wins. Plus, we stick around until you succeed. - -Learn More - -*Your team can operate like a pro today.* - -Ensure that your team succeeds by using our proven process and turnkey blueprints. Plus, we stick around until you succeed. - -
- 📚 See What's Included - -- **Reference Architecture.** You'll get everything you need from the ground up built using 100% infrastructure as code. -- **Deployment Strategy.** You'll have a battle-tested deployment strategy using GitHub Actions that's automated and repeatable. -- **Site Reliability Engineering.** You'll have total visibility into your apps and microservices. -- **Security Baseline.** You'll have built-in governance with accountability and audit logs for all changes. -- **GitOps.** You'll be able to operate your infrastructure via Pull Requests. -- **Training.** You'll receive hands-on training so your team can operate what we build. -- **Questions.** You'll have a direct line of communication between our teams via a Shared Slack channel. -- **Troubleshooting.** You'll get help to triage when things aren't working. -- **Code Reviews.** You'll receive constructive feedback on Pull Requests. -- **Bug Fixes.** We'll rapidly work with you to fix any bugs in our projects. -
- - ## License License @@ -271,7 +285,9 @@ Ensure that your team succeeds by using our proven process and turnkey blueprint Preamble to the Apache License, Version 2.0

+ Complete license is available in the [`LICENSE`](LICENSE) file. + ```text Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -295,6 +311,8 @@ under the License. ## Trademarks All other trademarks referenced herein are the property of their respective owners. + + --- Copyright © 2017-2024 [Cloud Posse, LLC](https://cpco.io/copyright) From c9acb2bb21bbba44c1a3566616f9c5625bdd8d6a Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Thu, 30 May 2024 17:35:37 +0200 Subject: [PATCH 426/501] Run update-changelog on public runners (#1053) --- .github/workflows/update-changelog.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 0e2daaa0f..ffc433478 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -12,9 +12,7 @@ permissions: jobs: update-changelog: - runs-on: - - "self-hosted" - + runs-on: ["ubuntu-latest"] steps: - name: Current Release id: current-release From d556d73a31d93aef38372abecd7827e9762b17cd Mon Sep 17 00:00:00 2001 From: Marat Bakeev Date: Fri, 31 May 2024 03:36:14 +1200 Subject: [PATCH 427/501] [vpc] update dynamic-subnet module version (#1048) Co-authored-by: Marat Bakeev --- modules/vpc/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/vpc/main.tf b/modules/vpc/main.tf index 24d4a2a18..ea9e41735 100644 --- a/modules/vpc/main.tf +++ b/modules/vpc/main.tf @@ -139,7 +139,7 @@ module "vpc_endpoints" { module "subnets" { source = "cloudposse/dynamic-subnets/aws" - version = "2.3.0" + version = "2.4.2" availability_zones = local.availability_zones availability_zone_ids = local.availability_zone_ids From cdb16cfd2aec589d76766dfcfe8b42c6e7c8cac3 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 30 May 2024 19:56:21 -0700 Subject: [PATCH 428/501] [eks/cluster] Add support for kubelet extra args, etc. (#1046) --- modules/eks/cluster/CHANGELOG.md | 12 ++++++++++++ modules/eks/cluster/README.md | 13 ++++++++++--- modules/eks/cluster/eks-node-groups.tf | 1 + .../eks/cluster/modules/node_group_by_az/main.tf | 15 ++++++++++++++- .../cluster/modules/node_group_by_az/variables.tf | 6 ++++++ .../modules/node_group_by_region/variables.tf | 6 ++++++ modules/eks/cluster/variables.tf | 12 ++++++++++++ 7 files changed, 61 insertions(+), 4 deletions(-) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 8351f297b..03371cb02 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,3 +1,15 @@ +## Release 1.452.0 + +Components PR [#1046](https://github.com/cloudposse/terraform-aws-components/pull/1046) + +Added support for passing extra arguments to `kubelet` and other startup modifications supported by EKS on Amazon Linux +2 via the +[`bootsrap.sh`](https://github.com/awslabs/amazon-eks-ami/blob/d87c6c49638216907cbd6630b6cadfd4825aed20/templates/al2/runtime/bootstrap.sh) +script. + +This support should be considered an `alpha` version, as it may change when support for Amazon Linux 2023 is added, and +does not work with Bottlerocket. + ## Breaking Changes: Components PR [#1033](https://github.com/cloudposse/terraform-aws-components/pull/1033) ### Major Breaking Changes diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index 569cc5a36..f8e6c29ed 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -186,6 +186,14 @@ components: # Tune the instance type according to your baseload requirements. - c7a.medium ami_type: AL2_x86_64 # use "AL2_x86_64" for standard instances, "AL2_x86_64_GPU" for GPU instances + node_userdata: + # WARNING: node_userdata is alpha status and will likely change in the future. + # Also, it is only supported for AL2 and some Windows AMIs, not BottleRocket or AL2023. + # Kubernetes docs: https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/ + kubelet_extra_args: >- + --kube-reserved cpu=100m,memory=0.6Gi,ephemeral-storage=1Gi + --system-reserved cpu=100m,memory=0.2Gi,ephemeral-storage=1Gi + --eviction-hard memory.available<200Mi,nodefs.available<10%,imagefs.available<15% block_device_map: # EBS volume for local ephemeral storage # IGNORED if legacy `disk_encryption_enabled` or `disk_size` are set! @@ -494,7 +502,6 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [aws\_ebs\_csi\_driver\_fargate\_profile](#module\_aws\_ebs\_csi\_driver\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [aws\_efs\_csi\_driver\_eks\_iam\_role](#module\_aws\_efs\_csi\_driver\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.1 | | [coredns\_fargate\_profile](#module\_coredns\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | -| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks\_cluster](#module\_eks\_cluster) | cloudposse/eks-cluster/aws | 4.1.0 | | [fargate\_pod\_execution\_role](#module\_fargate\_pod\_execution\_role) | cloudposse/eks-fargate-profile/aws | 1.3.0 | | [fargate\_profile](#module\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.0 | @@ -586,8 +593,8 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor | [map\_additional\_worker\_roles](#input\_map\_additional\_worker\_roles) | (Deprecated) AWS IAM Role ARNs of unmanaged Linux worker nodes to grant access to the EKS cluster.
In earlier versions, this could be used to grant access to worker nodes of any type
that were not managed by the EKS cluster. Now EKS requires that unmanaged worker nodes
be classified as Linux or Windows servers, in this input is temporarily retained
with the assumption that all worker nodes are Linux servers. (It is likely that
earlier versions did not work properly with Windows worker nodes anyway.)
This input is deprecated and will be removed in a future release.
In the future, this component will either have a way to separate Linux and Windows worker nodes,
or drop support for unmanaged worker nodes entirely. | `list(string)` | `[]` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = optional(string, null)
ami_type = optional(string, null)
attributes = optional(list(string), null)
availability_zones = optional(list(string)) # set to null to use var.availability_zones
cluster_autoscaler_enabled = optional(bool, null)
create_before_destroy = optional(bool, null)
desired_group_size = optional(number, null)
instance_types = optional(list(string), null)
kubernetes_labels = optional(map(string), {})
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), [])
kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version
max_group_size = optional(number, null)
min_group_size = optional(number, null)
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 50) # disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)
})
|
{
"block_device_map": {
"/dev/xvda": {
"ebs": {
"encrypted": true,
"volume_size": 20,
"volume_type": "gp2"
}
}
},
"desired_group_size": 1,
"instance_types": [
"t3.medium"
],
"kubernetes_version": null,
"max_group_size": 100
}
| no | -| [node\_groups](#input\_node\_groups) | List of objects defining a node group for the cluster |
map(object({
# EKS AMI version to use, e.g. "1.16.13-20200821" (no "v").
ami_release_version = optional(string, null)
# Type of Amazon Machine Image (AMI) associated with the EKS Node Group
ami_type = optional(string, null)
# Additional attributes (e.g. `1`) for the node group
attributes = optional(list(string), null)
# will create 1 auto scaling group in each specified availability zone
# or all AZs with subnets if none are specified anywhere
availability_zones = optional(list(string), null)
# Whether to enable Node Group to scale its AutoScaling Group
cluster_autoscaler_enabled = optional(bool, null)
# True to create new node_groups before deleting old ones, avoiding a temporary outage
create_before_destroy = optional(bool, null)
# Desired number of worker nodes when initially provisioned
desired_group_size = optional(number, null)
# Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided.
instance_types = optional(list(string), null)
# Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed
kubernetes_labels = optional(map(string), null)
# List of objects describing Kubernetes taints.
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), null)
# Desired Kubernetes master version. If you do not specify a value, the latest available version is used
kubernetes_version = optional(string, null)
# The maximum size of the AutoScaling Group
max_group_size = optional(number, null)
# The minimum size of the AutoScaling Group
min_group_size = optional(number, null)
# List of auto-launched resource types to tag
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional.
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 20) # Disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED:
# Enable disk encryption for the created launch template (if we aren't provided with an existing launch template)
# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided.
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)

}))
| `{}` | no | +| [node\_group\_defaults](#input\_node\_group\_defaults) | Defaults for node groups in the cluster |
object({
ami_release_version = optional(string, null)
ami_type = optional(string, null)
attributes = optional(list(string), null)
availability_zones = optional(list(string)) # set to null to use var.availability_zones
cluster_autoscaler_enabled = optional(bool, null)
create_before_destroy = optional(bool, null)
desired_group_size = optional(number, null)
instance_types = optional(list(string), null)
kubernetes_labels = optional(map(string), {})
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), [])
node_userdata = optional(object({
before_cluster_joining_userdata = optional(string)
bootstrap_extra_args = optional(string)
kubelet_extra_args = optional(string)
after_cluster_joining_userdata = optional(string)
}), {})
kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version
max_group_size = optional(number, null)
min_group_size = optional(number, null)
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 50) # disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)
})
|
{
"block_device_map": {
"/dev/xvda": {
"ebs": {
"encrypted": true,
"volume_size": 20,
"volume_type": "gp2"
}
}
},
"desired_group_size": 1,
"instance_types": [
"t3.medium"
],
"kubernetes_version": null,
"max_group_size": 100
}
| no | +| [node\_groups](#input\_node\_groups) | List of objects defining a node group for the cluster |
map(object({
# EKS AMI version to use, e.g. "1.16.13-20200821" (no "v").
ami_release_version = optional(string, null)
# Type of Amazon Machine Image (AMI) associated with the EKS Node Group
ami_type = optional(string, null)
# Additional attributes (e.g. `1`) for the node group
attributes = optional(list(string), null)
# will create 1 auto scaling group in each specified availability zone
# or all AZs with subnets if none are specified anywhere
availability_zones = optional(list(string), null)
# Whether to enable Node Group to scale its AutoScaling Group
cluster_autoscaler_enabled = optional(bool, null)
# True to create new node_groups before deleting old ones, avoiding a temporary outage
create_before_destroy = optional(bool, null)
# Desired number of worker nodes when initially provisioned
desired_group_size = optional(number, null)
# Set of instance types associated with the EKS Node Group. Terraform will only perform drift detection if a configuration value is provided.
instance_types = optional(list(string), null)
# Key-value mapping of Kubernetes labels. Only labels that are applied with the EKS API are managed by this argument. Other Kubernetes labels applied to the EKS Node Group will not be managed
kubernetes_labels = optional(map(string), null)
# List of objects describing Kubernetes taints.
kubernetes_taints = optional(list(object({
key = string
value = string
effect = string
})), null)
node_userdata = optional(object({
before_cluster_joining_userdata = optional(string)
bootstrap_extra_args = optional(string)
kubelet_extra_args = optional(string)
after_cluster_joining_userdata = optional(string)
}), {})
# Desired Kubernetes master version. If you do not specify a value, the latest available version is used
kubernetes_version = optional(string, null)
# The maximum size of the AutoScaling Group
max_group_size = optional(number, null)
# The minimum size of the AutoScaling Group
min_group_size = optional(number, null)
# List of auto-launched resource types to tag
resources_to_tag = optional(list(string), null)
tags = optional(map(string), null)

# block_device_map copied from cloudposse/terraform-aws-eks-node-group
# Keep in sync via copy and paste, but make optional.
# Most of the time you want "/dev/xvda". For BottleRocket, use "/dev/xvdb".
block_device_map = optional(map(object({
no_device = optional(bool, null)
virtual_name = optional(string, null)
ebs = optional(object({
delete_on_termination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number, null)
kms_key_id = optional(string, null)
snapshot_id = optional(string, null)
throughput = optional(number, null) # for gp3, MiB/s, up to 1000
volume_size = optional(number, 20) # Disk size in GB
volume_type = optional(string, "gp3")

# Catch common camel case typos. These have no effect, they just generate better errors.
# It would be nice to actually use these, but volumeSize in particular is a number here
# and in most places it is a string with a unit suffix (e.g. 20Gi)
# Without these defined, they would be silently ignored and the default values would be used instead,
# which is difficult to debug.
deleteOnTermination = optional(any, null)
kmsKeyId = optional(any, null)
snapshotId = optional(any, null)
volumeSize = optional(any, null)
volumeType = optional(any, null)
}))
})), null)

# DEPRECATED:
# Enable disk encryption for the created launch template (if we aren't provided with an existing launch template)
# DEPRECATED: disk_encryption_enabled is DEPRECATED, use `block_device_map` instead.
disk_encryption_enabled = optional(bool, null)
# Disk size in GiB for worker nodes. Terraform will only perform drift detection if a configuration value is provided.
# DEPRECATED: disk_size is DEPRECATED, use `block_device_map` instead.
disk_size = optional(number, null)

}))
| `{}` | no | | [oidc\_provider\_enabled](#input\_oidc\_provider\_enabled) | Create an IAM OIDC identity provider for the cluster, then you can create IAM roles to associate with a service account in the cluster, instead of using kiam or kube2iam. For more information, see https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html | `bool` | `true` | no | | [public\_access\_cidrs](#input\_public\_access\_cidrs) | Indicates which CIDR blocks can access the Amazon EKS public API server endpoint when enabled. EKS defaults this to a list with 0.0.0.0/0. | `list(string)` |
[
"0.0.0.0/0"
]
| no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | diff --git a/modules/eks/cluster/eks-node-groups.tf b/modules/eks/cluster/eks-node-groups.tf index 997801d1c..a0d43ea77 100644 --- a/modules/eks/cluster/eks-node-groups.tf +++ b/modules/eks/cluster/eks-node-groups.tf @@ -45,6 +45,7 @@ module "region_node_group" { instance_types = each.value.instance_types == null ? var.node_group_defaults.instance_types : each.value.instance_types kubernetes_labels = each.value.kubernetes_labels == null ? var.node_group_defaults.kubernetes_labels : each.value.kubernetes_labels kubernetes_taints = each.value.kubernetes_taints == null ? var.node_group_defaults.kubernetes_taints : each.value.kubernetes_taints + node_userdata = each.value.node_userdata == null ? var.node_group_defaults.node_userdata : each.value.node_userdata kubernetes_version = each.value.kubernetes_version == null ? local.node_group_default_kubernetes_version : each.value.kubernetes_version resources_to_tag = each.value.resources_to_tag == null ? var.node_group_defaults.resources_to_tag : each.value.resources_to_tag subnet_type_tag_key = local.subnet_type_tag_key diff --git a/modules/eks/cluster/modules/node_group_by_az/main.tf b/modules/eks/cluster/modules/node_group_by_az/main.tf index 7dcd6902f..5bc042d8c 100644 --- a/modules/eks/cluster/modules/node_group_by_az/main.tf +++ b/modules/eks/cluster/modules/node_group_by_az/main.tf @@ -28,11 +28,17 @@ locals { subnet_ids = local.subnet_ids_test[0] == local.sentinel ? null : local.subnet_ids_test az_map = var.cluster_context.az_abbreviation_type == "short" ? module.az_abbreviation.region_az_alt_code_maps.to_short : module.az_abbreviation.region_az_alt_code_maps.to_fixed az_attribute = local.az_map[var.availability_zone] + + before_cluster_joining_userdata = var.cluster_context.node_userdata.before_cluster_joining_userdata != null ? [trimspace(var.cluster_context.node_userdata.before_cluster_joining_userdata)] : [] + bootstrap_extra_args = var.cluster_context.node_userdata.bootstrap_extra_args != null ? [trimspace(var.cluster_context.node_userdata.bootstrap_extra_args)] : [] + kubelet_extra_args = var.cluster_context.node_userdata.kubelet_extra_args != null ? [trimspace(var.cluster_context.node_userdata.kubelet_extra_args)] : [] + after_cluster_joining_userdata = var.cluster_context.node_userdata.after_cluster_joining_userdata != null ? [trimspace(var.cluster_context.node_userdata.after_cluster_joining_userdata)] : [] + } module "eks_node_group" { source = "cloudposse/eks-node-group/aws" - version = "2.11.0" + version = "2.12.0" enabled = local.enabled @@ -57,6 +63,13 @@ module "eks_node_group" { resources_to_tag = local.enabled ? var.cluster_context.resources_to_tag : null subnet_ids = local.enabled ? local.subnet_ids : null + # node_userdata + before_cluster_joining_userdata = local.enabled ? local.before_cluster_joining_userdata : [] + bootstrap_additional_options = local.enabled ? local.bootstrap_extra_args : [] + kubelet_additional_options = local.enabled ? local.kubelet_extra_args : [] + after_cluster_joining_userdata = local.enabled ? local.after_cluster_joining_userdata : [] + + block_device_map = local.enabled ? var.cluster_context.block_device_map : null # Prevent the node groups from being created before the Kubernetes aws-auth configMap diff --git a/modules/eks/cluster/modules/node_group_by_az/variables.tf b/modules/eks/cluster/modules/node_group_by_az/variables.tf index ef4e486dc..a167d6ae1 100644 --- a/modules/eks/cluster/modules/node_group_by_az/variables.tf +++ b/modules/eks/cluster/modules/node_group_by_az/variables.tf @@ -30,6 +30,12 @@ variable "cluster_context" { value = string effect = string })) + node_userdata = object({ + before_cluster_joining_userdata = optional(string) + bootstrap_extra_args = optional(string) + kubelet_extra_args = optional(string) + after_cluster_joining_userdata = optional(string) + }) kubernetes_version = string module_depends_on = any resources_to_tag = list(string) diff --git a/modules/eks/cluster/modules/node_group_by_region/variables.tf b/modules/eks/cluster/modules/node_group_by_region/variables.tf index cd9afe73d..7b902c186 100644 --- a/modules/eks/cluster/modules/node_group_by_region/variables.tf +++ b/modules/eks/cluster/modules/node_group_by_region/variables.tf @@ -31,6 +31,12 @@ variable "cluster_context" { value = string effect = string })) + node_userdata = object({ + before_cluster_joining_userdata = optional(string) + bootstrap_extra_args = optional(string) + kubelet_extra_args = optional(string) + after_cluster_joining_userdata = optional(string) + }) kubernetes_version = string module_depends_on = optional(any) resources_to_tag = list(string) diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 3a7aadd02..88ad121bf 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -223,6 +223,12 @@ variable "node_groups" { value = string effect = string })), null) + node_userdata = optional(object({ + before_cluster_joining_userdata = optional(string) + bootstrap_extra_args = optional(string) + kubelet_extra_args = optional(string) + after_cluster_joining_userdata = optional(string) + }), {}) # Desired Kubernetes master version. If you do not specify a value, the latest available version is used kubernetes_version = optional(string, null) # The maximum size of the AutoScaling Group @@ -295,6 +301,12 @@ variable "node_group_defaults" { value = string effect = string })), []) + node_userdata = optional(object({ + before_cluster_joining_userdata = optional(string) + bootstrap_extra_args = optional(string) + kubelet_extra_args = optional(string) + after_cluster_joining_userdata = optional(string) + }), {}) kubernetes_version = optional(string, null) # set to null to use cluster_kubernetes_version max_group_size = optional(number, null) min_group_size = optional(number, null) From 8715e58b8835bcb3dbd355c270ed88f2cb6812fc Mon Sep 17 00:00:00 2001 From: Nuru Date: Fri, 31 May 2024 11:39:35 -0700 Subject: [PATCH 429/501] [Karpenter] Minor cleanups (#1056) --- modules/eks/karpenter-node-pool/README.md | 2 +- modules/eks/karpenter-node-pool/main.tf | 4 ++ modules/eks/karpenter-node-pool/variables.tf | 4 +- modules/eks/karpenter/README.md | 3 +- modules/eks/karpenter/controller-policy.tf | 2 +- modules/eks/karpenter/provider-helm.tf | 69 +++++++++++++++----- 6 files changed, 63 insertions(+), 21 deletions(-) diff --git a/modules/eks/karpenter-node-pool/README.md b/modules/eks/karpenter-node-pool/README.md index e972c2299..fbc7271bb 100644 --- a/modules/eks/karpenter-node-pool/README.md +++ b/modules/eks/karpenter-node-pool/README.md @@ -203,7 +203,7 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s|m|h).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, s|m|h).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
}))
| n/a | yes | +| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s|m|h).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, s|m|h).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
labels = optional(map(string))
annotations = optional(map(string))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
}))
| n/a | yes | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/eks/karpenter-node-pool/main.tf b/modules/eks/karpenter-node-pool/main.tf index a393c4d43..67c5b57b9 100644 --- a/modules/eks/karpenter-node-pool/main.tf +++ b/modules/eks/karpenter-node-pool/main.tf @@ -39,6 +39,10 @@ resource "kubernetes_manifest" "node_pool" { } ) template = { + metadata = { + labels = each.value.labels + annotations = each.value.annotations + } spec = merge({ nodeClassRef = { apiVersion = "karpenter.k8s.aws/v1beta1" diff --git a/modules/eks/karpenter-node-pool/variables.tf b/modules/eks/karpenter-node-pool/variables.tf index d768a0131..63e7d8d2e 100644 --- a/modules/eks/karpenter-node-pool/variables.tf +++ b/modules/eks/karpenter-node-pool/variables.tf @@ -70,7 +70,9 @@ variable "node_pools" { total_memory_limit = string # Set a weight for this node pool. # See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools - weight = optional(number, 50) + weight = optional(number, 50) + labels = optional(map(string)) + annotations = optional(map(string)) # Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details taints = optional(list(object({ key = string diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 64cbdafda..e0afff396 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -399,7 +399,8 @@ For more details on the CRDs, see: | [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | | [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | | [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | | [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | | [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | diff --git a/modules/eks/karpenter/controller-policy.tf b/modules/eks/karpenter/controller-policy.tf index 6e234378b..f2b4924f2 100644 --- a/modules/eks/karpenter/controller-policy.tf +++ b/modules/eks/karpenter/controller-policy.tf @@ -28,7 +28,7 @@ # "Resource": "arn:${local.aws_partition}:eks:${var.region}:${AWS::AccountId}:cluster/${local.eks_cluster_id}" # # NOTE: As a special case, the above multiple substitutions which create the ARN for the EKS cluster -# should be replaced with a single substitution, `${local.eks_cluster_arn}` to avoid neeeding to +# should be replaced with a single substitution, `${local.eks_cluster_arn}` to avoid needing to # look up the account ID and because it is more robust. # # Review the existing HEREDOC below to find conditionals such as: diff --git a/modules/eks/karpenter/provider-helm.tf b/modules/eks/karpenter/provider-helm.tf index 64459d4f4..91cc7f6d4 100644 --- a/modules/eks/karpenter/provider-helm.tf +++ b/modules/eks/karpenter/provider-helm.tf @@ -21,18 +21,35 @@ variable "kubeconfig_file_enabled" { type = bool default = false description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" + nullable = false } variable "kubeconfig_file" { type = string default = "" description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" + nullable = false } variable "kubeconfig_context" { type = string default = "" - description = "Context to choose from the Kubernetes kube config file" + description = <<-EOT + Context to choose from the Kubernetes config file. + If supplied, `kubeconfig_context_format` will be ignored. + EOT + nullable = false +} + +variable "kubeconfig_context_format" { + type = string + default = "" + description = <<-EOT + A format string to use for creating the `kubectl` context name when + `kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied. + Must include a single `%s` which will be replaced with the cluster name. + EOT + nullable = false } variable "kube_data_auth_enabled" { @@ -42,6 +59,7 @@ variable "kube_data_auth_enabled" { If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_enabled" { @@ -51,48 +69,62 @@ variable "kube_exec_auth_enabled" { If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. EOT + nullable = false } variable "kube_exec_auth_role_arn" { type = string default = "" description = "The role ARN for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_role_arn_enabled" { type = bool default = true description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" + nullable = false } variable "kube_exec_auth_aws_profile" { type = string default = "" description = "The AWS config profile for `aws eks get-token` to use" + nullable = false } variable "kube_exec_auth_aws_profile_enabled" { type = bool default = false description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" + nullable = false } variable "kubeconfig_exec_auth_api_version" { type = string default = "client.authentication.k8s.io/v1beta1" description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" + nullable = false } variable "helm_manifest_experiment_enabled" { type = bool default = false description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" + nullable = false } locals { kubeconfig_file_enabled = var.kubeconfig_file_enabled - kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled - kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + kubeconfig_file = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + kubeconfig_context = !local.kubeconfig_file_enabled ? "" : ( + length(var.kubeconfig_context) != 0 ? var.kubeconfig_context : ( + length(var.kubeconfig_context_format) != 0 ? format(var.kubeconfig_context_format, local.eks_cluster_id) : "" + ) + ) + + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled # Eventually we might try to get this from an environment variable kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version @@ -107,10 +139,11 @@ locals { ] : [] # Provide dummy configuration for the case where the EKS cluster is not available. - certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + certificate_authority_data = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_certificate_authority_data, null) + cluster_ca_certificate = local.kubeconfig_file_enabled ? null : try(base64decode(local.certificate_authority_data), null) # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") - eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") + eks_cluster_endpoint = local.kubeconfig_file_enabled ? null : try(module.eks.outputs.eks_cluster_endpoint, "") } data "aws_eks_cluster_auth" "eks" { @@ -121,15 +154,16 @@ data "aws_eks_cluster_auth" "eks" { provider "helm" { kubernetes { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" @@ -146,15 +180,16 @@ provider "helm" { provider "kubernetes" { host = local.eks_cluster_endpoint - cluster_ca_certificate = base64decode(local.certificate_authority_data) + cluster_ca_certificate = local.cluster_ca_certificate token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null - # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster - # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. - config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" - config_context = var.kubeconfig_context + # It is too confusing to allow the Kubernetes provider to use environment variables to set authentication + # in this module because we have so many options, so we override environment variables like `KUBE_CONFIG_PATH` + # in all cases. People can still use environment variables by setting TF_VAR_kubeconfig_file. + config_path = local.kubeconfig_file + config_context = local.kubeconfig_context dynamic "exec" { - for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + for_each = local.kube_exec_auth_enabled && local.certificate_authority_data != null ? ["exec"] : [] content { api_version = local.kubeconfig_exec_auth_api_version command = "aws" From 902271c5fea3c454cefb2f3cad1c581abe6a7307 Mon Sep 17 00:00:00 2001 From: Nuru Date: Sat, 1 Jun 2024 14:21:33 -0700 Subject: [PATCH 430/501] [eks/actions-runner-controller] Add ability to dynamically annotate pods once they start a job (#1055) --- .../eks/actions-runner-controller/README.md | 64 ++++--- .../charts/actions-runner/Chart.yaml | 2 +- .../templates/runnerdeployment.yaml | 178 ++++++++++++------ .../charts/actions-runner/values.yaml | 31 ++- modules/eks/actions-runner-controller/main.tf | 17 +- .../resources/values.yaml | 11 +- .../actions-runner-controller/variables.tf | 106 +++++------ 7 files changed, 238 insertions(+), 171 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 7ea596356..b9adc5f1a 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -26,7 +26,7 @@ components: name: "actions-runner" # avoids hitting name length limit on IAM role chart: "actions-runner-controller" chart_repository: "https://actions-runner-controller.github.io/actions-runner-controller" - chart_version: "0.22.0" + chart_version: "0.23.7" kubernetes_namespace: "actions-runner-system" create_namespace: true kubeconfig_exec_auth_api_version: "client.authentication.k8s.io/v1beta1" @@ -79,12 +79,11 @@ components: image: summerwind/actions-runner-dind # `scope` is org name for Organization runners, repo name for Repository runners scope: "org/infra" - # We can trade the fast-start behavior of min_replicas > 0 for the better guarantee - # that Karpenter will not terminate the runner while it is running a job. - # # Tell Karpenter not to evict this pod. This is only safe when min_replicas is 0. - # # If we do not set this, Karpenter will feel free to terminate the runner while it is running a job. - # pod_annotations: - # karpenter.sh/do-not-evict: "true" + # Tell Karpenter not to evict this pod while it is running a job. + # If we do not set this, Karpenter will feel free to terminate the runner while it is running a job, + # as part of its consolidation efforts, even when using "on demand" instances. + running_pod_annotations: + karpenter.sh/do-not-disrupt: "true" min_replicas: 1 max_replicas: 20 scale_down_delay_seconds: 100 @@ -96,7 +95,14 @@ components: cpu: 100m memory: 128Mi webhook_driven_scaling_enabled: true - webhook_startup_timeout: "30m" + # The name `webhook_startup_timeout` is misleading. + # It is actually the duration after which a job will be considered completed, + # (and the runner killed) even if the webhook has not received a "job completed" event. + # This is to ensure that if an event is missed, it does not leave the runner running forever. + # Set it long enough to cover the longest job you expect to run and then some. + # See https://github.com/actions/actions-runner-controller/blob/9afd93065fa8b1f87296f0dcdf0c2753a0548cb7/docs/automatically-scaling-runners.md?plain=1#L264-L268 + webhook_startup_timeout: "90m" + # Pull-driven scaling is obsolete and should not be used. pull_driven_scaling_enabled: false # Labels are not case-sensitive to GitHub, but *are* case-sensitive # to the webhook based autoscaler, which requires exact matches @@ -134,11 +140,12 @@ components: # # `scope` is org name for Organization runners, repo name for Repository runners # scope: "org/infra" # group: "ArmRunners" - # # Tell Karpenter not to evict this pod. This is only safe when min_replicas is 0. - # # If we do not set this, Karpenter will feel free to terminate the runner while it is running a job. - # pod_annotations: - # karpenter.sh/do-not-evict: "true" - # min_replicas: 0 + # # Tell Karpenter not to evict this pod while it is running a job. + # # If we do not set this, Karpenter will feel free to terminate the runner while it is running a job, + # # as part of its consolidation efforts, even when using "on demand" instances. + # running_pod_annotations: + # karpenter.sh/do-not-disrupt: "true" + # min_replicas: 0 # Set to so that no ARM instance is running idle, set to 1 for faster startups # max_replicas: 20 # scale_down_delay_seconds: 100 # resources: @@ -149,7 +156,7 @@ components: # cpu: 100m # memory: 128Mi # webhook_driven_scaling_enabled: true - # webhook_startup_timeout: "30m" + # webhook_startup_timeout: "90m" # pull_driven_scaling_enabled: false # # Labels are not case-sensitive to GitHub, but *are* case-sensitive # # to the webhook based autoscaler, which requires exact matches @@ -315,8 +322,10 @@ can assign one or more Runner pools (from the `runners` map) to groups (only one ### Using Webhook Driven Autoscaling (recommended) -We recommend using Webhook Driven Autoscaling until GitHub releases their own autoscaling solution (said to be "in the -works" as of April 2023). +We recommend using Webhook Driven Autoscaling until GitHub's own autoscaling solution is as capable as the Summerwind +solution this component deploys. See +[this discussion](https://github.com/actions/actions-runner-controller/discussions/3340) for some perspective on why the +Summerwind solution is currently (summer 2024) considered superior. To use the Webhook Driven Autoscaling, in addition to setting `webhook_driven_scaling_enabled` to `true`, you must also install the GitHub organization-level webhook after deploying the component (specifically, the webhook server). The URL @@ -424,7 +433,7 @@ spec: template: metadata: annotations: - karpenter.sh/do-not-evict: "true" + karpenter.sh/do-not-disrupt: "true" ``` When you set this annotation on the Pod, Karpenter will not evict it. This means that the Pod will stay on the Node it @@ -437,14 +446,14 @@ Since the Runner Pods terminate at the end of the job, this is not a problem for However, if you have set `minReplicas > 0`, then you have some Pods that are just idling, waiting for jobs to be assigned to them. These Pods are exactly the kind of Pods you want terminated and moved when the cluster is underutilized. Therefore, when you set `minReplicas > 0`, you should **NOT** set `karpenter.sh/do-not-evict: "true"` on -the Pod. +the Pod via the `pod_annotations` attribute of the `runners` input. (**But wait**, _there is good news_!) We have [requested a feature](https://github.com/actions/actions-runner-controller/issues/2562) that will allow you to -set `karpenter.sh/do-not-evict: "true"` and `minReplicas > 0` at the same time by only annotating Pods running jobs. -Meanwhile, another option is to set `minReplicas = 0` on a schedule using an ARC Autoscaler -[scheduled override](https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides). -At present, this component does not support that option, but it could be added in the future if our preferred solution -is not implemented. +set `karpenter.sh/do-not-disrupt: "true"` and `minReplicas > 0` at the same time by only annotating Pods running jobs. +Meanwhile, **we have implemented this for you** using a job startup hook. This hook will set annotations on the Pod when +the job starts. When the job finishes, the Pod will be deleted by the controller, so the annotations will not need to be +removed. Configure annotations that apply only to Pods running jobs in the `running_pod_annotations` attribute of the +`runners` input. ### Updating CRDs @@ -485,8 +494,8 @@ documentation for further details. | Name | Source | Version | |------|--------|---------| -| [actions\_runner](#module\_actions\_runner) | cloudposse/helm-release/aws | 0.10.0 | -| [actions\_runner\_controller](#module\_actions\_runner\_controller) | cloudposse/helm-release/aws | 0.10.0 | +| [actions\_runner](#module\_actions\_runner) | cloudposse/helm-release/aws | 0.10.1 | +| [actions\_runner\_controller](#module\_actions\_runner\_controller) | cloudposse/helm-release/aws | 0.10.1 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -515,6 +524,7 @@ documentation for further details. | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [context\_tags\_enabled](#input\_context\_tags\_enabled) | Whether or not to include all context tags as labels for each runner | `bool` | `false` | no | +| [controller\_replica\_count](#input\_controller\_replica\_count) | The number of replicas of the runner-controller to run. | `number` | `2` | no | | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | @@ -549,7 +559,7 @@ documentation for further details. | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: false # A Docker sidecar container will be deployed
image: summerwind/actions-runner # If dind_enabled=true, set this to 'summerwind/actions-runner-dind'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
busy_metrics = {
scale_up_threshold = 0.75
scale_down_threshold = 0.25
scale_up_factor = 2
scale_down_factor = 0.5
}
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "")
dind_enabled = bool
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})
tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = number
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = bool
webhook_startup_timeout = optional(string, null)
pull_driven_scaling_enabled = bool
labels = list(string)
storage = optional(string, null)
pvc_enabled = optional(bool, false)
resources = object({
limits = object({
cpu = string
memory = string
ephemeral_storage = optional(string, null)
})
requests = object({
cpu = string
memory = string
})
})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: true # A Docker daemon will be started in the runner Pod
image: summerwind/actions-runner-dind # If dind_enabled=false, set this to 'summerwind/actions-runner'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "summerwind/actions-runner-dind")
dind_enabled = optional(bool, true)
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})

# running_pod_annotations are only applied to the pods once they start running a job
running_pod_annotations = optional(map(string), {})

# affinity is too complex to model. Whatever you assigned affinity will be copied
# to the runner Pod spec.
affinity = optional(any)

tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = optional(number, 300)
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = optional(bool, true)
# The name `webhook_startup_timeout` is misleading.
# It is actually the duration after which a job will be considered completed,
# (and the runner killed) even if the webhook has not received a "job completed" event.
# This is to ensure that if an event is missed, it does not leave the runner running forever.
# Set it long enough to cover the longest job you expect to run and then some.
# See https://github.com/actions/actions-runner-controller/blob/9afd93065fa8b1f87296f0dcdf0c2753a0548cb7/docs/automatically-scaling-runners.md?plain=1#L264-L268
webhook_startup_timeout = optional(string, "1h")
pull_driven_scaling_enabled = optional(bool, false)
labels = optional(list(string), [])
docker_storage = optional(string, null)
# storage is deprecated in favor of docker_storage, since it is only storage for the Docker daemon
storage = optional(string, null)
pvc_enabled = optional(bool, false)
resources = optional(object({
limits = optional(object({
cpu = optional(string, "1")
memory = optional(string, "1Gi")
ephemeral_storage = optional(string, "10Gi")
}), {})
requests = optional(object({
cpu = optional(string, "500m")
memory = optional(string, "256Mi")
ephemeral_storage = optional(string, "1Gi")
}), {})
}), {})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_docker\_config\_json\_path](#input\_ssm\_docker\_config\_json\_path) | SSM path to the Docker config JSON | `string` | `null` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | @@ -559,7 +569,7 @@ documentation for further details. | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | | [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `null` | no | -| [webhook](#input\_webhook) | Configuration for the GitHub Webhook Server.
`hostname_template` is the `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`"
Typically something like `"echo.%[3]v.%[2]v.example.com"`.
`queue_limit` is the maximum number of webhook events that can be queued up processing by the autoscaler.
When the queue gets full, webhook events will be dropped (status 500). |
object({
enabled = bool
hostname_template = string
queue_limit = optional(number, 100)
})
|
{
"enabled": false,
"hostname_template": null,
"queue_limit": 100
}
| no | +| [webhook](#input\_webhook) | Configuration for the GitHub Webhook Server.
`hostname_template` is the `format()` string to use to generate the hostname via `format(var.hostname_template, var.tenant, var.stage, var.environment)`"
Typically something like `"echo.%[3]v.%[2]v.example.com"`.
`queue_limit` is the maximum number of webhook events that can be queued up for processing by the autoscaler.
When the queue gets full, webhook events will be dropped (status 500). |
object({
enabled = bool
hostname_template = string
queue_limit = optional(number, 1000)
})
|
{
"enabled": false,
"hostname_template": null,
"queue_limit": 1000
}
| no | ## Outputs diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml index b5c10525b..1ec5333d2 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.2 +version: 0.2.0 # This chart only deploys Resources for actions-runner-controller, so app version does not really apply. # We use Resource API version instead. diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index a44658dec..1321f22c8 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -1,34 +1,3 @@ -{{- if .Values.pvc_enabled }} ---- -# Persistent Volumes can be used for image caching -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ .Values.release_name }} -spec: - accessModes: - - ReadWriteMany - # StorageClassName comes from efs-controller and must be deployed first. - storageClassName: efs-sc - resources: - requests: - # EFS is not actually storage constrained, but this storage request is - # required. 100Gi is a ballpark for how much we initially request, but this - # may grow. We are responsible for docker pruning this periodically to - # save space. - storage: 100Gi -{{- end }} -{{- if .Values.docker_config_json_enabled }} ---- -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Values.release_name }}-regcred -type: kubernetes.io/dockerconfigjson -data: - .dockerconfigjson: {{ .Values.docker_config_json }} -{{- end }} ---- apiVersion: actions.summerwind.dev/v1alpha1 kind: RunnerDeployment metadata: @@ -38,13 +7,13 @@ spec: # See https://github.com/actions-runner-controller/actions-runner-controller/issues/206#issuecomment-748601907 # replicas: 1 template: - {{- with index .Values "pod_annotations" }} + {{- with .Values.pod_annotations }} metadata: annotations: {{- toYaml . | nindent 8 }} {{- end }} spec: - {{- if .Values.docker_config_json_enabled }} + {{- if .Values.docker_config_json_enabled }} # secrets volumeMount are always mounted readOnly so config.json has to be copied to the correct directory # https://github.com/kubernetes/kubernetes/issues/62099 # https://github.com/actions/actions-runner-controller/issues/2123#issuecomment-1527077517 @@ -82,14 +51,41 @@ spec: # - effect: NoSchedule # key: node-role.kubernetes.io/actions-runner # operator: Exists + {{- with .Values.node_selector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + + {{- with .Values.running_pod_annotations }} + # Run a pre-run hook to set pod annotations + # See https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/running-scripts-before-or-after-a-job#triggering-the-scripts + containers: + - name: runner + # ARC (Summerwind) has its own pre-run hook, so we do not want to set + # env: + # - name: ACTIONS_RUNNER_HOOK_JOB_STARTED + # value: /hooks/pre-run.sh # triggers when a job is started, and sets the pod to NOT safe-to-evict + # Instead, its pre-run hook runs scripts in /etc/arc/hooks/job-started.d/ + volumeMounts: + - name: hooks + mountPath: /etc/arc/hooks/job-started.d/ + {{- end }} - {{ if eq .Values.type "organization" }} + {{- if eq .Values.type "organization" }} organization: {{ .Values.scope }} {{- end }} - {{ if eq .Values.type "repository" }} + {{- if eq .Values.type "repository" }} repository: {{ .Values.scope }} {{- end }} - {{ if index .Values "group" }} + {{- if index .Values "group" }} group: {{ .Values.group }} {{- end }} # You can use labels to create subsets of runners. @@ -103,14 +99,6 @@ spec: {{- range .Values.labels }} - {{ . | quote }} {{- end }} - {{- if gt ( len (index .Values "node_selector") ) 0 }} - nodeSelector: - {{- toYaml .Values.node_selector | nindent 8 }} - {{- end }} - {{- if gt ( len (index .Values "tolerations") ) 0 }} - tolerations: - {{- toYaml .Values.tolerations | nindent 8 }} - {{- end }} # dockerdWithinRunnerContainer = false means access to a Docker daemon is provided by a sidecar container. dockerdWithinRunnerContainer: {{ .Values.dind_enabled }} image: {{ .Values.image | quote }} @@ -133,7 +121,7 @@ spec: {{- if index .Values.resources.requests "ephemeral_storage" }} ephemeral-storage: {{ .Values.resources.requests.ephemeral_storage }} {{- end }} - {{- if and .Values.dind_enabled .Values.storage }} + {{- if and .Values.dind_enabled .Values.docker_storage }} dockerVolumeMounts: - mountPath: /var/lib/docker name: docker-volume @@ -150,10 +138,10 @@ spec: - mountPath: /home/runner/.docker name: docker-config-volume {{- end }} - {{- end }} - {{- if or (and .Values.dind_enabled .Values.storage) (.Values.pvc_enabled) (.Values.docker_config_json_enabled) }} + {{- end }}{{/* End of volumeMounts */}} + {{- if or (and .Values.dind_enabled .Values.docker_storage) (.Values.pvc_enabled) (.Values.docker_config_json_enabled) (not (empty .Values.running_pod_annotations)) }} volumes: - {{- if and .Values.dind_enabled .Values.storage }} + {{- if and .Values.dind_enabled .Values.docker_storage }} - name: docker-volume ephemeral: volumeClaimTemplate: @@ -161,13 +149,13 @@ spec: accessModes: [ "ReadWriteOnce" ] # Only 1 pod can connect at a time resources: requests: - storage: {{ .Values.storage }} - {{- end }} - {{- if .Values.pvc_enabled }} + storage: {{ .Values.docker_storage }} + {{- end }} + {{- if .Values.pvc_enabled }} - name: shared-volume persistentVolumeClaim: claimName: {{ .Values.release_name }} - {{- end }} + {{- end }} {{- if .Values.docker_config_json_enabled }} - name: docker-secret secret: @@ -178,4 +166,88 @@ spec: - name: docker-config-volume emptyDir: {{- end }} - {{- end }} + {{- with .Values.running_pod_annotations }} + - name: hooks + configMap: + name: runner-hooks + defaultMode: 0755 # Set execute permissions for all files + {{- end }} + {{- end }}{{/* End of volumes */}} +{{- if .Values.pvc_enabled }} +--- +# Persistent Volumes can be used for image caching +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Values.release_name }} +spec: + accessModes: + - ReadWriteMany + # StorageClassName comes from efs-controller and must be deployed first. + storageClassName: efs-sc + resources: + requests: + # EFS is not actually storage constrained, but this storage request is + # required. 100Gi is a ballpark for how much we initially request, but this + # may grow. We are responsible for docker pruning this periodically to + # save space. + storage: 100Gi +{{- end }} +{{- if .Values.docker_config_json_enabled }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.release_name }}-regcred +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ .Values.docker_config_json }} +{{- end }} +{{- with .Values.running_pod_annotations }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: runner-hooks +data: + annotate.sh: | + #!/bin/bash + + # If we had kubectl and a KUBECONFIG, we could do this: + # kubectl annotate pod $HOSTNAME 'karpenter.sh/do-not-evict="true"' --overwrite + # kubectl annotate pod $HOSTNAME 'karpenter.sh/do-not-disrupt="true"' --overwrite + + # This is the same thing, the hard way + + # Metadata about the pod + NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + POD_NAME=$(hostname) + + # Kubernetes API URL + API_URL="https://kubernetes.default.svc" + + # Read the service account token + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + + # Content type + CONTENT_TYPE="application/merge-patch+json" + + PATCH_JSON=$(cat < Date: Sat, 1 Jun 2024 23:06:58 -0400 Subject: [PATCH 431/501] fix: allow component to deploy correctly when create_namespace is false (#1011) Co-authored-by: Nuru --- modules/eks/metrics-server/README.md | 5 ++-- modules/eks/metrics-server/main.tf | 40 ++++++++++++------------- modules/eks/metrics-server/variables.tf | 5 ++-- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 9c66f80c5..90c4d4f31 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -62,14 +62,13 @@ components: |------|--------|---------| | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | -| [metrics\_server](#module\_metrics\_server) | cloudposse/helm-release/aws | 0.10.0 | +| [metrics\_server](#module\_metrics\_server) | cloudposse/helm-release/aws | 0.10.1 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources | Name | Type | |------|------| -| [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | ## Inputs @@ -86,7 +85,7 @@ components: | [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"6.2.6"` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `true` | no | +| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `true`. | `bool` | `true` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | diff --git a/modules/eks/metrics-server/main.tf b/modules/eks/metrics-server/main.tf index e6ac57be8..49c0e0279 100644 --- a/modules/eks/metrics-server/main.tf +++ b/modules/eks/metrics-server/main.tf @@ -2,31 +2,27 @@ locals { enabled = module.this.enabled } -resource "kubernetes_namespace" "default" { - count = local.enabled && var.create_namespace ? 1 : 0 - - metadata { - name = var.kubernetes_namespace - - labels = module.this.tags - } +moved { + from = kubernetes_namespace.default + to = module.metrics_server.kubernetes_namespace.default } module "metrics_server" { source = "cloudposse/helm-release/aws" - version = "0.10.0" + version = "0.10.1" + + name = "" # avoids hitting length restrictions on IAM Role names + chart = var.chart + repository = var.chart_repository + description = var.chart_description + chart_version = var.chart_version + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout - name = "" # avoids hitting length restrictions on IAM Role names - chart = var.chart - repository = var.chart_repository - description = var.chart_description - chart_version = var.chart_version - kubernetes_namespace = join("", kubernetes_namespace.default.*.id) - create_namespace = false - wait = var.wait - atomic = var.atomic - cleanup_on_fail = var.cleanup_on_fail - timeout = var.timeout + kubernetes_namespace = var.kubernetes_namespace + create_namespace_with_kubernetes = var.create_namespace eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") @@ -47,7 +43,9 @@ module "metrics_server" { # metrics-server-specific values yamlencode({ podLabels = merge({ - chart = var.chart + chart = var.chart + # TODO: These should be configurable + # Chart should default to https://kubernetes-sigs.github.io/metrics-server/ repo = "bitnami" component = "hpa" namespace = var.kubernetes_namespace diff --git a/modules/eks/metrics-server/variables.tf b/modules/eks/metrics-server/variables.tf index ef1678456..eb563b1ee 100644 --- a/modules/eks/metrics-server/variables.tf +++ b/modules/eks/metrics-server/variables.tf @@ -18,7 +18,8 @@ variable "chart" { variable "chart_repository" { type = string description = "Repository URL where to locate the requested chart." - default = "https://charts.bitnami.com/bitnami" + # TODO: Chart should default to https://kubernetes-sigs.github.io/metrics-server/ + default = "https://charts.bitnami.com/bitnami" } variable "chart_version" { @@ -53,7 +54,7 @@ variable "resources" { variable "create_namespace" { type = bool - description = "Create the namespace if it does not yet exist. Defaults to `false`." + description = "Create the namespace if it does not yet exist. Defaults to `true`." default = true } From 9fde29c149114c413ef5ea659a59ee6d7e11aeaf Mon Sep 17 00:00:00 2001 From: Nuru Date: Sun, 2 Jun 2024 16:07:40 -0700 Subject: [PATCH 432/501] [eks/cluster] Bugfix: invalid count argument when creating new cluster (#1057) --- modules/eks/cluster/CHANGELOG.md | 6 ++++++ modules/eks/cluster/main.tf | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 03371cb02..085783949 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,3 +1,9 @@ +## Release 1.455.1 + +Components PR [#1057](https://github.com/cloudposse/terraform-aws-components/pull/1057) + +Fixed "Invalid count argument" argument when creating new cluster + ## Release 1.452.0 Components PR [#1046](https://github.com/cloudposse/terraform-aws-components/pull/1046) diff --git a/modules/eks/cluster/main.tf b/modules/eks/cluster/main.tf index 80cbdc86c..c9677bb9b 100644 --- a/modules/eks/cluster/main.tf +++ b/modules/eks/cluster/main.tf @@ -55,7 +55,7 @@ locals { # If Karpenter IAM role is enabled, give it access to the cluster to allow the nodes launched by Karpenter to join the EKS cluster karpenter_role_arn = one(aws_iam_role.karpenter[*].arn) - linux_worker_role_arns = compact(concat( + linux_worker_role_arns = local.enabled ? concat( var.map_additional_worker_roles, # As of Karpenter v0.35.0, there is no entry in the official Karpenter documentation # stating how to configure Karpenter node roles via EKS Access Entries. @@ -64,8 +64,8 @@ locals { # does not work if they are Windows nodes, but at the moment, this component # probably has other deficiencies that would prevent it from working with Windows nodes, # so we will stick with just saying Windows is not supported until we have some need for it. - [local.karpenter_role_arn], - )) + local.karpenter_iam_role_enabled ? [local.karpenter_role_arn] : [], + ) : [] # For backwards compatibility, we need to add the unmanaged worker role ARNs, but # historically we did not care whether they were LINUX or WINDOWS. From 7e21de5bc6d1a09335a8867bd930e94eff52b2c2 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 5 Jun 2024 09:49:55 +0200 Subject: [PATCH 433/501] Update bats workflow (#1058) Co-authored-by: Nuru --- .github/workflows/bats.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/bats.yml b/.github/workflows/bats.yml index 4915cdace..798006459 100644 --- a/.github/workflows/bats.yml +++ b/.github/workflows/bats.yml @@ -1,7 +1,7 @@ name: bats on: - pull_request_target: + pull_request: types: [labeled, opened, synchronize, unlabeled] jobs: @@ -19,10 +19,6 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - repository: ${{ github.event.pull_request.head.repo.full_name }} - # Check out the PR commit, not the merge commit - # Use `ref` instead of `sha` to enable pushing back to `ref` - ref: ${{ github.event.pull_request.head.ref }} - name: Run tests on modified modules id: get-modified-files From 58bee0f977112fed4c5b9140a789de40b557d634 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Wed, 5 Jun 2024 16:47:17 -0400 Subject: [PATCH 434/501] feat: add http_version,comment to spa-s3-cloudfront (#1059) --- modules/spa-s3-cloudfront/README.md | 2 ++ modules/spa-s3-cloudfront/main.tf | 2 ++ modules/spa-s3-cloudfront/variables.tf | 12 ++++++++++++ 3 files changed, 16 insertions(+) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index a511f7255..60c2b8263 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -201,6 +201,7 @@ components: | [cloudfront\_max\_ttl](#input\_cloudfront\_max\_ttl) | Maximum amount of time (in seconds) that an object is in a CloudFront cache. | `number` | `31536000` | no | | [cloudfront\_min\_ttl](#input\_cloudfront\_min\_ttl) | Minimum amount of time that you want objects to stay in CloudFront caches. | `number` | `0` | no | | [cloudfront\_viewer\_protocol\_policy](#input\_cloudfront\_viewer\_protocol\_policy) | Limit the protocol users can use to access content. One of `allow-all`, `https-only`, or `redirect-to-https`. | `string` | `"redirect-to-https"` | no | +| [comment](#input\_comment) | Any comments you want to include about the distribution. | `string` | `"Managed by Terraform"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [custom\_origins](#input\_custom\_origins) | A list of additional custom website [origins](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments) for this distribution. |
list(object({
domain_name = string
origin_id = string
origin_path = string
custom_headers = list(object({
name = string
value = string
}))
custom_origin_config = object({
http_port = number
https_port = number
origin_protocol_policy = string
origin_ssl_protocols = list(string)
origin_keepalive_timeout = number
origin_read_timeout = number
})
}))
| `[]` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | @@ -222,6 +223,7 @@ components: | [github\_runners\_environment\_name](#input\_github\_runners\_environment\_name) | The name of the environment where the CloudTrail bucket is provisioned | `string` | `"ue2"` | no | | [github\_runners\_stage\_name](#input\_github\_runners\_stage\_name) | The stage name where the CloudTrail bucket is provisioned | `string` | `"auto"` | no | | [github\_runners\_tenant\_name](#input\_github\_runners\_tenant\_name) | The tenant name where the GitHub Runners are provisioned | `string` | `null` | no | +| [http\_version](#input\_http\_version) | The maximum HTTP version to support on the distribution. Allowed values are http1.1, http2, http2and3 and http3 | `string` | `"http2"` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | | [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf index a6b3072a8..240593db4 100644 --- a/modules/spa-s3-cloudfront/main.tf +++ b/modules/spa-s3-cloudfront/main.tf @@ -87,6 +87,7 @@ module "spa_web" { s3_access_log_bucket_name = local.s3_access_log_bucket_name s3_access_log_prefix = var.origin_s3_access_log_prefix + comment = var.comment aliases = local.aliases external_aliases = local.external_aliases parent_zone_name = local.parent_zone_name @@ -97,6 +98,7 @@ module "spa_web" { acm_certificate_arn = module.acm_request_certificate.arn ipv6_enabled = var.cloudfront_ipv6_enabled + http_version = var.http_version allowed_methods = var.cloudfront_allowed_methods cached_methods = var.cloudfront_cached_methods custom_error_response = var.cloudfront_custom_error_response diff --git a/modules/spa-s3-cloudfront/variables.tf b/modules/spa-s3-cloudfront/variables.tf index e13b02fb8..1505e784d 100644 --- a/modules/spa-s3-cloudfront/variables.tf +++ b/modules/spa-s3-cloudfront/variables.tf @@ -549,3 +549,15 @@ variable "lambda_edge_destruction_delay" { EOT default = "20m" } + +variable "http_version" { + type = string + default = "http2" + description = "The maximum HTTP version to support on the distribution. Allowed values are http1.1, http2, http2and3 and http3" +} + +variable "comment" { + type = string + description = "Any comments you want to include about the distribution." + default = "Managed by Terraform" +} From 9386d29c2f6f54f70f1d6cae66ee662c82a996bb Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 6 Jun 2024 09:15:01 -0700 Subject: [PATCH 435/501] fix(DEV-2294): Docusarus Rendering (#1060) --- modules/eks/cluster/CHANGELOG.md | 2 +- modules/eks/karpenter-node-pool/README.md | 2 +- modules/eks/karpenter-node-pool/variables.tf | 4 ++-- modules/eks/karpenter/CHANGELOG.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 085783949..50951d128 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -439,7 +439,7 @@ these steps: [eks/storage-class](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/storage-class) module to create a replacement EFS StorageClass `efs-sc`. This component is new and you may need to add it to your cluster. -4. Deploy the EFS CSI Driver Add-On by adding `aws-efs-csi-driver` to the `addons` map (see [README](./README.md)). +4. Deploy the EFS CSI Driver Add-On by adding `aws-efs-csi-driver` to the `addons` map (see `README`). 5. Restore the Deployments you modified in step 1. ### More options for specifying Availability Zones diff --git a/modules/eks/karpenter-node-pool/README.md b/modules/eks/karpenter-node-pool/README.md index fbc7271bb..7a105a5f4 100644 --- a/modules/eks/karpenter-node-pool/README.md +++ b/modules/eks/karpenter-node-pool/README.md @@ -203,7 +203,7 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s|m|h).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, s|m|h).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
labels = optional(map(string))
annotations = optional(map(string))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
}))
| n/a | yes | +| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, smh).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, smh).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
labels = optional(map(string))
annotations = optional(map(string))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
}))
| n/a | yes | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/eks/karpenter-node-pool/variables.tf b/modules/eks/karpenter-node-pool/variables.tf index 63e7d8d2e..ff47feddf 100644 --- a/modules/eks/karpenter-node-pool/variables.tf +++ b/modules/eks/karpenter-node-pool/variables.tf @@ -24,13 +24,13 @@ variable "node_pools" { # If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods. consolidation_policy = optional(string, "WhenUnderutilized") - # The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s|m|h). + # The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, smh). # This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'. # You can choose to disable consolidation entirely by setting the string value 'Never' here. # Earlier versions of Karpenter called this field `ttl_seconds_after_empty`. consolidate_after = optional(string) - # The amount of time a Node can live on the cluster before being removed (`go` duration string, s|m|h). + # The amount of time a Node can live on the cluster before being removed (`go` duration string, smh). # You can choose to disable expiration entirely by setting the string value 'Never' here. # This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days). # Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`, diff --git a/modules/eks/karpenter/CHANGELOG.md b/modules/eks/karpenter/CHANGELOG.md index 72f3ee74c..6304d8034 100644 --- a/modules/eks/karpenter/CHANGELOG.md +++ b/modules/eks/karpenter/CHANGELOG.md @@ -22,7 +22,7 @@ time. Instead, we recommend you delete your existing Karpenter Provisioner (`kar new components, which will resume your pods. Full details of the recommended migration process for these components can be found in the -[Migration Guide](./docs/v1alpha-to-v1beta-migration.md). +[Migration Guide](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/eks/karpenter/docs/v1alpha-to-v1beta-migration.md). If you require a zero-downtime upgrade, please contact [Cloud Posse professional services](https://cloudposse.com/services/) for assistance. From eced166af941efdb7b614c323ae30f34738a7de8 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 6 Jun 2024 15:42:02 -0700 Subject: [PATCH 436/501] Improve `eks/karpenter-node-pool` Comments (#1062) --- modules/eks/karpenter-node-pool/README.md | 2 +- modules/eks/karpenter-node-pool/variables.tf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/eks/karpenter-node-pool/README.md b/modules/eks/karpenter-node-pool/README.md index 7a105a5f4..1d2c16355 100644 --- a/modules/eks/karpenter-node-pool/README.md +++ b/modules/eks/karpenter-node-pool/README.md @@ -203,7 +203,7 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, smh).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, smh).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
labels = optional(map(string))
annotations = optional(map(string))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
}))
| n/a | yes | +| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s, m, or h).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, s, m, or h).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
labels = optional(map(string))
annotations = optional(map(string))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
}))
| n/a | yes | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/eks/karpenter-node-pool/variables.tf b/modules/eks/karpenter-node-pool/variables.tf index ff47feddf..ae319b02e 100644 --- a/modules/eks/karpenter-node-pool/variables.tf +++ b/modules/eks/karpenter-node-pool/variables.tf @@ -24,13 +24,13 @@ variable "node_pools" { # If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods. consolidation_policy = optional(string, "WhenUnderutilized") - # The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, smh). + # The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s, m, or h). # This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'. # You can choose to disable consolidation entirely by setting the string value 'Never' here. # Earlier versions of Karpenter called this field `ttl_seconds_after_empty`. consolidate_after = optional(string) - # The amount of time a Node can live on the cluster before being removed (`go` duration string, smh). + # The amount of time a Node can live on the cluster before being removed (`go` duration string, s, m, or h). # You can choose to disable expiration entirely by setting the string value 'Never' here. # This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days). # Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`, From e0d9620406d1f59806a283873a2f402bdff7b11c Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 7 Jun 2024 13:05:20 -0400 Subject: [PATCH 437/501] feat: spa-s3-cloudfront creates cache policies (#1061) Co-authored-by: Dan Miller --- modules/spa-s3-cloudfront/README.md | 4 +- modules/spa-s3-cloudfront/main.tf | 2 +- modules/spa-s3-cloudfront/ordered_cache.tf | 49 ++++++++++++++++++++++ modules/spa-s3-cloudfront/variables.tf | 8 +++- 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 modules/spa-s3-cloudfront/ordered_cache.tf diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 60c2b8263..ab362226e 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -165,6 +165,8 @@ components: | Name | Type | |------|------| +| [aws_cloudfront_cache_policy.created_cache_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_cache_policy) | resource | +| [aws_cloudfront_origin_request_policy.created_origin_request_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_request_policy) | resource | | [aws_iam_policy.additional_lambda_edge_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.additional_lambda_edge_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | @@ -236,7 +238,7 @@ components: | [lambda\_edge\_runtime](#input\_lambda\_edge\_runtime) | The default Lambda@Edge runtime for all functions.

This value is deep merged in `module.lambda_edge_functions` with `var.lambda_edge_functions` and can be overwritten for any individual function. | `string` | `"nodejs16.x"` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [ordered\_cache](#input\_ordered\_cache) | An ordered list of [cache behaviors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#cache-behavior-arguments) resource for this distribution.
List in order of precedence (first match wins). This is in addition to the default cache policy.
Set `target_origin_id` to `""` to specify the S3 bucket origin created by this module. |
list(object({
target_origin_id = string
path_pattern = string

allowed_methods = list(string)
cached_methods = list(string)
compress = bool
trusted_signers = list(string)
trusted_key_groups = list(string)

cache_policy_id = string
origin_request_policy_id = string

viewer_protocol_policy = string
min_ttl = number
default_ttl = number
max_ttl = number
response_headers_policy_id = string

forward_query_string = bool
forward_header_values = list(string)
forward_cookies = string
forward_cookies_whitelisted_names = list(string)

lambda_function_association = list(object({
event_type = string
include_body = bool
lambda_arn = string
}))

function_association = list(object({
event_type = string
function_arn = string
}))
}))
| `[]` | no | +| [ordered\_cache](#input\_ordered\_cache) | An ordered list of [cache behaviors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#cache-behavior-arguments) resource for this distribution.
List in order of precedence (first match wins). This is in addition to the default cache policy.
Set `target_origin_id` to `""` to specify the S3 bucket origin created by this module.
Set `cache_policy_id` to `""` to use `cache_policy_name` for creating a new policy. At least one of the two must be set.
Set `origin_request_policy_id` to `""` to use `origin_request_policy_name` for creating a new policy. At least one of the two must be set. |
list(object({
target_origin_id = string
path_pattern = string

allowed_methods = list(string)
cached_methods = list(string)
compress = bool
trusted_signers = list(string)
trusted_key_groups = list(string)

cache_policy_name = optional(string)
cache_policy_id = optional(string)
origin_request_policy_name = optional(string)
origin_request_policy_id = optional(string)

viewer_protocol_policy = string
min_ttl = number
default_ttl = number
max_ttl = number
response_headers_policy_id = string

forward_query_string = bool
forward_header_values = list(string)
forward_cookies = string
forward_cookies_whitelisted_names = list(string)

lambda_function_association = list(object({
event_type = string
include_body = bool
lambda_arn = string
}))

function_association = list(object({
event_type = string
function_arn = string
}))
}))
| `[]` | no | | [origin\_allow\_ssl\_requests\_only](#input\_origin\_allow\_ssl\_requests\_only) | Set to `true` in order to have the origin bucket require requests to use Secure Socket Layer (HTTPS/SSL). This will explicitly deny access to HTTP requests | `bool` | `true` | no | | [origin\_deployment\_actions](#input\_origin\_deployment\_actions) | List of actions to permit `origin_deployment_principal_arns` to perform on bucket and bucket prefixes (see `origin_deployment_principal_arns`) | `list(string)` |
[
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:GetBucketLocation",
"s3:AbortMultipartUpload"
]
| no | | [origin\_deployment\_principal\_arns](#input\_origin\_deployment\_principal\_arns) | List of role ARNs to grant deployment permissions to the origin Bucket. | `list(string)` | `[]` | no | diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf index 240593db4..29b5aa03c 100644 --- a/modules/spa-s3-cloudfront/main.tf +++ b/modules/spa-s3-cloudfront/main.tf @@ -106,7 +106,7 @@ module "spa_web" { min_ttl = local.cloudfront_min_ttl max_ttl = local.cloudfront_max_ttl - ordered_cache = var.ordered_cache + ordered_cache = local.ordered_cache forward_cookies = var.forward_cookies forward_header_values = local.forward_header_values diff --git a/modules/spa-s3-cloudfront/ordered_cache.tf b/modules/spa-s3-cloudfront/ordered_cache.tf new file mode 100644 index 000000000..0b68f3c2b --- /dev/null +++ b/modules/spa-s3-cloudfront/ordered_cache.tf @@ -0,0 +1,49 @@ +resource "aws_cloudfront_cache_policy" "created_cache_policies" { + for_each = { + for cache in var.ordered_cache : cache.cache_policy_name => cache if cache.cache_policy_id == null + } + + comment = var.comment + default_ttl = each.value.default_ttl + max_ttl = each.value.max_ttl + min_ttl = each.value.min_ttl + name = each.value.cache_policy_name + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } + } +} + +resource "aws_cloudfront_origin_request_policy" "created_origin_request_policies" { + for_each = { + for cache in var.ordered_cache : cache.origin_request_policy_name => cache if cache.origin_request_policy_id == null + } + + comment = var.comment + name = each.value.origin_request_policy_name + cookies_config { + cookie_behavior = "none" + } + headers_config { + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "none" + } +} + +locals { + ordered_cache = [ + for cache in var.ordered_cache : merge(cache, { + cache_policy_id = cache.cache_policy_id == null ? aws_cloudfront_cache_policy.created_cache_policies[cache.cache_policy_name].id : cache.cache_policy_id + origin_request_policy_id = cache.origin_request_policy_id == null ? aws_cloudfront_origin_request_policy.created_origin_request_policies[cache.origin_request_policy_name].id : cache.origin_request_policy_id + }) + ] +} diff --git a/modules/spa-s3-cloudfront/variables.tf b/modules/spa-s3-cloudfront/variables.tf index 1505e784d..2831bb64c 100644 --- a/modules/spa-s3-cloudfront/variables.tf +++ b/modules/spa-s3-cloudfront/variables.tf @@ -398,8 +398,10 @@ variable "ordered_cache" { trusted_signers = list(string) trusted_key_groups = list(string) - cache_policy_id = string - origin_request_policy_id = string + cache_policy_name = optional(string) + cache_policy_id = optional(string) + origin_request_policy_name = optional(string) + origin_request_policy_id = optional(string) viewer_protocol_policy = string min_ttl = number @@ -428,6 +430,8 @@ variable "ordered_cache" { An ordered list of [cache behaviors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#cache-behavior-arguments) resource for this distribution. List in order of precedence (first match wins). This is in addition to the default cache policy. Set `target_origin_id` to `""` to specify the S3 bucket origin created by this module. + Set `cache_policy_id` to `""` to use `cache_policy_name` for creating a new policy. At least one of the two must be set. + Set `origin_request_policy_id` to `""` to use `origin_request_policy_name` for creating a new policy. At least one of the two must be set. EOT } From f4e8155d023683b6b34e53a2b1c5b0c6c8c9f576 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 11 Jun 2024 10:55:26 -0400 Subject: [PATCH 438/501] chore: update modules for spa-s3-cloudfront (#1064) --- modules/spa-s3-cloudfront/README.md | 4 ++-- modules/spa-s3-cloudfront/main.tf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index ab362226e..21c698c08 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -148,7 +148,7 @@ components: | Name | Source | Version | |------|--------|---------| -| [acm\_request\_certificate](#module\_acm\_request\_certificate) | cloudposse/acm-request-certificate/aws | 0.16.3 | +| [acm\_request\_certificate](#module\_acm\_request\_certificate) | cloudposse/acm-request-certificate/aws | 0.18.0 | | [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [gha\_assume\_role](#module\_gha\_assume\_role) | ../account-map/modules/team-assume-role-policy | n/a | | [gha\_role\_name](#module\_gha\_role\_name) | cloudposse/label/null | 0.25.0 | @@ -156,7 +156,7 @@ components: | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [lambda\_edge](#module\_lambda\_edge) | cloudposse/cloudfront-s3-cdn/aws//modules/lambda@edge | 0.92.0 | | [lambda\_edge\_functions](#module\_lambda\_edge\_functions) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | -| [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.92.0 | +| [spa\_web](#module\_spa\_web) | cloudposse/cloudfront-s3-cdn/aws | 0.95.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | | [waf](#module\_waf) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf index 29b5aa03c..7f0e9abb4 100644 --- a/modules/spa-s3-cloudfront/main.tf +++ b/modules/spa-s3-cloudfront/main.tf @@ -52,7 +52,7 @@ locals { # Create an ACM and explicitly set it to us-east-1 (requirement of CloudFront) module "acm_request_certificate" { source = "cloudposse/acm-request-certificate/aws" - version = "0.16.3" + version = "0.18.0" providers = { aws = aws.us-east-1 } @@ -68,7 +68,7 @@ module "acm_request_certificate" { module "spa_web" { source = "cloudposse/cloudfront-s3-cdn/aws" - version = "0.92.0" + version = "0.95.0" block_origin_public_access_enabled = local.block_origin_public_access_enabled encryption_enabled = var.origin_encryption_enabled From d4d4b83e61fcb2f578fd8cf1629e414028adbb69 Mon Sep 17 00:00:00 2001 From: yangci Date: Tue, 11 Jun 2024 15:09:20 -0400 Subject: [PATCH 439/501] feat(aurora-postgres): backup configs (#1063) --- modules/aurora-postgres/README.md | 3 +++ modules/aurora-postgres/cluster-regional.tf | 2 ++ modules/aurora-postgres/variables.tf | 12 ++++++++++++ 3 files changed, 17 insertions(+) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index b2a691029..7d6205c60 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -351,6 +351,9 @@ components: | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [vpc\_component\_name](#input\_vpc\_component\_name) | The name of the VPC component | `string` | `"vpc"` | no | +| [retention\_period](#input\_retention\_period) | Number of days to retain backups for | `number` | `5` | no | +| [backup\_window](#input\_backup\_window) | Daily time range during which the backups happen, UTC | `string` | `"07:00-09:00"` | no | + ## Outputs diff --git a/modules/aurora-postgres/cluster-regional.tf b/modules/aurora-postgres/cluster-regional.tf index cc9f8a3f1..ef0c923dd 100644 --- a/modules/aurora-postgres/cluster-regional.tf +++ b/modules/aurora-postgres/cluster-regional.tf @@ -53,6 +53,8 @@ module "aurora_postgres_cluster" { snapshot_identifier = var.snapshot_identifier allow_major_version_upgrade = var.allow_major_version_upgrade ca_cert_identifier = var.ca_cert_identifier + retention_period = var.retention_period + backup_window = var.backup_window cluster_parameters = concat([ { diff --git a/modules/aurora-postgres/variables.tf b/modules/aurora-postgres/variables.tf index 3702a5f41..ea71e52f0 100644 --- a/modules/aurora-postgres/variables.tf +++ b/modules/aurora-postgres/variables.tf @@ -340,3 +340,15 @@ variable "cluster_parameters" { default = [] description = "List of DB cluster parameters to apply" } + +variable "retention_period" { + type = number + default = 5 + description = "Number of days to retain backups for" +} + +variable "backup_window" { + type = string + default = "07:00-09:00" + description = "Daily time range during which the backups happen, UTC" +} From 0c910b674e7bc700a04742cd48e2362130815759 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 11 Jun 2024 12:33:39 -0700 Subject: [PATCH 440/501] feat: Grafana Prometheus and Loki (#1054) --- .pre-commit-config.yaml | 5 +- modules/eks/loki/README.md | 142 +++++++++ modules/eks/loki/context.tf | 279 ++++++++++++++++++ modules/eks/loki/main.tf | 190 ++++++++++++ modules/eks/loki/outputs.tf | 24 ++ modules/eks/loki/provider-helm.tf | 166 +++++++++++ modules/eks/loki/providers.tf | 19 ++ modules/eks/loki/remote-state.tf | 39 +++ modules/eks/loki/variables.tf | 127 ++++++++ modules/eks/loki/versions.tf | 22 ++ modules/eks/prometheus-scraper/README.md | 158 ++++++++++ .../charts/scraper-access/Chart.yaml | 24 ++ .../templates/clusterrole-binding.yml | 26 ++ .../charts/scraper-access/values.yaml | 8 + modules/eks/prometheus-scraper/context.tf | 279 ++++++++++++++++++ modules/eks/prometheus-scraper/main.tf | 68 +++++ modules/eks/prometheus-scraper/outputs.tf | 9 + .../eks/prometheus-scraper/provider-helm.tf | 166 +++++++++++ modules/eks/prometheus-scraper/providers.tf | 19 ++ .../eks/prometheus-scraper/remote-state.tf | 26 ++ modules/eks/prometheus-scraper/variables.tf | 137 +++++++++ modules/eks/prometheus-scraper/versions.tf | 18 ++ modules/eks/promtail/README.md | 123 ++++++++ modules/eks/promtail/context.tf | 279 ++++++++++++++++++ modules/eks/promtail/main.tf | 136 +++++++++ modules/eks/promtail/outputs.tf | 0 modules/eks/promtail/provider-helm.tf | 166 +++++++++++ modules/eks/promtail/providers.tf | 19 ++ modules/eks/promtail/remote-state.tf | 58 ++++ .../default_kubernetes_pods.yaml | 40 +++ modules/eks/promtail/variables.tf | 99 +++++++ modules/eks/promtail/versions.tf | 18 ++ modules/managed-grafana/api-key/README.md | 100 +++++++ modules/managed-grafana/api-key/context.tf | 279 ++++++++++++++++++ modules/managed-grafana/api-key/main.tf | 45 +++ modules/managed-grafana/api-key/outputs.tf | 4 + modules/managed-grafana/api-key/providers.tf | 19 ++ .../managed-grafana/api-key/remote-state.tf | 8 + modules/managed-grafana/api-key/variables.tf | 28 ++ modules/managed-grafana/api-key/versions.tf | 14 + modules/managed-grafana/dashboard/README.md | 105 +++++++ modules/managed-grafana/dashboard/context.tf | 279 ++++++++++++++++++ modules/managed-grafana/dashboard/main.tf | 35 +++ .../dashboard/provider-grafana.tf | 38 +++ .../managed-grafana/dashboard/providers.tf | 19 ++ .../managed-grafana/dashboard/variables.tf | 26 ++ modules/managed-grafana/dashboard/versions.tf | 18 ++ .../data-source/loki/README.md | 136 +++++++++ .../data-source/loki/context.tf | 279 ++++++++++++++++++ .../managed-grafana/data-source/loki/main.tf | 36 +++ .../data-source/loki/outputs.tf | 5 + .../data-source/loki/provider-grafana.tf | 38 +++ .../data-source/loki/provider-source.tf | 22 ++ .../data-source/loki/providers.tf | 19 ++ .../data-source/loki/remote-state.tf | 36 +++ .../data-source/loki/variables.tf | 4 + .../data-source/loki/versions.tf | 14 + .../data-source/managed-prometheus/README.md | 135 +++++++++ .../data-source/managed-prometheus/context.tf | 279 ++++++++++++++++++ .../data-source/managed-prometheus/main.tf | 20 ++ .../data-source/managed-prometheus/outputs.tf | 5 + .../managed-prometheus/provider-grafana.tf | 38 +++ .../managed-prometheus/providers.tf | 19 ++ .../managed-prometheus/remote-state.tf | 12 + .../managed-prometheus/variables.tf | 28 ++ .../managed-prometheus/versions.tf | 14 + modules/managed-grafana/workspace/README.md | 121 ++++++++ modules/managed-grafana/workspace/context.tf | 279 ++++++++++++++++++ modules/managed-grafana/workspace/main.tf | 46 +++ modules/managed-grafana/workspace/outputs.tf | 9 + .../managed-grafana/workspace/providers.tf | 19 ++ .../managed-grafana/workspace/remote-state.tf | 24 ++ .../managed-grafana/workspace/variables.tf | 37 +++ modules/managed-grafana/workspace/versions.tf | 10 + .../managed-prometheus/workspace/README.md | 105 +++++++ .../managed-prometheus/workspace/context.tf | 279 ++++++++++++++++++ modules/managed-prometheus/workspace/main.tf | 23 ++ .../managed-prometheus/workspace/outputs.tf | 29 ++ .../managed-prometheus/workspace/providers.tf | 19 ++ .../workspace/remote-state.tf | 22 ++ .../managed-prometheus/workspace/variables.tf | 49 +++ .../managed-prometheus/workspace/versions.tf | 10 + 82 files changed, 6125 insertions(+), 1 deletion(-) create mode 100644 modules/eks/loki/README.md create mode 100644 modules/eks/loki/context.tf create mode 100644 modules/eks/loki/main.tf create mode 100644 modules/eks/loki/outputs.tf create mode 100644 modules/eks/loki/provider-helm.tf create mode 100644 modules/eks/loki/providers.tf create mode 100644 modules/eks/loki/remote-state.tf create mode 100644 modules/eks/loki/variables.tf create mode 100644 modules/eks/loki/versions.tf create mode 100644 modules/eks/prometheus-scraper/README.md create mode 100644 modules/eks/prometheus-scraper/charts/scraper-access/Chart.yaml create mode 100644 modules/eks/prometheus-scraper/charts/scraper-access/templates/clusterrole-binding.yml create mode 100644 modules/eks/prometheus-scraper/charts/scraper-access/values.yaml create mode 100644 modules/eks/prometheus-scraper/context.tf create mode 100644 modules/eks/prometheus-scraper/main.tf create mode 100644 modules/eks/prometheus-scraper/outputs.tf create mode 100644 modules/eks/prometheus-scraper/provider-helm.tf create mode 100644 modules/eks/prometheus-scraper/providers.tf create mode 100644 modules/eks/prometheus-scraper/remote-state.tf create mode 100644 modules/eks/prometheus-scraper/variables.tf create mode 100644 modules/eks/prometheus-scraper/versions.tf create mode 100644 modules/eks/promtail/README.md create mode 100644 modules/eks/promtail/context.tf create mode 100644 modules/eks/promtail/main.tf create mode 100644 modules/eks/promtail/outputs.tf create mode 100644 modules/eks/promtail/provider-helm.tf create mode 100644 modules/eks/promtail/providers.tf create mode 100644 modules/eks/promtail/remote-state.tf create mode 100644 modules/eks/promtail/scrape_config/default_kubernetes_pods.yaml create mode 100644 modules/eks/promtail/variables.tf create mode 100644 modules/eks/promtail/versions.tf create mode 100644 modules/managed-grafana/api-key/README.md create mode 100644 modules/managed-grafana/api-key/context.tf create mode 100644 modules/managed-grafana/api-key/main.tf create mode 100644 modules/managed-grafana/api-key/outputs.tf create mode 100644 modules/managed-grafana/api-key/providers.tf create mode 100644 modules/managed-grafana/api-key/remote-state.tf create mode 100644 modules/managed-grafana/api-key/variables.tf create mode 100644 modules/managed-grafana/api-key/versions.tf create mode 100644 modules/managed-grafana/dashboard/README.md create mode 100644 modules/managed-grafana/dashboard/context.tf create mode 100644 modules/managed-grafana/dashboard/main.tf create mode 100644 modules/managed-grafana/dashboard/provider-grafana.tf create mode 100644 modules/managed-grafana/dashboard/providers.tf create mode 100644 modules/managed-grafana/dashboard/variables.tf create mode 100644 modules/managed-grafana/dashboard/versions.tf create mode 100644 modules/managed-grafana/data-source/loki/README.md create mode 100644 modules/managed-grafana/data-source/loki/context.tf create mode 100644 modules/managed-grafana/data-source/loki/main.tf create mode 100644 modules/managed-grafana/data-source/loki/outputs.tf create mode 100644 modules/managed-grafana/data-source/loki/provider-grafana.tf create mode 100644 modules/managed-grafana/data-source/loki/provider-source.tf create mode 100644 modules/managed-grafana/data-source/loki/providers.tf create mode 100644 modules/managed-grafana/data-source/loki/remote-state.tf create mode 100644 modules/managed-grafana/data-source/loki/variables.tf create mode 100644 modules/managed-grafana/data-source/loki/versions.tf create mode 100644 modules/managed-grafana/data-source/managed-prometheus/README.md create mode 100644 modules/managed-grafana/data-source/managed-prometheus/context.tf create mode 100644 modules/managed-grafana/data-source/managed-prometheus/main.tf create mode 100644 modules/managed-grafana/data-source/managed-prometheus/outputs.tf create mode 100644 modules/managed-grafana/data-source/managed-prometheus/provider-grafana.tf create mode 100644 modules/managed-grafana/data-source/managed-prometheus/providers.tf create mode 100644 modules/managed-grafana/data-source/managed-prometheus/remote-state.tf create mode 100644 modules/managed-grafana/data-source/managed-prometheus/variables.tf create mode 100644 modules/managed-grafana/data-source/managed-prometheus/versions.tf create mode 100644 modules/managed-grafana/workspace/README.md create mode 100644 modules/managed-grafana/workspace/context.tf create mode 100644 modules/managed-grafana/workspace/main.tf create mode 100644 modules/managed-grafana/workspace/outputs.tf create mode 100644 modules/managed-grafana/workspace/providers.tf create mode 100644 modules/managed-grafana/workspace/remote-state.tf create mode 100644 modules/managed-grafana/workspace/variables.tf create mode 100644 modules/managed-grafana/workspace/versions.tf create mode 100644 modules/managed-prometheus/workspace/README.md create mode 100644 modules/managed-prometheus/workspace/context.tf create mode 100644 modules/managed-prometheus/workspace/main.tf create mode 100644 modules/managed-prometheus/workspace/outputs.tf create mode 100644 modules/managed-prometheus/workspace/providers.tf create mode 100644 modules/managed-prometheus/workspace/remote-state.tf create mode 100644 modules/managed-prometheus/workspace/variables.tf create mode 100644 modules/managed-prometheus/workspace/versions.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f26946b9e..1aaef70c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,10 @@ repos: deprecated/github-actions-runner/runners/actions-runner/chart/templates/.*.yaml | modules/eks/cert-manager/cert-manager-issuer/templates/.*.yaml | modules/strongdm/charts/strongdm/templates/.*.yaml | - modules/eks/.*/charts/.*/templates/.*.yaml + modules/eks/.*/charts/.*/templates/.*.yaml | + modules/eks/.*/charts/.*/templates/.*.yml | + modules/eks/promtail/scrape_config/.*.yaml | + modules/eks/promtail/scrape_config/.*.yml )$ - repo: https://github.com/antonbabenko/pre-commit-terraform diff --git a/modules/eks/loki/README.md b/modules/eks/loki/README.md new file mode 100644 index 000000000..60f7fef1d --- /dev/null +++ b/modules/eks/loki/README.md @@ -0,0 +1,142 @@ +# Component: `eks/loki` + +Grafana Loki is a set of resources that can be combined into a fully featured logging stack. Unlike other logging +systems, Loki is built around the idea of only indexing metadata about your logs: labels (just like Prometheus labels). +Log data itself is then compressed and stored in chunks in object stores such as S3 or GCS, or even locally on a +filesystem. + +This component deploys the [grafana/loki](https://github.com/grafana/loki/tree/main/production/helm/loki) helm chart. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + eks/loki: + vars: + enabled: true + name: loki + alb_controller_ingress_group_component_name: eks/alb-controller-ingress-group/internal +``` + +> [!IMPORTANT] +> +> We recommend using an internal ALB for logging services. You must connect to the private network to access the Loki +> endpoint. + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | +| [random](#requirement\_random) | >= 2.3 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [random](#provider\_random) | >= 2.3 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [alb\_controller\_ingress\_group](#module\_alb\_controller\_ingress\_group) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [basic\_auth\_ssm\_parameters](#module\_basic\_auth\_ssm\_parameters) | cloudposse/ssm-parameter-store/aws | 0.13.0 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [loki](#module\_loki) | cloudposse/helm-release/aws | 0.10.1 | +| [loki\_storage](#module\_loki\_storage) | cloudposse/s3-bucket/aws | 4.2.0 | +| [loki\_tls\_label](#module\_loki\_tls\_label) | cloudposse/label/null | 0.25.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [random_pet.basic_auth_username](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | +| [random_string.basic_auth_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_schema\_config](#input\_additional\_schema\_config) | A list of additional `configs` for the `schemaConfig` for the Loki chart. This list will be merged with the default schemaConfig.config defined by `var.default_schema_config` |
list(object({
from = string
object_store = string
schema = string
index = object({
prefix = string
period = string
})
}))
| `[]` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_controller\_ingress\_group\_component\_name](#input\_alb\_controller\_ingress\_group\_component\_name) | The name of the eks/alb-controller-ingress-group component. This should be an internal facing ALB | `string` | `"eks/alb-controller-ingress-group"` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [basic\_auth\_enabled](#input\_basic\_auth\_enabled) | If `true`, enabled Basic Auth for the Ingress service. A user and password will be created and stored in AWS SSM. | `bool` | `true` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"loki"` | no | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus."` | no | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://grafana.github.io/helm-charts"` | no | +| [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | +| [default\_schema\_config](#input\_default\_schema\_config) | A list of default `configs` for the `schemaConfig` for the Loki chart. For new installations, the default schema config doesn't change. See https://grafana.com/docs/loki/latest/operations/storage/schema/#new-loki-installs |
list(object({
from = string
object_store = string
schema = string
index = object({
prefix = string
period = string
})
}))
|
[
{
"from": "2024-04-01",
"index": {
"period": "24h",
"prefix": "index_"
},
"object_store": "s3",
"schema": "v13",
"store": "tsdb"
}
]
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | Kubernetes namespace to install the release into | `string` | `"monitoring"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [ssm\_path\_template](#input\_ssm\_path\_template) | A string template to be used to create paths in AWS SSM to store basic auth credentials for this service | `string` | `"/%s/basic-auth/%s"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `300` | no | +| [verify](#input\_verify) | Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart | `bool` | `false` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [basic\_auth\_username](#output\_basic\_auth\_username) | If enabled, the username for basic auth | +| [id](#output\_id) | The ID of this deployment | +| [metadata](#output\_metadata) | Block status of the deployed release | +| [ssm\_path\_basic\_auth\_password](#output\_ssm\_path\_basic\_auth\_password) | If enabled, the path in AWS SSM to find the password for basic auth | +| [url](#output\_url) | The hostname used for this Loki deployment | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/loki) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/eks/loki/context.tf b/modules/eks/loki/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/loki/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/loki/main.tf b/modules/eks/loki/main.tf new file mode 100644 index 000000000..41c271214 --- /dev/null +++ b/modules/eks/loki/main.tf @@ -0,0 +1,190 @@ +locals { + enabled = module.this.enabled + + name = length(module.this.name) > 0 ? module.this.name : "loki" + ingress_host_name = format("%s.%s.%s", local.name, module.this.environment, module.dns_gbl_delegated.outputs.default_domain_name) + ingress_group_name = module.alb_controller_ingress_group.outputs.group_name + + ssm_path_password = format(var.ssm_path_template, module.this.id, "password") +} + +resource "random_pet" "basic_auth_username" { + count = local.enabled && var.basic_auth_enabled ? 1 : 0 +} + +resource "random_string" "basic_auth_password" { + count = local.enabled && var.basic_auth_enabled ? 1 : 0 + + length = 12 + special = true +} + +module "basic_auth_ssm_parameters" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.13.0" + + enabled = local.enabled && var.basic_auth_enabled + + parameter_write = [ + { + name = format(var.ssm_path_template, module.this.id, "username") + value = random_pet.basic_auth_username[0].id + description = "Basic Auth Username for ${module.this.id}" + type = "SecureString" + overwrite = true + }, + { + name = local.ssm_path_password + value = random_string.basic_auth_password[0].result + description = "Basic Auth Password for ${module.this.id}" + type = "SecureString" + overwrite = true + } + ] + + context = module.this.context +} + +module "loki_storage" { + source = "cloudposse/s3-bucket/aws" + version = "4.2.0" + + for_each = toset(["chunks", "ruler", "admin"]) + + name = local.name + attributes = [each.key] + + enabled = local.enabled + + context = module.this.context +} + +module "loki_tls_label" { + source = "cloudposse/label/null" + version = "0.25.0" + + enabled = local.enabled + + attributes = ["tls"] + + context = module.this.context +} + +module "loki" { + source = "cloudposse/helm-release/aws" + version = "0.10.1" + + enabled = local.enabled + + name = local.name + chart = var.chart + description = var.chart_description + repository = var.chart_repository + chart_version = var.chart_version + + kubernetes_namespace = var.kubernetes_namespace + create_namespace = var.create_namespace + + verify = var.verify + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") + + iam_role_enabled = true + iam_policy = [{ + statements = [ + { + sid = "AllowLokiStorageAccess" + effect = "Allow" + resources = [ + module.loki_storage["chunks"].bucket_arn, + module.loki_storage["ruler"].bucket_arn, + module.loki_storage["admin"].bucket_arn, + format("%s/*", module.loki_storage["chunks"].bucket_arn), + format("%s/*", module.loki_storage["ruler"].bucket_arn), + format("%s/*", module.loki_storage["admin"].bucket_arn), + ] + actions = [ + "s3:ListBucket", + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject" + ] + }, + ] + }] + + values = compact([ + yamlencode({ + loki = { + # For new installations, schema config doesnt change. See the following: + # https://grafana.com/docs/loki/latest/operations/storage/schema/#new-loki-installs + schemaConfig = { + configs = compact(concat(var.default_schema_config, var.additional_schema_config)) + } + storage = { + bucketNames = { + chunks = module.loki_storage["chunks"].bucket_id + ruler = module.loki_storage["ruler"].bucket_id + admin = module.loki_storage["admin"].bucket_id + }, + type = "s3", + s3 = { + region = var.region + } + } + } + # Do not use the default nginx gateway + gateway = { + enabled = false + } + # Instead, we want to use AWS ALB Ingress Controller + ingress = { + enabled = true + annotations = { + "kubernetes.io/ingress.class" = "alb" + "external-dns.alpha.kubernetes.io/hostname" = local.ingress_host_name + "alb.ingress.kubernetes.io/group.name" = local.ingress_group_name + # We dont need to supply "alb.ingress.kubernetes.io/certificate-arn" because of AWS ALB controller's auto discovery using the given host + "alb.ingress.kubernetes.io/backend-protocol" = "HTTP" + "alb.ingress.kubernetes.io/listen-ports" = "[{\"HTTP\": 80},{\"HTTPS\":443}]" + "alb.ingress.kubernetes.io/ssl-redirect" = "443" + "alb.ingress.kubernetes.io/scheme" = "internal" + "alb.ingress.kubernetes.io/target-type" = "ip" + } + hosts = [ + local.ingress_host_name + ] + tls = [ + { + secretName = module.loki_tls_label.id + hosts = [local.ingress_host_name] + } + ] + } + # Loki Canary does not work when gateway is disabled + # https://github.com/grafana/loki/issues/11208 + test = { + enabled = false + } + lokiCanary = { + enabled = false + } + }), + yamlencode( + var.basic_auth_enabled ? { + basicAuth = { + enabled = true + username = random_pet.basic_auth_username[0].id + password = random_string.basic_auth_password[0].result + } + } : {} + ), + yamlencode(var.chart_values), + ]) + + context = module.this.context +} diff --git a/modules/eks/loki/outputs.tf b/modules/eks/loki/outputs.tf new file mode 100644 index 000000000..8fe9b3aea --- /dev/null +++ b/modules/eks/loki/outputs.tf @@ -0,0 +1,24 @@ +output "metadata" { + value = module.loki.metadata + description = "Block status of the deployed release" +} + +output "id" { + value = module.this.id + description = "The ID of this deployment" +} + +output "url" { + value = local.ingress_host_name + description = "The hostname used for this Loki deployment" +} + +output "basic_auth_username" { + value = random_pet.basic_auth_username[0].id + description = "If enabled, the username for basic auth" +} + +output "ssm_path_basic_auth_password" { + value = local.ssm_path_password + description = "If enabled, the path in AWS SSM to find the password for basic auth" +} diff --git a/modules/eks/loki/provider-helm.tf b/modules/eks/loki/provider-helm.tf new file mode 100644 index 000000000..64459d4f4 --- /dev/null +++ b/modules/eks/loki/provider-helm.tf @@ -0,0 +1,166 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/loki/providers.tf b/modules/eks/loki/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/eks/loki/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/eks/loki/remote-state.tf b/modules/eks/loki/remote-state.tf new file mode 100644 index 000000000..0ff7ae72f --- /dev/null +++ b/modules/eks/loki/remote-state.tf @@ -0,0 +1,39 @@ +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "alb_controller_ingress_group_component_name" { + type = string + description = "The name of the eks/alb-controller-ingress-group component. This should be an internal facing ALB" + default = "eks/alb-controller-ingress-group" +} + +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + context = module.this.context +} + +module "alb_controller_ingress_group" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.alb_controller_ingress_group_component_name + + context = module.this.context +} + +module "dns_gbl_delegated" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + environment = "gbl" + component = "dns-delegated" + + context = module.this.context +} diff --git a/modules/eks/loki/variables.tf b/modules/eks/loki/variables.tf new file mode 100644 index 000000000..c51d15817 --- /dev/null +++ b/modules/eks/loki/variables.tf @@ -0,0 +1,127 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "basic_auth_enabled" { + type = bool + description = "If `true`, enabled Basic Auth for the Ingress service. A user and password will be created and stored in AWS SSM." + default = true +} + +variable "ssm_path_template" { + type = string + description = "A string template to be used to create paths in AWS SSM to store basic auth credentials for this service" + default = "/%s/basic-auth/%s" +} + +variable "chart_description" { + type = string + description = "Set release description attribute (visible in the history)." + default = "Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus." +} + +variable "chart" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." + default = "loki" +} + +variable "chart_repository" { + type = string + description = "Repository URL where to locate the requested chart." + default = "https://grafana.github.io/helm-charts" +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." + default = null +} + +variable "kubernetes_namespace" { + type = string + description = "Kubernetes namespace to install the release into" + default = "monitoring" +} + +variable "create_namespace" { + type = bool + description = "Create the Kubernetes namespace if it does not yet exist" + default = true +} + +variable "verify" { + type = bool + description = "Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart" + default = false +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`." + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used." + default = true +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails." + default = true +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = 300 +} + +variable "chart_values" { + type = any + description = "Additional values to yamlencode as `helm_release` values." + default = {} +} + +variable "default_schema_config" { + type = list(object({ + from = string + object_store = string + schema = string + index = object({ + prefix = string + period = string + }) + })) + description = "A list of default `configs` for the `schemaConfig` for the Loki chart. For new installations, the default schema config doesn't change. See https://grafana.com/docs/loki/latest/operations/storage/schema/#new-loki-installs" + default = [ + { + from = "2024-04-01" # for a new install, this must be a date in the past, use a recent date. Format is YYYY-MM-DD. + object_store = "s3" + store = "tsdb" + schema = "v13" + index = { + prefix = "index_" + period = "24h" + } + } + ] +} + +variable "additional_schema_config" { + type = list(object({ + from = string + object_store = string + schema = string + index = object({ + prefix = string + period = string + }) + })) + description = "A list of additional `configs` for the `schemaConfig` for the Loki chart. This list will be merged with the default schemaConfig.config defined by `var.default_schema_config`" + default = [] +} diff --git a/modules/eks/loki/versions.tf b/modules/eks/loki/versions.tf new file mode 100644 index 000000000..8b4106a3b --- /dev/null +++ b/modules/eks/loki/versions.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1, != 2.21.0" + } + random = { + source = "hashicorp/random" + version = ">= 2.3" + } + } +} diff --git a/modules/eks/prometheus-scraper/README.md b/modules/eks/prometheus-scraper/README.md new file mode 100644 index 000000000..20c7ce7b8 --- /dev/null +++ b/modules/eks/prometheus-scraper/README.md @@ -0,0 +1,158 @@ +# Component: `eks/prometheus-scraper` + +This component provisions the an Amazon Managed collector or scraper to connect Amazon Managed Prometheus (AMP) with an +EKS cluster. + +A common use case for Amazon Managed Service for Prometheus is to monitor Kubernetes clusters managed by Amazon Elastic +Kubernetes Service (Amazon EKS). Kubernetes clusters, and many applications that run within Amazon EKS, automatically +export their metrics for Prometheus-compatible scrapers to access. + +Amazon Managed Service for Prometheus provides a fully managed, agentless scraper, or collector, that automatically +discovers and pulls Prometheus-compatible metrics. You don't have to manage, install, patch, or maintain agents or +scrapers. An Amazon Managed Service for Prometheus collector provides reliable, stable, highly available, automatically +scaled collection of metrics for your Amazon EKS cluster. Amazon Managed Service for Prometheus managed collectors work +with Amazon EKS clusters, including EC2 and Fargate. + +An Amazon Managed Service for Prometheus collector creates an Elastic Network Interface (ENI) per subnet specified when +creating the scraper. The collector scrapes the metrics through these ENIs, and uses remote_write to push the data to +your Amazon Managed Service for Prometheus workspace using a VPC endpoint. The scraped data never travels on the public +internet. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + eks/prometheus-scraper: + vars: + enabled: true + name: prometheus-scraper + # This refers to the `managed-prometheus/workspace` Terraform component, + # but the component name can be whatever you choose to name the stack component + prometheus_component_name: prometheus +``` + +### Authenticating with EKS + +In order for this managed collector to authenticate with the EKS cluster, update auth map after deploying. + +Note the `scraper_role_arn` and `clusterrole_username` outputs and set them to `rolearn` and `username` respectively +with the `map_additional_iam_roles` input for `eks/cluster`. + +```yaml +components: + terraform: + eks/cluster: + vars: + map_additional_iam_roles: + # this role is used to grant the Prometheus scraper access to this cluster. See eks/prometheus-scraper + - rolearn: "arn:aws:iam::111111111111:role/AWSServiceRoleForAmazonPrometheusScraper_111111111111111" + username: "acme-plat-ue2-sandbox-prometheus-scraper" + groups: [] +``` + +Then reapply the given cluster component. + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [prometheus](#module\_prometheus) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [scraper\_access](#module\_scraper\_access) | cloudposse/helm-release/aws | 0.10.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_prometheus_scraper.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/prometheus_scraper) | resource | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"AWS Managed Prometheus (AMP) scrapper roles and role bindings"` | no | +| [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [eks\_scrape\_configuration](#input\_eks\_scrape\_configuration) | Scrape configuration for the agentless scraper that will installed with EKS integrations | `string` | `"global:\n scrape_interval: 30s\nscrape_configs:\n # pod metrics\n - job_name: pod_exporter\n kubernetes_sd_configs:\n - role: pod\n # container metrics\n - job_name: cadvisor\n scheme: https\n authorization:\n credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n kubernetes_sd_configs:\n - role: node\n relabel_configs:\n - action: labelmap\n regex: __meta_kubernetes_node_label_(.+)\n - replacement: kubernetes.default.svc:443\n target_label: __address__\n - source_labels: [__meta_kubernetes_node_name]\n regex: (.+)\n target_label: __metrics_path__\n replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor\n # apiserver metrics\n - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token\n job_name: kubernetes-apiservers\n kubernetes_sd_configs:\n - role: endpoints\n relabel_configs:\n - action: keep\n regex: default;kubernetes;https\n source_labels:\n - __meta_kubernetes_namespace\n - __meta_kubernetes_service_name\n - __meta_kubernetes_endpoint_port_name\n scheme: https\n # kube proxy metrics\n - job_name: kube-proxy\n honor_labels: true\n kubernetes_sd_configs:\n - role: pod\n relabel_configs:\n - action: keep\n source_labels:\n - __meta_kubernetes_namespace\n - __meta_kubernetes_pod_name\n separator: '/'\n regex: 'kube-system/kube-proxy.+'\n - source_labels:\n - __address__\n action: replace\n target_label: __address__\n regex: (.+?)(\\\\:\\\\d+)?\n replacement: $1:10249\n"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | Kubernetes namespace to install the release into | `string` | `"kube-system"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [prometheus\_component\_name](#input\_prometheus\_component\_name) | The name of the Amazon Managed Prometheus workspace component | `string` | `"managed-prometheus/workspace"` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `300` | no | +| [verify](#input\_verify) | Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart | `bool` | `false` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | The name of the vpc component | `string` | `"vpc"` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [clusterrole\_username](#output\_clusterrole\_username) | The username of the ClusterRole used to give the scraper in-cluster permissions | +| [scraper\_role\_arn](#output\_scraper\_role\_arn) | The Amazon Resource Name (ARN) of the IAM role that provides permissions for the scraper to discover, collect, and produce metrics | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/prometheus-scraper) - + Cloud Posse's upstream component +- [AMP Collector Documentation](https://docs.aws.amazon.com/prometheus/latest/userguide/AMP-collector-how-to.html#AMP-collector-eks-setup) + +[](https://cpco.io/component) diff --git a/modules/eks/prometheus-scraper/charts/scraper-access/Chart.yaml b/modules/eks/prometheus-scraper/charts/scraper-access/Chart.yaml new file mode 100644 index 000000000..7dcaf9a3e --- /dev/null +++ b/modules/eks/prometheus-scraper/charts/scraper-access/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: scraper-access +description: A Helm chart for identity provider roles + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/modules/eks/prometheus-scraper/charts/scraper-access/templates/clusterrole-binding.yml b/modules/eks/prometheus-scraper/charts/scraper-access/templates/clusterrole-binding.yml new file mode 100644 index 000000000..e2a8feced --- /dev/null +++ b/modules/eks/prometheus-scraper/charts/scraper-access/templates/clusterrole-binding.yml @@ -0,0 +1,26 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Values.cluster_role_name }} +rules: + - apiGroups: [""] + resources: ["nodes", "nodes/proxy", "nodes/metrics", "services", "endpoints", "pods", "ingresses", "configmaps"] + verbs: ["describe", "get", "list", "watch"] + - apiGroups: ["extensions", "networking.k8s.io"] + resources: ["ingresses/status", "ingresses"] + verbs: ["describe", "get", "list", "watch"] + - nonResourceURLs: ["/metrics"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.cluster_role_name }}-binding +subjects: +- kind: User + name: {{ .Values.cluster_user_name }} + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: {{ .Values.cluster_role_name }} + apiGroup: rbac.authorization.k8s.io diff --git a/modules/eks/prometheus-scraper/charts/scraper-access/values.yaml b/modules/eks/prometheus-scraper/charts/scraper-access/values.yaml new file mode 100644 index 000000000..009dde0b8 --- /dev/null +++ b/modules/eks/prometheus-scraper/charts/scraper-access/values.yaml @@ -0,0 +1,8 @@ +# Default values for scraper-access. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# These default values can be overridden per environment in conf/.yaml files + +cluster_role_name: "aps-collector-role" +cluster_user_name: "aps-collector-user" diff --git a/modules/eks/prometheus-scraper/context.tf b/modules/eks/prometheus-scraper/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/prometheus-scraper/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/prometheus-scraper/main.tf b/modules/eks/prometheus-scraper/main.tf new file mode 100644 index 000000000..01b3e6d0b --- /dev/null +++ b/modules/eks/prometheus-scraper/main.tf @@ -0,0 +1,68 @@ +locals { + enabled = module.this.enabled + + # This will be used as the name of the ClusterRole and binded User + aps_clusterrole_identity = module.this.id + + # Amazon EKS requires a different format for this ARN. You must adjust the format of the returned ARN + # arn:aws:iam::account-id:role/AWSServiceRoleForAmazonPrometheusScraper_unique-id + # + # For example, + # arn:aws:iam::111122223333:role/aws-service-role/scraper.aps.amazonaws.com/AWSServiceRoleForAmazonPrometheusScraper_1234abcd-56ef-7 + # must be changed be to + # arn:aws:iam::111122223333:role/AWSServiceRoleForAmazonPrometheusScraper_1234abcd-56ef-7 + aps_clusterrole_username = replace(aws_prometheus_scraper.this[0].role_arn, "role/aws-service-role/scraper.aps.amazonaws.com", "role") + +} + +resource "aws_prometheus_scraper" "this" { + count = local.enabled ? 1 : 0 + + source { + eks { + cluster_arn = module.eks.outputs.eks_cluster_arn + security_group_ids = [module.eks.outputs.eks_cluster_managed_security_group_id] + subnet_ids = module.vpc.outputs.private_subnet_ids + } + } + + destination { + amp { + workspace_arn = module.prometheus.outputs.workspace_arn + } + } + + scrape_configuration = var.eks_scrape_configuration +} + +module "scraper_access" { + source = "cloudposse/helm-release/aws" + version = "0.10.1" + + enabled = local.enabled + + name = length(module.this.name) > 0 ? module.this.name : "prometheus" + chart = "${path.module}/charts/scraper-access" + description = var.chart_description + + kubernetes_namespace = var.kubernetes_namespace + create_namespace = var.create_namespace + + verify = var.verify + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") + + values = compact([ + yamlencode({ + cluster_role_name = local.aps_clusterrole_identity + cluster_user_name = local.aps_clusterrole_identity + }), + yamlencode(var.chart_values), + ]) + + context = module.this.context +} diff --git a/modules/eks/prometheus-scraper/outputs.tf b/modules/eks/prometheus-scraper/outputs.tf new file mode 100644 index 000000000..55cc28502 --- /dev/null +++ b/modules/eks/prometheus-scraper/outputs.tf @@ -0,0 +1,9 @@ +output "scraper_role_arn" { + description = "The Amazon Resource Name (ARN) of the IAM role that provides permissions for the scraper to discover, collect, and produce metrics" + value = local.aps_clusterrole_username +} + +output "clusterrole_username" { + description = "The username of the ClusterRole used to give the scraper in-cluster permissions" + value = local.aps_clusterrole_identity +} diff --git a/modules/eks/prometheus-scraper/provider-helm.tf b/modules/eks/prometheus-scraper/provider-helm.tf new file mode 100644 index 000000000..64459d4f4 --- /dev/null +++ b/modules/eks/prometheus-scraper/provider-helm.tf @@ -0,0 +1,166 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/prometheus-scraper/providers.tf b/modules/eks/prometheus-scraper/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/eks/prometheus-scraper/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/eks/prometheus-scraper/remote-state.tf b/modules/eks/prometheus-scraper/remote-state.tf new file mode 100644 index 000000000..d05dbc0bc --- /dev/null +++ b/modules/eks/prometheus-scraper/remote-state.tf @@ -0,0 +1,26 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.vpc_component_name + + context = module.this.context +} + +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + context = module.this.context +} + +module "prometheus" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.prometheus_component_name + + context = module.this.context +} diff --git a/modules/eks/prometheus-scraper/variables.tf b/modules/eks/prometheus-scraper/variables.tf new file mode 100644 index 000000000..26fd56db3 --- /dev/null +++ b/modules/eks/prometheus-scraper/variables.tf @@ -0,0 +1,137 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "vpc_component_name" { + type = string + description = "The name of the vpc component" + default = "vpc" +} + +variable "prometheus_component_name" { + type = string + description = "The name of the Amazon Managed Prometheus workspace component" + default = "managed-prometheus/workspace" +} + +variable "eks_scrape_configuration" { + type = string + description = "Scrape configuration for the agentless scraper that will installed with EKS integrations" + default = <<-EOT + global: + scrape_interval: 30s + scrape_configs: + # pod metrics + - job_name: pod_exporter + kubernetes_sd_configs: + - role: pod + # container metrics + - job_name: cadvisor + scheme: https + authorization: + credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token + kubernetes_sd_configs: + - role: node + relabel_configs: + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - replacement: kubernetes.default.svc:443 + target_label: __address__ + - source_labels: [__meta_kubernetes_node_name] + regex: (.+) + target_label: __metrics_path__ + replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor + # apiserver metrics + - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + job_name: kubernetes-apiservers + kubernetes_sd_configs: + - role: endpoints + relabel_configs: + - action: keep + regex: default;kubernetes;https + source_labels: + - __meta_kubernetes_namespace + - __meta_kubernetes_service_name + - __meta_kubernetes_endpoint_port_name + scheme: https + # kube proxy metrics + - job_name: kube-proxy + honor_labels: true + kubernetes_sd_configs: + - role: pod + relabel_configs: + - action: keep + source_labels: + - __meta_kubernetes_namespace + - __meta_kubernetes_pod_name + separator: '/' + regex: 'kube-system/kube-proxy.+' + - source_labels: + - __address__ + action: replace + target_label: __address__ + regex: (.+?)(\\:\\d+)? + replacement: $1:10249 + EOT +} + +variable "chart_description" { + type = string + description = "Set release description attribute (visible in the history)." + default = "AWS Managed Prometheus (AMP) scrapper roles and role bindings" +} + +variable "kubernetes_namespace" { + type = string + description = "Kubernetes namespace to install the release into" + default = "kube-system" +} + +variable "create_namespace" { + type = bool + description = "Create the Kubernetes namespace if it does not yet exist" + default = true +} + +variable "verify" { + type = bool + description = "Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart" + default = false +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`." + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used." + default = true +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails." + default = true +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = 300 +} + +variable "chart_values" { + type = any + description = "Additional values to yamlencode as `helm_release` values." + default = {} +} diff --git a/modules/eks/prometheus-scraper/versions.tf b/modules/eks/prometheus-scraper/versions.tf new file mode 100644 index 000000000..fb8857fab --- /dev/null +++ b/modules/eks/prometheus-scraper/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1, != 2.21.0" + } + } +} diff --git a/modules/eks/promtail/README.md b/modules/eks/promtail/README.md new file mode 100644 index 000000000..5ecafa3b4 --- /dev/null +++ b/modules/eks/promtail/README.md @@ -0,0 +1,123 @@ +# Component: `eks/promtail` + +Promtail is an agent which ships the contents of local logs to a Loki instance. + +This component deploys the [grafana/promtail](https://github.com/grafana/helm-charts/tree/main/charts/promtail) helm +chart and expects `eks/loki` to be deployed. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + eks/promtail: + vars: + enabled: true + name: promtail +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [alb\_controller\_ingress\_group](#module\_alb\_controller\_ingress\_group) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [chart\_values](#module\_chart\_values) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [loki](#module\_loki) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [promtail](#module\_promtail) | cloudposse/helm-release/aws | 0.10.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_ssm_parameter.basic_auth_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_controller\_ingress\_group\_component\_name](#input\_alb\_controller\_ingress\_group\_component\_name) | The name of the eks/alb-controller-ingress-group component. This should be an internal facing ALB | `string` | `"eks/alb-controller-ingress-group"` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | `"promtail"` | no | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `"Promtail is an agent which ships the contents of local logs to a Loki instance"` | no | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://grafana.github.io/helm-charts"` | no | +| [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the Kubernetes namespace if it does not yet exist | `bool` | `true` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | Kubernetes namespace to install the release into | `string` | `"monitoring"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [loki\_component\_name](#input\_loki\_component\_name) | The name of the eks/loki component | `string` | `"eks/loki"` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [push\_api](#input\_push\_api) | Describes and configures Promtail to expose a Loki push API server with an Ingress configuration.

- enabled: Set this to `true` to enable this feature
- scrape\_config: Optional. This component includes a basic configuration by default, or override the default configuration here. |
object({
enabled = optional(bool, false)
scrape_config = optional(string, "")
})
| `{}` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [scrape\_configs](#input\_scrape\_configs) | A list of local path paths starting with this component's base path for Promtail Scrape Configs | `list(string)` |
[
"scrape_config/default_kubernetes_pods.yaml"
]
| no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `300` | no | +| [verify](#input\_verify) | Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart | `bool` | `false` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `true` | no | + +## Outputs + +No outputs. + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/promtail) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/eks/promtail/context.tf b/modules/eks/promtail/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/promtail/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/promtail/main.tf b/modules/eks/promtail/main.tf new file mode 100644 index 000000000..954ebbeeb --- /dev/null +++ b/modules/eks/promtail/main.tf @@ -0,0 +1,136 @@ +locals { + enabled = module.this.enabled + name = length(module.this.name) > 0 ? module.this.name : "promtail" + + # Assume basic auth is enabled if the loki component has a basic auth username output + basic_auth_enabled = local.enabled && length(module.loki.outputs.basic_auth_username) > 0 + + # These are the default values required to connect to eks/loki in the same namespace + loki_write_chart_values = { + config = { + clients = [ + { + # Intentionally choose the loki-write service not loki-gateway. Loki gateway is disabled + url = "http://loki-write:3100/loki/api/v1/push" + tenant_id = "1" + basic_auth = local.basic_auth_enabled ? { + username = module.loki.outputs.basic_auth_username + password = data.aws_ssm_parameter.basic_auth_password[0].value + } : {} + } + ] + } + } + + # These are optional values used to expose an endpoint for the Push API + # https://grafana.com/docs/loki/latest/send-data/promtail/configuration/#loki_push_api + push_api_enabled = local.enabled && var.push_api.enabled + ingress_host_name = local.push_api_enabled ? format("%s.%s.%s", local.name, module.this.environment, module.dns_gbl_delegated[0].outputs.default_domain_name) : "" + ingress_group_name = local.push_api_enabled ? module.alb_controller_ingress_group[0].outputs.group_name : "" + default_push_api_scrape_config = <<-EOT + - job_name: push + loki_push_api: + server: + http_listen_port: 3500 + grpc_listen_port: 3600 + labels: + push: default + EOT + push_api_chart_values = { + config = { + snippets = { + extraScrapeConfigs = length(var.push_api.scrape_config) > 0 ? var.push_api.scrape_config : local.default_push_api_scrape_config + } + } + extraPorts = { + push = { + name = "push" + containerPort = "3500" + protocol = "TCP" + service = { + type = "ClusterIP" + port = "3500" + } + ingress = { + annotations = { + "kubernetes.io/ingress.class" = "alb" + "external-dns.alpha.kubernetes.io/hostname" = local.ingress_host_name + "alb.ingress.kubernetes.io/group.name" = local.ingress_group_name + "alb.ingress.kubernetes.io/backend-protocol" = "HTTP" + "alb.ingress.kubernetes.io/listen-ports" = "[{\"HTTP\": 80},{\"HTTPS\":443}]" + "alb.ingress.kubernetes.io/ssl-redirect" = "443" + "alb.ingress.kubernetes.io/target-type" = "ip" + } + hosts = [ + local.ingress_host_name + ] + tls = [ + { + secretName = "${module.this.id}-tls" + hosts = [local.ingress_host_name] + } + ] + } + } + } + } + + scrape_config = join("\n", [for scrape_config_file in var.scrape_configs : file("${path.module}/${scrape_config_file}")]) + scrape_config_chart_values = { + config = { + snippets = { + scrapeConfigs = local.scrape_config + } + } + } +} + +data "aws_ssm_parameter" "basic_auth_password" { + count = local.basic_auth_enabled ? 1 : 0 + + name = module.loki.outputs.ssm_path_basic_auth_password +} + +module "chart_values" { + source = "cloudposse/config/yaml//modules/deepmerge" + version = "1.0.2" + + count = local.enabled ? 1 : 0 + + maps = [ + local.loki_write_chart_values, + jsondecode(local.push_api_enabled ? jsonencode(local.push_api_chart_values) : jsonencode({})), + local.scrape_config_chart_values, + var.chart_values + ] +} + +module "promtail" { + source = "cloudposse/helm-release/aws" + version = "0.10.1" + + enabled = local.enabled + + name = local.name + chart = var.chart + description = var.chart_description + repository = var.chart_repository + chart_version = var.chart_version + + kubernetes_namespace = var.kubernetes_namespace + create_namespace = var.create_namespace + + verify = var.verify + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + eks_cluster_oidc_issuer_url = replace(module.eks.outputs.eks_cluster_identity_oidc_issuer, "https://", "") + + values = compact([ + yamlencode(module.chart_values[0].merged), + ]) + + context = module.this.context +} diff --git a/modules/eks/promtail/outputs.tf b/modules/eks/promtail/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/modules/eks/promtail/provider-helm.tf b/modules/eks/promtail/provider-helm.tf new file mode 100644 index 000000000..64459d4f4 --- /dev/null +++ b/modules/eks/promtail/provider-helm.tf @@ -0,0 +1,166 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# It depends on 2 standard Cloud Posse data source modules to be already +# defined in the same component: +# +# 1. module.iam_roles to provide the AWS profile or Role ARN to use to access the cluster +# 2. module.eks to provide the EKS cluster information +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = false + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + # Provide dummy configuration for the case where the EKS cluster is not available. + certificate_authority_data = try(module.eks.outputs.eks_cluster_certificate_authority_data, "") + # Use coalesce+try to handle both the case where the output is missing and the case where it is empty. + eks_cluster_id = coalesce(try(module.eks.outputs.eks_cluster_id, ""), "missing") + eks_cluster_endpoint = try(module.eks.outputs.eks_cluster_endpoint, "") +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "helm" { + kubernetes { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } + } + experiments { + manifest = var.helm_manifest_experiment_enabled && module.this.enabled + } +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? one(data.aws_eks_cluster_auth.eks[*].token) : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled && length(local.certificate_authority_data) > 0 ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/promtail/providers.tf b/modules/eks/promtail/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/eks/promtail/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/eks/promtail/remote-state.tf b/modules/eks/promtail/remote-state.tf new file mode 100644 index 000000000..391ae4624 --- /dev/null +++ b/modules/eks/promtail/remote-state.tf @@ -0,0 +1,58 @@ +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.eks_component_name + + context = module.this.context +} + +variable "loki_component_name" { + type = string + description = "The name of the eks/loki component" + default = "eks/loki" +} + +module "loki" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.loki_component_name + + context = module.this.context +} + +variable "alb_controller_ingress_group_component_name" { + type = string + description = "The name of the eks/alb-controller-ingress-group component. This should be an internal facing ALB" + default = "eks/alb-controller-ingress-group" +} + +module "alb_controller_ingress_group" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.push_api_enabled ? 1 : 0 + + component = var.alb_controller_ingress_group_component_name + + context = module.this.context +} + +module "dns_gbl_delegated" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.push_api_enabled ? 1 : 0 + + environment = "gbl" + component = "dns-delegated" + + context = module.this.context +} diff --git a/modules/eks/promtail/scrape_config/default_kubernetes_pods.yaml b/modules/eks/promtail/scrape_config/default_kubernetes_pods.yaml new file mode 100644 index 000000000..23e58bc6c --- /dev/null +++ b/modules/eks/promtail/scrape_config/default_kubernetes_pods.yaml @@ -0,0 +1,40 @@ +# See also https://github.com/grafana/loki/blob/master/production/ksonnet/promtail/scrape_config.libsonnet for reference +- job_name: kubernetes-pods + pipeline_stages: + {{- toYaml .Values.config.snippets.pipelineStages | nindent 4 }} + kubernetes_sd_configs: + - role: pod + relabel_configs: + - source_labels: + - __meta_kubernetes_pod_controller_name + regex: ([0-9a-z-.]+?)(-[0-9a-f]{8,10})? + action: replace + target_label: __tmp_controller_name + - source_labels: + - __meta_kubernetes_pod_label_app_kubernetes_io_name + - __meta_kubernetes_pod_label_app + - __tmp_controller_name + - __meta_kubernetes_pod_name + regex: ^;*([^;]+)(;.*)?$ + action: replace + target_label: app + - source_labels: + - __meta_kubernetes_pod_label_app_kubernetes_io_instance + - __meta_kubernetes_pod_label_instance + regex: ^;*([^;]+)(;.*)?$ + action: replace + target_label: instance + - source_labels: + - __meta_kubernetes_pod_label_app_kubernetes_io_component + - __meta_kubernetes_pod_label_component + regex: ^;*([^;]+)(;.*)?$ + action: replace + target_label: component + {{- if .Values.config.snippets.addScrapeJobLabel }} + - replacement: kubernetes-pods + target_label: scrape_job + {{- end }} + {{- toYaml .Values.config.snippets.common | nindent 4 }} + {{- with .Values.config.snippets.extraRelabelConfigs }} + {{- toYaml . | nindent 4 }} + {{- end }} diff --git a/modules/eks/promtail/variables.tf b/modules/eks/promtail/variables.tf new file mode 100644 index 000000000..9acae3479 --- /dev/null +++ b/modules/eks/promtail/variables.tf @@ -0,0 +1,99 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "chart_description" { + type = string + description = "Set release description attribute (visible in the history)." + default = "Promtail is an agent which ships the contents of local logs to a Loki instance" +} + +variable "chart" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." + default = "promtail" +} + +variable "chart_repository" { + type = string + description = "Repository URL where to locate the requested chart." + default = "https://grafana.github.io/helm-charts" +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." + default = null +} + +variable "kubernetes_namespace" { + type = string + description = "Kubernetes namespace to install the release into" + default = "monitoring" +} + +variable "create_namespace" { + type = bool + description = "Create the Kubernetes namespace if it does not yet exist" + default = true +} + +variable "verify" { + type = bool + description = "Verify the package before installing it. Helm uses a provenance file to verify the integrity of the chart; this must be hosted alongside the chart" + default = false +} + +variable "wait" { + type = bool + description = "Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`." + default = true +} + +variable "atomic" { + type = bool + description = "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used." + default = true +} + +variable "cleanup_on_fail" { + type = bool + description = "Allow deletion of new resources created in this upgrade when upgrade fails." + default = true +} + +variable "timeout" { + type = number + description = "Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds" + default = 300 +} + +variable "chart_values" { + type = any + description = "Additional values to yamlencode as `helm_release` values." + default = {} +} + +variable "push_api" { + type = object({ + enabled = optional(bool, false) + scrape_config = optional(string, "") + }) + description = <<-EOT + Describes and configures Promtail to expose a Loki push API server with an Ingress configuration. + + - enabled: Set this to `true` to enable this feature + - scrape_config: Optional. This component includes a basic configuration by default, or override the default configuration here. + + EOT + default = {} +} + +variable "scrape_configs" { + type = list(string) + description = "A list of local path paths starting with this component's base path for Promtail Scrape Configs" + default = [ + "scrape_config/default_kubernetes_pods.yaml" + ] +} diff --git a/modules/eks/promtail/versions.tf b/modules/eks/promtail/versions.tf new file mode 100644 index 000000000..fb8857fab --- /dev/null +++ b/modules/eks/promtail/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1, != 2.21.0" + } + } +} diff --git a/modules/managed-grafana/api-key/README.md b/modules/managed-grafana/api-key/README.md new file mode 100644 index 000000000..fbbad996c --- /dev/null +++ b/modules/managed-grafana/api-key/README.md @@ -0,0 +1,100 @@ +# Component: `managed-grafana/api-key` + +This component is responsible for provisioning an API Key for an Amazon Managed Grafana workspace. + +We use this API with the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest) in +other `managed-grafana` sub components. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + grafana/api-key: + metadata: + component: managed-grafana/api-key + vars: + enabled: true + grafana_component_name: grafana +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [time](#requirement\_time) | >= 0.11.1 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [time](#provider\_time) | >= 0.11.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [managed\_grafana](#module\_managed\_grafana) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [ssm\_parameters](#module\_ssm\_parameters) | cloudposse/ssm-parameter-store/aws | 0.13.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_grafana_workspace_api_key.key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/grafana_workspace_api_key) | resource | +| [time_rotating.ttl](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/rotating) | resource | +| [time_static.ttl](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/static) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [grafana\_component\_name](#input\_grafana\_component\_name) | The name of the Grafana component | `string` | `"managed-grafana/workspace"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [key\_role](#input\_key\_role) | Specifies the permission level of the API key. Valid values are VIEWER, EDITOR, or ADMIN. | `string` | `"ADMIN"` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [minutes\_to\_live](#input\_minutes\_to\_live) | Specifies the time in minutes until the API key expires. Keys can be valid for up to 30 days. | `number` | `43200` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [ssm\_path\_format\_api\_key](#input\_ssm\_path\_format\_api\_key) | The path in AWS SSM to the Grafana API Key provisioned with this component | `string` | `"/grafana/%s/api_key"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [ssm\_path\_grafana\_api\_key](#output\_ssm\_path\_grafana\_api\_key) | The path in AWS SSM to the Grafana API Key provisioned with this component | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/managed-grafana/api-key) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/managed-grafana/api-key/context.tf b/modules/managed-grafana/api-key/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/managed-grafana/api-key/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/managed-grafana/api-key/main.tf b/modules/managed-grafana/api-key/main.tf new file mode 100644 index 000000000..8300a7981 --- /dev/null +++ b/modules/managed-grafana/api-key/main.tf @@ -0,0 +1,45 @@ +locals { + enabled = module.this.enabled + + ssm_path_api_key = format(var.ssm_path_format_api_key, module.this.id) +} + +resource "time_rotating" "ttl" { + rotation_minutes = var.minutes_to_live +} + +resource "time_static" "ttl" { + rfc3339 = time_rotating.ttl.rfc3339 +} + +resource "aws_grafana_workspace_api_key" "key" { + count = local.enabled ? 1 : 0 + + key_name = module.this.id + key_role = var.key_role + seconds_to_live = var.minutes_to_live * 60 + workspace_id = module.managed_grafana.outputs.workspace_id + + lifecycle { + replace_triggered_by = [ + time_static.ttl + ] + } +} + +module "ssm_parameters" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.13.0" + + parameter_write = [ + { + name = local.ssm_path_api_key + value = aws_grafana_workspace_api_key.key[0].key + type = "SecureString" + overwrite = "true" + description = "Grafana Workspace API Key" + } + ] + + context = module.this.context +} diff --git a/modules/managed-grafana/api-key/outputs.tf b/modules/managed-grafana/api-key/outputs.tf new file mode 100644 index 000000000..74e0f5375 --- /dev/null +++ b/modules/managed-grafana/api-key/outputs.tf @@ -0,0 +1,4 @@ +output "ssm_path_grafana_api_key" { + description = "The path in AWS SSM to the Grafana API Key provisioned with this component" + value = local.ssm_path_api_key +} diff --git a/modules/managed-grafana/api-key/providers.tf b/modules/managed-grafana/api-key/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/managed-grafana/api-key/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/managed-grafana/api-key/remote-state.tf b/modules/managed-grafana/api-key/remote-state.tf new file mode 100644 index 000000000..dfdfa67dd --- /dev/null +++ b/modules/managed-grafana/api-key/remote-state.tf @@ -0,0 +1,8 @@ +module "managed_grafana" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.grafana_component_name + + context = module.this.context +} diff --git a/modules/managed-grafana/api-key/variables.tf b/modules/managed-grafana/api-key/variables.tf new file mode 100644 index 000000000..4a5cba6c6 --- /dev/null +++ b/modules/managed-grafana/api-key/variables.tf @@ -0,0 +1,28 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "grafana_component_name" { + type = string + description = "The name of the Grafana component" + default = "managed-grafana/workspace" +} + +variable "ssm_path_format_api_key" { + type = string + description = "The path in AWS SSM to the Grafana API Key provisioned with this component" + default = "/grafana/%s/api_key" +} + +variable "key_role" { + type = string + description = "Specifies the permission level of the API key. Valid values are VIEWER, EDITOR, or ADMIN." + default = "ADMIN" +} + +variable "minutes_to_live" { + type = number + description = "Specifies the time in minutes until the API key expires. Keys can be valid for up to 30 days." + default = 43200 # 30 days +} diff --git a/modules/managed-grafana/api-key/versions.tf b/modules/managed-grafana/api-key/versions.tf new file mode 100644 index 000000000..00e0b097d --- /dev/null +++ b/modules/managed-grafana/api-key/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.11.1" + } + } +} diff --git a/modules/managed-grafana/dashboard/README.md b/modules/managed-grafana/dashboard/README.md new file mode 100644 index 000000000..834b81f35 --- /dev/null +++ b/modules/managed-grafana/dashboard/README.md @@ -0,0 +1,105 @@ +# Component: `managed-grafana/dashboard` + +This component is responsible for provisioning a dashboard an Amazon Managed Grafana workspace. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + grafana/dashboard/prometheus: + metadata: + component: managed-grafana/dashboard + vars: + enabled: true + name: "prometheus-dashboard" + grafana_component_name: grafana + grafana_api_key_component_name: grafana/api-key + dashboard_url: "https://grafana.com/api/dashboards/315/revisions/3/download" + config_input: + "${DS_PROMETHEUS}": "acme-plat-ue2-sandbox-prometheus" # Input Value : Data source UID +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [grafana](#requirement\_grafana) | >= 2.18.0 | +| [http](#requirement\_http) | >= 3.4.2 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [grafana](#provider\_grafana) | >= 2.18.0 | +| [http](#provider\_http) | >= 3.4.2 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [config\_json](#module\_config\_json) | cloudposse/config/yaml//modules/deepmerge | 1.0.2 | +| [grafana](#module\_grafana) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [grafana\_api\_key](#module\_grafana\_api\_key) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [grafana_dashboard.this](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/dashboard) | resource | +| [aws_ssm_parameter.grafana_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [http_http.grafana_dashboard_json](https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_config](#input\_additional\_config) | Additional dashboard configuration to be merged with the provided dashboard JSON | `map(any)` | `{}` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [config\_input](#input\_config\_input) | A map of string replacements used to supply input for the dashboard config JSON | `map(string)` | `{}` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [dashboard\_name](#input\_dashboard\_name) | The name to use for the dashboard. This must be unique. | `string` | n/a | yes | +| [dashboard\_url](#input\_dashboard\_url) | The marketplace URL of the dashboard to be created | `string` | n/a | yes | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [grafana\_api\_key\_component\_name](#input\_grafana\_api\_key\_component\_name) | The name of the component used to provision an Amazon Managed Grafana API key | `string` | `"managed-grafana/api-key"` | no | +| [grafana\_component\_name](#input\_grafana\_component\_name) | The name of the component used to provision an Amazon Managed Grafana workspace | `string` | `"managed-grafana/workspace"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +No outputs. + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/managed-grafana/dashboard) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/managed-grafana/dashboard/context.tf b/modules/managed-grafana/dashboard/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/managed-grafana/dashboard/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/managed-grafana/dashboard/main.tf b/modules/managed-grafana/dashboard/main.tf new file mode 100644 index 000000000..98e59054a --- /dev/null +++ b/modules/managed-grafana/dashboard/main.tf @@ -0,0 +1,35 @@ +locals { + enabled = module.this.enabled + + # Replace each of the keys in var.config_input with the given value in the module.config_json[0].merged result + config_json = join("", [for k in keys(var.config_input) : replace(jsonencode(module.config_json[0].merged), k, var.config_input[k])]) +} + +data "http" "grafana_dashboard_json" { + count = local.enabled ? 1 : 0 + + url = var.dashboard_url +} + +module "config_json" { + source = "cloudposse/config/yaml//modules/deepmerge" + version = "1.0.2" + + count = local.enabled ? 1 : 0 + + maps = [ + jsondecode(data.http.grafana_dashboard_json[0].response_body), + { + "title" : var.dashboard_name, + "uid" : var.dashboard_name, + "id" : var.dashboard_name + }, + var.additional_config + ] +} + +resource "grafana_dashboard" "this" { + count = local.enabled ? 1 : 0 + + config_json = local.config_json +} diff --git a/modules/managed-grafana/dashboard/provider-grafana.tf b/modules/managed-grafana/dashboard/provider-grafana.tf new file mode 100644 index 000000000..51d6e65ea --- /dev/null +++ b/modules/managed-grafana/dashboard/provider-grafana.tf @@ -0,0 +1,38 @@ +variable "grafana_component_name" { + type = string + description = "The name of the component used to provision an Amazon Managed Grafana workspace" + default = "managed-grafana/workspace" +} + +variable "grafana_api_key_component_name" { + type = string + description = "The name of the component used to provision an Amazon Managed Grafana API key" + default = "managed-grafana/api-key" +} + +module "grafana" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.grafana_component_name + + context = module.this.context +} + +module "grafana_api_key" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.grafana_api_key_component_name + + context = module.this.context +} + +data "aws_ssm_parameter" "grafana_api_key" { + name = module.grafana_api_key.outputs.ssm_path_grafana_api_key +} + +provider "grafana" { + url = format("https://%s/", module.grafana.outputs.workspace_endpoint) + auth = data.aws_ssm_parameter.grafana_api_key.value +} diff --git a/modules/managed-grafana/dashboard/providers.tf b/modules/managed-grafana/dashboard/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/managed-grafana/dashboard/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/managed-grafana/dashboard/variables.tf b/modules/managed-grafana/dashboard/variables.tf new file mode 100644 index 000000000..198c88941 --- /dev/null +++ b/modules/managed-grafana/dashboard/variables.tf @@ -0,0 +1,26 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "dashboard_name" { + type = string + description = "The name to use for the dashboard. This must be unique." +} + +variable "dashboard_url" { + type = string + description = "The marketplace URL of the dashboard to be created" +} + +variable "additional_config" { + type = map(any) + description = "Additional dashboard configuration to be merged with the provided dashboard JSON" + default = {} +} + +variable "config_input" { + type = map(string) + description = "A map of string replacements used to supply input for the dashboard config JSON" + default = {} +} diff --git a/modules/managed-grafana/dashboard/versions.tf b/modules/managed-grafana/dashboard/versions.tf new file mode 100644 index 000000000..e3912e5ae --- /dev/null +++ b/modules/managed-grafana/dashboard/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + grafana = { + source = "grafana/grafana" + version = ">= 2.18.0" + } + http = { + source = "hashicorp/http" + version = ">= 3.4.2" + } + } +} diff --git a/modules/managed-grafana/data-source/loki/README.md b/modules/managed-grafana/data-source/loki/README.md new file mode 100644 index 000000000..52816afe6 --- /dev/null +++ b/modules/managed-grafana/data-source/loki/README.md @@ -0,0 +1,136 @@ +# Component: `managed-grafana/data-source/loki` + +This component is responsible for provisioning a Loki data source for an Amazon Managed Grafana workspace. + +Use this component alongside the `eks/loki` component. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + grafana/datasource/defaults: + metadata: + component: managed-grafana/data-source/managed-prometheus + type: abstract + vars: + enabled: true + grafana_component_name: grafana + grafana_api_key_component_name: grafana/api-key + + grafana/datasource/plat-sandbox-loki: + metadata: + component: managed-grafana/data-source/loki + inherits: + - grafana/datasource/defaults + vars: + name: plat-sandbox-loki + loki_tenant_name: plat + loki_stage_name: sandbox + + grafana/datasource/plat-dev-loki: + metadata: + component: managed-grafana/data-source/loki + inherits: + - grafana/datasource/defaults + vars: + name: plat-dev-loki + loki_tenant_name: plat + loki_stage_name: dev + + grafana/datasource/plat-prod-loki: + metadata: + component: managed-grafana/data-source/loki + inherits: + - grafana/datasource/defaults + vars: + name: plat-prod-loki + loki_tenant_name: plat + loki_stage_name: prod +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [grafana](#requirement\_grafana) | >= 2.18.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [aws.source](#provider\_aws.source) | >= 4.0 | +| [grafana](#provider\_grafana) | >= 2.18.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [grafana](#module\_grafana) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [grafana\_api\_key](#module\_grafana\_api\_key) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../../account-map/modules/iam-roles | n/a | +| [loki](#module\_loki) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [source\_account\_role](#module\_source\_account\_role) | ../../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [grafana_data_source.loki](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source) | resource | +| [aws_ssm_parameter.basic_auth_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.grafana_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [grafana\_api\_key\_component\_name](#input\_grafana\_api\_key\_component\_name) | The name of the component used to provision an Amazon Managed Grafana API key | `string` | `"managed-grafana/api-key"` | no | +| [grafana\_component\_name](#input\_grafana\_component\_name) | The name of the component used to provision an Amazon Managed Grafana workspace | `string` | `"managed-grafana/workspace"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [loki\_component\_name](#input\_loki\_component\_name) | The name of the loki component | `string` | `"eks/loki"` | no | +| [loki\_environment\_name](#input\_loki\_environment\_name) | The environment where the loki component is deployed | `string` | `""` | no | +| [loki\_stage\_name](#input\_loki\_stage\_name) | The stage where the loki component is deployed | `string` | `""` | no | +| [loki\_tenant\_name](#input\_loki\_tenant\_name) | The tenant where the loki component is deployed | `string` | `""` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [uid](#output\_uid) | The UID of this dashboard | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/managed-grafana/data-source/loki) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/managed-grafana/data-source/loki/context.tf b/modules/managed-grafana/data-source/loki/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/managed-grafana/data-source/loki/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/managed-grafana/data-source/loki/main.tf b/modules/managed-grafana/data-source/loki/main.tf new file mode 100644 index 000000000..5b33013d0 --- /dev/null +++ b/modules/managed-grafana/data-source/loki/main.tf @@ -0,0 +1,36 @@ +locals { + enabled = module.this.enabled + + # Assume basic auth is enabled if the loki component has a basic auth username output + basic_auth_enabled = local.enabled && length(module.loki.outputs.basic_auth_username) > 0 +} + +data "aws_ssm_parameter" "basic_auth_password" { + provider = aws.source + + count = local.basic_auth_enabled ? 1 : 0 + + name = module.loki.outputs.ssm_path_basic_auth_password +} + +resource "grafana_data_source" "loki" { + count = local.enabled ? 1 : 0 + + type = "loki" + name = module.loki.outputs.id + uid = module.loki.outputs.id + url = format("https://%s", module.loki.outputs.url) + + basic_auth_enabled = local.basic_auth_enabled + basic_auth_username = local.basic_auth_enabled ? module.loki.outputs.basic_auth_username : "" + secure_json_data_encoded = jsonencode(local.basic_auth_enabled ? { + basicAuthPassword = data.aws_ssm_parameter.basic_auth_password[0].value + } : {}) + + http_headers = { + # https://grafana.com/docs/loki/latest/operations/authentication/ + # > When using Loki in multi-tenant mode, Loki requires the HTTP header + # > X-Scope-OrgID to be set to a string identifying the tenant + "X-Scope-OrgID" = "1" + } +} diff --git a/modules/managed-grafana/data-source/loki/outputs.tf b/modules/managed-grafana/data-source/loki/outputs.tf new file mode 100644 index 000000000..c2ec8d52c --- /dev/null +++ b/modules/managed-grafana/data-source/loki/outputs.tf @@ -0,0 +1,5 @@ +output "uid" { + # The output id is not the uid. It follows a format like "1:uid" + value = split(":", grafana_data_source.loki[0].id)[1] + description = "The UID of this dashboard" +} diff --git a/modules/managed-grafana/data-source/loki/provider-grafana.tf b/modules/managed-grafana/data-source/loki/provider-grafana.tf new file mode 100644 index 000000000..51d6e65ea --- /dev/null +++ b/modules/managed-grafana/data-source/loki/provider-grafana.tf @@ -0,0 +1,38 @@ +variable "grafana_component_name" { + type = string + description = "The name of the component used to provision an Amazon Managed Grafana workspace" + default = "managed-grafana/workspace" +} + +variable "grafana_api_key_component_name" { + type = string + description = "The name of the component used to provision an Amazon Managed Grafana API key" + default = "managed-grafana/api-key" +} + +module "grafana" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.grafana_component_name + + context = module.this.context +} + +module "grafana_api_key" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.grafana_api_key_component_name + + context = module.this.context +} + +data "aws_ssm_parameter" "grafana_api_key" { + name = module.grafana_api_key.outputs.ssm_path_grafana_api_key +} + +provider "grafana" { + url = format("https://%s/", module.grafana.outputs.workspace_endpoint) + auth = data.aws_ssm_parameter.grafana_api_key.value +} diff --git a/modules/managed-grafana/data-source/loki/provider-source.tf b/modules/managed-grafana/data-source/loki/provider-source.tf new file mode 100644 index 000000000..83876cb9b --- /dev/null +++ b/modules/managed-grafana/data-source/loki/provider-source.tf @@ -0,0 +1,22 @@ +module "source_account_role" { + source = "../../../account-map/modules/iam-roles" + + stage = var.loki_stage_name + tenant = var.loki_tenant_name + + context = module.this.context +} + +provider "aws" { + alias = "source" + region = var.region + + profile = module.source_account_role.terraform_profile_name + + dynamic "assume_role" { + for_each = compact([module.source_account_role.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} diff --git a/modules/managed-grafana/data-source/loki/providers.tf b/modules/managed-grafana/data-source/loki/providers.tf new file mode 100644 index 000000000..59ec32354 --- /dev/null +++ b/modules/managed-grafana/data-source/loki/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/managed-grafana/data-source/loki/remote-state.tf b/modules/managed-grafana/data-source/loki/remote-state.tf new file mode 100644 index 000000000..87cf04b5e --- /dev/null +++ b/modules/managed-grafana/data-source/loki/remote-state.tf @@ -0,0 +1,36 @@ +variable "loki_component_name" { + type = string + description = "The name of the loki component" + default = "eks/loki" +} + +variable "loki_stage_name" { + type = string + description = "The stage where the loki component is deployed" + default = "" +} + +variable "loki_environment_name" { + type = string + description = "The environment where the loki component is deployed" + default = "" +} + +variable "loki_tenant_name" { + type = string + description = "The tenant where the loki component is deployed" + default = "" +} + +module "loki" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.loki_component_name + + stage = length(var.loki_stage_name) > 0 ? var.loki_stage_name : module.this.stage + environment = length(var.loki_environment_name) > 0 ? var.loki_environment_name : module.this.environment + tenant = length(var.loki_tenant_name) > 0 ? var.loki_tenant_name : module.this.tenant + + context = module.this.context +} diff --git a/modules/managed-grafana/data-source/loki/variables.tf b/modules/managed-grafana/data-source/loki/variables.tf new file mode 100644 index 000000000..0753180bf --- /dev/null +++ b/modules/managed-grafana/data-source/loki/variables.tf @@ -0,0 +1,4 @@ +variable "region" { + type = string + description = "AWS Region" +} diff --git a/modules/managed-grafana/data-source/loki/versions.tf b/modules/managed-grafana/data-source/loki/versions.tf new file mode 100644 index 000000000..0965af1f8 --- /dev/null +++ b/modules/managed-grafana/data-source/loki/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + grafana = { + source = "grafana/grafana" + version = ">= 2.18.0" + } + } +} diff --git a/modules/managed-grafana/data-source/managed-prometheus/README.md b/modules/managed-grafana/data-source/managed-prometheus/README.md new file mode 100644 index 000000000..f261ef614 --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/README.md @@ -0,0 +1,135 @@ +# Component: `managed-grafana/data-source/managed-prometheus` + +This component is responsible for provisioning an Amazon Managed Prometheus data source for an Amazon Managed Grafana +workspace. + +Use this component alongside the `managed-prometheus/workspace` component. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + grafana/datasource/defaults: + metadata: + component: managed-grafana/data-source/managed-prometheus + type: abstract + vars: + enabled: true + grafana_component_name: grafana + grafana_api_key_component_name: grafana/api-key + prometheus_component_name: prometheus + + grafana/datasource/plat-sandbox-prometheus: + metadata: + component: managed-grafana/data-source/managed-prometheus + inherits: + - grafana/datasource/defaults + vars: + name: plat-sandbox-prometheus + prometheus_tenant_name: plat + prometheus_stage_name: sandbox + + grafana/datasource/plat-dev-prometheus: + metadata: + component: managed-grafana/data-source/managed-prometheus + inherits: + - grafana/datasource/defaults + vars: + name: plat-dev-prometheus + prometheus_tenant_name: plat + prometheus_stage_name: dev + + grafana/datasource/plat-prod-prometheus: + metadata: + component: managed-grafana/data-source/managed-prometheus + inherits: + - grafana/datasource/defaults + vars: + name: plat-prod-prometheus + prometheus_tenant_name: plat + prometheus_stage_name: prod +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [grafana](#requirement\_grafana) | >= 2.18.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [grafana](#provider\_grafana) | >= 2.18.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [grafana](#module\_grafana) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [grafana\_api\_key](#module\_grafana\_api\_key) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../../account-map/modules/iam-roles | n/a | +| [prometheus](#module\_prometheus) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [grafana_data_source.managed_prometheus](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source) | resource | +| [aws_ssm_parameter.grafana_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [grafana\_api\_key\_component\_name](#input\_grafana\_api\_key\_component\_name) | The name of the component used to provision an Amazon Managed Grafana API key | `string` | `"managed-grafana/api-key"` | no | +| [grafana\_component\_name](#input\_grafana\_component\_name) | The name of the component used to provision an Amazon Managed Grafana workspace | `string` | `"managed-grafana/workspace"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [prometheus\_component\_name](#input\_prometheus\_component\_name) | The name of the Amazon Managed Prometheus component to be added as a Grafana data source | `string` | `"managed-prometheus/workspace"` | no | +| [prometheus\_environment\_name](#input\_prometheus\_environment\_name) | The environment where the Amazon Managed Prometheus component is deployed | `string` | `""` | no | +| [prometheus\_stage\_name](#input\_prometheus\_stage\_name) | The stage where the Amazon Managed Prometheus component is deployed | `string` | `""` | no | +| [prometheus\_tenant\_name](#input\_prometheus\_tenant\_name) | The tenant where the Amazon Managed Prometheus component is deployed | `string` | `""` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [uid](#output\_uid) | The UID of this dashboard | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/managed-grafana/data-source/managed-prometheus) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/managed-grafana/data-source/managed-prometheus/context.tf b/modules/managed-grafana/data-source/managed-prometheus/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/managed-grafana/data-source/managed-prometheus/main.tf b/modules/managed-grafana/data-source/managed-prometheus/main.tf new file mode 100644 index 000000000..98a0d7c70 --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/main.tf @@ -0,0 +1,20 @@ +locals { + enabled = module.this.enabled +} + +resource "grafana_data_source" "managed_prometheus" { + count = local.enabled ? 1 : 0 + + type = "prometheus" + name = module.prometheus.outputs.id + uid = module.prometheus.outputs.id + url = module.prometheus.outputs.workspace_endpoint + + json_data_encoded = jsonencode({ + sigV4Auth = true + httpMethod = "POST" + sigV4AuthType = "ec2_iam_role" + sigV4AssumeRoleArn = module.prometheus.outputs.access_role_arn + sigV4Region = module.prometheus.outputs.workspace_region + }) +} diff --git a/modules/managed-grafana/data-source/managed-prometheus/outputs.tf b/modules/managed-grafana/data-source/managed-prometheus/outputs.tf new file mode 100644 index 000000000..c4013c809 --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/outputs.tf @@ -0,0 +1,5 @@ +output "uid" { + # The output "id" includes orgId (orgId:uid). We only want uid + value = split(":", grafana_data_source.managed_prometheus[0].id)[1] + description = "The UID of this dashboard" +} diff --git a/modules/managed-grafana/data-source/managed-prometheus/provider-grafana.tf b/modules/managed-grafana/data-source/managed-prometheus/provider-grafana.tf new file mode 100644 index 000000000..51d6e65ea --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/provider-grafana.tf @@ -0,0 +1,38 @@ +variable "grafana_component_name" { + type = string + description = "The name of the component used to provision an Amazon Managed Grafana workspace" + default = "managed-grafana/workspace" +} + +variable "grafana_api_key_component_name" { + type = string + description = "The name of the component used to provision an Amazon Managed Grafana API key" + default = "managed-grafana/api-key" +} + +module "grafana" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.grafana_component_name + + context = module.this.context +} + +module "grafana_api_key" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.grafana_api_key_component_name + + context = module.this.context +} + +data "aws_ssm_parameter" "grafana_api_key" { + name = module.grafana_api_key.outputs.ssm_path_grafana_api_key +} + +provider "grafana" { + url = format("https://%s/", module.grafana.outputs.workspace_endpoint) + auth = data.aws_ssm_parameter.grafana_api_key.value +} diff --git a/modules/managed-grafana/data-source/managed-prometheus/providers.tf b/modules/managed-grafana/data-source/managed-prometheus/providers.tf new file mode 100644 index 000000000..59ec32354 --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/managed-grafana/data-source/managed-prometheus/remote-state.tf b/modules/managed-grafana/data-source/managed-prometheus/remote-state.tf new file mode 100644 index 000000000..0494020a4 --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/remote-state.tf @@ -0,0 +1,12 @@ +module "prometheus" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.prometheus_component_name + + stage = length(var.prometheus_stage_name) > 0 ? var.prometheus_stage_name : module.this.stage + environment = length(var.prometheus_environment_name) > 0 ? var.prometheus_environment_name : module.this.environment + tenant = length(var.prometheus_tenant_name) > 0 ? var.prometheus_tenant_name : module.this.tenant + + context = module.this.context +} diff --git a/modules/managed-grafana/data-source/managed-prometheus/variables.tf b/modules/managed-grafana/data-source/managed-prometheus/variables.tf new file mode 100644 index 000000000..635194b47 --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/variables.tf @@ -0,0 +1,28 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "prometheus_component_name" { + type = string + description = "The name of the Amazon Managed Prometheus component to be added as a Grafana data source" + default = "managed-prometheus/workspace" +} + +variable "prometheus_stage_name" { + type = string + description = "The stage where the Amazon Managed Prometheus component is deployed" + default = "" +} + +variable "prometheus_environment_name" { + type = string + description = "The environment where the Amazon Managed Prometheus component is deployed" + default = "" +} + +variable "prometheus_tenant_name" { + type = string + description = "The tenant where the Amazon Managed Prometheus component is deployed" + default = "" +} diff --git a/modules/managed-grafana/data-source/managed-prometheus/versions.tf b/modules/managed-grafana/data-source/managed-prometheus/versions.tf new file mode 100644 index 000000000..0965af1f8 --- /dev/null +++ b/modules/managed-grafana/data-source/managed-prometheus/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + grafana = { + source = "grafana/grafana" + version = ">= 2.18.0" + } + } +} diff --git a/modules/managed-grafana/workspace/README.md b/modules/managed-grafana/workspace/README.md new file mode 100644 index 000000000..27e92e1c0 --- /dev/null +++ b/modules/managed-grafana/workspace/README.md @@ -0,0 +1,121 @@ +# Component: `managed-grafana/workspace` + +This component is responsible for provisioning an Amazon Managed Grafana workspace. + +Amazon Managed Grafana is a fully managed service for Grafana, a popular open-source analytics platform that enables you +to query, visualize, and alert on your metrics, logs, and traces. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +```yaml +components: + terraform: + grafana: + metadata: + component: managed-grafana/workspace + vars: + enabled: true + name: grafana + private_network_access_enabled: true + sso_role_associations: + - role: "ADMIN" + group_ids: + - "11111111-2222-3333-4444-555555555555" + # This grafana workspace will be allowed to assume the cross + # account access role from these prometheus components + prometheus_source_accounts: + - component: prometheus + tenant: plat + stage: sandbox + - component: prometheus + tenant: plat + stage: dev +``` + +> [!NOTE] +> +> We would prefer to have a custom URL for the provisioned Grafana workspace, but at the moment it's not supported +> natively and implementation would be non-trivial. We will continue to monitor that Issue and consider alternatives, +> such as using Cloudfront. +> +> [Issue #6: Support for Custom Domains](https://github.com/aws/amazon-managed-grafana-roadmap/issues/6) + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [managed\_grafana](#module\_managed\_grafana) | cloudposse/managed-grafana/aws | 0.1.0 | +| [prometheus](#module\_prometheus) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [security\_group](#module\_security\_group) | cloudposse/security-group/aws | 2.2.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_grafana_role_association.sso](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/grafana_role_association) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [private\_network\_access\_enabled](#input\_private\_network\_access\_enabled) | If set to `true`, enable the VPC Configuration to allow this workspace to access the private network using outputs from the vpc component | `bool` | `false` | no | +| [prometheus\_policy\_enabled](#input\_prometheus\_policy\_enabled) | Set this to `true` to allow this Grafana workspace to access Amazon Managed Prometheus in this account | `bool` | `false` | no | +| [prometheus\_source\_accounts](#input\_prometheus\_source\_accounts) | A list of objects that describe an account where Amazon Managed Prometheus is deployed. This component grants this Grafana IAM role permission to assume the Prometheus access role in that target account. Use this for cross-account access |
list(object({
component = optional(string, "managed-prometheus/workspace")
stage = string
tenant = optional(string, "")
environment = optional(string, "")
}))
| `[]` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [sso\_role\_associations](#input\_sso\_role\_associations) | A list of role to group ID list associations for granting Amazon Grafana access |
list(object({
role = string
group_ids = list(string)
}))
| `[]` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [workspace\_endpoint](#output\_workspace\_endpoint) | The returned URL of the Amazon Managed Grafana workspace | +| [workspace\_id](#output\_workspace\_id) | The ID of the Amazon Managed Grafana workspace | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/managed-grafana/workspace) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/managed-grafana/workspace/context.tf b/modules/managed-grafana/workspace/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/managed-grafana/workspace/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/managed-grafana/workspace/main.tf b/modules/managed-grafana/workspace/main.tf new file mode 100644 index 000000000..dc6ab5e8c --- /dev/null +++ b/modules/managed-grafana/workspace/main.tf @@ -0,0 +1,46 @@ +locals { + enabled = module.this.enabled + + additional_allowed_roles = compact([for prometheus in module.prometheus : prometheus.outputs.access_role_arn]) +} + +module "security_group" { + source = "cloudposse/security-group/aws" + version = "2.2.0" + + enabled = local.enabled && var.private_network_access_enabled + + allow_all_egress = true + rules = [] + vpc_id = module.vpc.outputs.vpc_id + + context = module.this.context +} + +module "managed_grafana" { + source = "cloudposse/managed-grafana/aws" + version = "0.1.0" + + enabled = local.enabled + + prometheus_policy_enabled = var.prometheus_policy_enabled + additional_allowed_roles = local.additional_allowed_roles + + vpc_configuration = var.private_network_access_enabled ? { + subnet_ids = module.vpc.outputs.private_subnet_ids + security_group_ids = [module.security_group.id] + } : {} + + context = module.this.context +} + +resource "aws_grafana_role_association" "sso" { + for_each = local.enabled ? { + for association in var.sso_role_associations : association.role => association + } : {} + + role = each.value.role + group_ids = each.value.group_ids + + workspace_id = module.managed_grafana.workspace_id +} diff --git a/modules/managed-grafana/workspace/outputs.tf b/modules/managed-grafana/workspace/outputs.tf new file mode 100644 index 000000000..4b45b5041 --- /dev/null +++ b/modules/managed-grafana/workspace/outputs.tf @@ -0,0 +1,9 @@ +output "workspace_id" { + description = "The ID of the Amazon Managed Grafana workspace" + value = module.managed_grafana.workspace_id +} + +output "workspace_endpoint" { + description = "The returned URL of the Amazon Managed Grafana workspace" + value = module.managed_grafana.workspace_endpoint +} diff --git a/modules/managed-grafana/workspace/providers.tf b/modules/managed-grafana/workspace/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/managed-grafana/workspace/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/managed-grafana/workspace/remote-state.tf b/modules/managed-grafana/workspace/remote-state.tf new file mode 100644 index 000000000..6d7e102f8 --- /dev/null +++ b/modules/managed-grafana/workspace/remote-state.tf @@ -0,0 +1,24 @@ +module "prometheus" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + for_each = local.enabled ? { + for target in var.prometheus_source_accounts : "${target.tenant}:${target.stage}:${target.environment}" => target + } : {} + + component = each.value.component + stage = each.value.stage + environment = length(each.value.environment) > 0 ? each.value.environment : module.this.environment + tenant = length(each.value.tenant) > 0 ? each.value.tenant : module.this.tenant + + context = module.this.context +} + +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "vpc" + + context = module.this.context +} diff --git a/modules/managed-grafana/workspace/variables.tf b/modules/managed-grafana/workspace/variables.tf new file mode 100644 index 000000000..6bb68e35d --- /dev/null +++ b/modules/managed-grafana/workspace/variables.tf @@ -0,0 +1,37 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "sso_role_associations" { + type = list(object({ + role = string + group_ids = list(string) + })) + description = "A list of role to group ID list associations for granting Amazon Grafana access" + default = [] +} + +variable "prometheus_policy_enabled" { + type = bool + description = "Set this to `true` to allow this Grafana workspace to access Amazon Managed Prometheus in this account" + default = false +} + +variable "prometheus_source_accounts" { + type = list(object({ + component = optional(string, "managed-prometheus/workspace") + stage = string + tenant = optional(string, "") + environment = optional(string, "") + })) + description = "A list of objects that describe an account where Amazon Managed Prometheus is deployed. This component grants this Grafana IAM role permission to assume the Prometheus access role in that target account. Use this for cross-account access" + default = [] +} + + +variable "private_network_access_enabled" { + type = bool + description = "If set to `true`, enable the VPC Configuration to allow this workspace to access the private network using outputs from the vpc component" + default = false +} diff --git a/modules/managed-grafana/workspace/versions.tf b/modules/managed-grafana/workspace/versions.tf new file mode 100644 index 000000000..f33ede77f --- /dev/null +++ b/modules/managed-grafana/workspace/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} diff --git a/modules/managed-prometheus/workspace/README.md b/modules/managed-prometheus/workspace/README.md new file mode 100644 index 000000000..9f270b9ae --- /dev/null +++ b/modules/managed-prometheus/workspace/README.md @@ -0,0 +1,105 @@ +# Component: `managed-prometheus/workspace` + +This component is responsible for provisioning a workspace for Amazon Managed Service for Prometheus, also known as +Amazon Managed Prometheus (AMP). + +This component is intended to be deployed alongside Grafana. For example, use our `managed-grafana/workspace` component. + +## Usage + +**Stack Level**: Regional + +Here's an example snippet for how to use this component. + +We prefer to name the stack component with a simplier name, whereas the Terraform component should remain descriptive. + +```yaml +components: + terraform: + prometheus: + metadata: + component: managed-prometheus/workspace + vars: + enabled: true + name: prometheus + # Create cross-account role for core-auto to access AMP + grafana_account_name: core-auto +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [managed\_prometheus](#module\_managed\_prometheus) | cloudposse/managed-prometheus/aws | 0.1.1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_environment\_name](#input\_account\_map\_environment\_name) | The name of the environment where `account_map` is provisioned | `string` | `"gbl"` | no | +| [account\_map\_stage\_name](#input\_account\_map\_stage\_name) | The name of the stage where `account_map` is provisioned | `string` | `"root"` | no | +| [account\_map\_tenant\_name](#input\_account\_map\_tenant\_name) | The name of the tenant where `account_map` is provisioned | `string` | `"core"` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alert\_manager\_definition](#input\_alert\_manager\_definition) | The alert manager definition that you want to be applied. | `string` | `""` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [grafana\_account\_name](#input\_grafana\_account\_name) | The name of the account allowed to access AMP in this account. If defined, this module will create a cross-account IAM role for accessing AMP. Use this for cross-account Grafana. If not defined, no roles will be created. | `string` | `""` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [rule\_group\_namespaces](#input\_rule\_group\_namespaces) | A list of name, data objects for each Amazon Managed Service for Prometheus (AMP) Rule Group Namespace |
list(object({
name = string
data = string
}))
| `[]` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [vpc\_endpoint\_enabled](#input\_vpc\_endpoint\_enabled) | If set to `true`, restrict traffic through a VPC endpoint | `string` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [access\_role\_arn](#output\_access\_role\_arn) | If enabled with `var.allowed_account_id`, the Role ARN used for accessing Amazon Managed Prometheus in this account | +| [id](#output\_id) | The ID of this component deployment | +| [workspace\_arn](#output\_workspace\_arn) | The ARN of this Amazon Managed Prometheus workspace | +| [workspace\_endpoint](#output\_workspace\_endpoint) | The endpoint URL of this Amazon Managed Prometheus workspace | +| [workspace\_id](#output\_workspace\_id) | The ID of this Amazon Managed Prometheus workspace | +| [workspace\_region](#output\_workspace\_region) | The region where this workspace is deployed | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/managed-prometheus/workspace) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/managed-prometheus/workspace/context.tf b/modules/managed-prometheus/workspace/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/managed-prometheus/workspace/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/managed-prometheus/workspace/main.tf b/modules/managed-prometheus/workspace/main.tf new file mode 100644 index 000000000..a8fad18ec --- /dev/null +++ b/modules/managed-prometheus/workspace/main.tf @@ -0,0 +1,23 @@ +locals { + enabled = module.this.enabled + + grafana_account_id = local.enabled && length(var.grafana_account_name) > 0 ? module.account_map.outputs.full_account_map[var.grafana_account_name] : "" + + vpc_endpoint_enabled = module.this.enabled && var.vpc_endpoint_enabled +} + +module "managed_prometheus" { + source = "cloudposse/managed-prometheus/aws" + version = "0.1.1" + + enabled = local.enabled + + alert_manager_definition = var.alert_manager_definition + allowed_account_id = local.grafana_account_id + rule_group_namespaces = var.rule_group_namespaces + scraper_deployed = true + + vpc_id = local.vpc_endpoint_enabled ? module.vpc[0].outputs.vpc_id : "" + + context = module.this.context +} diff --git a/modules/managed-prometheus/workspace/outputs.tf b/modules/managed-prometheus/workspace/outputs.tf new file mode 100644 index 000000000..750f37934 --- /dev/null +++ b/modules/managed-prometheus/workspace/outputs.tf @@ -0,0 +1,29 @@ +output "id" { + description = "The ID of this component deployment" + value = module.this.id +} + +output "workspace_id" { + description = "The ID of this Amazon Managed Prometheus workspace" + value = module.managed_prometheus.workspace_id +} + +output "workspace_arn" { + description = "The ARN of this Amazon Managed Prometheus workspace" + value = module.managed_prometheus.workspace_arn +} + +output "workspace_endpoint" { + description = "The endpoint URL of this Amazon Managed Prometheus workspace" + value = module.managed_prometheus.workspace_endpoint +} + +output "workspace_region" { + description = "The region where this workspace is deployed" + value = var.region +} + +output "access_role_arn" { + description = "If enabled with `var.allowed_account_id`, the Role ARN used for accessing Amazon Managed Prometheus in this account" + value = module.managed_prometheus.access_role_arn +} diff --git a/modules/managed-prometheus/workspace/providers.tf b/modules/managed-prometheus/workspace/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/managed-prometheus/workspace/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/managed-prometheus/workspace/remote-state.tf b/modules/managed-prometheus/workspace/remote-state.tf new file mode 100644 index 000000000..9516f64f2 --- /dev/null +++ b/modules/managed-prometheus/workspace/remote-state.tf @@ -0,0 +1,22 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "account-map" + tenant = var.account_map_tenant_name + environment = var.account_map_environment_name + stage = var.account_map_stage_name + + context = module.this.context +} + +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.vpc_endpoint_enabled ? 1 : 0 + + component = "vpc" + + context = module.this.context +} diff --git a/modules/managed-prometheus/workspace/variables.tf b/modules/managed-prometheus/workspace/variables.tf new file mode 100644 index 000000000..f6f4f850d --- /dev/null +++ b/modules/managed-prometheus/workspace/variables.tf @@ -0,0 +1,49 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "alert_manager_definition" { + type = string + description = "The alert manager definition that you want to be applied." + default = "" +} + +variable "rule_group_namespaces" { + type = list(object({ + name = string + data = string + })) + description = "A list of name, data objects for each Amazon Managed Service for Prometheus (AMP) Rule Group Namespace" + default = [] +} + +variable "grafana_account_name" { + type = string + description = "The name of the account allowed to access AMP in this account. If defined, this module will create a cross-account IAM role for accessing AMP. Use this for cross-account Grafana. If not defined, no roles will be created." + default = "" +} + +variable "account_map_tenant_name" { + type = string + description = "The name of the tenant where `account_map` is provisioned" + default = "core" +} + +variable "account_map_environment_name" { + type = string + description = "The name of the environment where `account_map` is provisioned" + default = "gbl" +} + +variable "account_map_stage_name" { + type = string + description = "The name of the stage where `account_map` is provisioned" + default = "root" +} + +variable "vpc_endpoint_enabled" { + type = string + description = "If set to `true`, restrict traffic through a VPC endpoint" + default = true +} diff --git a/modules/managed-prometheus/workspace/versions.tf b/modules/managed-prometheus/workspace/versions.tf new file mode 100644 index 000000000..f33ede77f --- /dev/null +++ b/modules/managed-prometheus/workspace/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} From 0ada946f35c8ceb743fa317c5b1d7b258ebe6c84 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Wed, 12 Jun 2024 23:06:13 +0200 Subject: [PATCH 441/501] Drop wrong workflows (#1065) --- .github/workflows/release-branch.yml | 20 -------------------- .github/workflows/release-published.yml | 16 ---------------- 2 files changed, 36 deletions(-) delete mode 100644 .github/workflows/release-branch.yml delete mode 100644 .github/workflows/release-published.yml diff --git a/.github/workflows/release-branch.yml b/.github/workflows/release-branch.yml deleted file mode 100644 index 852d5e3ea..000000000 --- a/.github/workflows/release-branch.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: release-branch -on: - push: - branches: - - main - - release/v* - paths-ignore: - - '.github/**' - - 'docs/**' - - 'examples/**' - - 'test/**' - - 'README.md' - -permissions: {} - -jobs: - terraform-module: - uses: cloudposse/github-actions-workflows-terraform-module/.github/workflows/release-branch.yml@main - secrets: inherit diff --git a/.github/workflows/release-published.yml b/.github/workflows/release-published.yml deleted file mode 100644 index 25c362459..000000000 --- a/.github/workflows/release-published.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: release-published -on: - release: - types: - - published - -permissions: - id-token: write - contents: write - pull-requests: write - -jobs: - terraform-module: - uses: cloudposse/github-actions-workflows-terraform-module/.github/workflows/release-published.yml@main - secrets: inherit From f1ce3c00822b995947a7412585186b58ad0e995d Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 17 Jun 2024 14:50:03 -0700 Subject: [PATCH 442/501] External-Secrets: Add variable for decrypting aliased KMS keys (#1068) --- .../eks/external-secrets-operator/README.md | 4 ++++ modules/eks/external-secrets-operator/main.tf | 21 ++++++++++++++++++- .../external-secrets-operator/variables.tf | 6 ++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 8f2a90600..44053c5fd 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -89,6 +89,8 @@ components: # chart_values: # installCRDs: true chart_values: {} + kms_aliases_allow_decrypt: [] + # - "alias/foo/bar" ``` @@ -126,6 +128,7 @@ components: |------|------| | [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_kms_alias.kms_aliases](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source | | [kubernetes_resources.crd](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/resources) | data source | ## Inputs @@ -150,6 +153,7 @@ components: | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [kms\_aliases\_allow\_decrypt](#input\_kms\_aliases\_allow\_decrypt) | A list of KMS aliases that the SecretStore is allowed to decrypt. | `list(string)` | `[]` | no | | [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | | [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | | [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf index a360438b0..5dd92ce35 100644 --- a/modules/eks/external-secrets-operator/main.tf +++ b/modules/eks/external-secrets-operator/main.tf @@ -62,7 +62,17 @@ module "external_secrets_operator" { "arn:aws:ssm:${var.region}:${local.account}:*" ] }], - local.overridable_additional_iam_policy_statements + local.overridable_additional_iam_policy_statements, + length(var.kms_aliases_allow_decrypt) > 0 ? [ + { + sid = "DecryptKMS" + effect = "Allow" + actions = [ + "kms:Decrypt" + ] + resources = local.kms_aliases_target_arns + } + ] : [] ) }] @@ -133,3 +143,12 @@ module "external_ssm_secrets" { module.external_secrets_operator, ] } + +data "aws_kms_alias" "kms_aliases" { + for_each = { for i, v in var.kms_aliases_allow_decrypt : v => v } + name = each.value +} + +locals { + kms_aliases_target_arns = [for k, v in data.aws_kms_alias.kms_aliases : data.aws_kms_alias.kms_aliases[k].target_key_arn] +} diff --git a/modules/eks/external-secrets-operator/variables.tf b/modules/eks/external-secrets-operator/variables.tf index d95d9ca22..48b22c69e 100644 --- a/modules/eks/external-secrets-operator/variables.tf +++ b/modules/eks/external-secrets-operator/variables.tf @@ -34,3 +34,9 @@ variable "resources" { }) description = "The cpu and memory of the deployment's limits and requests." } + +variable "kms_aliases_allow_decrypt" { + type = list(string) + description = "A list of KMS aliases that the SecretStore is allowed to decrypt." + default = [] +} From b93cf82f927da04410e26e08b6b5244c4051cf94 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 17 Jun 2024 14:54:14 -0700 Subject: [PATCH 443/501] Update eks/cluster to use eks-node-group v3 (#1069) --- modules/eks/cluster/CHANGELOG.md | 8 ++++++++ modules/eks/cluster/modules/node_group_by_az/main.tf | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 50951d128..c1205f431 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,3 +1,11 @@ +## Components PR [#1069](https://github.com/cloudposse/terraform-aws-components/pull/1069) + +Update `cloudposse/eks-node-group/aws` to v3.0.0 + +- Enable use of Amazon Linux 2023 +- Other bug fixes and improvements +- See https://github.com/cloudposse/terraform-aws-eks-node-group/releases/tag/3.0.0 + ## Release 1.455.1 Components PR [#1057](https://github.com/cloudposse/terraform-aws-components/pull/1057) diff --git a/modules/eks/cluster/modules/node_group_by_az/main.tf b/modules/eks/cluster/modules/node_group_by_az/main.tf index 5bc042d8c..b5e7eb62b 100644 --- a/modules/eks/cluster/modules/node_group_by_az/main.tf +++ b/modules/eks/cluster/modules/node_group_by_az/main.tf @@ -38,7 +38,7 @@ locals { module "eks_node_group" { source = "cloudposse/eks-node-group/aws" - version = "2.12.0" + version = "3.0.0" enabled = local.enabled From 68924b6efcdffef882c48552a68c8a9ef4eef9d8 Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 19 Jun 2024 20:54:32 -0700 Subject: [PATCH 444/501] [alb-controller] Make default_ingress_ip_address_type default to `ipv4` (#1070) --- modules/eks/alb-controller/CHANGELOG.md | 17 ++++++++++++++++- modules/eks/alb-controller/README.md | 2 +- modules/eks/alb-controller/variables.tf | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/eks/alb-controller/CHANGELOG.md b/modules/eks/alb-controller/CHANGELOG.md index 56d5ba294..22d64cd5a 100644 --- a/modules/eks/alb-controller/CHANGELOG.md +++ b/modules/eks/alb-controller/CHANGELOG.md @@ -1,4 +1,19 @@ -## PR [#821](https://github.com/cloudposse/terraform-aws-components/pull/821) +## PR 1070 + +PR [#1070](https://github.com/cloudposse/terraform-aws-components/pull/1070) + +Change default for `default_ingress_ip_address_type` from `dualstack` to `ipv4`. When `dualstack` is configured, the +Ingress will fail if the VPC does not have an IPv6 CIDR block, which is still a common case. When `ipv4` is configured, +the Ingress will work with only an IPv4 CIDR block, and simply will not use IPv6 if it exists. This makes `ipv4` the +more conservative default. + +## Release 1.432.0 + +Better support for Kubeconfig authentication + +## Release 1.289.1 + +PR [#821](https://github.com/cloudposse/terraform-aws-components/pull/821) ### Update IAM Policy and Change How it is Managed diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 33c2d4d2f..6887162d5 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -109,7 +109,7 @@ components: | [default\_ingress\_class\_name](#input\_default\_ingress\_class\_name) | Class name for default ingress | `string` | `"default"` | no | | [default\_ingress\_enabled](#input\_default\_ingress\_enabled) | Set `true` to deploy a default IngressClass. There should only be one default per cluster. | `bool` | `true` | no | | [default\_ingress\_group](#input\_default\_ingress\_group) | Group name for default ingress | `string` | `"common"` | no | -| [default\_ingress\_ip\_address\_type](#input\_default\_ingress\_ip\_address\_type) | IP address type for default ingress, one of `ipv4` or `dualstack`. | `string` | `"dualstack"` | no | +| [default\_ingress\_ip\_address\_type](#input\_default\_ingress\_ip\_address\_type) | IP address type for default ingress, one of `ipv4` or `dualstack`. | `string` | `"ipv4"` | no | | [default\_ingress\_load\_balancer\_attributes](#input\_default\_ingress\_load\_balancer\_attributes) | A list of load balancer attributes to apply to the default ingress load balancer.
See [Load Balancer Attributes](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#load-balancer-attributes). | `list(object({ key = string, value = string }))` | `[]` | no | | [default\_ingress\_scheme](#input\_default\_ingress\_scheme) | Scheme for default ingress, one of `internet-facing` or `internal`. | `string` | `"internet-facing"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | diff --git a/modules/eks/alb-controller/variables.tf b/modules/eks/alb-controller/variables.tf index 0c14e0a45..840969ea7 100644 --- a/modules/eks/alb-controller/variables.tf +++ b/modules/eks/alb-controller/variables.tf @@ -120,7 +120,7 @@ variable "default_ingress_scheme" { variable "default_ingress_ip_address_type" { type = string description = "IP address type for default ingress, one of `ipv4` or `dualstack`." - default = "dualstack" + default = "ipv4" validation { condition = contains(["ipv4", "dualstack"], var.default_ingress_ip_address_type) From 421116075c2ff810c0c2ed8840784a29db03f96a Mon Sep 17 00:00:00 2001 From: Nuru Date: Wed, 19 Jun 2024 22:19:37 -0700 Subject: [PATCH 445/501] [eks/cluster] Update eks-node-group to v3.0.1 (#1071) --- modules/eks/alb-controller/CHANGELOG.md | 2 +- modules/eks/cluster/CHANGELOG.md | 20 ++++++++++++++----- .../cluster/modules/node_group_by_az/main.tf | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/modules/eks/alb-controller/CHANGELOG.md b/modules/eks/alb-controller/CHANGELOG.md index 22d64cd5a..638dd2e73 100644 --- a/modules/eks/alb-controller/CHANGELOG.md +++ b/modules/eks/alb-controller/CHANGELOG.md @@ -1,4 +1,4 @@ -## PR 1070 +## Release 1.466.0 PR [#1070](https://github.com/cloudposse/terraform-aws-components/pull/1070) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index c1205f431..6ba400649 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,4 +1,14 @@ -## Components PR [#1069](https://github.com/cloudposse/terraform-aws-components/pull/1069) +## Release 1.466.1 + +PR [#1071](https://github.com/cloudposse/terraform-aws-components/pull/1071) + +Bugfix: Update `cloudposse/eks-node-group/aws` to v3.0.1. + +- Fixes failure to create userdata for AL2 and Windows when using it to run `bootstrap.sh`. + +## Release 1.465.0 + +Components PR [#1069](https://github.com/cloudposse/terraform-aws-components/pull/1069) Update `cloudposse/eks-node-group/aws` to v3.0.0 @@ -117,18 +127,18 @@ rakkess See this error: ```plaintext -To work with module.eks_cluster.kubernetes_config_map.aws_auth_ignore_changes[0] (orphan) its original provider configuration +To work with module.eks_cluster.kubernetes_config_map.aws_auth[0] (orphan) its original provider configuration ``` Note, in other documentation, the exact "address" of the orphaned resource may be different, and the documentation may say to refer to the address of the resource in the error message. In this case, because we are using this component as -the root module, the address should be exactly as shown above. (Possibly ending with `aws_auth[0]` instead of -`aws_auth_ignore_changes[0]`.) +the root module, the address should be exactly as shown above. (Possibly ending with `aws_auth_ignore_changes[0]` +instead of `aws_auth[0]`.) 3. Remove the orphaned resource from the state file with ``` -atmos terraform state rm eks/cluster 'module.eks_cluster.kubernetes_config_map.aws_auth_ignore_changes[0]' -s +atmos terraform state rm eks/cluster 'module.eks_cluster.kubernetes_config_map.aws_auth[0]' -s ``` 4. `atmos terraform plan eks/cluster -s ` diff --git a/modules/eks/cluster/modules/node_group_by_az/main.tf b/modules/eks/cluster/modules/node_group_by_az/main.tf index b5e7eb62b..8172f0a05 100644 --- a/modules/eks/cluster/modules/node_group_by_az/main.tf +++ b/modules/eks/cluster/modules/node_group_by_az/main.tf @@ -38,7 +38,7 @@ locals { module "eks_node_group" { source = "cloudposse/eks-node-group/aws" - version = "3.0.0" + version = "3.0.1" enabled = local.enabled From 23f29ccd8727cc1fbe8f19ab6b3b71dd37316a00 Mon Sep 17 00:00:00 2001 From: Nuru Date: Thu, 20 Jun 2024 08:09:27 -0700 Subject: [PATCH 446/501] [eks/cluster] Fix AWS SSO support (#1072) --- modules/eks/cluster/CHANGELOG.md | 15 +++++++++++++-- modules/eks/cluster/aws-sso.tf | 11 ++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index 6ba400649..bef5b7e2f 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -1,4 +1,13 @@ -## Release 1.466.1 +## Release 1.468.0 + +PR [#1072](https://github.com/cloudposse/terraform-aws-components/pull/1072) + +Bugfix: + +- Correctly map AWS SSO Permission Sets referenced by `aws_sso_permission_sets_rbac` to IAM Role ARNs. +- Broken in Release 1.431.1: Update to use AWS Auth API + +## Release 1.467.0 PR [#1071](https://github.com/cloudposse/terraform-aws-components/pull/1071) @@ -34,7 +43,9 @@ script. This support should be considered an `alpha` version, as it may change when support for Amazon Linux 2023 is added, and does not work with Bottlerocket. -## Breaking Changes: Components PR [#1033](https://github.com/cloudposse/terraform-aws-components/pull/1033) +## Release 1.431.1: Breaking Changes + +Components PR [#1033](https://github.com/cloudposse/terraform-aws-components/pull/1033) ### Major Breaking Changes diff --git a/modules/eks/cluster/aws-sso.tf b/modules/eks/cluster/aws-sso.tf index 48a398b0f..5e2eaf36f 100644 --- a/modules/eks/cluster/aws-sso.tf +++ b/modules/eks/cluster/aws-sso.tf @@ -4,7 +4,7 @@ locals { aws_sso_access_entry_map = { - for role in var.aws_sso_permission_sets_rbac : data.aws_iam_roles.sso_roles[role.aws_sso_permission_set] => { + for role in var.aws_sso_permission_sets_rbac : tolist(data.aws_iam_roles.sso_roles[role.aws_sso_permission_set].arns)[0] => { kubernetes_groups = role.groups } } @@ -14,4 +14,13 @@ data "aws_iam_roles" "sso_roles" { for_each = toset(var.aws_sso_permission_sets_rbac[*].aws_sso_permission_set) name_regex = format("AWSReservedSSO_%s_.*", each.value) path_prefix = "/aws-reserved/sso.amazonaws.com/" + + lifecycle { + postcondition { + condition = length(self.arns) == 1 + error_message = length(self.arns) == 0 ? "Could not find Role ARN for the AWS SSO permission set: ${each.value}" : ( + "Found more than one (${length(self.arns)}) Role ARN for the AWS SSO permission set: ${each.value}" + ) + } + } } From 2c7ac726f134a209c8000fdb81d08a9d9106b2de Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 1 Jul 2024 09:40:50 -0700 Subject: [PATCH 447/501] fix(`eks/loki`): Remove Unnecessary `compact` (#1073) --- modules/eks/loki/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/eks/loki/main.tf b/modules/eks/loki/main.tf index 41c271214..e7d78f2d9 100644 --- a/modules/eks/loki/main.tf +++ b/modules/eks/loki/main.tf @@ -123,7 +123,7 @@ module "loki" { # For new installations, schema config doesnt change. See the following: # https://grafana.com/docs/loki/latest/operations/storage/schema/#new-loki-installs schemaConfig = { - configs = compact(concat(var.default_schema_config, var.additional_schema_config)) + configs = concat(var.default_schema_config, var.additional_schema_config) } storage = { bucketNames = { From 25e9a3d3e3f1588c70c45c43163f523036828a44 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 8 Jul 2024 09:06:27 -0700 Subject: [PATCH 448/501] [eks/karpenter] Add support for `kubelet` config, fix IAM support for `v1alpha` cleanup (#1076) --- modules/eks/karpenter-node-pool/CHANGELOG.md | 15 +++++++++ modules/eks/karpenter-node-pool/README.md | 2 +- modules/eks/karpenter-node-pool/main.tf | 12 +++++-- modules/eks/karpenter-node-pool/variables.tf | 10 ++++-- modules/eks/karpenter/CHANGELOG.md | 32 ++++++++++++++++++- modules/eks/karpenter/README.md | 2 ++ .../karpenter/controller-policy-v1alpha.tf | 30 ++++++++++++++--- modules/eks/karpenter/main.tf | 2 +- 8 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 modules/eks/karpenter-node-pool/CHANGELOG.md diff --git a/modules/eks/karpenter-node-pool/CHANGELOG.md b/modules/eks/karpenter-node-pool/CHANGELOG.md new file mode 100644 index 000000000..2a110392c --- /dev/null +++ b/modules/eks/karpenter-node-pool/CHANGELOG.md @@ -0,0 +1,15 @@ +## Components [PR #1076](https://github.com/cloudposse/terraform-aws-components/pull/1076) + +- Allow specifying elements of `spec.template.spec.kubelet` +- Make taint values optional + +The `var.node_pools` map now includes a `kubelet` field that allows specifying elements of `spec.template.spec.kubelet`. +This is useful for configuring the kubelet to use custom settings, such as reserving resources for system daemons. + +For more information, see: + +- [Karpenter documentation](https://karpenter.sh/docs/concepts/nodepools/#spectemplatespeckubelet) +- [Kubernetes documentation](https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/) + +The `value` fields of the `taints` and `startup_taints` lists in the `var.node_pools` map are now optional. This is in +alignment with the Kubernetes API, where `key` and `effect` are required, but the `value` field is optional. diff --git a/modules/eks/karpenter-node-pool/README.md b/modules/eks/karpenter-node-pool/README.md index 1d2c16355..449fb589d 100644 --- a/modules/eks/karpenter-node-pool/README.md +++ b/modules/eks/karpenter-node-pool/README.md @@ -203,7 +203,7 @@ components: | [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s, m, or h).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, s, m, or h).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
labels = optional(map(string))
annotations = optional(map(string))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = string
})))
startup_taints = optional(list(object({
key = string
effect = string
value = string
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
}))
| n/a | yes | +| [node\_pools](#input\_node\_pools) | Configuration for node pools. See code for details. |
map(object({
# The name of the Karpenter provisioner. The map key is used if this is not set.
name = optional(string)
# Whether to place EC2 instances launched by Karpenter into VPC private subnets. Set it to `false` to use public subnets.
private_subnets_enabled = bool
# The Disruption spec controls how Karpenter scales down the node group.
# See the example (sadly not the specific `spec.disruption` documentation) at https://karpenter.sh/docs/concepts/nodepools/ for details
disruption = optional(object({
# Describes which types of Nodes Karpenter should consider for consolidation.
# If using 'WhenUnderutilized', Karpenter will consider all nodes for consolidation and attempt to remove or
# replace Nodes when it discovers that the Node is underutilized and could be changed to reduce cost.
# If using `WhenEmpty`, Karpenter will only consider nodes for consolidation that contain no workload pods.
consolidation_policy = optional(string, "WhenUnderutilized")

# The amount of time Karpenter should wait after discovering a consolidation decision (`go` duration string, s, m, or h).
# This value can currently (v0.36.0) only be set when the consolidationPolicy is 'WhenEmpty'.
# You can choose to disable consolidation entirely by setting the string value 'Never' here.
# Earlier versions of Karpenter called this field `ttl_seconds_after_empty`.
consolidate_after = optional(string)

# The amount of time a Node can live on the cluster before being removed (`go` duration string, s, m, or h).
# You can choose to disable expiration entirely by setting the string value 'Never' here.
# This module sets a default of 336 hours (14 days), while the Karpenter default is 720 hours (30 days).
# Note that Karpenter calls this field "expiresAfter", and earlier versions called it `ttl_seconds_until_expired`,
# but we call it "max_instance_lifetime" to match the corresponding field in EC2 Auto Scaling Groups.
max_instance_lifetime = optional(string, "336h")

# Budgets control the the maximum number of NodeClaims owned by this NodePool that can be terminating at once.
# See https://karpenter.sh/docs/concepts/disruption/#disruption-budgets for details.
# A percentage is the percentage of the total number of active, ready nodes not being deleted, rounded up.
# If there are multiple active budgets, Karpenter uses the most restrictive value.
# If left undefined, this will default to one budget with a value of nodes: 10%.
# Note that budgets do not prevent or limit involuntary terminations.
# Example:
# On Weekdays during business hours, don't do any deprovisioning.
# budgets = {
# schedule = "0 9 * * mon-fri"
# duration = 8h
# nodes = "0"
# }
budgets = optional(list(object({
# The schedule specifies when a budget begins being active, using extended cronjob syntax.
# See https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#schedule-syntax for syntax details.
# Timezones are not supported. This field is required if Duration is set.
schedule = optional(string)
# Duration determines how long a Budget is active after each Scheduled start.
# If omitted, the budget is always active. This is required if Schedule is set.
# Must be a whole number of minutes and hours, as cron does not work in seconds,
# but since Go's `duration.String()` always adds a "0s" at the end, that is allowed.
duration = optional(string)
# The percentage or number of nodes that Karpenter can scale down during the budget.
nodes = string
})), [])
}), {})
# Karpenter provisioner total CPU limit for all pods running on the EC2 instances launched by Karpenter
total_cpu_limit = string
# Karpenter provisioner total memory limit for all pods running on the EC2 instances launched by Karpenter
total_memory_limit = string
# Set a weight for this node pool.
# See https://karpenter.sh/docs/concepts/scheduling/#weighted-nodepools
weight = optional(number, 50)
labels = optional(map(string))
annotations = optional(map(string))
# Karpenter provisioner taints configuration. See https://aws.github.io/aws-eks-best-practices/karpenter/#create-provisioners-that-are-mutually-exclusive for more details
taints = optional(list(object({
key = string
effect = string
value = optional(string)
})))
startup_taints = optional(list(object({
key = string
effect = string
value = optional(string)
})))
# Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details
metadata_options = optional(object({
httpEndpoint = optional(string, "enabled")
httpProtocolIPv6 = optional(string, "disabled")
httpPutResponseHopLimit = optional(number, 2)
# httpTokens can be either "required" or "optional"
httpTokens = optional(string, "required")
}), {})
# The AMI used by Karpenter provisioner when provisioning nodes. Based on the value set for amiFamily, Karpenter will automatically query for the appropriate EKS optimized AMI via AWS Systems Manager (SSM)
ami_family = string
# Karpenter nodes block device mappings. Controls the Elastic Block Storage volumes that Karpenter attaches to provisioned nodes.
# Karpenter uses default block device mappings for the AMI Family specified.
# For example, the Bottlerocket AMI Family defaults with two block device mappings,
# and normally you only want to scale `/dev/xvdb` where Containers and there storage are stored.
# Most other AMIs only have one device mapping at `/dev/xvda`.
# See https://karpenter.sh/docs/concepts/nodeclasses/#specblockdevicemappings for more details
block_device_mappings = list(object({
deviceName = string
ebs = optional(object({
volumeSize = string
volumeType = string
deleteOnTermination = optional(bool, true)
encrypted = optional(bool, true)
iops = optional(number)
kmsKeyID = optional(string, "alias/aws/ebs")
snapshotID = optional(string)
throughput = optional(number)
}))
}))
# Set acceptable (In) and unacceptable (Out) Kubernetes and Karpenter values for node provisioning based on Well-Known Labels and cloud-specific settings. These can include instance types, zones, computer architecture, and capacity type (such as AWS spot or on-demand). See https://karpenter.sh/v0.18.0/provisioner/#specrequirements for more details
requirements = list(object({
key = string
operator = string
# Operators like "Exists" and "DoesNotExist" do not require a value
values = optional(list(string))
}))
# Any values for spec.template.spec.kubelet allowed by Karpenter.
# Not fully specified, because they are subject to change.
# See:
# https://karpenter.sh/docs/concepts/nodepools/#spectemplatespeckubelet
# https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/
kubelet = optional(any, {})
}))
| n/a | yes | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/eks/karpenter-node-pool/main.tf b/modules/eks/karpenter-node-pool/main.tf index 67c5b57b9..d43d8d2ac 100644 --- a/modules/eks/karpenter-node-pool/main.tf +++ b/modules/eks/karpenter-node-pool/main.tf @@ -8,6 +8,11 @@ locals { public_subnet_ids = module.vpc.outputs.public_subnet_ids node_pools = { for k, v in var.node_pools : k => v if local.enabled } + kubelets_specs_filtered = { for k, v in local.node_pools : k => { + for kk, vv in v.kubelet : kk => vv if vv != null + } + } + kubelet_specs = { for k, v in local.kubelets_specs_filtered : k => v if length(v) > 0 } } # https://karpenter.sh/docs/concepts/nodepools/ @@ -40,8 +45,8 @@ resource "kubernetes_manifest" "node_pool" { ) template = { metadata = { - labels = each.value.labels - annotations = each.value.annotations + labels = coalesce(each.value.labels, {}) + annotations = coalesce(each.value.annotations, {}) } spec = merge({ nodeClassRef = { @@ -64,6 +69,9 @@ resource "kubernetes_manifest" "node_pool" { }, try(length(each.value.startup_taints), 0) == 0 ? {} : { startupTaints = each.value.startup_taints + }, + try(local.kubelet_specs[each.key], null) == null ? {} : { + kubelet = local.kubelet_specs[each.key] } ) } diff --git a/modules/eks/karpenter-node-pool/variables.tf b/modules/eks/karpenter-node-pool/variables.tf index ae319b02e..522e79e77 100644 --- a/modules/eks/karpenter-node-pool/variables.tf +++ b/modules/eks/karpenter-node-pool/variables.tf @@ -77,12 +77,12 @@ variable "node_pools" { taints = optional(list(object({ key = string effect = string - value = string + value = optional(string) }))) startup_taints = optional(list(object({ key = string effect = string - value = string + value = optional(string) }))) # Karpenter node metadata options. See https://karpenter.sh/docs/concepts/nodeclasses/#specmetadataoptions for more details metadata_options = optional(object({ @@ -120,6 +120,12 @@ variable "node_pools" { # Operators like "Exists" and "DoesNotExist" do not require a value values = optional(list(string)) })) + # Any values for spec.template.spec.kubelet allowed by Karpenter. + # Not fully specified, because they are subject to change. + # See: + # https://karpenter.sh/docs/concepts/nodepools/#spectemplatespeckubelet + # https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/ + kubelet = optional(any, {}) })) description = "Configuration for node pools. See code for details." nullable = false diff --git a/modules/eks/karpenter/CHANGELOG.md b/modules/eks/karpenter/CHANGELOG.md index 6304d8034..6d7a8f2d2 100644 --- a/modules/eks/karpenter/CHANGELOG.md +++ b/modules/eks/karpenter/CHANGELOG.md @@ -1,6 +1,36 @@ +## Components [PR #1076](https://github.com/cloudposse/terraform-aws-components/pull/1076) + +#### Bugfix + +- Fixed issues with IAM Policy support for cleaning up `v1alpha` resources. + +With the previous release of this component, we encouraged users to delete their `v1alpha` Karpenter resources before +upgrading to `v1beta`. However, certain things, such as EC2 Instance Profiles, would not be deleted by Terraform because +they were created or modified by the Karpenter controller. + +To enable the `v1beta` Karpenter controller to clean up these resources, we added a second IAM Policy to the official +Karpenter IAM Policy document. This second policy allows the Karpenter controller to delete the `v1alpha` resources. +However, there were 2 problems with that. + +First, the policy was subtly incorrect, and did not, in fact, allow the Karpenter controller to delete all the +resources. This has been fixed. + +Second, a long EKS cluster name could cause the Karpenter IRSA's policy to exceed the maximum character limit for an IAM +Policy. This has also been fixed by making the `v1alpha` policy a separate managed policy attached to the Karpenter +controller's role, rather than merging the statements into the `v1beta` policy. This change also avoids potential +conflicts with policy SIDs. + +:::note Innocuous Changes + +Terraform will show IAM Policy changes, including deletion of statements from the existing policy and creation of a new +policy. This is expected and innocuous. The IAM Policy has been split into 2 to avoid exceeding length limits, but the +current (`v1beta`) policy remains the same and the now separate (`v1alpha`) policy has been corrected. + +::: + ## Version 1.445.0 -Components PR #1039 +Components [PR #1039](https://github.com/cloudposse/terraform-aws-components/pull/1039) :::warning Major Breaking Changes diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index e0afff396..5732ce94c 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -362,6 +362,8 @@ For more details on the CRDs, see: |------|------| | [aws_cloudwatch_event_rule.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | | [aws_cloudwatch_event_target.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_iam_policy.v1alpha](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role_policy_attachment.v1alpha](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_sqs_queue.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue) | resource | | [aws_sqs_queue_policy.interruption_handler](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | diff --git a/modules/eks/karpenter/controller-policy-v1alpha.tf b/modules/eks/karpenter/controller-policy-v1alpha.tf index 0c4d010d8..d2c5f6b29 100644 --- a/modules/eks/karpenter/controller-policy-v1alpha.tf +++ b/modules/eks/karpenter/controller-policy-v1alpha.tf @@ -13,8 +13,10 @@ # v1alpha API tag "karpenter.sh/provisioner-name" and to manage the EC2 Instance Profile # created by the EKS cluster component. # -# WARNING: it is important that the SID values do not conflict with the SID values in the -# controller-policy.tf file, otherwise they will be overwritten. +# We create a separate policy and attach it separately to the Karpenter controller role +# because the main policy is near the 6,144 character limit for an IAM policy, and +# adding this to it can push it over. See: +# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html#reference_iam-quotas-entities # locals { @@ -35,10 +37,10 @@ locals { ], "Condition": { "StringEquals": { - "aws:ResourceTag/kubernetes.io/cluster/${local.eks_cluster_id}": "owned" + "ec2:ResourceTag/karpenter.k8s.aws/cluster": "${local.eks_cluster_id}" }, "StringLike": { - "aws:ResourceTag/karpenter.sh/provisioner-name": "*" + "ec2:ResourceTag/karpenter.sh/provisioner-name": "*" } } }, @@ -65,3 +67,23 @@ locals { } EndOfPolicy } + +# We create a separate policy and attach it separately to the Karpenter controller role +# because the main policy is near the 6,144 character limit for an IAM policy, and +# adding this to it can push it over. See: +# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html#reference_iam-quotas-entities +resource "aws_iam_policy" "v1alpha" { + count = local.enabled ? 1 : 0 + + name = "${module.this.id}-v1alpha" + description = "Legacy Karpenter controller policy for v1alpha workloads" + policy = local.controller_policy_v1alpha_json + tags = module.this.tags +} + +resource "aws_iam_role_policy_attachment" "v1alpha" { + count = local.enabled ? 1 : 0 + + role = module.karpenter.service_account_role_name + policy_arn = one(aws_iam_policy.v1alpha[*].arn) +} diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf index a038b645b..3d930b117 100644 --- a/modules/eks/karpenter/main.tf +++ b/modules/eks/karpenter/main.tf @@ -77,7 +77,7 @@ module "karpenter" { service_account_role_arn_annotation_enabled = true iam_role_enabled = true - iam_source_policy_documents = [local.controller_policy_v1alpha_json, local.controller_policy_json] + iam_source_policy_documents = [local.controller_policy_json] values = compact([ yamlencode({ From 96429477c1e48dca6cda3530dd53a500694cafd2 Mon Sep 17 00:00:00 2001 From: Nuru Date: Mon, 8 Jul 2024 09:06:46 -0700 Subject: [PATCH 449/501] [eks/actions-runner-controller] Multiple bug fixes and enhancements (#1075) --- .../actions-runner-controller/CHANGELOG.md | 126 +++++++++ .../eks/actions-runner-controller/README.md | 75 +++--- .../charts/actions-runner/Chart.yaml | 2 +- .../templates/horizontalrunnerautoscaler.yaml | 25 +- .../templates/runnerdeployment.yaml | 246 +++++++++++------- .../charts/actions-runner/values.yaml | 2 +- modules/eks/actions-runner-controller/main.tf | 8 +- .../eks/actions-runner-controller/outputs.tf | 23 ++ .../resources/values.yaml | 9 +- .../actions-runner-controller/variables.tf | 40 ++- 10 files changed, 410 insertions(+), 146 deletions(-) create mode 100644 modules/eks/actions-runner-controller/CHANGELOG.md diff --git a/modules/eks/actions-runner-controller/CHANGELOG.md b/modules/eks/actions-runner-controller/CHANGELOG.md new file mode 100644 index 000000000..d0ca5cd41 --- /dev/null +++ b/modules/eks/actions-runner-controller/CHANGELOG.md @@ -0,0 +1,126 @@ +## PR [#1075](https://github.com/cloudposse/terraform-aws-components/pull/1075) + +New Features: + +- Add support for + [scheduled overrides](https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides) + of Runner Autoscaler min and max replicas. +- Add option `tmpfs_enabled` to have runners use RAM-backed ephemeral storage (`tmpfs`, `emptyDir.medium: Memory`) + instead of disk-backed storage. +- Add `wait_for_docker_seconds` to allow configuration of the time to wait for the Docker daemon to be ready before + starting the runner. +- Add the ability to have the runner Pods add annotations to themselves once they start running a job. (Actually + released in release 1.454.0, but not documented until now.) + +Changes: + +- Previously, `syncPeriod`, which sets the period in which the controller reconciles the desired runners count, was set + to 120 seconds in `resources/values.yaml`. This setting has been removed, reverting to the default value of 1 minute. + You can still set this value by setting the `syncPeriod` value in the `values.yaml` file or by setting `syncPeriod` in + `var.chart_values`. +- Previously, `RUNNER_GRACEFUL_STOP_TIMEOUT` was hardcoded to 90 seconds. That has been reduced to 80 seconds to expand + the buffer between that and forceful termination from 10 seconds to 20 seconds, increasing the chances the runner will + successfully deregister itself. +- The inaccurately named `webhook_startup_timeout` has been replaced with `max_duration`. `webhook_startup_timeout` is + still supported for backward compatibility, but is deprecated. + +Bugfixes: + +- Create and deploy the webhook secret when an existing secret is not supplied +- Restore proper order of operations in creating resources (broken in release 1.454.0 (PR #1055)) +- If `docker_storage` is set and `dockerdWithinRunnerContainer` is `true` (which is hardcoded to be the case), properly + mount the docker storage volume into the runner container rather than the (non-existent) docker sidecar container. + +### Discussion + +#### Scheduled overrides + +Scheduled overrides allow you to set different min and max replica values for the runner autoscaler at different times. +This can be useful if you have predictable patterns of load on your runners. For example, you might want to scale down +to zero at night and scale up during the day. This feature is implemented by adding a `scheduled_overrides` field to the +`var.runners` map. + +See the +[Actions Runner Controller documentation](https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides) +for details on how they work and how to set them up. + +#### Use RAM instead of Disk via `tmpfs_enabled` + +The standard `gp3` EBS volume used for EC2 instance's disk storage is limited (unless you pay extra) to 3000 IOPS and +125 MB/s throughput. This is fine for average workloads, but it does not scale with instance size. A `.48xlarge` +instance could host 90 Pods, but all 90 would still be sharing the same single 3000 IOPS and 125 MB/s throughput EBS +volume attached to the host. This can lead to severe performance issues, as the whole Node gets locked up waiting for +disk I/O. + +To mitigate this issue, we have added the `tmpfs_enabled` option to the `runners` map. When set to `true`, the runner +Pods will use RAM-backed ephemeral storage (`tmpfs`, `emptyDir.medium: Memory`) instead of disk-backed storage. This +means the Pod's impact on the Node's disk I/O is limited to the overhead required to launch and manage the Pod (e.g. +downloading the container image and writing logs to the disk). This can be a significant performance improvement, +allowing you to run more Pods on a single Node without running into disk I/O bottlenecks. Without this feature enabled, +you may be limited to running something like 14 Runners on an instance, regardless of instance size, due to disk I/O +limits. With this feature enabled, you may be able to run 50-100 Runners on a single instance. + +The trade-off is that the Pod's data is stored in RAM, which increases its memory usage. Be sure to increase the amount +of memory allocated to the runner Pod to account for this. This is generally not a problem, as Runners typically use a +small enough amount of disk space that it can be reasonably stored in the RAM allocated to a single CPU in an EC2 +instance, so it is the CPU that remains the limiting factor in how many Runners can be run on an instance. + +:::warning You must configure a memory request for the runner Pod + +When using `tmpfs_enabled`, you must configure a memory request for the runner Pod. If you do not, a single Pod would be +allowed to consume half the Node's memory just for its disk storage. + +::: + +#### Configure startup timeout via `wait_for_docker_seconds` + +When the runner starts and Docker-in-Docker is enabled, the runner waits for the Docker daemon to be ready before +registering marking itself ready to run jobs. This is done by polling the Docker daemon every second until it is ready. +The default timeout for this is 120 seconds. If the Docker daemon is not ready within that time, the runner will exit +with an error. You can configure this timeout by setting `wait_for_docker_seconds` in the `runners` map. + +As a general rule, the Docker daemon should be ready within a few seconds of the runner starting. However, particularly +when there are disk I/O issues (see the `tmpfs_enabled` feature above), the Docker daemon may take longer to respond. + +#### Add annotations to runner Pods once they start running a job + +You can now configure the runner Pods to add annotations to themselves once they start running a job. The idea is to +allow you to have idle pods allow themselves to be interrupted, but then mark themselves as uninterruptible once they +start running a job. This is done by setting the `running_pod_annotations` field in the `runners` map. For example: + +```yaml +running_pod_annotations: + # Prevent Karpenter from evicting or disrupting the worker pods while they are running jobs + # As of 0.37.0, is not 100% effective due to race conditions. + "karpenter.sh/do-not-disrupt": "true" +``` + +As noted in the comments above, this was intended to prevent Karpenter from evicting or disrupting the worker pods while +they are running jobs, while leaving Karpenter free to interrupt idle Runners. However, as of Karpenter 0.37.0, this is +not 100% effective due to race conditions: Karpenter may decide to terminate the Node the Pod is running on but not +signal the Pod before it accepts a job and starts running it. Without the availability of transactions or atomic +operations, this is a difficult problem to solve, and will probably require a more complex solution than just adding +annotations to the Pods. Nevertheless, this feature remains available for use in other contexts, as well as in the hope +that it will eventually work with Karpenter. + +#### Bugfix: Deploy webhook secret when existing secret is not supplied + +Because deploying secrets with Terraform causes the secrets to be stored unencrypted in the Terraform state file, we +give users the option of creating the configuration secret externally (e.g. via +[SOPS](https://github.com/getsops/sops)). Unfortunately, at some distant time in the past, when we enabled this option, +we broke this component insofar as the webhook secret was no longer being deployed when the user did not supply an +existing secret. This PR fixes that. + +The consequence of this bug was that, since the webhook secret was not being deployed, the webhook did not reject +unauthorized requests. This could have allowed an attacker to trigger the webhook and perform a DOS attack by killing +jobs as soon as they were accepted from the queue. A more practical and unintentional consequence was if a repo webhook +was installed alongside an org webhook, it would not keep guard against the webhook receiving the same payload twice if +one of the webhooks was missing the secret or had the wrong secret. + +#### Bugfix: Restore proper order of operations in creating resources + +In release 1.454.0 (PR [#1055](https://github.com/cloudposse/terraform-aws-components/pull/1055)), we reorganized the +RunnerDeployment template in the Helm chart to put the RunnerDeployment resource first, since it is the most important +resource, merely to improve readability. Unfortunately, the order of operations in creating resources is important, and +this change broke the deployment by deploying the RunnerDeployment before creating the resources it depends on. This PR +restores the proper order of operations. diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index b9adc5f1a..9d7886ef7 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -73,19 +73,30 @@ components: kubernetes.io/os: "linux" kubernetes.io/arch: "amd64" type: "repository" # can be either 'organization' or 'repository' - dind_enabled: false # If `true`, a Docker sidecar container will be deployed + dind_enabled: true # If `true`, a Docker daemon will be started in the runner Pod. # To run Docker in Docker (dind), change image to summerwind/actions-runner-dind # If not running Docker, change image to summerwind/actions-runner use a smaller image image: summerwind/actions-runner-dind # `scope` is org name for Organization runners, repo name for Repository runners scope: "org/infra" - # Tell Karpenter not to evict this pod while it is running a job. - # If we do not set this, Karpenter will feel free to terminate the runner while it is running a job, - # as part of its consolidation efforts, even when using "on demand" instances. - running_pod_annotations: - karpenter.sh/do-not-disrupt: "true" - min_replicas: 1 + min_replicas: 0 # Default, overridden by scheduled_overrides below max_replicas: 20 + # Scheduled overrides. See https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides + # Order is important. The earlier entry is prioritized higher than later entries. So you usually define + # one-time overrides at the top of your list, then yearly, monthly, weekly, and lastly daily overrides. + scheduled_overrides: + # Override the daily override on the weekends + - start_time: "2024-07-06T00:00:00-08:00" # Start of Saturday morning Pacific Standard Time + end_time: "2024-07-07T23:59:59-07:00" # End of Sunday night Pacific Daylight Time + min_replicas: 0 + recurrence_rule: + frequency: "Weekly" + # Keep a warm pool of runners during normal working hours + - start_time: "2024-07-01T09:00:00-08:00" # 9am Pacific Standard Time (8am PDT), start of workday + end_time: "2024-07-01T17:00:00-07:00" # 5pm Pacific Daylight Time (6pm PST), end of workday + min_replicas: 2 + recurrence_rule: + frequency: "Daily" scale_down_delay_seconds: 100 resources: limits: @@ -95,13 +106,12 @@ components: cpu: 100m memory: 128Mi webhook_driven_scaling_enabled: true - # The name `webhook_startup_timeout` is misleading. - # It is actually the duration after which a job will be considered completed, + # max_duration is the duration after which a job will be considered completed, # (and the runner killed) even if the webhook has not received a "job completed" event. # This is to ensure that if an event is missed, it does not leave the runner running forever. # Set it long enough to cover the longest job you expect to run and then some. # See https://github.com/actions/actions-runner-controller/blob/9afd93065fa8b1f87296f0dcdf0c2753a0548cb7/docs/automatically-scaling-runners.md?plain=1#L264-L268 - webhook_startup_timeout: "90m" + max_duration: "90m" # Pull-driven scaling is obsolete and should not be used. pull_driven_scaling_enabled: false # Labels are not case-sensitive to GitHub, but *are* case-sensitive @@ -156,7 +166,7 @@ components: # cpu: 100m # memory: 128Mi # webhook_driven_scaling_enabled: true - # webhook_startup_timeout: "90m" + # max_duration: "90m" # pull_driven_scaling_enabled: false # # Labels are not case-sensitive to GitHub, but *are* case-sensitive # # to the webhook based autoscaler, which requires exact matches @@ -353,8 +363,8 @@ is a delivery (of a "ping" event) with a green check mark. If not, verify all th The `HorizontalRunnerAutoscaler scaleUpTriggers.duration` (see [Webhook Driven Scaling documentation](https://github. com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#webhook-driven-scaling)) is -controlled by the `webhook_startup_timeout` setting for each Runner. The purpose of this timeout is to ensure, in case a -job cancellation or termination event gets missed, that the resulting idle runner eventually gets terminated. +controlled by the `max_duration` setting for each Runner. The purpose of this timeout is to ensure, in case a job +cancellation or termination event gets missed, that the resulting idle runner eventually gets terminated. #### How the Autoscaler Determines the Desired Runner Pool Size @@ -371,50 +381,49 @@ will scale down the pool by 2 instead of 1: once because the capacity reservatio finished. This will also cause starvation of waiting jobs, because the next in line will have its timeout timer started but will not actually start running because no runner is available. And if `minReplicas` is set to zero, the pool will scale down to zero before finishing all the jobs, leaving some waiting indefinitely. This is why it is important to set -the `webhook_startup_timeout` to a time long enough to cover the full time a job may have to wait between the time it is -queued and the time it finishes, assuming that the HRA scales up the pool by 1 and runs the job on the new runner. +the `max_duration` to a time long enough to cover the full time a job may have to wait between the time it is queued and +the time it finishes, assuming that the HRA scales up the pool by 1 and runs the job on the new runner. :::info If there are more jobs queued than there are runners allowed by `maxReplicas`, the timeout timer does not start on the capacity reservation until enough reservations ahead of it are removed for it to be considered as representing -and active job. Although there are some edge cases regarding `webhook_startup_timeout` that seem not to be covered -properly (see +and active job. Although there are some edge cases regarding `max_duration` that seem not to be covered properly (see [actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)), they only merit adding a few extra minutes to the timeout. ::: -### Recommended `webhook_startup_timeout` Duration +### Recommended `max_duration` Duration -#### Consequences of Too Short of a `webhook_startup_timeout` Duration +#### Consequences of Too Short of a `max_duration` Duration -If you set `webhook_startup_timeout` to too short a duration, the Horizontal Runner Autoscaler will cancel capacity -reservations for jobs that have not yet finished, and the pool will become too small. This will be most serious if you -have set `minReplicas = 0` because in this case, jobs will be left in the queue indefinitely. With a higher value of +If you set `max_duration` to too short a duration, the Horizontal Runner Autoscaler will cancel capacity reservations +for jobs that have not yet finished, and the pool will become too small. This will be most serious if you have set +`minReplicas = 0` because in this case, jobs will be left in the queue indefinitely. With a higher value of `minReplicas`, the pool will eventually make it through all the queued jobs, but not as quickly as intended due to the incorrectly reduced capacity. -#### Consequences of Too Long of a `webhook_startup_timeout` Duration +#### Consequences of Too Long of a `max_duration` Duration If the Horizontal Runner Autoscaler misses a scale-down event (which can happen because events do not have delivery -guarantees), a runner may be left running idly for as long as the `webhook_startup_timeout` duration. The only problem -with this is the added expense of leaving the idle runner running. +guarantees), a runner may be left running idly for as long as the `max_duration` duration. The only problem with this is +the added expense of leaving the idle runner running. #### Recommendation -As a result, we recommend setting `webhook_startup_timeout` to a period long enough to cover: +As a result, we recommend setting `max_duration` to a period long enough to cover: - The time it takes for the HRA to scale up the pool and make a new runner available - The time it takes for the runner to pick up the job from GitHub - The time it takes for the job to start running on the new runner - The maximum time a job might take -Because the consequences of expiring a capacity reservation before the job is finished are so severe, we recommend -setting `webhook_startup_timeout` to a period at least 30 minutes longer than you expect the longest job to take. -Remember, when everything works properly, the HRA will scale down the pool as jobs finish, so there is little cost to -setting a long duration, and the cost looks even smaller by comparison to the cost of having too short a duration. +Because the consequences of expiring a capacity reservation before the job is finished can be severe, we recommend +setting `max_duration` to a period at least 30 minutes longer than you expect the longest job to take. Remember, when +everything works properly, the HRA will scale down the pool as jobs finish, so there is little cost to setting a long +duration, and the cost looks even smaller by comparison to the cost of having too short a duration. -For lightly used runner pools expecting only short jobs, you can set `webhook_startup_timeout` to `"30m"`. As a rule of -thumb, we recommend setting `maxReplicas` high enough that jobs never wait on the queue more than an hour. +For lightly used runner pools expecting only short jobs, you can set `max_duration` to `"30m"`. As a rule of thumb, we +recommend setting `maxReplicas` high enough that jobs never wait on the queue more than an hour. ### Interaction with Karpenter or other EKS autoscaling solutions @@ -559,7 +568,7 @@ documentation for further details. | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: true # A Docker daemon will be started in the runner Pod
image: summerwind/actions-runner-dind # If dind_enabled=false, set this to 'summerwind/actions-runner'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "summerwind/actions-runner-dind")
dind_enabled = optional(bool, true)
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})

# running_pod_annotations are only applied to the pods once they start running a job
running_pod_annotations = optional(map(string), {})

# affinity is too complex to model. Whatever you assigned affinity will be copied
# to the runner Pod spec.
affinity = optional(any)

tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = optional(number, 300)
min_replicas = number
max_replicas = number
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = optional(bool, true)
# The name `webhook_startup_timeout` is misleading.
# It is actually the duration after which a job will be considered completed,
# (and the runner killed) even if the webhook has not received a "job completed" event.
# This is to ensure that if an event is missed, it does not leave the runner running forever.
# Set it long enough to cover the longest job you expect to run and then some.
# See https://github.com/actions/actions-runner-controller/blob/9afd93065fa8b1f87296f0dcdf0c2753a0548cb7/docs/automatically-scaling-runners.md?plain=1#L264-L268
webhook_startup_timeout = optional(string, "1h")
pull_driven_scaling_enabled = optional(bool, false)
labels = optional(list(string), [])
docker_storage = optional(string, null)
# storage is deprecated in favor of docker_storage, since it is only storage for the Docker daemon
storage = optional(string, null)
pvc_enabled = optional(bool, false)
resources = optional(object({
limits = optional(object({
cpu = optional(string, "1")
memory = optional(string, "1Gi")
ephemeral_storage = optional(string, "10Gi")
}), {})
requests = optional(object({
cpu = optional(string, "500m")
memory = optional(string, "256Mi")
ephemeral_storage = optional(string, "1Gi")
}), {})
}), {})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: true # A Docker daemon will be started in the runner Pod
image: summerwind/actions-runner-dind # If dind_enabled=false, set this to 'summerwind/actions-runner'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "summerwind/actions-runner-dind")
dind_enabled = optional(bool, true)
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})

# running_pod_annotations are only applied to the pods once they start running a job
running_pod_annotations = optional(map(string), {})

# affinity is too complex to model. Whatever you assigned affinity will be copied
# to the runner Pod spec.
affinity = optional(any)

tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = optional(number, 300)
min_replicas = number
max_replicas = number
# Scheduled overrides. See https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides
# Order is important. The earlier entry is prioritized higher than later entries. So you usually define
# one-time overrides at the top of your list, then yearly, monthly, weekly, and lastly daily overrides.
scheduled_overrides = optional(list(object({
start_time = string # ISO 8601 format, eg, "2021-06-01T00:00:00+09:00"
end_time = string # ISO 8601 format, eg, "2021-06-01T00:00:00+09:00"
min_replicas = optional(number)
max_replicas = optional(number)
recurrence_rule = optional(object({
frequency = string # One of Daily, Weekly, Monthly, Yearly
until_time = optional(string) # ISO 8601 format time after which the schedule will no longer apply
}))
})), [])
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = optional(bool, true)
# max_duration is the duration after which a job will be considered completed,
# even if the webhook has not received a "job completed" event.
# This is to ensure that if an event is missed, it does not leave the runner running forever.
# Set it long enough to cover the longest job you expect to run and then some.
# See https://github.com/actions/actions-runner-controller/blob/9afd93065fa8b1f87296f0dcdf0c2753a0548cb7/docs/automatically-scaling-runners.md?plain=1#L264-L268
# Defaults to 1 hour programmatically (to be able to detect if both max_duration and webhook_startup_timeout are set).
max_duration = optional(string)
# The name `webhook_startup_timeout` was misleading and has been deprecated.
# It has been renamed `max_duration`.
webhook_startup_timeout = optional(string)
# Adjust the time (in seconds) to wait for the Docker in Docker daemon to become responsive.
wait_for_docker_seconds = optional(string, "")
pull_driven_scaling_enabled = optional(bool, false)
labels = optional(list(string), [])
# If not null, `docker_storage` specifies the size (as `go` string) of
# an ephemeral (default storage class) Persistent Volume to allocate for the Docker daemon.
# Takes precedence over `tmpfs_enabled` for the Docker daemon storage.
docker_storage = optional(string, null)
# storage is deprecated in favor of docker_storage, since it is only storage for the Docker daemon
storage = optional(string, null)
# If `pvc_enabled` is true, a Persistent Volume Claim will be created for the runner
# and mounted at /home/runner/work/shared. This is useful for sharing data between runners.
pvc_enabled = optional(bool, false)
# If `tmpfs_enabled` is `true`, both the runner and the docker daemon will use a tmpfs volume,
# meaning that all data will be stored in RAM rather than on disk, bypassing disk I/O limitations,
# but what would have been disk usage is now additional memory usage. You must specify memory
# requests and limits when using tmpfs or else the Pod will likely crash the Node.
tmpfs_enabled = optional(bool)
resources = optional(object({
limits = optional(object({
cpu = optional(string, "1")
memory = optional(string, "1Gi")
ephemeral_storage = optional(string, "10Gi")
}), {})
requests = optional(object({
cpu = optional(string, "500m")
memory = optional(string, "256Mi")
ephemeral_storage = optional(string, "1Gi")
}), {})
}), {})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_docker\_config\_json\_path](#input\_ssm\_docker\_config\_json\_path) | SSM path to the Docker config JSON | `string` | `null` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml index 1ec5333d2..95f7916b1 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.0 +version: 0.3.0 # This chart only deploys Resources for actions-runner-controller, so app version does not really apply. # We use Resource API version instead. diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml index fa5c96452..eda4813a7 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/horizontalrunnerautoscaler.yaml @@ -10,6 +10,27 @@ spec: name: {{ .Values.release_name }} minReplicas: {{ .Values.min_replicas }} maxReplicas: {{ .Values.max_replicas }} + {{- with .Values.scheduled_overrides }} + scheduledOverrides: + {{- range . }} + - startTime: "{{ .start_time }}" + endTime: "{{ .end_time }}" + {{- with .recurrence_rule }} + recurrenceRule: + frequency: {{ .frequency }} + {{- if .until_time }} + untilTime: "{{ .until_time }}" + {{- end }} + {{- end }} + {{- with .min_replicas }} + minReplicas: {{ . }} + {{- end }} + {{- with .max_replicas }} + maxReplicas: {{ . }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.pull_driven_scaling_enabled }} metrics: - type: PercentageRunnersBusy @@ -31,7 +52,7 @@ spec: - githubEvent: workflowJob: {} amount: 1 - {{- if .Values.webhook_startup_timeout }} - duration: "{{ .Values.webhook_startup_timeout }}" + {{- if .Values.max_duration }} + duration: "{{ .Values.max_duration }}" {{- end }} {{- end }} diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index 1321f22c8..27077abae 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -1,7 +1,106 @@ +{{- $release_name := .Values.release_name }} +{{- /* To avoid the situation where a value evaluates to +a string value of "false", which has a boolean value of true, +we explicitly convert to boolean based on the string value */}} +{{- $use_tmpfs := eq (printf "%v" .Values.tmpfs_enabled) "true" }} +{{- $use_pvc := eq (printf "%v" .Values.pvc_enabled) "true" }} +{{- $use_dockerconfig := eq (printf "%v" .Values.docker_config_json_enabled) "true" }} +{{- $use_dind := eq (printf "%v" .Values.dind_enabled) "true" }} +{{- /* Historically, the docker daemon was run in a sidecar. + At some point, the option became available to use dockerdWithinRunnerContainer, + and we now default to that. In fact, at this moment, the sidecar option is not configurable. + We keep the logic here in case we need to revert to the sidecar option. */}} +{{- $use_dind_in_runner := $use_dind }} +{{- if $use_pvc }} +# Persistent Volumes can be used for image caching +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $release_name }} +spec: + accessModes: + - ReadWriteMany + # StorageClassName comes from efs-controller and must be deployed first. + storageClassName: efs-sc + resources: + requests: + # EFS is not actually storage constrained, but this storage request is + # required. 100Gi is a ballpark for how much we initially request, but this + # may grow. We are responsible for docker pruning this periodically to + # save space. + storage: 100Gi +{{- end }} +{{- if $use_dockerconfig }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ $release_name }}-regcred +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ .Values.docker_config_json }} +{{- end }} +{{- with .Values.running_pod_annotations }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $release_name }}-runner-hooks +data: + annotate.sh: | + #!/bin/bash + + # If we had kubectl and a KUBECONFIG, we could do this: + # kubectl annotate pod $HOSTNAME 'karpenter.sh/do-not-evict="true"' --overwrite + # kubectl annotate pod $HOSTNAME 'karpenter.sh/do-not-disrupt="true"' --overwrite + + # This is the same thing, the hard way + + # Metadata about the pod + NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + POD_NAME=$(hostname) + + # Kubernetes API URL + API_URL="https://kubernetes.default.svc" + + # Read the service account token + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + + # Content type + CONTENT_TYPE="application/merge-patch+json" + + PATCH_JSON=$(cat <<'EOF' + { + "metadata": { + "annotations": + {{- . | toJson | nindent 10 }} + } + } + EOF + ) + + # Use curl to patch the pod + curl -sSk -X PATCH \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: $CONTENT_TYPE" \ + -H "Accept: application/json" \ + -d "$PATCH_JSON" \ + "$API_URL/api/v1/namespaces/$NAMESPACE/pods/$POD_NAME" | jq .metadata.annotations \ + && AT=$(date -u +"%Y-%m-%dT%H:%M:%S.%3Nz") || code=$? + + if [ -z "$AT" ]; then + echo "Failed (curl exited with status ${code}) to annotate pod with annotations:\n '%s'\n" '{{ . | toJson }}' + exit $code + else + printf "Annotated pod at %s with annotations:\n '%s'\n" "$AT" '{{ . | toJson }}' + fi + +--- +{{ end }} apiVersion: actions.summerwind.dev/v1alpha1 kind: RunnerDeployment metadata: - name: {{ .Values.release_name }} + name: {{ $release_name }} spec: # Do not use `replicas` with HorizontalRunnerAutoscaler # See https://github.com/actions-runner-controller/actions-runner-controller/issues/206#issuecomment-748601907 @@ -13,7 +112,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} spec: - {{- if .Values.docker_config_json_enabled }} + {{- if $use_dockerconfig }} # secrets volumeMount are always mounted readOnly so config.json has to be copied to the correct directory # https://github.com/kubernetes/kubernetes/issues/62099 # https://github.com/actions/actions-runner-controller/issues/2123#issuecomment-1527077517 @@ -38,8 +137,13 @@ spec: # It should be less than the terminationGracePeriodSeconds above so that it has time # to report its status and deregister itself from the runner pool. - name: RUNNER_GRACEFUL_STOP_TIMEOUT - value: "90" - + value: "80" + {{- with .Values.wait_for_docker_seconds }} + # If Docker is taking too long to start (which is likely due to some other performance issue), + # increase the timeout from the default of 120 seconds. + - name: WAIT_FOR_DOCKER_SECONDS + value: "{{ . }}" + {{- end }} # You could reserve nodes for runners by labeling and tainting nodes with # node-role.kubernetes.io/actions-runner # and then adding the following to this RunnerDeployment @@ -96,16 +200,16 @@ spec: # to explicitly include the "self-hosted" label in order to match the # workflow_job to it. - self-hosted - {{- range .Values.labels }} + {{- range .Values.labels }} - {{ . | quote }} - {{- end }} + {{- end }} # dockerdWithinRunnerContainer = false means access to a Docker daemon is provided by a sidecar container. - dockerdWithinRunnerContainer: {{ .Values.dind_enabled }} + dockerdWithinRunnerContainer: {{ $use_dind_in_runner }} image: {{ .Values.image | quote }} imagePullPolicy: IfNotPresent - {{- if .Values.docker_config_json_enabled }} + {{- if $use_dockerconfig }} imagePullSecrets: - - name: {{ .Values.release_name }}-regcred + - name: {{ $release_name }}-regcred {{- end }} serviceAccountName: {{ .Values.service_account_name }} resources: @@ -121,28 +225,48 @@ spec: {{- if index .Values.resources.requests "ephemeral_storage" }} ephemeral-storage: {{ .Values.resources.requests.ephemeral_storage }} {{- end }} - {{- if and .Values.dind_enabled .Values.docker_storage }} + {{- if and (not $use_dind_in_runner) (or .Values.docker_storage $use_tmpfs) }} + {{- /* dockerVolumeMounts are mounted into the docker sidecar, and ignored if running with dockerdWithinRunnerContainer */}} dockerVolumeMounts: - mountPath: /var/lib/docker name: docker-volume {{- end }} - {{- if or (.Values.pvc_enabled) (.Values.docker_config_json_enabled) }} + {{- if or $use_pvc $use_dockerconfig $use_tmpfs }} volumeMounts: - {{- if .Values.pvc_enabled }} + {{- if and $use_dind_in_runner (or .Values.docker_storage $use_tmpfs) }} + - mountPath: /var/lib/docker + name: docker-volume + {{- end }} + {{- if $use_pvc }} - mountPath: /home/runner/work/shared name: shared-volume {{- end }} - {{- if .Values.docker_config_json_enabled }} + {{- if $use_dockerconfig }} - mountPath: /home/.docker/ name: docker-secret - mountPath: /home/runner/.docker name: docker-config-volume {{- end }} + {{- if $use_tmpfs }} + - mountPath: /tmp + name: tmp + - mountPath: /runner/_work + name: work + {{- end }} {{- end }}{{/* End of volumeMounts */}} - {{- if or (and .Values.dind_enabled .Values.docker_storage) (.Values.pvc_enabled) (.Values.docker_config_json_enabled) (not (empty .Values.running_pod_annotations)) }} + {{- if or (and $use_dind (or .Values.docker_storage $use_tmpfs)) $use_pvc $use_dockerconfig (not (empty .Values.running_pod_annotations)) }} volumes: - {{- if and .Values.dind_enabled .Values.docker_storage }} + {{- if $use_tmpfs }} + - name: work + emptyDir: + medium: Memory + - name: tmp + emptyDir: + medium: Memory + {{- end }} + {{- if and $use_dind (or .Values.docker_storage $use_tmpfs) }} - name: docker-volume + {{- if .Values.docker_storage }} ephemeral: volumeClaimTemplate: spec: @@ -150,16 +274,20 @@ spec: resources: requests: storage: {{ .Values.docker_storage }} + {{- else }} + emptyDir: + medium: Memory + {{- end }} {{- end }} - {{- if .Values.pvc_enabled }} + {{- if $use_pvc }} - name: shared-volume persistentVolumeClaim: - claimName: {{ .Values.release_name }} + claimName: {{ $release_name }} {{- end }} - {{- if .Values.docker_config_json_enabled }} + {{- if $use_dockerconfig }} - name: docker-secret secret: - secretName: {{ .Values.release_name }}-regcred + secretName: {{ $release_name }}-regcred items: - key: .dockerconfigjson path: config.json @@ -169,85 +297,7 @@ spec: {{- with .Values.running_pod_annotations }} - name: hooks configMap: - name: runner-hooks + name: {{ $release_name }}-runner-hooks defaultMode: 0755 # Set execute permissions for all files {{- end }} {{- end }}{{/* End of volumes */}} -{{- if .Values.pvc_enabled }} ---- -# Persistent Volumes can be used for image caching -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ .Values.release_name }} -spec: - accessModes: - - ReadWriteMany - # StorageClassName comes from efs-controller and must be deployed first. - storageClassName: efs-sc - resources: - requests: - # EFS is not actually storage constrained, but this storage request is - # required. 100Gi is a ballpark for how much we initially request, but this - # may grow. We are responsible for docker pruning this periodically to - # save space. - storage: 100Gi -{{- end }} -{{- if .Values.docker_config_json_enabled }} ---- -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Values.release_name }}-regcred -type: kubernetes.io/dockerconfigjson -data: - .dockerconfigjson: {{ .Values.docker_config_json }} -{{- end }} -{{- with .Values.running_pod_annotations }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: runner-hooks -data: - annotate.sh: | - #!/bin/bash - - # If we had kubectl and a KUBECONFIG, we could do this: - # kubectl annotate pod $HOSTNAME 'karpenter.sh/do-not-evict="true"' --overwrite - # kubectl annotate pod $HOSTNAME 'karpenter.sh/do-not-disrupt="true"' --overwrite - - # This is the same thing, the hard way - - # Metadata about the pod - NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) - POD_NAME=$(hostname) - - # Kubernetes API URL - API_URL="https://kubernetes.default.svc" - - # Read the service account token - TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) - - # Content type - CONTENT_TYPE="application/merge-patch+json" - - PATCH_JSON=$(cat < Date: Tue, 9 Jul 2024 17:17:57 -0700 Subject: [PATCH 450/501] [eks/actions-runner-controller] Fix misconfigured document separators in Helm chart template (#1077) --- modules/eks/actions-runner-controller/CHANGELOG.md | 13 ++++++++++++- .../charts/actions-runner/Chart.yaml | 2 +- .../actions-runner/templates/runnerdeployment.yaml | 3 ++- modules/eks/karpenter-node-pool/CHANGELOG.md | 4 +++- modules/eks/karpenter/CHANGELOG.md | 4 +++- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/modules/eks/actions-runner-controller/CHANGELOG.md b/modules/eks/actions-runner-controller/CHANGELOG.md index d0ca5cd41..d3c2cc338 100644 --- a/modules/eks/actions-runner-controller/CHANGELOG.md +++ b/modules/eks/actions-runner-controller/CHANGELOG.md @@ -1,4 +1,15 @@ -## PR [#1075](https://github.com/cloudposse/terraform-aws-components/pull/1075) +## Release 1.470.1 + +Components PR [#1077](https://github.com/cloudposse/terraform-aws-components/pull/1077) + +Bugfix: + +- Fix templating of document separators in Helm chart template. Affects users who are not using + `running_pod_annotations`. + +## Release 1.470.0 + +Components PR [#1075](https://github.com/cloudposse/terraform-aws-components/pull/1075) New Features: diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml index 95f7916b1..d4a340fa3 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.0 +version: 0.3.1 # This chart only deploys Resources for actions-runner-controller, so app version does not really apply. # We use Resource API version instead. diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index 27077abae..dbdfd1a84 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -12,6 +12,7 @@ we explicitly convert to boolean based on the string value */}} We keep the logic here in case we need to revert to the sidecar option. */}} {{- $use_dind_in_runner := $use_dind }} {{- if $use_pvc }} +--- # Persistent Volumes can be used for image caching apiVersion: v1 kind: PersistentVolumeClaim @@ -95,8 +96,8 @@ data: printf "Annotated pod at %s with annotations:\n '%s'\n" "$AT" '{{ . | toJson }}' fi ---- {{ end }} +--- apiVersion: actions.summerwind.dev/v1alpha1 kind: RunnerDeployment metadata: diff --git a/modules/eks/karpenter-node-pool/CHANGELOG.md b/modules/eks/karpenter-node-pool/CHANGELOG.md index 2a110392c..c8402e80e 100644 --- a/modules/eks/karpenter-node-pool/CHANGELOG.md +++ b/modules/eks/karpenter-node-pool/CHANGELOG.md @@ -1,4 +1,6 @@ -## Components [PR #1076](https://github.com/cloudposse/terraform-aws-components/pull/1076) +## Release 1.470.0 + +Components PR [#1076](https://github.com/cloudposse/terraform-aws-components/pull/1076) - Allow specifying elements of `spec.template.spec.kubelet` - Make taint values optional diff --git a/modules/eks/karpenter/CHANGELOG.md b/modules/eks/karpenter/CHANGELOG.md index 6d7a8f2d2..bd5b3bc24 100644 --- a/modules/eks/karpenter/CHANGELOG.md +++ b/modules/eks/karpenter/CHANGELOG.md @@ -1,4 +1,6 @@ -## Components [PR #1076](https://github.com/cloudposse/terraform-aws-components/pull/1076) +## Release 1.470.0 + +Components PR [#1076](https://github.com/cloudposse/terraform-aws-components/pull/1076) #### Bugfix From 0c301a3cd24f204996d5a3e0765f41b0720ba239 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 12 Jul 2024 17:42:18 -0400 Subject: [PATCH 451/501] fix(`aws-team-roles`): Remove Deprecated Support and Billing Custom Policies (#1078) --- modules/aws-team-roles/README.md | 9 ---- modules/aws-team-roles/policy-billing.tf | 45 -------------------- modules/aws-team-roles/policy-support.tf | 54 ------------------------ 3 files changed, 108 deletions(-) delete mode 100644 modules/aws-team-roles/policy-billing.tf delete mode 100644 modules/aws-team-roles/policy-support.tf diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index 3ae6961c6..4e9feda9a 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -188,22 +188,13 @@ components: | Name | Type | |------|------| -| [aws_iam_policy.billing_admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | -| [aws_iam_policy.billing_read_only](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.eks_viewer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | -| [aws_iam_policy.support](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [local_file.account_info](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource | -| [aws_iam_policy.aws_billing_admin_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | -| [aws_iam_policy.aws_billing_read_only_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | -| [aws_iam_policy.aws_support_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | | [aws_iam_policy_document.assume_role_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.billing_admin_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.eks_view_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.eks_viewer_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.support_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.support_access_trusted_advisor](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs diff --git a/modules/aws-team-roles/policy-billing.tf b/modules/aws-team-roles/policy-billing.tf deleted file mode 100644 index 3363c84b3..000000000 --- a/modules/aws-team-roles/policy-billing.tf +++ /dev/null @@ -1,45 +0,0 @@ -locals { - billing_read_only_policy_enabled = contains(local.configured_policies, "billing_read_only") - billing_admin_policy_enabled = contains(local.configured_policies, "billing_admin") -} - -# Billing Read-Only Policies / Roles -data "aws_iam_policy" "aws_billing_read_only_access" { - count = local.billing_read_only_policy_enabled ? 1 : 0 - - arn = "arn:${local.aws_partition}:iam::aws:policy/AWSBillingReadOnlyAccess" -} - -resource "aws_iam_policy" "billing_read_only" { - count = local.billing_read_only_policy_enabled ? 1 : 0 - - name = format("%s-billing", module.this.id) - policy = data.aws_iam_policy.aws_billing_read_only_access[0].policy - - tags = module.this.tags -} - -# Billing Admin Policies / Roles -data "aws_iam_policy" "aws_billing_admin_access" { - count = local.billing_admin_policy_enabled ? 1 : 0 - - arn = "arn:${local.aws_partition}:iam::aws:policy/job-function/Billing" -} - -data "aws_iam_policy_document" "billing_admin_access_aggregated" { - count = local.billing_admin_policy_enabled ? 1 : 0 - - source_policy_documents = [ - data.aws_iam_policy.aws_billing_admin_access[0].policy, - data.aws_iam_policy.aws_support_access[0].policy, # Include support access for the billing role, defined in `support-policy.tf` - ] -} - -resource "aws_iam_policy" "billing_admin" { - count = local.billing_admin_policy_enabled ? 1 : 0 - - name = format("%s-billing-admin", module.this.id) - policy = data.aws_iam_policy_document.billing_admin_access_aggregated[0].json - - tags = module.this.tags -} diff --git a/modules/aws-team-roles/policy-support.tf b/modules/aws-team-roles/policy-support.tf deleted file mode 100644 index ef75409b2..000000000 --- a/modules/aws-team-roles/policy-support.tf +++ /dev/null @@ -1,54 +0,0 @@ -# This Terraform configuration file which creates a customer-managed policy exists in both aws-teams and aws-team-roles. -# -# The reason for this is as follows: -# -# The support role (unlike most roles in the identity account) needs specific access to -# resources in the identity account. Policies must be created per-account, so the identity -# account needs a support policy, and that has to be created in aws-teams. -# -# Other custom roles are only needed in either the identity or the other accounts, not both. -# - -locals { - support_policy_enabled = contains(local.configured_policies, "support") -} - -data "aws_iam_policy_document" "support_access_trusted_advisor" { - count = local.support_policy_enabled ? 1 : 0 - - statement { - sid = "AllowTrustedAdvisor" - effect = "Allow" - actions = [ - "trustedadvisor:Describe*", - ] - - resources = [ - "*", - ] - } -} - -data "aws_iam_policy" "aws_support_access" { - count = local.support_policy_enabled ? 1 : 0 - - arn = "arn:${local.aws_partition}:iam::aws:policy/AWSSupportAccess" -} - -data "aws_iam_policy_document" "support_access_aggregated" { - count = local.support_policy_enabled ? 1 : 0 - - source_policy_documents = [ - data.aws_iam_policy.aws_support_access[0].policy, - data.aws_iam_policy_document.support_access_trusted_advisor[0].json - ] -} - -resource "aws_iam_policy" "support" { - count = local.support_policy_enabled ? 1 : 0 - - name = format("%s-support", module.this.id) - policy = data.aws_iam_policy_document.support_access_aggregated[0].json - - tags = module.this.tags -} From e43208c1124a50ac96b911b0ed0234b294b981b9 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Tue, 23 Jul 2024 16:46:16 +0200 Subject: [PATCH 452/501] Added branch restrictions to GHA IAM role (#1082) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: screenshot-action 📷 --- .github/banner.png | Bin 1032325 -> 1032367 bytes .../github-assume-role-policy.mixin.tf | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/banner.png b/.github/banner.png index a045735ab731698b194b54d132abceda895b7587..6d94327be3a352df238999b9167670e383d5be4d 100644 GIT binary patch delta 850704 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�`qr1m%#erF%|gtABm8=*-?ZvqE6hcp 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 z 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∓q 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

>cv(O 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 m*X!~X2uyYtwlf}*A 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

e4!}!6B#{w$WZ)h({z*}#?_=s^ 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

v%6Yr1g`r<}?ld1e(d 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

1iL-SjtJ zH2cV53J1aX;f(VDV14|RcVM3^x~br`+Fv}ZR@dejDec&GB~HeuJXz=#_(2AlYO)Cm zfJ0v6@q40aw%bQqIq1m%EA{yc-}P%QnX6%v-Tc1GdX>WztlP16kz-_NM)U&k@F;wkGJ9^DFFuj{P2+X62X6JJLZCAIaBuW zTUrpx!mpnigvpNWiB>3I6CA&9Lpi_-Zw2 zuLC1nHvl{)|3f`Btx8&r#{@M$kp7qd3s=4IT?m8gMT@uT3nGn}#4ySp?X#$1r7ZF| z#u~eB&l92|8%A$m_VbA?<}rnWX1KcN9sAo@qxMA2-3N5^#ZDg$d(uzJKM%CuIT0s2S~}vkp&Tj2 zd=|HfnwkXhbvjreWPaCfVwowaWCq=&(#s=s2gh*pe(S}D4$BWFNPx`tC`Qb zRR)06s|h`nul`r4@sok=CV5#7j_F*c$yesIeT>*hCWfwQiZFH z+RwaP17|$RqmX(F`jP4?VkVCltj4X% z1{U%p8|O0~dk|elf+~zeQ;OV+w~)yjT*0cmFRdB$#yG!+95~4W;(^ma z_#Y0?slcfV41>Oo!SDZ-!0bHZwW@qr0SmNj?hFNqxpgKF@-;lNcs7_!l_u)+9azX4 z#dfTDcHXw44Zsjf}$yzl!3bT}mXkV55J*G94+(Zf`TS2T7| z?z3lV9a#ZiwB)+VhmhvO<(-?)Ea&2JDEy1`> zi}4&Ao{!HA4nyy~Q#H@4x;_}=1dYL68V8@<*SE|Hyd7(tv=RTY9#vipp{l$XD7g4N zrR~o+)}&x6iC?_fshWnum+>0DxslV7njTKMxjiX#!k|S_YE9M;XR~}j;K+J-rM-%c z8I_k@Tul>`STTGDOnniez!gMTdKGJaRnf%6@^81$SS{0Iz^27%xhLYd_>#O{L+LPQ zza}4?vMo?ZF=s1UeNT8$h$ouxW!@S}dw4B^3nSnvZycEY{8qI~EX|KFPQf08OuQpL zrr{RJcrc&*u{{4C)t4Rs)5R1BF<8NKJf{0Tb#s4tj9w0Te5~q-6Uh49knsc^s-!*y zyxcbo!Hx;HCd`^Il}|5W| za_M$A0VyY?6tYewFrm@u=m>P$QNrO(6(dKz+~X#HeYY~I^q`%1x>zO?f`eH*Usy~v zlghMvS&naK0IN*pqBBWUSk?smiRV3^mGMC)yuFnQ-`BB}`<@Ou z!YQN%*ZO6=&{;Sp#;znhh96^F(nFW#LqOFU2yixF$vK~zY)SjRS+_R0;Lvu~O*u`# zFVIU`u7l|rj1A03y>v{~4xSu4FG2RNKn>dY-`R8Q?SJj^ZXMA8OL{oTOTt6qk3xg+ zvm|0;)Hy<~`rZDa@T`C@BF7cgbQm(&HLQ8fDHJ7}F+N+t;i-sS;QPW`nw^S-$-=s&qwv%CHY!qMvA6Lagj7hD66 z+IO4#+&O#vYjioQwpNg-=ue=fJGejYx)Xq-cx3PgF^GPn-=rNgHOb^3`{T<*ibOaA zy$^4uHO}djU>k=L#Be4A)no8XNdeXiW-WlaVvge37q@i6nWSKOTP?Q>E&{FPY@qtG z$PM?ofX#OfTQ^fN?NUV=Hfmw3ch%mXfhY(gQ_ks+HX?;DxgL-iB|b2t7mRrT2F8Dl zQDub8fV$Lo-+bSV?Ywlb{pK=4S#h9No8P(L<#Sg2fg91+NM`eMmaT_Y z>)wpJ>1$P*yQ`a!8d|nG<)V`z<op~foK*|QHA-tG*S;0Vp>|to zvfX5J3vo{V%09^L#XI}@Z2ftX-fD`=7gUa{&7Nv~;kQ0wSuP$=HE!$+O?IHchGXH> zuueXS1o0x)HjyRrffggH1zB+mohp|I#Ce!K-X-FD5_ov*^kVp%vRks;JMhv^+ucK-4Y$K(!R-- zMIEJ9*$)lUr6qlyMX@GfwNfb?%Vp$vSk5`gb>1)Kk7k`q(hE8plLG~3MtgV?wcgY$ z%4vy?031-&qHgcXqQkN{uehO>0W*BlJwuKX5OrMH@M(jH~dkdi5(FeW1KY_Zm7L-6`K! zQIB_I>)`hB$k}fm-4x_%F3Dsn^H?z|1>R#ihw4IsFQ0`5E+4~=s?`IwsiYxp+%%m4XR*2Os!ga`%MQ87G z)lk<%wdslufElf_&Z1Tkv*BVpHM`#kT}~*ypIBjw_5!CvyT19$u#8K$DfuVSP5_Jo z^hIACCUC@bgKUvUqrM4+`y!(H8+TR2QUlekK} zb_o6;*}9R%29~3bC=-V-ml(i=!79~P{s#*Ws2zY05cf&cMkL!tLAo`rqu}-{<1ZOu z^2aj|$$n^$-J#Oh^M8Kn0^;wB%i8U?#7pp(6dX=8`1NlGx4*EEjk}6jrl>{BPtJSe z0j%7_hu<|6LwaX#S8BLa#*s0hVDC!cP~$eM>cV8wEZ_ScX-v0DSCsy>Aj-#EN7*~e z=tV({0vC)1QX@V6QKIi#Gi>|&-S5PU`w8GKN4-mHLVnM;#HzoK*{q7@f9y*@)7peXN4_?KX7ZYL(8u2NHT`r zikeYI7MI(3AAAfVyT^8C2^f2xv(0Ey{pPHHdjBh`KNldfzUjtZ#{uUh*C_nn>`4LB z4I9QEgr>W;#%+(SYFM{0jWGp!@*|s4D#}?_(6~inM6uxLzup?;UtVX5gG?aMY?Omb z=Sk_E7B}qPGw^RTO>nYD5KM$L4da+#eR%p8fw?WZ;vh8KpKm~^6G^AMcg;LdaTquP)TD#DY6O=x+1=>J^I9<)4{?aIC3sospDLK>y2oj-N{#zw2*WmIe$1ykMm|$*;kFWn_iV&>%vI40;=bf8H4fF?S z2i?9XghxFY$jUhU9i3MV>9-cztdJPyLPEK(#EJcM^i$*;!Q}^Vqzeka@WZwH)_gb=QPSHM%-7`Rw z-P*IAdm=;s60%*xOOqy;po7GIUU{$+cvqj+Fq&46=jv&Oe3E1NC#fiwv?h(ia}x}j z@Qa$-w;|~%O78#-b{iwbzTrb7qso?Ec!i7EMr>gr>ySYijW+Oml(TdbXZ$nyHn6^& z`sW#wqFP+a$Onr=;UBwciWAY5ip6yu2?60Xzo=vR%yc7}Sf#tyZ0%A#!yj)8tp8IO zTNl8cXj~(8uy?_@ejFuu4R&pW*@9s}Cuw+GMZm%D5)a%``0u@diG8H|asnk!8eRK+ z*P2uoCnXz21waE=eveCFs3T%gXbfTc%#h3KHb15N=fPhR!#|u8=KtuksZr9Ke)kD@ zk9J`Xzbo(Mx<^VBCBzl5wOv|Y@2wm8l3Jmb-m$_93;dPo^gQ>fdifVjDGYNx3kl!y zE{rV|>bxbmYR~d+I*h!Y!%TP_+7navP4#H&j7AmS^}Z?i?@`gy-bjj{&^)BLG5K(G z`TWt3(S1o#2Khm3{2?3Hzj=>Y5}x}_tP{mA^i6xu3kM?2X$vIG%L0uixe0iJ%^cXQ zxlNRYN`Y34(UZfL2LZYNHoQcLDj%p?$moy0BIqE{pZL335P}6yGH1;ehG^JK8Knnu zTN{`2*>#Q@oXS8}tdos*qEoZrrkkzNE8fGHUT(kIdqw)+D1n^R4+{T zUdIfY{KJP12k9AC`$&Suqv8X4F7#v&*%S8UAHd>;f#2AHf9r_4*3i-7*Vn`!+3WAo zHx<(FjHrYk7UwNIFxt0gD|!Z-FRlByE}g9WxpM0Yh1n{~5nsFt`6(xrq#ws(DM`>9 z-wlZmSeOWNP0k#b7qi&b|1k5O#H=eBlf(_J)USsEzw zXES!?0-~hKtNLiudJi2m!7R9s>}p=@=1mkk5%A zzm>(Uor=ja*nRG^-59L&2!PAY;B1M*Er^)_i)LGd`856ZTL->UNTUzpBX-b1J( zqGRuja_*=5!^=Z>6juDWTQs;i^smu9ci>zuy zAI|5n^?JK+^+TP0%0E+#jdc8sJAHhB-$m>5stKm>gfn%EM*W=un}70@rw0Zq4}*(G z-p-nPv2xXH_$4Uj>t@*xWVJ8B0w>eDp_?P{;?XQOc}KujiKF0?Dp-9$CyUVVPRLeS zfxMVu)_ljHYd3Y{1~Vtj{FQq$siR7i&znMtZ)a+SBlbjZAL@T9QzMG(#;VVKHGQ*Y zz97jRPEk}TI;DghiFW&{1xq~Tv?*K+3fTQq5hlC1Sz%(X|7`y8%9LL;SQA$}^k3}w zO#57p@q8&T9^-feP@od~!`L;7D-9K~DFb=2qD1_QG%O>VCmDLK(YG>%A1B{??x|BB z*kCAJ+wM;=U4&?N-w?2;e(TiyR%)L;75$ZlGOB*u{!_G?6Mdh@C!S9!IPOW9mZ`GV z`UtUsE&iOjQay!XP~eHL8mK}2s}@Ugw9%d&`Qs!2NV(^ryY|UN>xNXGzi7nC_}kF& zxYnc;%b9{L*|&p-ulen>b*Yb(v0dx6V_a%z7g#W6M~xojM}8H8%6cU9^1-|Fc#^qJ zf9cDm0Lto48;+T$pW+#n5B6(Vkj=3?O2QL1W)9xz-BynE zPh>~|!pAhc(fU8|KxfO?`9n3X%HnrYlJOXP6uz0IR{#rArqNAdd!8Hu9e6{}ZR-Syce$f|)&kW)Q&WUbxf?-M*@LbQu|?dievT zDeR-YL?iA5FVMtAJbe5Qe9 z%LU6uDV~S=*jqjF3m)d#A)Rrwq8X8{T~*=$LX`)WTwl|ZtfQXQ05-` zB=>cWl34m<%9mcNLd7+OU2NwrH_x~uHHBK(VK&RM1UaIwOAWvjZ@J-RtwGfy(&iR4zVzzcyf6GRv#Lgyd z^=&6?-TTr;WrhglK^Ez0Wbb$rT96X=jd!cg%7pu!mNBCCsF?BH5JO0pvq^U@#lr9M z(6t3KJNSy@x$J{;S?_^cds=7wZ10l0VQ0Z7y@BI+Ds1-eCV`tppdM^{47j6zO8Lmz z<}AeqvvWvU3_M)qn#E5aqwCDKcONV4g8Zo&)B3bLu=!!$=;wp9P0O!ptQFSm1(jC^ zQ)538mnhD$9VS^e?3dqBwxtn2qwne(l1O;3#S&iOp9Z}->rBpTo_s}77J&bB6{c*1 z4-KVez?z~bg<9~Ee-Y`8&RSIswz2D>L>rk@#>>J_LY7?Kn-_Iv&|TGRDLu^1{Amop4bIjD)ERU5X8oRU zc>LJ39h@Hiua73YkLEV@U0dLGq@RT8Ao|`qa_o-#9Fe$M5=hVQPYa7tq-8C6q z%dG)@xepJZ-~!kx+ddMwfw7_ilZ%%Kr2AY9B~PT@^wjKS4qdIoq7;4LX!sThj z%Cs@$;uVwxzkohFylo(F2QlpBs|ggVXLV?ZDo9zp`M%mrtHG#!pF8*NTvO=%+e0$o zi>Dc*|GB#V5v?1BG4#luv_f>jO<0ndM^X0HY#t^%Ick};7&L0ry(i@!AtqX0E^o=B zuM5rv{JfjB_KQb{I2t!h%$}8(n-jc9Yl6E>UA65R-Jc(ip?a~1;`_vHSZYwgt7!k( ze8j{jKuDT@;cR<49A*9a1;Vg1Mb$hIl9}SLaQQc@1LglUN^|J&pEpbj@+_i_GsG@O z+|@tk}~uE`u;o3CoFOyejoGa5_3(;UfS#%Wf8jE4_&^9*!Q?fWBkvE&jT3u3*U>u z7{afu9#Ot92n(mKn-eizz2u5Z`vQbMqI4E9ee^6wu}MwT*7QHfi)TIelGX2r!~Kqz zjlP*09-0fORkKZ-{Bt@oF*F>i^L$4 zO<|<QT9lK_grQg*^ z7N9wL^tH^WY*D6ppItC3Z0n!5j#PQaVAbOk1Cy0p)NlJk&Pe{~{T~;wS0_7X4akDU zB@7zDTkftH#xr8?6?HlEsx+;d98(F~4k% z1ZXRe=j1QiXC$OcJP&>szJrH3Exo9MshnjPED(3Y-_Y!rZud3Rd! z&X0JujJaj`WG^2hUs-BK-t?i$;;nhEe@9_==rIB)^p|_@m|nf;44Ck#M_DL}jsifgwnD#y=*wn>UrF zfkZ2pM_DJbOhK=kj+X?}f{xm&=i3?n2LP3C6MXvle)O>z$J0I==D`4&F6UHgzw$etL#%moBL&dMXE6%RQA$wYP(QshRaaFQCYWXYaeY{=25#6>` ze+B>B&wPtBYpw5fC|+dUbn_|Y?@)UjTXOb1x192toVy_|s z2fCDCn$CJLeYx3=I*-^X{d(=xBHPHB0N+gZBBTF^XN8&jJnXY3)bd7&j;!dZe`0B) zAKY8b+;2j71DA14plisxoaeS}H$6|B>Yd0t?0$@3Q^<2cXJ5YF7gp!6+=P_Aa9|AH z*?p5sbm#J5am|X|Nza7q6W195PqA*mYlfA&QQ|kgfGs&6yZyeg~p_l(i!J(M7QS`rq@n@yK~#@`#=>eW6G! zI$k}4zlfeuf-AgBGx-hs*ajft!5#fs%W7!``z>|P0?(o}>37<$hr5fy-@i)8>{jN` zSZL0ga?AF|3E*{hy{lwyO+>O5I1si4|WsS=p<2`_-d28(3hesTiMymyoId4!{3RXiG0P+y>B%)Md&=l!z4l zADq}tDj}rgRKSdr21}Y@pSxGf?nV~Ml^@i?lvvLko<~JLzw$>Orgy17(%;|(ox{IW z?=o}Bq|jUpBI$!>p~Bcboi;m?Oh;bL<2O?%=*LylZ>aC+25%mVCeteQWj8?$<{alx zE=trnfmK#DpP~)*ZV@UAj#D&4(J%E>NS*P@VY0=Bpw6oe*l345*WF~65*^69!^mV} zjmJ{2c4p`Q;jP?eNVJI6&@;F0mrR2*L9TGg5o$?`%C8SK$h9@?kW1!5PkXly1>6JS zqs9Nu?`SW#C6oF)4lOTVB*;qaT~3ZYv)lufAjegaA7&qG02|MVYO=|4#Hu@$SBDu5 z_g97Nik{5RHC%zWP7X`KC6sr21{&JIQCi4ofLyfR9hU<78;F!1rba zUSfwTe~vSMn63#03D`D|TA3efE-+oDE^?Xf;^5+pQivX|r+c33=Qan~&B0bSwJM&q zDrQd!WVn~SfXqh^Q4ES@kU_xQGfIeF8zX0d1C#k(*4c~q7hw^}GgW=tR|yJG>ucOM z+}jm_z5drFJN?w*Mt}0m3<8-L;;bi~_I?xDb#0%gMc*PaOWrU-0}#{hKF7H_`UczL zS$`3_dK$peKK}mwaL8Qb-<+8vN{Y7(j1KM^0o?7t7&!kx<%Z%qF$gn50j?Qyr-e!N zF{NR|r6k7E?CWa=SXn`;0t(*UXI@IE@l6%v*&X)J11~9`M7n}~P{ciJ#lf(u#aXiY z{{r?lUkBWr2!S(R-j{$Oy2Qo(|?3_-N|i7@OL`5u5xeuEoEz}kJ@R&=W-Blrt7rA8d2#i zaQIm9_GoUuHF%33bD)Fc#x3KoRV1)`^Q}|9y9-_3*w|6o7Ec=XVKRQe$?rq9CC}DR z_>bx47uFrM-(HPnQy1piJ(;U>u^V@f1ul6Y*G133TBzQ}{n^#}=`MF<3B&!yT;RKb%v43XGa;;rvO8^?E-T`z5v0 z@USUxKd7?92la9@`1G2~qD6Q6bKc7Hm0#BaTO3bY zw;qUag(t#A?0+Z{yK;vZ8x!XV#sdxW103Bvl3b+S=D*7(Sku1B&W^nAGHWB1y{u%@ zN%ZnyuNXMr7L05lTAUIdea#q8k*b!gDfg0`KAi1i>brYi8}z)dfBvxvxzFX$SB{l+ z4>Hj#*NDD&b6hj6OI(*>uNb43e-PHOOFs{9TJsKDZWz7ZRTv{X`)E>a9p?Z%P9^al zU@c5dc+x8XoAhSk-klluHu5@R$tONJcsNh(2kF#7D`$Yd!7r-2rOE!4 zC0w2!BIo>3`F7o!d$_$yG*}-r-XX4@?&C8cZwP#`>83OYp1uHD<%pioS33gy z(Vm9ZV%L)ltee)6Eq#&(B?al?!{Y@qoH_2CKTd%jdy(SJ^N! zBVPO9;%<`J`glf~hd=7hSx(kI(KG%WT!UG*Xy&?e>Ib54*v{KNX;wdBj_e`%_&yVZ z$@F1N&)!pOX$3h*8xjz^KvP#KB%fZ$1Tsbz8+9ozCxhOs)dKr6q*;Fob#b>vOIc)E zbDjnU=%*PC#^(g(I!0Y>ZY`biA5uTxl?>5)pRBhCHhM4jXR{#iLryw080DKS=mzL&f6zOmYR%`eo7HXpa8o ztL64n2S^9%iJ-JP>GK<;BSqZ`#nwq1amhWx4J;H!)@k*x_d;$iW2{sVRzF(2pa+9J z)bg4zon~^+ACmclNJ3l0)j*YzKYvBO5;1=4O+2}#&<@D)XPr5NGtZq~XNp%%x3wS5 z8p8BIppTM2&=2|cQ)|l~GCMgR7`Sp6;Wv;kHMswawEw5T{pl^p`TdUx_|lC5H?mpx z7m1A$VbNMQjw}a9Fb&njiii5Konk|~oYK~}e@Z-o( z@N90GrwQ#A^cF~tFLsbQ0#IB0bRSu-5I~Zr7>hh^0GtMy~VZF0}uMJu)}TI z67a{H-ITd2&|6@JEVj=Be1AqjGhGL#UptnD8{bZ2AH@5ueiGRVmIKNHmax`zNa6D3 zzGk~-`0&KB@}`b3X*9x-2vPqv#_wJBa{3^DAkc1H8~p0fo=V5rzklBMm})4X0bbX# z+N^xjIu5SiG0DLrai*BYEBFfhA@4xpNLS^~bMnE})w7;&At|2qSr{Bx!SVvvw^)}pBaj6(a4G@WOaxBU+-FDWT|Ca|{Pw@1ou&C037{H_IcR+xaFp?!0jAAi@dx&o6+kAr%fg+{DCFYngzc}GFa$QFl48Q=C#um64_#g7>^nXhgib7l8$qmTGg z+Un;N=CR>(3WFR6hpDUk z?iLGg;`L^4!7h1{CrI*Q3TO*l4JbL$OOUyyn1S7fVt&iAYJLdC|CF9)Pt$k2=i0{| z{;Mykpg})<;eovp$I6uJl!o1^pt~9XB0PX0_udQvM0XP8t_@v4W_J~nK7R0o4s8mi zSbnoUipgF>Z?s!gRX@q+)9&Tt7>Ca}*|wqSdLH_Ypr}=hM#=a;)AZr}2xArri{VyjQbcwMlKba9k z3DGgj0|vg5Ij5%m=+U7LCu7#>PUulq%`46krO~~LDeU8b*hRK}eZUc&AjTU$Rnhd? zculp>Q+LxQImWmlW&okQ;Pj10uvM@7??|3&p=ye&c5ez1fc(CaoXymU_ zJ(DUduFlZ|Q+n+be@<-sU|y6vPU9wwxL3@dy#|K47Mb>NY;CH8GzC zE)95elkdG_=i!pBI8>}U{u{~Vb3K_dsM*N&YRluFe1w5b(6iz5y{im%u8De<94FAT zFLMH}B|E`goqzb-J%TGcDk*Tpw^@S^w@_d8?ij*!!f%eQ1vKin@J1~NGBvff(vBBq zo)SfEY=4vPS-wJaM6WX`2F4d&DYcSD15X6@U&V1TO+$wfv__!s8&s)q)=l|PsQQSB z5*IhbM~x<_R)23Y>-`z}50b$L(3Y&^-)u(`>Jx7$NPRF>(^Wl@qW9lpCY!Y!j!w9h z?wZcp#Ss|El?FL2{^e$4x2$;ssqzIig4q?NSUGSIjHZ@7dtW(Rq0^eRh?5pjk~F+} zzvrGuF8!V~?`f-9s`Y|p8j}oOzWlXztkcgY76MOU1*+ynTCy;wH1)Yk>uW8QIbUm* z65&DBl^BwY@^J~`GP&L3p4C;MmpnwokJVywK`mN7=FN9Xf*5+;`|=G1RQu$Dm6%V~ z`p$yNG1>!-^}CgIiG3I;KkHz?vy*G{Kdlw}%Kgbi>FM*V8?vCaVwmNZeX^XfUaO#@ zU!*iY63JYT4dt36Um5J+CR_n6{wEpqHt0~FDj&(RFP*S`6afl zfsAS?z}s)Yc2>L3Y9720*_m9z)I2_I0>4s6ZJtW-`R%v6g?6;BEcSkXAj=Sc1Xfyd zneDfO_T`RosHrUbe+2Jry#K>>Ma1%-$hw>s*<{jO%$@wuTy{zO+s-rxt%ryu98UD=e&wP zFL=J53=+pNerrHMlRwT3suAf^jH8xCuE)Xz+s544J#*5g$59$Y_d9|{xGj80FMNX{)X5c1WxF(?;S_ z>)KCf5xgw66!6z=30E8%urKZ29SriI>)KXu?^6L^TV7wovD%nrD2@?x0lEQW3IL1{ zrWtlYgjt8(jGdMN=LXlC)~LaJ>nj{!g$V9C-3j3_wrpPVbFF@H1Ij#*uC%%*@*Q); zVhAtn5`d`@)xhk=-Z^xl_Y*jSOT?aEL^)9QMTr~S#%gO#3^0yj*VU@nv74P4tUa%f zrw2|1ZH#!8_dw%c@}i%)V$I1*b?1QCgcRF?Yo*AcQ}(HE+EQ$&h%i+z`KOz z;ISD^!qUKJg+mgY!|F=9tHiZdh{-*m8mD6kl;*}@%hxwd5GtrgBlwGU`KA(=Qg~_sm zTc>2wyK`RIhxc#}sT2;YZWtRoS2nIYI~@Geea<8c{b+b?Nh+>(sAyvqWijzWGpX-B zPvh>DM!km6P9V?xD=kKV8BG<(qDQxtS1fuTtbnya5UZ z8bspL3(4>A75BkIwzM%vFf8QFf<2P5LgGn?Q6sU1Ch6+-vuwlkv!Uzv-K|iEq9CIC zq{#1Ie>n}xv}n~@E&&K<2*B)jG~IR1(MVrlKt@9S;T?8tIIqpY<0YIa=iws_D_ zy*m6(C&2b0n^(EMtdir`yUzHBu~ydq=4x~VBS_)O65QnJ21AoWgT->hwhugzxA5Qs z&#nJT?)VWeoTG>)#%Xz^0scfH?!#?~lq!-9yTk+cU4(@h0lS>`^-PLZ97da4mr2`v zTue8(98Fj)qNfsaZi9CN3!=VbejSFp;dzPxV=kggG(qLQ5#)PL>fY1{O+9eTel#ibL{@^bj>}zDMnFXf7VEGgA}i+S z{34d<9DC!-40;lw)H=IeAOM;Vfssk^GZ}2^#Ee5h`^Q$#QIqSt`^(LXEq=%Q;t1eo z`dz?+r+81xrJoI>pCh@naeZ*8pLfubdq4UB?9VibCOM8&Mi)(B3O2+$tuMeo&#^+d z09t_22~wN0@kN|OdbcGcrx+KKat0ZV3^EN(9@v`P=nH{Cpi9^`DEeZ;h(~bB_vj!n zS7wO)y*L9jq3-(o;^jMJrcqcbK;Yh5{$Q@>*qfb3+%eyU=b+jjUgm?qA7C zw$cAidv&p)ar3KFgXcZnL`x@rh0c$E*UE>XPzn zQ(tip_nkgl=Yf>TDLhc>F?ruIit1T^iQtAM#X-||8>X!w#G)HT@=x`(TCKISHTIkG z0?Jye*e(>Z=UD(W;9~x=NLW=ukfN5?>+jYFb)u^fY~JtO!{S-XnAE=}_6|RWJ~|Ro zI?nK5L^CQ4UBVw}F*?&Wrqeu`?@2rgFsS-n%;!?)eKkQ@qA?5nZ|56)UbFGR(~{iM zrTLrT8il-YOsm)KQ>tk@I%A=dST!@{fpp_>UfSM_QZeSmG(0Dw>+?r*g~%j2x9o>X zVlE={F_cZj#LCKg!mES*xwpFMY?=*JH4Npfz)$YLrg!j){N5E)+SpKXL&oR^6?D%t zkJ78*i%~B&krMEFf>MJRr+#@cQ2ifQ#51%11{kLRR+5OXJy~91Q%xV8&4b^i`c~$A z(#j}g#g>h4Y|>wf*iUE*M%&6qWCpjsj0qS(q);UMSNN6^XK{7g24rWE<;Uo}UN9J} znE8m_Pq_Rfhx;RI{w^%@>ntnt@=G3^o_ZGaGky#_y5Rz>JW0?8MK=@}2-0W_h=IL( zzKRP)CrdSgD|ZW}Dt%lYcAcWo9%E^Y*}++r7J+Jvu*4KfLvL7h_>wBlth<6Ic=1EH zkVd{}ng06YX!@SZ0yRerb+24CQ^mi*HTx$SS-B4qtv z?kA-;*fMw4II%5aB5(!fqVVlG7^Q{RMtm=IS|w5E0? zY#1!!W4)Mc*z?L!s@x@*4s*#wvN4~-D3kK(OF*yhZaTf`9@%$`qOH69Y+m2h@)ln6 z?A0aP6xL3dGe30ak0g~OAM2o172$;)QDk(c^(yu@xJD9+7E10s}knt0((;1rK*GN+3J$L90X1E zH-M0x8^L`UUE&sJ*;=Z(g{WhN zgpp{#jY%gBRsw#f-w6IerbGJ-5@FEJxZP&N@wKgEZqY;`R6&-1@|A|E(5B(HcR{4JKuKus(_`Z4CB!8SZ~W24k8V zxwg&xe>9zCP?Ycc_ANwEK?wngl`f@~W|i)4>F(~>`>T|c#1bm0EZra-DZ0ehs1XyLwuw zKZr9I3ne?Ry-HWdTl2bj9?u_{9v&4?F{+^p4o?UCmL$BpWLyfhSlk2qg;x9lwEc9Et{0miuOx|<|Sa)73ZJzbGvfvx8%~m@x z&Wqvt_=DlRpR1#3*WLv?o{*(K5>+?K*Q*k3YymZ}pT)>Bti|lnvt9Ags>^xoI4b+E z&RN!P{$Q}-zd#~*$*xv7s`Lk*aTlHgPm)sksGd>Oo!!mFq!BMD1b~|{&H}VCwPH5t zID2U2zdv5n@rfawYy=r4Sn;Xe>Gf$S9MRY{^v>vJVqLGvWSEl`FN5~AEHj>u#l4l#E!57*8l47xF8{YewO6_YY8Nq$KFf9NY~m{E<@U`6yt?L zYBOt1?G)=c>!jZ})5d!?2aCROw~$n(13e z_?nu!Jr$xn2ja1IMjWe!D!l88)^(h|{KOp?dC|ey^>&f@6z`v7NUy^#IrDZg+GH)U z@@qfN-0`l9!nZdu->YVqy%`Ow+U3(6(Cky|*%ZO;k*`~_`-y%LiEeT!nzeZn1OM~U z8P79?UOC^R@EFJ{{x^qX`}$aPW`nX{YkjakkA7>O&%EJI6a4)CJZv(~tUcJ+SK6=z zCtu3pV9!2%EM8Hl+iku5kq7!%;b~kqMJFP>r^9h6beFZvg8C~Pk3up`6n)pzVn$Wx zK8#B^nc1D%RX4`(rS++k?`z&eK);eAoh#gl9cK9b^B{@#Hu>70wG0L#5hqbZOl!qN zHrLfpq&A5MMOg)=VDOti^uK!y?T%l1=x;0oA3ysFK7t+&=7Xn*Afv~Pi_i`%$|tB0GY zyDw!x|4NzV>4yPkaW`HGz>XfB*J@}NP|1~1H(}{P!{IzFq*+F*Gb`I2Zz71O#$kF+8nf;ZKRufDf+4&*FcurA;`$8uIm7frg+$3+JLI&8T2XLY#kck=_HW1UNr=^YSReG=3-r5pF z8$%Cn5*yy1LEOm;blOrra!Z2_)G1gd?3cIxJm&$1j@eto;`CRiDn?rKseaH`){vZ1 z`sb2+fAQEHBdVWxnbJg3Ul*`Zvr*?^7|;0MeID>U{TJWK~r#l%z*6 zk11{WiB!~>KNZ*Jw&N(IDw)k;TaxhMSx@Lfk$2=yHJ_LEB?mkrV^XrP9M4KwyS!R^ z;~AktmOzGTHpu!Bp7qNir$|ej=y^Qj-z~#=D)vVaZKYc`{_Gu|TeGj*Q>`2+CH)Z} zX2yeVZA0`~c!3O+y8>TS%;H9zYL|;2{+BjpYXFBVCmhl^ExEFb7qc0&$=zFogmaT} zn{~qT+!RI07Q<7wM28Jz&`OiXUmp`SQp&6zhW<-=uFPHI{bfMaR`9TS-DAG_&z0hN zn{F_HwyV1$FAkUIQ6OIQ_wfi^t)9aXimL^kbEPL_l)#!U+-q~x=DnvEw#c|z8!JE7 zqqybz5|P{`PqlBQo&nPDtPYHb@BnK-qC;ftjh4miZw}6EnL&J@g0qM}{GYShN9U23 zZveYcdiLLT3#*@@01Oz)?8-pJ9#JksP)CImCY1r~|T<3V>o;CooE zrQOfj;BKHG8D#Invz1gTVCOTOIN4Q`X!MIVWvu>+NFK$`1H-CyDW6BK` z$FF`q?r`n=T^eO?uv<8s#Oq2N-B*AdH{6&qu5X0jVtHXSgcW?hE3J(WoRGamU-M%!#id}4` zJ#tQ?q0{K>C2&%a5$UJ&mch!eJQ{mGaagiHl3tSK+PqzLf#Y!tA}_iony&9^ffV#v zjX(n>7KD|>x5P0j-;Z{~jo~A}_XKB@Lw@{Wwlp12vp3s>;^G)4n3z21m1XCnV+fvc zo?Is;k&Ku_`eB9!nY3nWGuCA7&j#jfQsjS)FQy7Jeg^4SnxwoTYwr}%_f9C|K6c*X zt!ode&w^`&KV?Qv%jY+JqOOuR8`qzi(F`) zt)LkR7LH@gSwyrOi?w3|i}rgDMwzEnWIF4{EjFW0@yO(Nho|vZE za3oH(w)4DXBIxr3W6cc6l~q62IcKGEktmR64aa%tL|L*CG43l7ALkyg)3(l-dDB(3 zIn%`OKK|y{hf__wTL_r38FQP9M@chNbEm2q4#7^-KK<@zqt=O@M7mL3*WGqxxEu4( zE|rQG|I`Ewz%%fykBUdcsOB4Nd@>D8&*EYj3mJ{ovk-QfcX-W6O7q{P-qg!#Gr-K& zyw&5Y4J?-QQH*kmbz(d>Y^v6=Jq@EbrnP6OOCVlmdQokd8la(?QWN)L;)Bi|b=BHx zq3wsYLPI74o1dO_-A$_hwK>AYdS9XMhj+fpPI_{zKb%FED(Y3$l$g4Bdo@R(x0tyu zOM$=w%ujJ^p5Po@fF{YrPx=~im>VmS`U%82A9`vu8zi!hL3>wtLw^sYQ7;y!9WZp( zy&-)s@kpSF;J*+{gBPE4$=kdk^%2SYB*Yga^`A0QB~kA+xvFd&a)du*hd%PDIXW^}=(;NHR~uq=#L?@qcfhnOy~BIu?*p6a1Iyoa1CjCxsXt5ZBK4S&XTzHF>n55K5do@bYc9^7t@tu*=$<) z_M4?#A}`mOIs~Zrsr!N;6y$jKahFrYmW7wQ4rYjR?MddTFe(WD6+6AEMG z{)X5b{=8==hDjYk`t6b9#oOtkQf~NSj8InDj&Da*F&RI?Uk$h9VaT~mPWt8gy^wMB zLx0gRPVOZB7!Kdqyof8&WlxkuK7(dD`#_lcvv&nOJf0eJ$7Hf(?~{%`&se}yI8y4JUk!2VzoADE9BB#j>2c0I$D zU`&8Z+*PbmKHhi*Mm{lZHq;g@`j9n`L0Y(-5-%aM?pMFH+ClhpFq#&!Es9eM+u&R=y6W`I z^n4`~@Nb)awxe@2r{gaj&1T>Hlg*lV!PmKSM~N!%>n0TWqWtXQia(`cRjb_{xXzj6 z3ctViVD<88M!0dKoAr^s^S8(dhN`9Oa|p1i$;Z@uNzgyZh$c*I5S##OSWsBOH!3t` zqVSq_xAvEzWzC{D9v1OU?WjIN>fG$(&a0NY?)`>ia(0GY8ElJ^xAPVLFjw8$PvSne zkybgf2^Oz4&O0G&#~3cfviPSV$7IOBbo2{^!kKg^PDkd6CaiVYCx8twC#u zyM*QhjQu5hZWmepph&9fyLn55H8M zN}bag#vc{jQbs)^DKNeJS>(A8iJ(ujj_%%ws*3u-3;QyLT+dr=^bFYr&nGiQX;%~dl*x)`(%i4If^xylHX7b~k4^8Va zcpUL*J^rx&WEg%>zk3=PN@ zJv7UE7uBJuz98!@^DV8LMvQ^V8fU?JtZ^M|k1L_lbe57G+f284^#?@j*ejubUrE2h zDf6J>i7_Ievd$+v*<-*$n55sjag`K^xy2z}fA-CA>5=>qE4#V!)qLwt&%;U<#~kV% zdG6-kSFtTsi@|}*TfzE zuFe?s4t*Fv!;FXQ5T@LO+9RLE#Ccm$=FwlC76U18&ADgyyYeudG`GPp16VM?o&4?j zL%$%ohNL+Iu9q=9#)4_J%YK_U#p79j+gZx&?o3d>DR)h8LwM46T;||L-)AoNw|&7& zwLVswlz%VyaSRMHUsm9}yPPdc(fV`O!6rsdyWHI>S=#II7-l>?y7?>*82*Y?Y1Tw5RIAB9x3s!(#eBCAef|M^c`nhwFyWUq z>;N?e^=+FQL->*+$^tsL5s0ROtL-=LYG?;N!`UW1vd9G^YM;!+WSfDnWWs|#CUN%m z;yJQ6qEX?`6=8cC^ox1V%Ojx@0r}-z2eo*}eARzg<_@qco_G!&(_acGIX{ucx-(?p z=F+@)+0^fjj8zgDr&+#lkl5SQmC7}0b<{W zSv!-{lCYI28tIRO=K&Xit4A1Tgnj9`h0AOTQpOd|^KWq+8@ANEw~_XfA}^sw#Kufh zbhIOf)}2tF6b7#qE|`n8Rb8H6V?JIzMi|9Gl&p`7`>tQ3&=24y*_kGIS1ig77ycp5 zH>hr3aH?ob-Izbu@8LQpHFWLBj+(NNHbU95;B zy3a+;qnhB3V*XUEW%holLV=3dGr_wT+}i~H6Lb5%yXZ@F@TCR(F8A$+52r^io53;o z^xk*PySa=23fH5?vzpj+*jJjl^B{^A^*~JGLs=i>&+FCAeUfQ zoo%50{{ajij!#<-8<~APzWRl{+5}_H^dW@^tv)pODR3NiTuJCqaXmto3J>Llf-O~} zBLAVsRuYr2d!fX^jmOvzO>1MgD_1c9cL(|#ctV99u}UwZ6TDn)w1iy6tkklB1rAy- ztNOh*qV%GFXEKylhv)7|gn&NNI`QY-a5lsFn70ROAar{h@8VL-z!$W2PP?@l<5?4% z6pB!E{pKP4*qY$|zRwWXGxZqEm+(N+Czp3&xr2y~sS!KzkVX zf3IqUe-g?xl-T^^KvrE$MOMH;&er~Kqde$p4cdIvXYLF1iMEG>A;(R#YlWM|VoTh- zPn?A!x{(XX94dnjlWowGyGBAP&l-}7RN z5bFPZs@uajii=Or2H%8lPth@(@72JHnB0T9Ja_`4RSGM-49FD&8eqw$2bn|GX(xX0 z3+L4v<*m~ndOFGUJO4STk=2q`!5;i(OJUp6)sR&yYo~zd_w_!ZY&KS?XboexkoKQR ztyORKWBhDPMbfb9vg;lq1w7avnQGG&l5?r?n4~74xL&>Suv{&AKWn|&=x^M%9=Fm{ zeB-$qQ5}w3QQy%EXnjglp5b=qXg2*wi^{=-7uJ%tD;EcQ85btBQVP!nhgw%5ebzAw zwV;L1kzZ7#DVP|8+uuSrjrX;v=)b4YM~N71uNsNJbm%1wr08tT0FDu8l2%CWb>K*x zNo2%zHr5r|Dn?48(^osk`{ic<`d{{IXmiv{YW28VD#4US17J5Aw0xL2d!5CI^V_tk zIfH>Pecmu41)fFQ|B{w49|_VUEQ-fA;tr#v68P-C4;YJ zM?IUtir9{{!O|fMVDB>i>?WFPD0Z3V>`P-zB%?|1EUtL_V+Swx~EOGKL9kpGhXBASCzF5QQ0XnicVOd(OUYUA6VUF_#2 zzEc&RE`*=ZRcI7MT$butRKD$X)(SEg1Lm_PiaKMK>?%%7WshA{|QB;Z=lQu2AIFo`0Kt?UvUzNzqLtp*R|m7OL0lcU_aOqg^= zhJtYTNi+;P9on=!bkURyCfp8&S9)siA*%x=Tn^*=6|y!DRQ|I`dox$bRf>Y zzjn1g_y?7Zqxl$y?_FX3yY~xeRpdYdjz`}R=4Z&~dtoojO1rK{JmE3FJCy=!?xdlGm z^A+>GHsfD+U-IuEKnDp-S~VV!Iq|VHY(b^+-K3;Zt=Z-oruEE!MBcCs5(=m+NZ$<}#Vr#Rvk(yq?m0qWN1OWRMK_30Yt)mxRkqA{j)0tg` z;AG|_y&y~1uE+TI7$YD+ROULblYaVET(fIc`y&wcs_kTd6_+hNPH%e|S*`hyE#7(Z ze6%BF*)5gQfFiH?VF(`F$R8$3>5`bhTo{@D#P} zBb~ozw-EI8>uOU zA)drKek!tR(g3}@bb#xn`BC7{eM}P&Joe(=uPFP6SCZf>vast}9F>2ivfq?vbFL4# zgvZuMVkl}}yLXAqi-1X+HZqIO@>uK{tMQp8tuSl2L*Gx!n>}y{6 ziRO!Q!Kr&5z|C~l>7P)h&6*B3nhtygk4U`#1u+`^j9LXYM5^~~OHb4|c;CmZ#pUiB z_xDMU-_O`gHu}FB_e8XQozY$SxAOjU8s17H7}*H%ZAQ%y4!aVY-!|@o+p|S^(ufbpsL?PC;1tvlLK3Y2^newnWl&v965B*6wRK)&Ivu9Gd5z^qZJAoL4l4gbM{KXYrv>p}R2IDM=tyA9Nu zQR={i9J?K>$Pb1R@^$K&Kirj6bZSFJ<-whOs=+DO^nc{$!D^EA;H?19K_{5L(XRJx z7UVR~Qh@f{pJ95Y{&*M2Tcicybv(0)>J3T3&da6+n^x|So159!<&SFp&sT5KrFp|@ z%kRXnjgFTBb8nNhRFN*^y)I+qF-)~p2e!niSH%9}dn*!DQGZnvANm7^*9LadU;9Ou zm;|AB%b(OL&!K1KQI!h*aGpj~R2hZPj!hpcqU}9Cu?#;NR$Z2cpG4g@$4>xds)>`P z-ESrdtd=b1d;`Ubsq0UqV+`pzNE-vVI?p*4u8Zn2UFgqjqJU2{IxglphZz7KOa6Kt zbdJ>R|JJ3$Y0IsbzW5u#Etrqde1~q0lJZp>0{zz~H2NKvQk+*OS%!v`_Cb|z?Q7js z7ko%Ri-(0pnp?ghU3mF#V+$)6 zDU?=Xz+!;531{Rt&`Fpa$OqA-QdzG+_@T`rXnp4V!Q^^fr~kSxuKB0uk_vnh;pM_) zb6t2q>=4dmPNe1Qq`@1;LxH722LRn2|1sCuph4Z0w3FcB1fH?+48d-GMB zBMW}~d>Is?r_~W~GhTubdDjWIzd{@lU_J39*s3kq{%S2 zD$T=cSIZ9SR@;u{{WiA@H+}_B$ZEp0#Tl8>$)#hNHiRxSPXv87nw7>Ua+KeE*-5&N zgmp(y-95{;x`Fk0P5SnDbl*nWyvQ~e=B#KP`YLB$xe>{d^sKg?m8smX$L*BfNBAk+S?y%bP`Fue0A_WDaRnc~@V_Z3)^Cm&r(~4i*k@nCFCGs|f z=_41_z6M*z{yXbJU--=d&2fKAJ<4FP2(i5CPfP%)ys-ZIqs*xAp2OhRd4A{Z?6)ts zlY4%BL~&>P5(Z_*tHGNiF|1`g8*vSpwl_x_bNE)*)1tuTgi9bC|Ndn3a+PljY?z<= z$Bs=w;(N_E5f5KlWtOjG3KGIGX3h9up}@8|`FL%4p-I}@m*q6v%MLF`Hg^6a{f{G| z(4c1PBtZW8_&QDcooCR1J5iOzf6!>l#X!$DTgvl?6m?(sk3+_jsk2IQ{*i26pdP=U z-Pn~m-@N(_zLE(8)(SlP5i34-8<7hlsCPrImzGhCMxGv6JGO~L(+WFwi5OIKg}7Pq zoPjvovpI0eGN(e>40t&7?PTA){DWV8m9D!B9R0b{U8IpJiMB_x-xB)lY|9V7DKn=~ zv7v(G>L3v+&qJUh_ZsQBO$y(ABmdN$YxpVNW72%_REYaOK$O{OtoH*ookmHl^k-)ayfe`{_(g3tI$_4doT2_ z^R?BazrmKWCBONn?gH{gjKtP0BpM{_Y*0PUtd;S>3?=ZY-aE6 z`Y#Su@jh^l$9Sh=yk+Bwxy z1-Uithkrmq^{(D(qX+P;h=Y(0P0#2%2N2D70x9Nt2nikDM;wfwXNm+6;E&&tUwY2O z?WC0fpeo(js#Zt000Wz`ZI*HB7~%O-Q%4G=Awq=Ast$M|xRg!jU(G`Min7b3_bye% z-<0g0z$ecg5gGy^XX7n-STK&E%1k=N=_=VnP&SJ-^VxNj;m$a=xH@mw{*?({XR{l_ zcj@a3sxuM6}?&LPDT4wj~!x?_n&`nfs}>xlr$o+^>DkSzFyf zASfr*`+(00GFh!OnauMLvBTNTHuy0;zYNHwJ%k~`BWXg6KM}8pR4aq%?&WoD6Xzv$ zp1YSnAj%}qTs25Ln_KjKE_%>u*H3-1`8S4D-qJ^MU)a1fwY(og0$#3Iyow z^qh10b*|28xy$&;==Og`gwsEh{vxuP^0t3_?7D5%4EN~#O`W+aQp&pRaz|_KcxUCf zBk$GLWzzfVAjf_*SDO_wx`}t?D5%X2Im0Eni`)=v+ed{W&uH6ZTyT}x=)9zVH#rcv zJ=PSyia^B|VCpXWu~PTMvay8xhxo?27BamDIUqmCCW?9{-lFihT%mxhNP{z6R@YXh zmU(VR?oHP}j;g`gENi-549(|`Y|iE$vt!6=Wp8G4(cG1-z&(J_H~SD6zALcKrZ!9M z%Fp|VZ%%U|epwCi1w(uk?lU+RI={{9{b!|87>`@~Aa^#=GiKENMI7G+ut(mu=y0H} zS`iLK?}eYGi)fyvZHJrM~1z{9U=rJV3w z@vw?OhD7>6z3m$+=wgm{K-p{+U7L~DM`^T%suHo`&ecV~gailZnAeb2y_r>17LkdZM1Ogsq zR+>6@^SmhS75ire4vrJj3~#=d2poVQ;HrfV=j5|nzd*Pnn)gIePt3uF@iSbPC|#M4 z$4wd}^**%k zNO_0j7+rKEfxb`MyqW7z{6D1{!oVgXt(XuRke7!GYctHH#@oC&vlLmUejkp%*44-mM5S{7RpFV5Wj~m!#0ed_vjb=T%tql+Hrqo35LAUBg)_pnDWQGm4^=_ zA5Gbv`Ao7&p?;&p#EVPr_=TTr9*;tbt+(M9E%hCSSzYORA4^3@9?Pdra%R8!YMN#a zv-lJiljeoQm3s{`VCM!m`8SvLeK#ysN?%<9?^%XxHd5+&bsAX5l3b`ii~szx6DSxd zRaFx9bF^&6^ZA0G2URdHzo}8zC^;26n`%wh7E2heTAxu0%_}Jz|CjtBsDtf3)2ys& zc?(vqAZz4&(vg+uUngEwvBBDpBTtY7Om@s zf!1((EM@@pltqDMM*}(}lPBRta6vGib(AQe;Unt1!1)C-EESB>8m3qxf&O+)z!6TeR ztscO)b4;upIw`h^*3eiTi+OS2#_8L#2!xSjt=QR{GBR0^eIS%!QXanW!GIq_q`87z zLN#Yeg34mvotBjdJxn6!2i;KZmgMBc7@XEhJqy=Tc(2&^hy-y3hN!&zImlw~P` zAvaUtkF8Jm6AuwXgtP`EiSuS=z~SWsguf;WQ8{il8k&aYvLyw9?x_JBX2YRC`nRN? zSU813(3xIXkX*{EX^9tb&bFSZ!t)mz(W)M()S7v-dsE*reBzM@Dl^neJIqrOR4JdR zN>!DOA&kQvtA8<-W1WfW%rNuD%*aK2!AX9hKNAOa$kJ7a8x>iZdHV8(#`>?V(8@3; zQB>AZtGYL1aU1a0#PSkWJt_f|{=>g?Q+m9t>GXIeF1iLe9`V|_F5>&ZF6rH6oE6p= zFHvl@%rtoAxSXeCy_k2cOzr{vP9%?@7Ecv`U((bV@T6Y<*Ry@SpDO2vKiQ1$|B7Eq zkX-aU7vl|wH?NQ>MSr;!8amyeN1%m4Um7CQP^Ec(sB^J2f4Z+@b78=+Wep5x1Mxyb zz}-_#oFBN_1StY9`CGYL=aLF!9wiD=@)brWl%poy(BZW8Hkw%}=x-tmS>%bk$w}NO z$-EdrIK2eW=kx0ON|Ae$H*zIwHKPA~*yINLd(-82v%D5=E5JIEk$a&4(l#B_VSd<@ zHB%MR_WtKE<-)lP9QXy1tF}z|Cs~Cr#ZWNq65n9Tkqo4us+ z#@~iBqtme`TO%D5)``r<-Q+;uRteJZ&$#dX`A|iWrvYlYya5(;kp=fN$M7pjoz$S4 z@ykaSBZY{va#8w1#>pfch>(Wn0sSFG(^+<%V-1o|){qCD(NO#%(wBPITR_6`BbWBI zI0r|rJZ9re_<5%Mj}2L~PXvVQ$Bp|#ETmjqd@u4}lHIYkaxjkhYW*eM`k8~}%8>-% zxBTAUA3WCf@8)xEHLi5^Tpv89XFhB`?Qa>ryuYh5nq}L!yp1|OOga>H`v%O~a7gB} zJ33s84Ig&`t{d|Gu|>gwd>*Bip&c==;Mk0tAMbM7|78SrlClcNjpC6RfzjVIjNp*Qa`T}wEdFR^YGO_{##2yNJit%t)~FT!NQynb!PsJ6}{=1V270O4! z)Y+{0&^50{9ZQzP6*?2*wEvR2bb^WAQZmVQpKLFg zH=PyU&Uu|#08DP0qqm{F?DmFx596e`*0o(zjTk$rQtV(o+bUIq(L$f?a-D#QW~f}C zZs%((%Q^EIG^A6x;GoOgJC3B%~ic=}8b8 zY}LNu;o&c+vsUe7eTk4rUD{^|Sswi7O06F1vTf~~cd-4z55N0^B`5j3K$daOw**rD z4Xa36GP*N52#LyO-YLwpgC7IayXo`Q)aow$o9C%epM0){U%V7Ri%!-o>`r3;qs}Y$Q!k7es6fFk0{)lsp?7)ExE{Ks0FV@j=VQ; z#v5~fT|VBIIy%;_70TBedkSN4N{dhbXsy8%sYCiQyYCw&Rcu*ms$C1{c9uOaW~DIq zshamWQrLa%2G3y;Bs^nItXntMMPXs9^N*A$j7f6r+_$~LC>9>)eSNToB6$1TM8;n}N1|gPZ0vo^&2ja+03S;K0w8be?q+K)WQ({55m%o{nAYX$F6o6;Y$d zKBxRlO{t+FqG~tj^x8j5@bdE@yDt-D>m6YkoXC%n9F$MY-*DbC{Qm=V&)c)Cf}=nN$$C z9bW(du^pEytZh{yGdaw&l?AyzTr1cvaQ}evxLU_??#+`U`0jd!|6=Obu-y<#$xwb7 zylI*}a4%vr*PjZvT_I}KwCpG%8lj3Fvof&w>T~k_jrA_$3g)xX+B9(5y>fKBWSXM< ziJkBRwwRH@9f4p}mU3TWHraWIvOf9pY6?(4m-x<@vGL5GgzTeW?8W#Ju~3W)X~Bo5 zwKNPOjpV@^OGYe`S}`p)vyTJ#^ge$wh&i8Hao_h&S|tXd|7Cd(FyF-M@<vU1-ahjd&d z0-qdZE}fwk$9ZNUcLR=SsdvF?zRTkJf|$Caz+?X)XB{uZ-xD-&)pz8eA zee0QNONrs{in9Fc809DD=O3rr_nrBl-_jBCqslc*EBvUN@2ky5>#80uou1B9ne$h3=f}IVl8?xyxbE7%V7WMjCfr$-N;L|xdO^w%F!fGKhP2Yn zRN*Xmhy9j`Wk*FltOPb|NgWMj@5Dlm0!P>9Iq@=|!TbBr5KEM2hgKR&wGBdK8@O>@ zco?X0p~FT|V5J&Y=$&JwFsXc>mx0$I_71!F;B8AW$z!Tg20NMtU2En|x_2pugGc4w zNjy2kKP}YoRI1j)Qcb0qTSMQXZilEWrJ53U$oc2NAM!iay%GbL=oaA3yjF-pDp5-I z&u&I2UDsx{T_L=+J%Kd9N`aqcGW`IreA{@3a;gtLIW_9Q%0b*ark_j&TBiAH_$oJf1=I`%2%ICsH)D zkaJE)Xj}iD9r9@(LjkW#r{@r<7NaCKpx0HA@@3xC)P?fg0$hb)Y-lZZpmU<#p-{5& z3AgcuH+RMbzy6$CrF!w8V+ht>mpA3xmwV+GUF~3P)x;O64D#0OGBR({h=MKj>&8BB zrOI~P(<9O$jpR7h4&NYTV`dJm(dNFtB)7A;x(X}5EZEAPyad!PNEgJ*YC8dzP{LKk zr6ANYkU(>wRmr8wpH;H^wlK|EnML_xGGqmXbz+xAJWBUfUd9J>K)sr*57%8fExMi;pf@|O(my9SdGEvSv%+`M@BBW_-7;Vkj&Q9Vqk=u985Oa_e`EtN` z`yyMd!PO0<0p!a62pYl`jY`sTvT4h}Ma538e4HSwD_R>B_uar<68nV_uV(CCF+X*I zyUQ{S{7a%s8oc_uJDP}75E@OPw{+hv%9zsF<<}UeDZU=gorNKlV9>uG<%DuY6en;_ z9Wso--M-=~(BW=KwoApLhL)S;@i$t!mzx7$y^}L;02pITS4eizx@#%JgTF9pX%A$u z2ef@+;ppzrU|znrT&(|q4`Y#(C>im3Rd6OUNpWQbc-Pb;CRj)NIN|nou-W6_@X+wO z^Zws84SL_h85zp51@31gU52(fpnosAvtY%Rn%#Lp0cK?2BjP{hv7?hq?Vzk><=LCs z`P#k^K(*8Z(UhGks$){?PMEwVOl-1XNw{RK=+$1a^wYen zu}SX(=P_hMmAf;ZbEI1@7gEBW^g`t1w{MaXU|88xMvW-imbq=UpE@1nW?o6kopk`q z_0E!hYL-EL1TO~bPUipgKq|frcRG(eE_i4eK8++CFHp-ch7~k!R-XNr8Qd?S`X{LW z%W>_lN6@MgPV{rg5HmDHtLaZ?wXa1Y=!V3~Z!;ms#m0SB_FL({C*_7h_h+;(7#R0bQXj`?BbaE83N_2&&inAhSWT7{WyzHR-_hRW*BwwvZ?li zAhkUEBQ7s*%ueCBjb5?MO(3S@VnvS|tP<)w_by%;_~)psfz?iDu&6N9=b`4VH(cNm znHlZU7i8lRx|YrUyOmw7ANnu7!X`O^o=HnnnOS1wCy6Lsj--%$wRWQvq~Yo z2#d(cRqXSt?m;n~b6X#^sJ-80>UUn+;A^DMWha7=crg6&lN{W>iR-l8_8r3G?Z)3wPNCYNvB4)BTN?HRr!1 z%AeXQFA?h7D$dS5-N)(i++6?*wzs)hge=u$WbZoNWuW6zeH#)qXTFY?3VI4a`To!oA&>4(rk znSFtaANM$K5^VEO`j}}jX8*}+Mw#>i_M=YhZ8A%ZE%C}VB8xHkT0`YH+2|=)AP4$M z%g=mt^tXRT@)IhL>XCH3g^j5@G?5|$h(yDU@&sKg`$M3p2_;i|*EBX^*ITxzoN8Z< zw{O~xg>faLli2Y7tEtwHJagxI^Zu>;%>IJ}O|3x*B6zhlk1zsPq7sRc8aUTsGth0_ zzL;EP4P~MR?8646B2C9i%|@leC~(T>nXOumX>JO^6!(d1i)$^;CL$Xxg;u3fPNyI_ zNO5r;8vrf`UgbH{QTQ@&nIbI*`^6)3IoWBFR%|7=J@cU(+)C$|4W>POsEk+W zwbwZ+!cCvg8Xp;@9Z2!^uyk(yLKwa9aSV&)vYFQ+GE5K|%2%DQIXc!Ii20f1Lo_BQ z38IwTn+_8i6;imPc!s&7fjc|6ZeINi(I34 z-!JlFJ7&i$7gWl|;@er^taITnzvB*M)Pu+iIQgw*lG~EPwe=zg#22 zOS}IfUf+yu1^2O!VRyFcAMIeR%YA88+6KMvP}F7lAEe&#WRj%Qe!A#Os$ubPA-1ea z6tGhgvn40$vHEgTHn%^PNAP6|!+7S#8hl0H^8PdCto~#n57=%{WuWg*GObKmN)Atl zI9Ld=CD@sX)CMdyT1zwejM_aXZe224r6~H6vp0CIZJn^#=XkU!A#(kYE@4IhBKvt@1hfp$Mi#;o06eJ+(>wEGEjQ{>ZqumSL9(;k2YJ0M-XEpz zYY7Nf>oNF+?yu5O1g7wQ6BD)w?v$OMw!<`BJ-)v!FcugKa{qKT8uI^WI_sdOAODZ5 zfCxy4ARr~(EmC73p>(H`QqnLG81mR2|-#U-ki=G@4+D zk7DxvA_s==OeXM;a_9ETb4|(-_RwAbm2AZ%GmPa_z1^1L|EPbbMD)(_iqD)X31i~P z@!yWaOmI_gGdf$%e|H@9VZ4x*z3YGjb}8<^BeNP)g1Ji)zD+!=iQJlyJTALz5xA4i zh4%bT@&yE#rWJ;(p-rmmmGrwk-8c~>CIY#qhyIVm%w)WE7QyP8{*(7F$I@1!(l9ki z*s0SH*)HMp-Rb-Y`p@1u$C91r%Rp3yba-v&mEox%rdmz8hnYl1< z{pM2GfZ65qL10jWb9OVi1!~k-3rDS?_{*bi0KFdzgPx8awRx-tmE5i*`BtGW_N9(D zCon(atOAs1l@cBp^TJ8GoOP(X2n_q>T~srj`lHmc`Zal6KNx(!69y?yxpPEv|%22BHr`NHL39N8ekz$kAg9y0Os;LQ&acZ?;PgUrp+=dPzf-vm|ljn_vpcL6aM z6eNk01RV4s6TqqTWuakqD2<4%Q5ZLJE^^4LiJZej+o*o}$y>%?@4Tyr{zrRah-3V{ zMaM2ivrh6fbDta?I^_YP#Mx)ua&nr1z7Q zozP^HObCI@yCek=kC=gUf~n1_e9`AgZcJ$6AP`h~Nomf^S-|{_MF)o~NKxv1ZXvd7 zI%5(OIBM@RpVxISu!xLYsSE<(Ga*w|=EwYUTl7h&noj0pWxAXvcRL5oIuP`Xw~pBt z;Z#L6gvPegTD!d~4HrFmUE6xufdl3i1FSL;%U8R8 zwVzJC8geCFm9a+^%VXAAlkVQAvMWPtD0=^0*IQ58KA{wKtbhLZ?K%i3_|*AA?R47R z=WIcPsCv57mi&p8f+C!md&F2u!16Uh@|zN@=xbLAp^M{9>l41Ldr;~pketj z0rS&DNP()7xh+h$e)-!uO_a4M*9ke-eMuX9T=|{DqB|QlOP8twLpH(!lGt0r94;Ly zSf|$Tsa5&Wz($cz?$$XF$WP-sjA~3wk7rdVnIU3d4i`?GQ8^cWu!e2c42&+!%$fz! zFBYiEyyTt4GFs}uzxn@Iix=Z6p<8rn*6Zw^o18y zy`LTI>YmGN(E&w~!(9I;N!|phj;n5tOn9rUD=>%W3DIcz0FW!OEDD-OOc1PO$u(_QcKxyKDuay%z-0pdTIIwY{XqtrtQzJ^ z;?iviZgyUlC{yu8Gqwt7fH#dg|Mm|ehLjC%3q1#`h-yDvc+1qyh z@g?VnaS|79*(VVbeZvA^2F2fny3c+zJ4+qq)R;!-cBe|Esfx~C!U}KLM zYsJhcf%tJsw?(GN>G}K&PX5qoSHdhoKt?GqF6Ju7elhL1nZ*vUlPkJTzwtUK7xbb? zjaS04DITiXScRg~DFbL~iR}Sm2O#+8eW(jijuaZUCpG`yy=a@UwB!J3Diq&b=mq zzTuMGFN=UE6|C)mpBL(e$I}rx>G)^Nm_{(M^rdp@EY3G-uA@dod|#vhh-6R(vpr`u z5t#e51_tr!(zMtVP;F*nlVU0x^{FZ64qr<->hAtU*u@mSra$sI)89~^GkuI|SV5bh z_RB>o7!Z?4I8iC4D;xSqzV)65kIG3bGI@GTwHcr8+50A(gdip3SY_EzE|C4#3FW$) z7n4aOQ|heSFV?~(9>26Q&a;=UmQ`_6VfwnGiq^X}nV&yvtGY8!KVRK?oxoakBFc43 zX{MB`7N@#El&xAs&jeGEo}%LxW0gjP){g+$Pl5Rfs?L>Nw?D#+g$23Mg!bRT1lwaE z#>D`Ue>UB?9dZ{T^PRtTqw7iC1HXM?bupOnjLKF~X)KyEIB6dxbSYA+m@yuv%=ja( z^qCzM6GDUEey5V(aU}c2dR2BLy4ck2&+XTfeAR=|^=bZX-$%;X*CCWYKS^=iJ}mrP z*5e241|mJe#>CYrsO@3fi4*52vf*X5_Mp&ZQRFi%R<=)ZmJ>XC5{dZx73%?9PRSWL zfh1*nH$foR>7pMhI|NPUA)!xg69vhX!;+;=>kGbCZ@*oRN~2i%kseu z3NcF^-fzQwtTAjsJa+C<2P>DR=6DkS4U*|LqTX9#Tj+PR+w@=UpmL-p9i0W&A`d!>}^%T2GchZfS^`CIwgeSqh~ zr!<+vrP$d#SB)^jUwCwJsLS4L_*19Lj|(ebaP=7eyCWYD?R1A+N6nXmvP)aujU%lg zZr*YC#bF08k=lQRYp!`(2yOI(BLp;&D8Ws{?Mk&C__tS9Tp%g&(uBC{-l*i(eC^HW zW~_n|Y9;eh8=bZ&@VsNuC2euf7kIxi(XLw-kjZ!Wj^kW%#{(_f5XipkVS5X&I%H`i z*l<&P-9sLPk2b2!0lo8*UbZgT^wlCA7_v|!Txs*5@Lp=a2e^&Yb8SUX|)^6rdNUpVo5L17(>cxmt6rzE~hq56f@mv`ev_5Yq=p~`p>Kz{H3Uui}k zdbkV<*?L{Rs1+A(owUnAJdwkW_k&EXYQ$~_k#xOzQkOdG^D|(yHeiutuNghY1)GI> zJe!@Gs_6A<_{Zj2-?BXT`NR~?> zo=LZoI~utDu-X-~M1%uXbFG#{Bfkl;GkcC+&=&ROFBSbX`^-nQb61zh}n`ZBF}C0*B7U8|!9*fgkW=Jmkr zr2c8G^CAiM`PfpWJD=~)xb)17FWwU$TMEGSJM6;$k4w?{&fSTh)#CW(q_w?7Uf^B+ zwh`k2ig8xgH@-pal6@hw_BL!i|F*k#EWfRud972hLS8>i(*5?dVLgHby)f!L)T_79 zt!yN~^%in#+^ z9ba_AbYbP*y%lwV%j|n|LQxqaMA(NFpo0Ma?!?#s$-Dgsir+1nq~HF&d)O0!HMw}6Xigh1g@l8x~!tbC(eiKhGrZzPH$bOW3L()o4HKs)Q)hiBeJ zh`rsl>st=Z>GbwFAd2hvFn8mfkp|lDdoO#*Fqk!xkEVixtFl@wN4K1*xd``~kuLwW z-Qq{T1`)$=>7z;*0bCN$#&Oq-GxqP2jp^qRH+kNoIcc^u5f)RCxZvGbNqPAJ^9zN> zrck^ci~|KfH`#B{)36@}2bcnbuiVW1FQH|;d~K06@hIa|_s&Q;WAjKL`R*fqvDby| zEwNXZ{@N=dq;`0{fsj1rht%A&X^|=dMM=4@?;EK z&s{9LGVWpPqyI9s_~jADU*)%qQS$NPwPKTu#eR2w_ZixOj5$BUQum5IU=3<+lA7}M zW*x-;22`H7026NIwGYIvt8nk849hh<2hsAW#wgerWV}@bK8R&{h3#E@3?8i)E-J34 z+jRSWejD3^exet22Q-pIJm=0u$sN!~uA+3zQrz$KsE_g9RFJd4N3V_gudsUY@}O-@ z_8=#MJd8&VN^tl_%b<{Ox3j#4v{3Cj-_^s?-+$tuOvokgF7z6@oqfn|H{ zA%`Bddwm}84UN22)MrQIWF>W#1BMi2s_H%8y>wL?Y>LsbGpE5qhoiyI?Y6p$vU~le z4kgN3Zo1ilry2SQW9xVMBpxO&k_%m{=0HVY$d<$+9nbjlO`Tpmspy?aa2K;_ z>e+2;_blwt+FTvgI}z~7$HuGPVRN*Ub7%chKo6bj0HPfB(HsPxy=7ZVykHE~uIs7g zkOFYaVJ#XH54nm*CsJ5AKIKVW4ybbQ-35i8N^0L}CTsL-aM~CIUg7LOO{~-pCXLd( zvvu+OG^PVwtSR-H0Qm^_x4~C*-A{RJOk5cC8I^8{=c_08E3RR$+qK~y1`k=;bkff3 zqKYwDw^AT@d`bFS6kfX1RkgKLJ}Hdv7>(6B8)jZW;s=20Z|+GB%_V2zkPsMAo4-;SY6%NVHi-IijC z)Px37Inxdq>)_2PWjiFJ6ZcD-pP1~HTgS|%dhi}d;_g!^RN@)$t<;m4hv7M5BLIIo zv}iAwYvL=$xXx4d%2jklf9F{0%i@5#pFz70$m{ZJiIA!DK!x-p$*~MyqtE&W zyZls}FMe6*P}4C8nKKA)LMy*y$pb0z_XmB=P^VJgL+_PN{nt%OUn~(vXso+d8#O;$ zg&n22!7b+}Ma8$U3c2H1$3Nd;ev5g3CotFj>0)+IW-8~uIKOYULP^_x_%Fu?;meRj zfZc)4P2xOV*VLfc?OLDSYiz#1d_ou47nJl4^VS%Xfdko!#s9RUlVwuR%2J<=Sfas;pN3_^?)Dv64|vf6+^?~vm>P0ryCFb zfc#Wg$VRqP2U^~4&ztxefC7OwI={Jx4T$gYGqR9`jAWhO8x|K#A59RL-3{Y1oaO1b z#kR=dt9}DX$H7#LGJ-a@dP(q$yn4*8N`q@BE$nibo|)kL?54tSls8fA`AWDnUgqUZ z%^gJq{T(h=pPW_;ef0bRx-V@E`p5{7@xMs#Zb4$lUd9|?%KxUcw|Y)=_PeS3;+Sm4 z3O9aFrM*9`GpCb-5g+r$h-wC;V?Mm8GJ4WWbno))ZFj6@;M%^fN^{nKo!5P*6GLYtp}QDP)jvH*{9F0En{68Hc+ zE_%5Vz^W*pkZ)z8=$^<0Y`&7dQ=fCFIx17b1(lBhXDH}1uqO?AAr~bH#L>}Bbj?|y(qNT zYI5I*W)yw9h*SBdsApl##;@~0@02<5eulqQ&#|xk!1trJ$r`zx{MujdXkw8SD2P#& znTsI&W`x^9zmkG;j?<=0u#24CkmveO>q~g7(uF*Os-Vh?VM_ed* z_;oIqdLow*7!lUT6Uej9To0pZi~SrO@(tt@f!jY?4{;M3UMdIGcB;j^@R}Q5G-@)P z5!kMUIFacXz24L=YdY!_sFd9hVhZSxJK#9-=hW|TD4qt|!E7j=HI;<9AGeG{EA_#} z%$hnFu43~l&k@2=6=U+>3`&=?waaXjR)Mi_OF8U z38yEx7T`cDu3yRGYcq}YyZF;yo(^iQ{Ch@RWcdR@JtN^G+&~eb=?+MiC;Q0XmazqF zsD-4X4~&|wxxc^qOF||hNEx(XtV;L=gvsBNU%Y}qG(xb|nOcdv8XR&(w*w~D7_ydp z``rXyaVmh>IZi{HwlkswSW&SXr+N}(?I3!Y zX49;w&-csC|M~Z2*9;>olM#t%)e%!Yy2%med0%_T+uBvq{P{p%|M}f;tpx%HGoR?J zJ2I)fu}kH;Tp9TQ7o@4u$hhw>XkaEc-K*PIILP9LQwZ4%^csph{O6t&gFkGkq{n^& zXje>rsFcu&xI=Ge)+*iuMV zb)MKJDnpfprJw)x-C}K^2><1^=x{43{b>)|rfF8l>&>V0)5up;N+Ux4Z9QeC&!k*N zVJB@ob=KOzg|nf$YFg!hL>|yl5U_mk)lp0W$UVTvHN1bh$HD*eq&Qv!<*ezr&}q*D zK3^lMwV8Tlvnm`R_vN%&8{Dk!zjUb2C{?vGK2 z7mM>!qS)|o837YiaGv+5b!L~vRMS$if}0_ihvTljDa-951Bt~ot9KV!O7DCyI{sH8 zX??u`n`e>m$A|rqV@9wm7eS+pglwNLD)d{P#SzxZ^5>V|YCr4iJu7{n)h9UmdzOWj zA5z`RHJQNtR4@*%2*_r)7peW$C1i4=mMu|!XCv5Z@IiYd&4zN{oj#ZUAG9!g$o%%K z*r>^0-SvD`e`@Ld;UHE7&{SEYkZLtqF%}Wrb0@*E#$e^6j;lkSBnSzMpL`Zj6gA_q z&&ypY7v)YLh1D0NR{{8wNrQNCS7r6J5{}SmlXac=0ObS z%^y}eqk6^!GJuDvL&(6gtx2x=Yog^4pWMZyxDj2b!k-`R0acWjeehVaICK~@6SA(L znopR&Oh3{tA~KO7c&la^7*zynvQntFfqe5Q2UHAR zdBXD*)D3uX*2vuZH(pg)d`$i+VNz56)-&~&2l2j0=^gNiB#>(5rv}P$oqm5&c;akBcQZD9Oq~&zJbZ#z#n(z2E z?f`D7+_`PWV0u8QNAT1m=}hWtqoy#BJ!%2xLBN9Suc(_|{13gyHy;Nw{KB zPnY1gm8X1|1*)2!LSl;MGt4oTKY-DZhgNB{VUOrR84dhD!v~wyybU#s-?ezLfRBF? z)k3FcF2sKRYOWR&TeZ*a2ps+Hd^w##^`GM5`}s-<&wp+dJK!wMGwhLuG)kbgI&C$t z+!JLQeKf@gpV9Q2%%!Mi>u&s(@~#w=<_F=wQV3!hrV=oU!`y5rQZM5@qMS-d<%8np zfzvQykL;eJk6P+%PMdQNEM`?lv8f^rv!4bHf8<1hNv*j`$~ee6eNlb2Qqyu8{>b&g z{kSx%$Vb5xe*_MCLQn#EolkqY)EZn{Y105y+EwHujVDQVW?!n_H*3y!9h7e`BJb$Z zjG`Vffp%$-R#O92Q_DGBCF-;->$=#Fy)~$EG)-HaaUTf?HhnL_YgXsP2lqb|tM&bJ zz3m*zrI97(+G~t~q%}o7(joT~v%vBl?Cpx~sp}@%<10;*G@9r~+<*upn#Aki5{Sla zwf?Q*zt?5{)5=lI-7oRM!x!+n1p+-9JlcCgmHB7*Nd?IwhCRiE1O+u4Oe6eEk{+rg z9aI$C1c4263#Dg-ogi))fYebDT}M=z{_aa<4}o1 zl?nbDMKdbdD@DAlYZB-Rz-GwVn!`rAX^tl`C==NP!)w+Qv@77eY;v zSradoK#yg99|7I$naXz&t{Ak%rd z)nax>W8oz2A?qYx$tz-O>hNYMcKV^9~H45Sod9_GP}NC z*EO#}ttbq6HsniM^;pSET}t#tkyc4>ha;N;(x|Ogc!QC^(S*D$F8Q7^(^rZsvO$^; z0kTDMO`6B#UKzEVNs$G~26_~1vQVzqZmjCTO6dho&wxkRmD+O6xJlc|A1EB^JHDz} zn-5OBerL>~oyew>?hA_D<(mdYkAxLj7;$f!mMGPHbC;bkPvHb-LYTxH@HB8s&()K{ z>{rt`!xN?R-6k3X4cHEo>~9|Nultq_l@}#bJ+LYn&PoVXO~1(bmgdQ=`lePTh^+FW zKa|J;*$xcuK#eCmmG^SA@1|HSL^X}(wGJCXnDWWXTC4MPtdYmM$8C+#6ZQi#dJjcw zQXnt5zG>eo-{<3Iad7I^R7ohhdAl2|Gf?Dq682nRA&z|BXwfFe&zv(%6RJUelEUUq zU1F1!$H&xUtXQ_BikY$Jfoe#QX?gvScR#&$vt%Zlv%Obvhndxo(U{!Vv~+mVcS(NW&6<6|LBw; zY*#-}Z$3{p`GK&-v*{wmTx z@dmIzMbDHWLr-nrMpGl9lOc_v!0Q=_5lMGXi<#mjuo2v@ByyCp-m=%pMc@4eHF-FT zOB_1?e8a@8P4{OLY)JAxu8593G1`LD2^nUVHr=XpV{wgXXdOwB(j`fYST`VTE* zwy!U)iT~+|3EjjBn1$?Z4e1qnBk^3;MU;P=ZwVbS~PoR8X&G0*=0` zFV-Ffm9bd!zpH()*nFV(hAg#Dg;3s#d*ZK!yTrToAiq?N`2c<_aCqgq@T%4kCb1n-_LpVstZNkteHt(4|Kun>Xxlb(9IfFd(W>SK<^E8u71+&iJY{P(gySa@a-y@zOaEHsf? z9v5P>@3qQ0t^>fNsMQ8FE+$SqX;xOXu-)mf&Dx<4iX*#B335#TzAAN<#aMf+_FbDZ|Bnz;M1lt2J4hac>r9tZrjIe(# z8eNW_jeSU&E7MBThM=6I%D$}vT`__dmW@&IRpTpxeho{4G-QmfA;siD>n1q+Hvbfh zsN%L=Qn{3g4$Y$)R!%9Mj9QFibtyF{M(hb83;MRp<-Mb?l^#TOzF>W(t@m2bVu$C? z2HOOpBv=O&No|%49synua#qq6ZQNFrCm+ibs&CzKV^7JiRL3i5c-1HY>ry&v{^tQ7 zAyEUb1?g3O`KJTu2RBG#DhuSHtkH3EPtDKKuu>VzXNI`*U-V3Ew`$MpQ_)b)GxJyk`> zC$+@&yDuuxzegeO+EgOkzu%FO&#;Wn{=Ku;x;Rv;bmBQz7fL8&i)k4QGrwG`_7>1h z|1VAp1pM=NT4)VZu3RC2Mk!Sl>pWn^>+Z3KMCtgiU&OPP1yj&CK9+A+9sW=smnqoY zj>*>d^sM9r{#|*?l_`m-wNI^C@!XjjJd7zi4Bl$1Q5NZxIhQ-JtW5s>l1rGgUTTqm z8Cv(jgf&b;op5qZJ1oXXQAd_Wxiu!F(hzKBm)%Bq+Wx(#r~bjWW|%EdeF4?e{I}YG z4j@34D{EQ!rRhHM++o8h*Q9Znw9=0hei25x>Hbg=ctyz7ro!`zd|vgUKNhmAp(Y%? z(fe$#upn!Sm9Nl*iZZrKY@cbr{F;N?X$t(HvbAc4BjjAwKq$MbB+Y{yQc1x0{zSZV z-(q-w{PclY&kG(rF=_r$6&{A*ln3^74kmqfB>}Hr!}$d^)_({9TIgWP!eM27gbtTe zGFp#nYU}e)M*IPLti|Am9oDRieSqJmJfO~ycTmGzrvpq4#SEXQ$Uud)Eb_yRv zPUX2awYvx#Ceqy5HjJYk69XGPv=Fnqr+wC)q2G1Bg>FtFaic;@m>G@OhA%n|HOxf92aSqp_g= z8*RtKH_upTFzS_i3MV>#ejQf#nr8vziQfHDj#bCrkYv3mj6)OF-tGjJei2A!|m5MA`%_i!iSXZ{#z)#F>D!1+Om4+7h35e5hZV<*}Bmc1__TFGaB=>jhb7r%%7 z;>@CXpGVe1?M7ejRX&NeL1y_5ZH=zirILmqm)m?s4m?~QW2AC@H6C^&n(;1M;sd=^ z^hv!_Jb#PT^cPbtf6btp3i}NN1waC*NcO3h(^oef`nS)IF^K%R0z96h-+<(yw{Yj# z$fo0Q0xICC>NA-{V{eaLPY=_H?$<$fl)f>9D0>0>21#j(9*}-+E@%jIIu=cp`ktXaYzd?;4hjBRVFLcw zepoUQBp#ucF5Q)@oL-B2s;b#dPix0FZ@Mj3dYA1z_`rWd+Pr@!W%qjr)K^{1R1BZd z@6fO=((DyeJpp|#2VnWr(+6A&VGcW=g1Zvb7(G{8(@}0~5ICvU`N|~BzytcD;r+4r zdn=g2ALj0#e&nIBRVtpH$zdZ~G9;;>v7`Lp>y9F!#iIpf8q$n8F6G%>sEsl?`;w3p zj3_z3XKC?Z&<=cAwW_Rmw5iJQlVcSn3aWP^j(_^gQa_cI1wecm=FqZ!bPeYDN=Q)r z&mE@qI^xiy^vjI{MX#sPTh(w%snVRuF#U&T?`X8LZxpHz*upM_@e@{6n(mT%rGYz{i3BU$#yM3shi||Fxu({wI4cgoot*PRp-#@o|l$-KSpou)^d4vWkZ9uWW zq$C-lji)F%jS(2}ZhG)Nln;66D8)EjpszLZxssw(sZMB&Bgq-MQnt%-rP7oBOgm$| zL5?E{VxMd9`huv4f_naK%zb%{G-%E|#y&AEt+vStXi-iO#bqaDsUz~2wwgQ7$)>z! zBPYS|7V!|$s?SnJ@#5;F>mpPr@Sm~?8GNG<)>B@y%OBvgbQMY}%9-;ENviDzb3Y4q zw_He>0Sx9Y!Z@wV1L8wf`3i);oq5yDzNLTm9%WUm9128cv zm5HR{=Ph`%t8rE}Lmk=@T2-!}<;MwPE)+HmB*-2n>AXl7i26rCv8M;M$;F9*6g79y z9V~G3gv+W#Js-W*du=Akj3-^X8|nGk!#|T(i_0c{vojw zN$O(*xI+&CmkNa;F24GDA*KYP$o^RpGq!de*x4rI3b#37=D5zD=pL(DV1Y`|*1~WA zuLG6aMM&eOQgz{lUH|^vT?(hk4DFBtmlhX}t^2ya7~D6kigAHpIANkG@t7Ra+Mlr^ z=kWJv6U1~984foFi_1r6UN+ghdx_ERl& z-VkCgrhZfCVyE+0+ERKsE3=mqt2Tg{yrt(~x_R$O!G~$y+zLMzxCR>I_0zZbVE(=7 zN|n8qX+JHq#bfRl5=AsaBr!F-F3Axyki2AcDkQDn@=^A23tj*ww3m7`;&x@@?{J0p z;vuO+OSM;O;3!Jh4;JUVq{oMF8FB~V!pD;$;k`?pE=@RP%SwAHaFV8lG9boOb$_LG zZ%f?W5S;pnd3?V;c~2nx9g=sxKEZD0PxX1LrGPsBZdZ#63pr6+0N!P__UnA7;s(CR*vA3(x&I2(Y%_A;Cpm=` z)g1jdYS~=vs#`r4b#8Ozn{^eABjP})>^1-GCo){S#klcrt%`n9Sm>T|ZaD0vIlSb? zii5t}ERR=B4NJO^Buz_-X>?jH+h%_nXmQ^TgRS?Lt`>Wa>$d<19E=Y?gK-=GJcxK? z-~#$(T$JvKRCJT5z){oFDEp=9z(e!n!KY<^1KFU6cq`)hu0RB zwxjZ_$Q5HOdV#G!Mq$M8GsAuB2hYH@k1;pR0)Jt4tJuQ-<+eRCPTT)hMqrQ%?CxIW znJyVo(2&pz7_9eMC*nxKnjkS7igc)Twc=-g-6^eAagv`0JRF4l2 zHy@uB)D|tuhj~w1$99jAj}uWtLJ3vJ&_s60p4r>0z}Gg-;{mpjNg^3K!+b%)zyQ~-wo>~ zS$W)Qo(9bzxqsyUy>eE<4@jV;Pml$*xbk&wJ>QR3a!EwaStu#}@{-{e^F2KJ(b?aM z_&R%g-E&Fd#Yz?YOclz&q_`-nSfMA^ zhJ-7G>9@V&P-Zg6X@@YnlzK)y@gvbKww|J{R1Q=qS3gO1su(4}*WR$M=SzBee!J-P z6=H1YET!kLtb>+RoFVW3h$fHV3`n^BH9>D(qN>duxAB^3UdiVj$@>b3Hm+V9lV8~} z^{(l_6<<_@3KO4p(S-&Kls|M0KbD-x<4L4n{6xt<- zy&_!He@x34)jlm#ps<&9Uf!2W0M)y&lVh)X+?G75uR)k1n3uM*i7$AisFYLi@680w zdA+&nUk5*C)@#WqE09@(O7`nyaiSFzw+*~eCMMD3z=K(g(4KF+EkG}9?6h7Jl2)0e zaUp+Z@?KF`pE56Ew{+suL3(3)r!^ppFyeVv?#@Ui=a-Q5k&?xlJsAzfXCPpl$0x2L=qX`%>J! zl5{)AGqT5Ni#W4Li%V^k%&`kzDr+^IzuIy%Fq}kb+k&h|q)uOs<+20R5EIDCWRSBI z@xaxW=hYGn$@J`TKLimxv##wJ@tTucdk?92&j4ZuM;TU$ci>q|0{qz31I%!Oj?lma z+gnyw;qQUEr{UM`A3J`{#{&*48*Y*{lXN&YkL<7RJ(ebIS}&7yeTmhDu4qtT6+Z(} zv=_kxOk+rJUA)|d21FmbkLVX?L|Zj|%O?G*hZV1w&*RzicVJwGoo@W5MTNzNj@2pm zgwe%;<)YZrIOt27)0a6YF_|Ty{;{ZCo-G>h=rLI0X z9Qpnk%erv!-`#O*E9rd*nVjqmaP2 z+_=W0AWCsWj(M-EOM6S!&6K17dmShyALM8l-mkRcU$NM;&NO z5c4X~bH2ur?c3^PqX&|c|7JiGTZWs&sAo0X%q};7Y763$1bAR&bngV|A^%RsXxZHZ zIxjoT|2}2BA(^SXTh=o5cUormze`#jl*q#{z41;yUSrb!Hs;jK=1;8FHZN1wD^G|Y zW=+SEfgezG(OXS7Q1o}@z{&{1P7j~-K|&VIw_*=2tBVo0Dl(BY z=E*LmR#-&d0HK}~Ige)V&R*RhOS&U|=iDBS4*+7 zey2JdwK$5(-Wvy-d|9}!BH>Xr3AUn5wK3-T9yyf>2o{JmP6)OYol)eNg%PqC;!)sn z6|#5495$g|a205KW{$b+pR2bgit71e&Hub4?A@V0^>1G~ zy(P0Ij{Zv&FN<1 zD_`1HT$-@$^KK|;S|UKe%0z9?&M$0+y7mJjI>q+(OUo z-s{Qcdt|Cu>O92VM^f`}e3X}{wH`y$D@^8BjM|4VA(RKGR?U?T3=>8OeLP4M($~hlB6l~Qw1v5n#dF2bmFC2Z{h*kdKwaO_WOlX?3DQ}^R zQU2L|j8=ndjNk~j(KdsQ1q3zz^Uk?JMF&o%;mx<2v#9Td{*|cZc^9j_GG||UG|wdZCqLuj32hdne4^ynBNj>p$NyS)uj`&R0Pvn#FOw-Dpu)Du%mh9 zF3W1hWr--wz<+W}VDz039leJ7^)ac-&xlnNpA)dss!yZJ5h4x!Xw>G<*L`7bsBTX$ zO3(u`5w|gE($!C^*!A9{?hi(%qRdwDP<(?I*rF()v4G}YSOixD7b(u@sP^9=t`|Mh zZmYshtL@V7YtF#z9U$*jN5nfRuaSiY1@_S=%ntyaS1OQme;f(72kw zJE&wl?-iTaM5dS3S5q3&)Clf|FYKF4?{gQn&rmzZ`udb`^v=ZbPvA!KnShDJvb@FE zc5WN#T9>OdewsNX39IuXNK;3UZ%?6G~Day;@iYt8R+R^ zA{E=-7c=C7yHd65df=fv?EV7eU)kOVLkU>f#H^z*fK4hivH~VzLNygmhbMFAMiU7M zJ>1y}ex8U=-s_bj-C=`XPl9oZ|5VH%#(*1@svBgO!az~%Tuq9^QqzdQUL{}1+Yt5V|{rJ-+8?47q0|Roox{+MFFKkDEomSCKFe%}KxadxN?m2&!$^poN&lej#) zrN_hjh7r3S3CY|lFK(CKOY;MW{MVGEfKeNqyHy*L;E{3tzE=F0*kDuP!(z7$e)mxg z)q1DE4Oy0$^FDIRSB+G_BvP~M(8T0w+5Am?j2I#@*_KdLz(UC_ftRpKOJ(g%7^_}h z*ptZDL-YY`hhfu$Me?F~j&=~X4ZNY-aN$SWvRvfbg0q!m>0h_i#E$?l`TQv_jnWXI zWF;pL?i+H-q@;?k78#`PU;Q6V-x<|J_kFFRqJp5J0#YKPbg9xy9z{U96p2a+NE1=2 zbjXM(MVbf*NK+6HsS1dcB=imeDWN9x79c?z4xp3 z3{;~}#`R!ylb}eU!ck-|{l3@DlD}KV@TXX>zniLuWO7ct9$80guKv!(Mr1?0&w(bY zRT+!Co7nHzR_(cUxmWKp;q~Ne(*quvbM3YHK36rb0g|N;C8JN@;K=aN+X7nxf23Qe zpEPKdmUoht9;a$&-CVy&P~+yPxbV2My7k`9$UAEy=d$? zdE5!UCHlVkbpGeUK*p}W>%FH5=K$a#?#}zUI-~T`#k?&~sWYWjFxYrsKp=|}Xm1eZ zQrufDJGC~p4Cd2O)ZyyZA5>nDnoDCC^9*!WewCBT)pEclorAlO6o5(wQ zjkz}y(~Be)s99Z@m`hM5mw+H8*hy zi`P3!&nhFPbMi{<>(HVi+E3OLF3G?;68xXUz*i6cF;nxzPpbp{&G%oTM*P-ZvRK6_ z_xqV;J(J4=C4p=BZn_X_QfAa}h@ARSC7~wA@XCD1h1l=^5#A1gM7CFeUh})#=)0HB z2xU^mm)Jf2glTY%g-|Z2xyV25`{wH>J4=jdu`B6)Uiz=l)A1U+e2A!@o<>!KhR*2z zckR@WG^w!FG@Vxwq@#y15&!Ks#cBH*hP?2OzP)zD$;YWw`LT5)+4t(;(y#2NY70?fBuv&2;*W9c$35mMN*Y#LsK|poq+Us?h`XQVPVN3!|E>W4AGF4-B`^<9aDe?b1Pp5BPE>iKDd|Lzd zyEvq|=APp7?Lo$cPT4?Nsb~TTJ72uyx3TB^NQdAVqr%u5C|Jh5-L3W zU&w00&|GQj`%^zoq$vOMt@727Yn0z#$qE67hrCdIck+PrBe@GxA4&&(DTR<^n$RIl@f4Br+EC*<)CO)nw~0({@uohd$6%9B_Lh$y zhXAQC@suqO{|wlW`Q57ohHY|*@c*EsWY0pudBN4eC$X#=jMo{c}xCcHyItgDitfUq0>+XV!1Ra^2loBOR(nfYHV7Z;@=8$%{^9u&F*x7Q=um zpFVl&@Q@$dqKnk)AmwW6`;u7VxAp*YP3bSCZ%V?z*b7vZ+x#NugV6X8zr5jBFUBP7 zdrwi@!uX){IpI^z&=~hSG51sRC#M$U`3`PtL~>NM{W%v>z_3fp$()5;ID6rqloaXk zrPStbFivuOsO^T-XlHxHj|WA$CQm)S?rw(Ou(;Lt=FwH^X~cm6vXyGi)T*Qdd$%R} z?KL~pj=h|&KE(?=>ONf8M8xWmZ&y&~;`bBYX;xkl(Q8QV;g@%*HTj2iw}yQ%+t;q z8h+p&KX;|eN?JiralV~ZF7>ka(*-vnws)i)-KXJFXAlXFw)y;@Po3iFiSRw%zpls#uc&D^o<9S zRUh$?1L{kZtvp!Ox>YMi@2BBV0Mk|^)+OQ336 zp8k2iHLWdzA~t0Z!|o+9B-we#;`Qw#yRtm!qSk(8tjjG+KDmTvNfreUaX&t)y3Cz| zLbB`tuPfJY#GSD@;?{5d_y9;zXjW8IlN#ke-S@`V?$JGcBmdu36CgdKn8CgFK(_l6@qtnrh1pKo>2L)n7>BOd za>T7WN2-~-9bHxKY0e)Vfr>k9@*xjXLEmy}qA$DtB8^)%1vJ}bi};`4x7x(}Gv#dv zwf*M3Lz7|?W>0>Q#1Y%#Yc=}4^1jc?0 zpNw-guN>NJ(1d2)e~5c8&NTRzPM2bdhfavYlZbJ7gEB7AvcT>YWFgDn@&ty{5sD;jtxz{>Z=zP%< z=G`m1T0bg76rKy8SLMzoOfvvaot`9@t*hyUh0Xmt%Khw9BLj_!EnX#o$XE8~_+a)5 z&7XZMZC~MTNX_jKf0t}SnjGri;Vb`DFObyu7EG&#B2U%>X(h|K`XOJqr9fic7gp}t zvlEa>xX70~CfZ7TT)g2+*JgcjY{C6a4)1mI4uee$8jmsvz8DCQ6yT}ylWL?gH64;G z$)se!PF$aNtqS+Tldgzse1~~>^^Mx~tqnbiMbpRQLo4t!_4s{mq32$|V`ROjzSpO- zMn4sHqpg5YI4WdQN4yM^`Uvr&QzQIf3CnV^6O2V881I8Iz1@y3-`DH3#gw%Fz?A+c z?Ewov@~(z$ceyZ>yX!hPn%CPlnz?fcFrSc^xnZ?B&1)qg#tnbUrgiG47VfT?$^=H# z%J_B`A-l%G4MK8Zo&I|tAYc%6ryl*9neP+>uAvSnZh4;=!NMn}I1f_ZvcPiLDBZ#wfB(ZV*xwA{#shy4k$RS2tqw?? zqDTMk{wR1`jo~auKEB(c6!O4FRy9yTf{NCa`p|~>RhDkh-2P=ed7+#PumOkrYx5)^ zOv-IrlcaLM5>H>kLH}N=$ND9IWJpY3eh=rkciIFS8(n|!Fh^IteC_=EjeQbc6ZHXz z^rPgTJNPytF+65g)KD+i0-ien0l^1^yd((yD1MiU(t6Q1*t;OXlzJX-K3&bMBlP0$otI9L z`2A#HgtuNn_>%%ZTH<=7p{>+=3n`PdASu(Q|6JmWZ;Z1A-EMPd9to(?^O=I|LV^F8 ztB&U`7s-m8@6`%SBqg<9+{{V4xI|}AHkn=CmAO&Oz27{0pTKmsEnpZn>gT;=C~{IJ zvxyJcSpVS9nKLJVspbkMrHW)?~?s9HFuXa zC$gh@`LLNDJ{fT9(fq4rA>7lMMhW$S!}M21K}~D4$c<3&Cly%cg$ZwKuH=}$qi;?H zkCod_?#}sx+M1XQr#?14^RKmrWH&qdPQnN}y zXEBGLtpKK6nD?dEwthJ=uHzdaC$15Oj~&0nbM}(_UEd3@M$??83fj0rJPkjSKMP@= z?qjB(PfFb5i1D!H(v*qg9J$76{N3ZzCB3)T&wMyG`JA&u>viD>&l`OtnPIikSvo*d zl*VzAk<2jg3L^UhegeK`?Yd&!e#E6s`59C7eUKO6RZ`&jDBs0bp4}6+>n_Zcf)oFl zHsTRsmEYm2Ub6+_I;8Z}nIKu(lsLjj)!SiMn`}4eh9)~S-_55J(L2H5`dtX}(3D~e zZ1ZlUC+pk?6I#h5`IJ$7NjCyt2f2~j5A&L9{AwP)MIe?TX;k6}mclvL`1g7Ix!OVi ziIFvH58ONDz~jzEu2|rqeK_pRC7sSk$>Ce&;K8Y|4LseLGAtL7$$*3DFiI9TA{j&1 z^=@$1u<|UzGS}DE^XW^;!}y59(e2)SQD5TY935zLa#j3Fh7R*4eH+OzCIU1$q!EbL z04mI9WRd@63x|<}Qk4M&Wd$hNfYQ6fHy}(rwy*?w)LaX3Ag@-ho5#ezjGjS5^!OaA z-mBgER|j-JnzQ7xKlE%gC`G0D5-wm6Yd>QS$pYXSu zz3DCN!b_<;$Ra-uA7|N*rVfPZy0X-Igx3|mrVn%XC5wiuXi%gv6WBz9%~+BfjGJ^8 zByaw&%z@C8Uds-~hEhT(?#%W24PE9gbR;pG*XMyA6rBOj=`dTUE83;#xSebre3eWh zyGn3lYCmLp?>>aM2dR&C!`9AH=*i*H2&TPv0XJhdc{G`+iHD1_OYd13ePbyf=}{`6 zR5nT?lp!(K4ETP_b_+lAfTG!sQ;KZmT)vXXjR;g|6bAKW#h}^se@p+iT^v+w#%f6~ zfs{m#v(2|9bW}Yuxq})w_E(-wmYHv@Ly#GWWQ3}lLjrF$47XWBN;l|Qa-0$g{j&~0 z&)zdH6Vbk$&wI)#6?6TEW?Z(~nfo^~JZ^k3E~*0d07;4_u#F!rgHq4XxAN&{DN|tT z60t`b)L4**r-mPJBkwa8^MN!Nl}Sh6fut3fO{6v~KVBU*{gi zX}KOo6vdI=LcIrJu~{F#Gd6{5F48xRN#5b%2B&vE}WhTDIX$_KH|F* zQ~*dicb#qrp}8le+c|ITn0Y}-Y)Q_>2^iQkH7TPt-X5SEn~wlYEKP{w4{U(H0%R0J zZ7w34yH1BpBv^YYhl*AOxY}h4*TZD?%6pG<#eWt2VND?c`%aV$5XC+eo!p!V+cXE> zu8hIld%KV&R0Y9Sw;0vC*!=VUI^)0c#iQvPP zbD_zPmqikuT#XO^S(D078@pqXJ0&Z7a;+Czb(Aj2e`LN(fu^EN4VNu6k=89$zY(@{ zE>eU9^J>Wl#lhQ0&)%cUe}b&EvS}f4g>bCZ>B%fb)5N>Oi+UJFKwV2tOjYSAcn8|X z7-Rd$`$9ZmcN2k<%HRyWwCv=R=SIAb2Sd`A(hf-oKK$o!-K@v%!v?l5!ngY^-5aI% z{+d}oZ;(wUv=LQC>|}`y(;+Lb82015AM2BJU3j;5Z8e*f>;Fc^D5Oh%0!j6bNoZxX zm+d~adjdVCxxBZ0;7d=t3hywLjd)exKF0Ovx7HIt$3GRc=#k=XwxudIE;_^{BCVq9)rYA#X{6QE>ZmX92lwTu;3ZBvi98YNaQ6d(VGPSMVFQNlT>&Dn9oUsh?SS z0y-X0rGOY+3jXAPIjyPQnPhO7lqLZ49Yj?gt@yQ|vSTDpR&}IH>}j!}l zB9wVl#2eUd3^w%OR3C31kOCv1b?zefzhMjIfCWi47q)??+w~>$SX!d_KLaq$gF!Xo-=8g(K{2i)hsj~S0w`cdfOy!Ayq^p##uGb0uug?-HL=={aqbCnvTmV^snB9F zbCTQcXKo>?gt;qy2m91v!!f_P34S;+n59D~t@;;+B}YL1p}N6)x4y`A(Z6es;V;5= zrtrgH#(Qi5K;uB_RRK$d#K`<`ar%1x;qlP5FyTU$o)F&11*lykjHRMR#F-@C^+FKi z4!sp0k>^0coG(Zv&N3O7P4;i*sK0dIg0a#P1IufaqHE*azUTJT=_*SCs@@}V%+ch3 zU}g!Z2#wgeLiA>Cg>9HKn(#&S^cI0>mb-I=ENc}s9%uoUULn3Nilm>6AoQ-?|4sQ} za9LHM63Dw+Poka~jmwO+*&W4(=P@u?`XfphK!$8kf%!z@D45z#6o+lj5sSMW;5R7{ znq<+x4CL20{*5{Cv)9Y*~ z1540iD5C_a%iKpZ-mKczgy(1SCqZYH#j>oh4o_)$&y(Iptw}^H6Ym75i z4{+GD!#WS+Cb7wZpQkcA98(P)c%>5_?oytqjg`i$HcBRjVG{B~D5oefOkCBl9Nimf zgr|M0|Hlra#H_P{Nm0aURRhhk>7mOlk3 zA@2d{>SjQdiQmcGe8+@Q1y4vfW=&;c)^@xIO z=Bf>Z334Z9wzI#@fWatB(68c*dBSV*T+oOnYZI<35yt{Cm7c%&b&9YhWlH7%Saeio)QfcOce zuRTyG@~H^k(M^31?LrqunJ@3wW{FSf2I(g$%Jot{iZvrt4a3FSC1SEUPaeq_>P+=< zhJv4Q${+bW&yXbzSC2a78il5lqm{(tfZo{7c z3-0~9==obbZdKVCpG@EDHM`n^K3uUV+tL!kcf zR7AuNMAxIP_<4<24mN5I=O&J>-Eus4IYJaio~+c<*&hDVUH7oKcOmdFt2@h=P-8^^ zcm$K4WS(%s=pHpe^zOK&vsm`Hx}tJ7IA=Col~+>hxajVbgM zfiC6Wd@B!@X@9UXNz1X!WMDZ@CW){1b2vufp9>mr&KR%oK8^di@1ni-!Tq7Gs&HKt>*yu?SC6Ui5y$Pt%kkp zsyoYFs5NdoIb7Cm#JfF><(-iohk$m~c6WY^5{(;Rl5T_Qhe@ zAC)tv50h_=@RdlneG}fPKyaKP$uR_v$2)H&YUQZ2p>zZ<(_jn!b-J?>>ZkNFZ93LhCrwe(1S8YDB2pumIE@l^QOx{6I zWkZY@U5U0txCgKIa1AOvChva-&T1-ZG!+Vb)?qey52KO$6)T1Yi{g*bS&3iPrKf)r~}fsrHmY6f9SV z+5y=})}aQ3(w3kFyh!T5v)cllbN*@04MEvF$GeuD21!z;Ivmpw1mUj8K|->xmd+aQD1 zb4C{Elm->#GueR^7tL!Cd9*GiVg{3dRB&{YZY4jy8oGRi`yW+jmy6P3-kgou%j;ez zd52#y9c8dZ7F`ad^sa6A=O3Mm*b34y1yOr=h_g!AadA`(8Pt28_<+yidR4UP=e%EB zP@W>B_%O#J`q7Fr>{}Dc=x7tIa}R&C0I;?i*u71ptilnjd{i7`d)vi_U%15XayDPEt2Bj)KpM>p5^YMLlD- zn0(?9{A_6_6J`27u=0Ri1sfS@o(cZRO6;+y=+?7Y>$HH3-`BKWvSUz631Qq)(HKUh>L_>f+HGP4KA1=Q;6QbljoV?T^((%4_eR7K0sB=h zV#kx%iHgV-X3|k394Ppi1jm_Bo#Sj*ltf6OT+HnK8=y+=mD5PiVAepc$2EnAb$dRX zkKuDqiiO)}*+PGYelv+yA_761nZkEj^TE*SHA8N8k(%%F;J7>|=9>{|xvD)Ad*)l1 zbv4IQcWY9D_T~PYr#OAOs_dQvB_0Jw1?l_99t6)^1Zs|`3qCLl#V5m~FpRwVVH_xI zd%^Co3Cj@HW)5%=E5!2)t7s$Wjk0{;6N1)IHBw`)jd$?mQ>X$I3;E@<&-2y#-D{qt z>q)-mRdR71XG?*$lV`(J;oI8Y-&7;8xQ+AP5x3z7rd0)K_=YlZSX%gb{PfyU^X@=B zs}KXC^C+96M3S72UOqrG{=W;}dbhg5o_y8w9^kPNud+%%*2XxkWq;M*hVfeiM9Yo}KAoOI^*;hoIKOn!1VE_qQ0 zQz?IY-)Zrq$oVzjz6US37TD=ea@`L@9LYc$@A5(p=>0&-@u)aksFF$Uk7O~B?H)If zz4B#t$=5)07Z*)G5f7$y#&jGgPOo7&_5 zeyOyGUt+U~g^}wW zWSN5!220_Z{V5o1gWoc*nIt^_+%kASO0(biXe|W`E`$jsdP0;d>BmTWI&l0pfao$j zmPR}4)4xr2`deNF!sfAia^%+t!VmU?kA|bjBLW~>F6Cy5ax&|Snl%Rg_nOT`kbh75 z;3iD|hhF6YJ59s^C2X;GIEmjWYLOwyW@6vDCJi|qeg#PjIr4|=aBGKywJDo1|HIB6 zaO=DVY46TQ5a)HtBP{DW)6q1KM(I)e(fz*i`1mZZxi1@w&x*5Ey+%0Xnjgq%k#9s$ z-}7!XxFdB@FdU#6?+ic4Psz=Z|i&?Uy**kE&SD)nQV0Qib>L5oWOR*D9gl7{gymid6lvNJmAnQUE5#$e{ zHnhhqZXTWbTSGifQ>?MH5C7}gNBn|DzIC8BLI4(gMVFro!)m|A(t}2|$uht5icM6# z?S=;h>esslZC?|*PDS851&Bl|)TouYwgF0qzP%#JA^zB9Li4RXLBC<_7Rf6OyEWoZ zcLT2`<9E5V8x`S=?{rShsZ$zKVeVtJb;^FC^Ucd{jqm#-g+mWvlzd98N}rtp|7qr#Vn51{(_y zod@JO^s|WwG7A!w<<_5V3<2({Iy4&oZ@sJtH5`QWk3t+RD-nsTda@pR=rx)Dfs=jz zr0t!vr`(d}F9Zu|SpINFu2k1kdn!XUog&+cFK=Z9-fH9z>QNPUNINt8x!2oTI5e!w zmJ&tDTA+5Fi8|sr)OFg@yzCv0bR7ARIr0D_{3Dx9Jzytz6bRY_o7e`{&G3+}10e0H+(ao>bn7ya7KG~nEsj^5hTa{UUpT_+Z$65pUwMvsmnBUAqzjufr7=b zss`63D6i}@=I>3p6WY3B$V9f{u<3HM(&uV`4u$n|ND3PvM2lo^0E!V=@E9u2mR^*5McBI*LY$UHz6yAup>$gyw+G zK>`p=4n!j2zB1SLq}rM7PyPf3%eJES$a`w;)y}~J1$F)-5XS+wjq-lyIaKcNDZZ7$ zl)34n5sH>nGisGq2D!f%G--xBx>Vj2KdX+Pdubl7=6V6~mUfbGN)SDn?3xjAq$L%) ze$XS5;M?4Ba25UO-%*SNB52QV&|H1C9thIT7Z?NzO|_-5iAR3(8Or!N7#rcZCV-*x zudUI)NzaXGg|BzThPevY{NqA=OxIjGs8)V9QKph#T}AJ*q%RC@_^uxl1%Rzk+=9aN z*!CJ=;QiOjz%x+!a`#R!e+TMH=alFlf7k&hbE-@A@6uC-|95&5)xQlvSRnyt7n?eK z*FG8$>U7nfi}kx293tO6$#s)pCuG2M)OUFR&VK(K=wsz3V_4G6lv#kwcuGRuC*e~K7!=rO224* z;*~N>N=iNb=u7JlwGE`8jq$fvx!)0EhMpl6T(bHZzS@Xrc79D-YQRoh;ZEGAyL27S zc%6*ayb%uETxxj9n7`r`3<~Yt93N(`?SKy_G0x&DC`{x6?q8B-hU4g&Qed;!eFI60 z2ecD1JO_Y6@v=^kw^kOsm)He!Yq`*!?Jnf}r+txkI+eg7`ouZ5;Dt|-idhW;Q5wHC z@LVh`T{QaKPF1&u*3cm{E#r1;+I7GK_JqbYRjDMb4P@ z6^;anwqTXf`#g`Hxhc67--`yj3g2`4F9$!=wd3df*l2q@y~V`xKa{v*@C#CnQ+OM8 z$#dk*FUjGV(z*OB@9)s022;|DlUrv{{w%e6#NCQf`#rc+cazN(7fYP3eTTY)T ztem$krz64Qc#XY>Nrci2|8cKlg^j)h8}q(%ZsP$U()6aAY0<@ z!@3Nl;+lm2O=ncvWt?`mQkr?u$%L16)}rRZ+)i7U+zg-R|EihB)i_@p$jiF8okMcZ zyPf*Yz3=j>5d)|rHv!KNuJdhqJmuPdGV2f~wVkUhTM$0&G#iq%m{3D?q4%aK*$?z> zwGa6IVBZ3t3(-ftpW1Aje_(FSEk2xjN#Xj+&4i4)pU^%YiX`5>78Vk*W#Vy#~U+b^dXz_L#?|Sm4@85sJLDR8lkVxBkQH2ZG z3;go$AMBt$NV3o^%f0V3Z(q|2R0K!ahvPv@aZ|fV2ryKd-3_?j?A929TA&K=qCX|8PUZZRSr6?`o2c z4mBLLp*(Z^E+SU#3vX?VrF_z^*4QF!&So)cfO%y`O+|P13o%jL&f7ttf}r2aU79c> zNBO!hn|>ek8uMq}c&Hj?xus>*#m^;92sk1QTAgT`Kh&Agdbs}@b@ST%*-XH_+ovb6 znml+LP=s>%2!f78ZX>u9ks|u~qeXdwN=(Cnmt8|q0fj#FAQo}Wz=cb&YE<}XMuMCnhZ%{Gl^<1yDi2WWKlYj7yCi#^V`)nkm#0+B0tl8NGtX2A~zgihm( zbe`EuCvca?r%t=dRV4<@+T+r+yXbjlX019v$`oFcWGc=@C83uBD{#oGqg-%t&$USV z-T!`JVsO3H_{+j#MBIWya$q^x;j|}AaoRnKGMwa;H!>Aq=S>!;G46Vb3;n2aUD;*0 zZiY0ICkWLG?P7y+t?l9yL*}Ir<$rVuq5uMMqAMoz56+y94X6N%)e{o4(ebz8z!P*+ z*#tG0gptj)2az}|=(5OKXC%`a+^~{?-h^X8o$pEEFPIC~%!dnsf`Z*6>%-gf!rt>l z1nXEd@_82Uitq<_6Lib2)clUw(-pxRH*L>WS3#OZK_jby#$(^1Fn{fjc0~MhF zypnO@^Kl`;^CVJ~=#N*U4%dK5>4y)2CLiEm0Gx4oYq|ajV7eFJ(R~4Gvy;KURu~#k zvTM<#-6@yzMwK?1Os5&&X zLYhay5h2JX#=AJxLDQ#l zp<}*fE#HP9-XEz~)yt&vBv)S;(o^(@ioeJ!7m)x-D ze(;=G6PL~P1gC%#kz{?eiEFNai8cPIqaa-m_NzH(zTk-;zDB$>n5v7J4_#-c;hn*K@jpK( z$^4#J@ww1iAQGZo1C!^6pW7GV;V$$MINYwT4U~<4Cplq`Z2mZXXwuU2C|r>5_6?iZ z`rlKhDRw{k0rN3#LF7kOg3pS=1}NVWx=rcK?>zGT0Yx z_u?)!fk}SQ&#O(cerg}y?vq3^*vk7wSPvf69%9wyJq|Q2TS?)0=6eO{2cTD zV7kY3FoXr2*_;1-eM3ac1v&_cH$xf*d3dZfi*3CPhGg$lozypc4$^~lch3k>&+c}1 z-Alv+;8l;4dl#nHrseije5ChYzm|p?t&(lL!?jn9A1(3=m~Cay4s-r;#qG_YpbFRy z8tTw=&|G2Xx{~ABRrNV7u^jP){VP}~SSN*XpK#yhejTNC;Ee|N{E-+zrBSV^B%+LR z8D+k8@2rykrrPw8r{>u6MYui0a#dfLcR~qJHOq#%YBRj6qD~5}dYy)Ye!QwqP`*%$ zY&ixRZAvFy_?7(^mTA@$P*dGdoUkmwapwYlwflC^$^Os?1V1xsVJJXrEu)q>gL^@z z8(i8w@Hb+1LrT&E95j*1T#*T@+yW%V!@Cc?jg&y+c!S9<+zYgUK~lMXw7?m!Do21V zTw+#etnd|bg_Eo@W^iPOOscDaRf7uZT0@U+ZIUJ4cw=oMnv;TdDE><`0r|M3DzDk3 z%`5hi7_uvtQH_~-s##;B_?yH5=rJ=8pmfAY+9tW}dp2DE`wJf_! zB8H>KARV>#(5BGmT4#>)c3UOk0KHpV^w*p=wzVlJSZsKPD2>o z*beiIVIFkY^VCarv$^_3c?`&p*9#a3-;neZs>bz*uRmha}zwcRgZ`W|C zj{GIn!d-ARPSROA;7A~B@-93~d%5!j=|MLUqDcNRAlwWYZ$GmvkCtYOVdUrky@5Ho zCLU0=C>of{vg8U^BXrI2)1 zk442=&7>&i#JB^;m0f{X{wFE62@#%Q=WrG!K{wIH(UK5=Uw0UKIXL(E)uSnFnZr{! zPK6A~%ULtMkNWr`e|dB;Q0jw{q^xU*kj;wtX=8%GEWQACe_E7q|1;DJrc27}kd5yH zyA!_W_a^`RqZFr;65(e~elVWo_K}?SW9ZTJZO*s)c>~MeIcu^G4e90PUjhgCTAqum zS(PfKB=p|^xVxf9KC9-k>~lScSF-%x{0en)doqoRQNfjGx5>nm9=+<{YDMb;C(U!N z{uBQ%Zo5f=c_&9g@-yseqHY|&W~wkl+MmENu=Uu{PMfXPwXrS%b=i?*Rb*q7cs4$> z)Ya$)(lSf(ml30TzhHJ>F!*h43;o?fnS$rp!h<`{fEKgs-mb$vraJv@(uzwmfr_`> zp4l>x^C5fQ1^G&QtH#A$@7Q1;NrAsmGJ!t_w4~&mgue!^8*eoWT{?}t1)Gx(JmFj_ zUzaY1z2{Ld0lYw#9Nbq_b~8%PP}H{hRrb!r>?^X(PWyOa{AARt{OK4)^MJjHjs~gh z1;4psZ(y{;d{Kp-`@5r442V^!k~Q>T;y1}P5YG^G*F zfjf!n)_kp*DMK?tYeK9<)a?WlbeMbRwuwn1;1RPVEkZDJMgHUK`&mPcp2BCW@@9FY zY$MKyif<0O@C}JoM@{qDjmVxP1Ygs9x_B1~HWtjcybg-v=y_fdLp|5=$nCB)|^`ct)Z-(Pu_2m8AMv|Q6KRGVlR+r-39-CsB~Wy zu<}bH|7q3+#}aZ?cb~ag37HYOK>%VTC2RE0?#%Ets^vQttA3*Wc53FLh7IsI%@x#Y z2`vdl+MVmUr8Iht>g@ETGx+pCc#GZBL8pUT`0Qc1FSe#Ie}sP>W@*IdKVg79 zwL1oHc@KUmdIA#M8k5#r$WT4ynCB_6WG;{ za1`rT`(s+4xi2c6rhg1SORO6(1>dP$IhdZ)1Ev@ch*_T|M0Mkz{cA?NpzZXX{I{W49cX18b8{2*CKq1Oo$Q%Wv~)&O{bKJWiT~gD7S1|c zU3$t(=IR(RGmi>}U&W77u?*f9pq#szWooiVHIMM-CV?czlKEpyO2+x+Ga_d+?U{Zd z$}71UD$8fW&(L=1ke*|Qs`ZV_XSz-Rc9;V{PdojkU`?Z{da5?gqKPHwlmeX@5^siT z&&gF$jxOr(K@QUFX9>Q(dcaadTmLq<`{;w3(i(Q2`PhrSXmKlZt{{>Jk?MjQb@h?< z5R&`H$yYa&cy#urm3b)hty%-+b#oqCR!e2S6{klkeN_0#8!cX5RY(aiX$L6JFJZ;A z>#G=)|DhE93o}ZUwxI#m-t{CH(mrw9ud`CK3A@kZ33Kh2OQcP!(~3#`zp$94HMxrh z`o13Sb_7!QlGHO$M%3TAPHOJstECf0_LoiFtWb+Z7@aBaEaGNmX#D5}klX_4=4mVA zz8*)?tXyC%;PRZj>!BHLp;+~M+ z$y~2cOV5}+HW3}0;xs{}x2{}voAI50dq%Z;xy^mrajnF&J1@6NtISzu?TUIA<>juA z@(wf(9X=Vz;68gtjK}eY^QuCJTAX46yOvqfSqPt44(P{eY26%opqn50X%K&wmLxoM zqbchRtlyDS{J5-a-gQxz#X(QUX#a$3c?#c;iCP=AywQ8qmHo@<*s7`6Xzc`0)c||UK$UA@qd<_S|f@3*yG45eK{Wxx=*o)6xPWW(oukQZm+?9$x zVF?_bq!wmR{=oJ<25uxgYkO^xw%0D`H)^Wna@x8Wc_XxFz-&i8Reoc0;oai0;X8^e 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

}gESojzkmKx>%^ur2yeThUv8 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

T87Xws3gEspWoZ!)%nP2gFyg}Mv*n$G8o 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 LNkvXXu0mjf9~3x2 diff --git a/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf b/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf index 18004e16f..04a63d3d2 100644 --- a/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf +++ b/modules/account-map/modules/team-assume-role-policy/github-assume-role-policy.mixin.tf @@ -25,6 +25,19 @@ locals { github_oidc_enabled = length(var.trusted_github_repos) > 0 } +locals { + trusted_github_repos_regexp = "^(?:(?P[^://]*)\\/)?(?P[^://]*):?(?P[^://]*)?$" + trusted_github_repos_sub = [for r in var.trusted_github_repos : regex(local.trusted_github_repos_regexp, r)] + + github_repos_sub = [ + for r in local.trusted_github_repos_sub : ( + r["branch"] == "" ? + format("repo:%s/%s:*", coalesce(r["org"], var.trusted_github_org), r["repo"]) : + format("repo:%s/%s:ref:refs/heads/%s", coalesce(r["org"], var.trusted_github_org), r["repo"], r["branch"]) + ) + ] +} + data "aws_iam_policy_document" "github_oidc_provider_assume" { count = local.github_oidc_enabled ? 1 : 0 @@ -32,6 +45,7 @@ data "aws_iam_policy_document" "github_oidc_provider_assume" { sid = "OidcProviderAssume" actions = [ "sts:AssumeRoleWithWebIdentity", + "sts:SetSourceIdentity", "sts:TagSession", ] @@ -51,7 +65,7 @@ data "aws_iam_policy_document" "github_oidc_provider_assume" { test = "StringLike" variable = "token.actions.githubusercontent.com:sub" - values = [for r in var.trusted_github_repos : "repo:${contains(split("", r), "/") ? r : "${var.trusted_github_org}/${r}"}:*"] + values = local.github_repos_sub } } } From ce59b028295c9e8347119231273135dc15fce3fd Mon Sep 17 00:00:00 2001 From: Yonatan Koren <10080107+korenyoni@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:16:25 -0400 Subject: [PATCH 453/501] feat: add additional variables and outputs for `spa-s3-cloudfront` (#1080) --- modules/spa-s3-cloudfront/README.md | 3 +++ modules/spa-s3-cloudfront/main.tf | 22 ++++++++++++---------- modules/spa-s3-cloudfront/outputs.tf | 5 +++++ modules/spa-s3-cloudfront/variables.tf | 23 +++++++++++++++++++++++ 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 21c698c08..2a8f8e034 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -240,6 +240,7 @@ components: | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [ordered\_cache](#input\_ordered\_cache) | An ordered list of [cache behaviors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution#cache-behavior-arguments) resource for this distribution.
List in order of precedence (first match wins). This is in addition to the default cache policy.
Set `target_origin_id` to `""` to specify the S3 bucket origin created by this module.
Set `cache_policy_id` to `""` to use `cache_policy_name` for creating a new policy. At least one of the two must be set.
Set `origin_request_policy_id` to `""` to use `origin_request_policy_name` for creating a new policy. At least one of the two must be set. |

list(object({
target_origin_id = string
path_pattern = string

allowed_methods = list(string)
cached_methods = list(string)
compress = bool
trusted_signers = list(string)
trusted_key_groups = list(string)

cache_policy_name = optional(string)
cache_policy_id = optional(string)
origin_request_policy_name = optional(string)
origin_request_policy_id = optional(string)

viewer_protocol_policy = string
min_ttl = number
default_ttl = number
max_ttl = number
response_headers_policy_id = string

forward_query_string = bool
forward_header_values = list(string)
forward_cookies = string
forward_cookies_whitelisted_names = list(string)

lambda_function_association = list(object({
event_type = string
include_body = bool
lambda_arn = string
}))

function_association = list(object({
event_type = string
function_arn = string
}))
}))
| `[]` | no | | [origin\_allow\_ssl\_requests\_only](#input\_origin\_allow\_ssl\_requests\_only) | Set to `true` in order to have the origin bucket require requests to use Secure Socket Layer (HTTPS/SSL). This will explicitly deny access to HTTP requests | `bool` | `true` | no | +| [origin\_bucket](#input\_origin\_bucket) | Name of an existing S3 bucket to use as the origin. If this is not provided, this component will create a new s3 bucket using `var.name` and other context related inputs | `string` | `null` | no | | [origin\_deployment\_actions](#input\_origin\_deployment\_actions) | List of actions to permit `origin_deployment_principal_arns` to perform on bucket and bucket prefixes (see `origin_deployment_principal_arns`) | `list(string)` |
[
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:GetBucketLocation",
"s3:AbortMultipartUpload"
]
| no | | [origin\_deployment\_principal\_arns](#input\_origin\_deployment\_principal\_arns) | List of role ARNs to grant deployment permissions to the origin Bucket. | `list(string)` | `[]` | no | | [origin\_encryption\_enabled](#input\_origin\_encryption\_enabled) | When set to 'true' the origin Bucket will have aes256 encryption enabled by default. | `bool` | `true` | no | @@ -255,6 +256,7 @@ components: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [s3\_object\_ownership](#input\_s3\_object\_ownership) | Specifies the S3 object ownership control on the origin bucket. Valid values are `ObjectWriter`, `BucketOwnerPreferred`, and 'BucketOwnerEnforced'. | `string` | `"ObjectWriter"` | no | +| [s3\_origins](#input\_s3\_origins) | A list of S3 [origins](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments) (in addition to the one created by this component) for this distribution.
S3 buckets configured as websites are `custom_origins`, not `s3_origins`.
Specifying `s3_origin_config.origin_access_identity` as `null` or `""` will have it translated to the `origin_access_identity` used by the origin created by this component. |
list(object({
domain_name = string
origin_id = string
origin_path = string
s3_origin_config = object({
origin_access_identity = string
})
}))
| `[]` | no | | [s3\_website\_enabled](#input\_s3\_website\_enabled) | Set to true to enable the created S3 bucket to serve as a website independently of CloudFront,
and to use that website as the origin.

Setting `preview_environment_enabled` will implicitly set this to `true`. | `bool` | `false` | no | | [s3\_website\_password\_enabled](#input\_s3\_website\_password\_enabled) | If set to true, and `s3_website_enabled` is also true, a password will be required in the `Referrer` field of the
HTTP request in order to access the website, and CloudFront will be configured to pass this password in its requests.
This will make it much harder for people to bypass CloudFront and access the S3 website directly via its website endpoint. | `bool` | `false` | no | | [site\_fqdn](#input\_site\_fqdn) | Fully qualified domain name of site to publish. Overrides site\_subdomain and parent\_zone\_name. | `string` | `""` | no | @@ -269,6 +271,7 @@ components: |------|-------------| | [cloudfront\_distribution\_alias](#output\_cloudfront\_distribution\_alias) | Cloudfront Distribution Alias Record. | | [cloudfront\_distribution\_domain\_name](#output\_cloudfront\_distribution\_domain\_name) | Cloudfront Distribution Domain Name. | +| [cloudfront\_distribution\_identity\_arn](#output\_cloudfront\_distribution\_identity\_arn) | CloudFront Distribution Origin Access Identity IAM ARN. | | [failover\_s3\_bucket\_name](#output\_failover\_s3\_bucket\_name) | Failover Origin bucket name, if enabled. | | [github\_actions\_iam\_role\_arn](#output\_github\_actions\_iam\_role\_arn) | ARN of IAM role for GitHub Actions | | [github\_actions\_iam\_role\_name](#output\_github\_actions\_iam\_role\_name) | Name of IAM role for GitHub Actions | diff --git a/modules/spa-s3-cloudfront/main.tf b/modules/spa-s3-cloudfront/main.tf index 7f0e9abb4..cb4faf960 100644 --- a/modules/spa-s3-cloudfront/main.tf +++ b/modules/spa-s3-cloudfront/main.tf @@ -32,7 +32,16 @@ locals { s3_website_enabled = var.s3_website_enabled || local.preview_environment_enabled s3_website_password_enabled = var.s3_website_password_enabled || local.preview_environment_enabled s3_object_ownership = local.preview_environment_enabled ? "BucketOwnerEnforced" : var.s3_object_ownership - block_origin_public_access_enabled = var.block_origin_public_access_enabled && !local.preview_environment_enabled + s3_failover_origin = local.failover_enabled ? [{ + domain_name = data.aws_s3_bucket.failover_bucket[0].bucket_domain_name + origin_id = data.aws_s3_bucket.failover_bucket[0].bucket + origin_path = null + s3_origin_config = { + origin_access_identity = null # will get translated to the origin_access_identity used by the origin created by this module. + } + }] : [] + s3_origins = local.enabled ? concat(local.s3_failover_origin, var.s3_origins) : [] + block_origin_public_access_enabled = var.block_origin_public_access_enabled && !local.preview_environment_enabled # SSL Requirements by s3 bucket configuration # | s3 website enabled | preview enabled | SSL Enabled | @@ -120,15 +129,7 @@ module "spa_web" { lambda_function_association = local.cloudfront_lambda_function_association custom_origins = var.custom_origins - - s3_origins = local.failover_enabled ? [{ - domain_name = data.aws_s3_bucket.failover_bucket[0].bucket_domain_name - origin_id = data.aws_s3_bucket.failover_bucket[0].bucket - origin_path = null - s3_origin_config = { - origin_access_identity = null # will get translated to the origin_access_identity used by the origin created by this module. - } - }] : [] + origin_bucket = var.origin_bucket origin_groups = local.failover_enabled ? [{ primary_origin_id = null # will get translated to the origin id of the origin created by this module. failover_origin_id = data.aws_s3_bucket.failover_bucket[0].bucket @@ -136,6 +137,7 @@ module "spa_web" { }] : [] s3_object_ownership = local.s3_object_ownership + s3_origins = local.s3_origins context = module.this.context } diff --git a/modules/spa-s3-cloudfront/outputs.tf b/modules/spa-s3-cloudfront/outputs.tf index eafbcf7a0..529bde7bb 100644 --- a/modules/spa-s3-cloudfront/outputs.tf +++ b/modules/spa-s3-cloudfront/outputs.tf @@ -18,6 +18,11 @@ output "cloudfront_distribution_alias" { description = "Cloudfront Distribution Alias Record." } +output "cloudfront_distribution_identity_arn" { + value = module.spa_web.cf_identity_iam_arn + description = "CloudFront Distribution Origin Access Identity IAM ARN." +} + output "failover_s3_bucket_name" { value = try(data.aws_s3_bucket.failover_bucket[0].bucket, null) description = "Failover Origin bucket name, if enabled." diff --git a/modules/spa-s3-cloudfront/variables.tf b/modules/spa-s3-cloudfront/variables.tf index 2831bb64c..cfa689846 100644 --- a/modules/spa-s3-cloudfront/variables.tf +++ b/modules/spa-s3-cloudfront/variables.tf @@ -66,6 +66,29 @@ variable "s3_object_ownership" { description = "Specifies the S3 object ownership control on the origin bucket. Valid values are `ObjectWriter`, `BucketOwnerPreferred`, and 'BucketOwnerEnforced'." } +variable "s3_origins" { + type = list(object({ + domain_name = string + origin_id = string + origin_path = string + s3_origin_config = object({ + origin_access_identity = string + }) + })) + default = [] + description = <<-EOT + A list of S3 [origins](https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html#origin-arguments) (in addition to the one created by this component) for this distribution. + S3 buckets configured as websites are `custom_origins`, not `s3_origins`. + Specifying `s3_origin_config.origin_access_identity` as `null` or `""` will have it translated to the `origin_access_identity` used by the origin created by this component. + EOT +} + +variable "origin_bucket" { + type = string + default = null + description = "Name of an existing S3 bucket to use as the origin. If this is not provided, this component will create a new s3 bucket using `var.name` and other context related inputs" +} + variable "origin_s3_access_logging_enabled" { type = bool default = null From 2c73ce30d43c4d1bd3345158ea2f6c8616e4a46f Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:07:52 -0500 Subject: [PATCH 454/501] Upgrade Supported ArgoCD Chart Version (#1081) --- modules/eks/argocd/README.md | 2 +- modules/eks/argocd/variables-helm.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 054d16ca7..6013062b3 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -530,7 +530,7 @@ Reference: https://stackoverflow.com/questions/75046330/argo-cd-error-server-sec | [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | | [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | `"https://argoproj.github.io/argo-helm"` | no | | [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values. | `any` | `{}` | no | -| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"5.19.12"` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `"5.55.0"` | no | | [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [create\_github\_webhook](#input\_create\_github\_webhook) | Enable GitHub webhook creation

Use this to create the GitHub Webhook for the given ArgoCD repo using the value created when `var.github_webhook_enabled` is `true`. | `bool` | `true` | no | diff --git a/modules/eks/argocd/variables-helm.tf b/modules/eks/argocd/variables-helm.tf index ddbafbd81..af8411c3e 100644 --- a/modules/eks/argocd/variables-helm.tf +++ b/modules/eks/argocd/variables-helm.tf @@ -26,7 +26,7 @@ variable "chart_repository" { variable "chart_version" { type = string description = "Specify the exact chart version to install. If this is not specified, the latest version is installed." - default = "5.19.12" + default = "5.55.0" } variable "resources" { From b17bb96c24ed405929ec82f186225d2af8bf200e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 24 Jul 2024 15:41:00 -0400 Subject: [PATCH 455/501] Fix README Format (#1083) --- modules/ec2-client-vpn/README.md | 4 +++- .../ecs-service/docs/ecs-partial-task-definitions.md | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index 41db52018..04b6de797 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -86,7 +86,9 @@ A browser will launch and allow you to connect to the VPN. client vpn is deployed (e.g. `ue2-corp`) 1. Use `nmap` to test if the port is `open`. If the port is `filtered` then it's not open. - nmap -p +```console +nmap -p +``` Successful tests have been seen with MSK and RDS. diff --git a/modules/ecs-service/docs/ecs-partial-task-definitions.md b/modules/ecs-service/docs/ecs-partial-task-definitions.md index e67802b1e..18c7366a2 100644 --- a/modules/ecs-service/docs/ecs-partial-task-definitions.md +++ b/modules/ecs-service/docs/ecs-partial-task-definitions.md @@ -71,11 +71,13 @@ This also means that when something goes wrong, it becomes harder to troubleshoo
This bucket should be in the same account as the ECS Cluster. -
S3 Bucket Default Definition +
+
+ S3 Bucket Default Definition ```yaml components: - Terraform: + terraform: s3-bucket/defaults: metadata: type: abstract @@ -114,7 +116,7 @@ This also means that when something goes wrong, it becomes harder to troubleshoo noncurrent_version_expiration: {} ``` -
+
```yaml import: @@ -173,7 +175,8 @@ This also means that when something goes wrong, it becomes harder to troubleshoo should have the input `mirror_to_s3_bucket` set to the S3 bucket name. the variable `use_partial_taskdefinition` should be set to `'true'` -
Example GitHub Action Step +
+ Example GitHub Action Step ```yaml - name: Deploy From d12201d0affeffd14e5d47276934cfd4b91c2d15 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 25 Jul 2024 16:09:04 -0400 Subject: [PATCH 456/501] Fix: README Formatting for Docusarus (#1084) --- modules/account/README.md | 30 ++++++++++++------- modules/datadog-lambda-forwarder/README.md | 4 +-- modules/datadog-lambda-forwarder/variables.tf | 2 +- modules/dns-primary/README.md | 4 ++- modules/ecr/README.md | 6 ++-- .../eks/actions-runner-controller/README.md | 8 +++-- modules/eks/datadog-agent/README.md | 5 +++- modules/eks/github-actions-runner/README.md | 6 ++-- modules/github-runners/README.md | 12 +++++--- modules/redshift/README.md | 4 +-- modules/redshift/versions.tf | 2 +- 11 files changed, 53 insertions(+), 30 deletions(-) diff --git a/modules/account/README.md b/modules/account/README.md index b9f6fa874..9e0613bfb 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -4,7 +4,9 @@ This component is responsible for provisioning the full account hierarchy along includes the ability to associate Service Control Policies (SCPs) to the Organization, each Organizational Unit and account. -:::info Part of a +:::info + +Part of a [cold start](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) so it has to be initially run with `SuperAdmin` role. @@ -176,11 +178,13 @@ SuperAdmin) credentials you have saved in 1Password. #### Request an increase in the maximum number of accounts allowed -:::caution Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is -necessary to expedite the quota increase requests, which could take several days on a basic support plan. Without it, -AWS support will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the -requests. AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to your -AWS TAM. See [AWS](https://docs.cloudposse.com/reference-architecture/reference/aws). +:::caution + +Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is necessary +to expedite the quota increase requests, which could take several days on a basic support plan. Without it, AWS support +will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the requests. +AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to your AWS TAM. +See [AWS](https://docs.cloudposse.com/reference-architecture/reference/aws). ::: @@ -314,15 +318,19 @@ atmos terraform import account --stack core-gbl-root 'aws_organizations_organiza AWS accounts and organizational units are generated dynamically by the `terraform/account` component using the configuration in the `gbl-root` stack. -:::info _**Special note:**_ \*\*\*\* In the rare case where you will need to be enabling non-default AWS Regions, -temporarily comment out the `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore -it later, after enabling the optional Regions. See related: +:::info _**Special note:**_ + +In the rare case where you will need to be enabling non-default AWS Regions, temporarily comment out the +`DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore it later, after enabling the +optional Regions. See related: [Decide on Opting Into Non-default Regions](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) ::: -:::caution **You must wait until your quota increase request has been granted.** If you try to create the accounts -before the quota increase is granted, you can expect to see failures like `ACCOUNT_NUMBER_LIMIT_EXCEEDED`. +:::caution You must wait until your quota increase request has been granted + +If you try to create the accounts before the quota increase is granted, you can expect to see failures like +`ACCOUNT_NUMBER_LIMIT_EXCEEDED`. ::: diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 6de150851..a80caa4c8 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -85,7 +85,7 @@ components: |------|-------------|------|---------|:--------:| | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [cloudwatch\_forwarder\_event\_patterns](#input\_cloudwatch\_forwarder\_event\_patterns) | Map of title => CloudWatch Event patterns to forward to Datadog. Event structure from here:
Example:
hcl
cloudwatch_forwarder_event_rules = {
"guardduty" = {
source = ["aws.guardduty"]
detail-type = ["GuardDuty Finding"]
}
"ec2-terminated" = {
source = ["aws.ec2"]
detail-type = ["EC2 Instance State-change Notification"]
detail = {
state = ["terminated"]
}
}
}
|
map(object({
version = optional(list(string))
id = optional(list(string))
detail-type = optional(list(string))
source = optional(list(string))
account = optional(list(string))
time = optional(list(string))
region = optional(list(string))
resources = optional(list(string))
detail = optional(map(list(string)))
}))
| `{}` | no | +| [cloudwatch\_forwarder\_event\_patterns](#input\_cloudwatch\_forwarder\_event\_patterns) | Map of title to CloudWatch Event patterns to forward to Datadog. Event structure from here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html#CloudWatchEventsPatterns
Example:
hcl
cloudwatch_forwarder_event_rules = {
"guardduty" = {
source = ["aws.guardduty"]
detail-type = ["GuardDuty Finding"]
}
"ec2-terminated" = {
source = ["aws.ec2"]
detail-type = ["EC2 Instance State-change Notification"]
detail = {
state = ["terminated"]
}
}
}
|
map(object({
version = optional(list(string))
id = optional(list(string))
detail-type = optional(list(string))
source = optional(list(string))
account = optional(list(string))
time = optional(list(string))
region = optional(list(string))
resources = optional(list(string))
detail = optional(map(list(string)))
}))
| `{}` | no | | [cloudwatch\_forwarder\_log\_groups](#input\_cloudwatch\_forwarder\_log\_groups) | Map of CloudWatch Log Groups with a filter pattern that the Lambda forwarder will send logs from. For example: { mysql1 = { name = "/aws/rds/maincluster", filter\_pattern = "" } | `map(map(string))` | `{}` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [context\_tags](#input\_context\_tags) | List of context tags to add to each monitor | `set(string)` |
[
"namespace",
"tenant",
"environment",
"stage"
]
| no | @@ -154,7 +154,7 @@ components: ## References -- Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys +- Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) - [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-lambda-forwarder) - Cloud Posse's upstream component diff --git a/modules/datadog-lambda-forwarder/variables.tf b/modules/datadog-lambda-forwarder/variables.tf index 0ef5ecc55..69d8410e2 100644 --- a/modules/datadog-lambda-forwarder/variables.tf +++ b/modules/datadog-lambda-forwarder/variables.tf @@ -127,7 +127,7 @@ variable "cloudwatch_forwarder_event_patterns" { detail = optional(map(list(string))) })) description = <<-EOF - Map of title => CloudWatch Event patterns to forward to Datadog. Event structure from here: + Map of title to CloudWatch Event patterns to forward to Datadog. Event structure from here: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html#CloudWatchEventsPatterns Example: ```hcl cloudwatch_forwarder_event_rules = { diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index 668dd0bf0..9d6d0df29 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -93,7 +93,9 @@ components: YourVeryLongStringGoesHere ``` -:::info Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component for more advanced certificate +:::info + +Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component for more advanced certificate requirements. ::: diff --git a/modules/ecr/README.md b/modules/ecr/README.md index 413b40394..ea55966cf 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -6,8 +6,10 @@ This utilizes to assign accounts to various roles. It is also compatible with the [GitHub Actions IAM Role mixin](https://github.com/cloudposse/terraform-aws-components/blob/master/mixins/github-actions-iam-role/README-github-action-iam-role.md). -:::caution Older versions of our reference architecture have an`eks-iam` component that needs to be updated to provide -sufficient IAM roles to allow pods to pull from ECR repos +:::caution + +Older versions of our reference architecture have an`eks-iam` component that needs to be updated to provide sufficient +IAM roles to allow pods to pull from ECR repos ::: diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 9d7886ef7..ea1a144b3 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -384,9 +384,11 @@ scale down to zero before finishing all the jobs, leaving some waiting indefinit the `max_duration` to a time long enough to cover the full time a job may have to wait between the time it is queued and the time it finishes, assuming that the HRA scales up the pool by 1 and runs the job on the new runner. -:::info If there are more jobs queued than there are runners allowed by `maxReplicas`, the timeout timer does not start -on the capacity reservation until enough reservations ahead of it are removed for it to be considered as representing -and active job. Although there are some edge cases regarding `max_duration` that seem not to be covered properly (see +:::info + +If there are more jobs queued than there are runners allowed by `maxReplicas`, the timeout timer does not start on the +capacity reservation until enough reservations ahead of it are removed for it to be considered as representing and +active job. Although there are some edge cases regarding `max_duration` that seem not to be covered properly (see [actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)), they only merit adding a few extra minutes to the timeout. diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index 6885ab282..c4373bab5 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -105,8 +105,11 @@ for `.yaml`. #### Sample Yaml -:::caution The key of a filename must match datadog docs, which is `.yaml` +:::caution + +The key of a filename must match datadog docs, which is `.yaml` [Datadog Cluster Checks](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) + ::: Cluster Checks **can** be used for external URL testing (loadbalancer endpoints), whereas annotations **must** be used diff --git a/modules/eks/github-actions-runner/README.md b/modules/eks/github-actions-runner/README.md index e9b2cd90b..ab149c1e8 100644 --- a/modules/eks/github-actions-runner/README.md +++ b/modules/eks/github-actions-runner/README.md @@ -27,8 +27,10 @@ NodeJS or `dcarbone/install-jq-action` to install `jq`. You can also install pac `awalsh128/cache-apt-pkgs-action`, which has the advantage of being able to skip the installation if the package is already installed, so you can more efficiently run the same workflow on GitHub hosted as well as self-hosted runners. -:::info There are (as of this writing) open feature requests to add some commonly needed packages to the official Runner -Sets runner image. You can upvote these requests +:::info + +There are (as of this writing) open feature requests to add some commonly needed packages to the official Runner Sets +runner image. You can upvote these requests [here](https://github.com/actions/actions-runner-controller/discussions/3168) and [here](https://github.com/orgs/community/discussions/80868) to help get them implemented. diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 04da5bf8f..8cd4fa30a 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -2,7 +2,9 @@ This component is responsible for provisioning EC2 instances for GitHub runners. -:::info We also have a similar component based on +:::info + +We also have a similar component based on [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) for Kubernetes. ::: @@ -177,9 +179,11 @@ permissions “mode” for Self-hosted runners to Read-Only. The instructions fo ### Creating Registration Token -:::info We highly recommend using a GitHub Application with the github-action-token-rotator module to generate the -Registration Token. This will ensure that the token is rotated and that the token is stored in SSM Parameter Store -encrypted with KMS. +:::info + +We highly recommend using a GitHub Application with the github-action-token-rotator module to generate the Registration +Token. This will ensure that the token is rotated and that the token is stored in SSM Parameter Store encrypted with +KMS. ::: diff --git a/modules/redshift/README.md b/modules/redshift/README.md index 90e9e6bca..52d0fd6a0 100644 --- a/modules/redshift/README.md +++ b/modules/redshift/README.md @@ -46,14 +46,14 @@ components: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.17, <=4.67.0 | +| [aws](#requirement\_aws) | >= 4.17, <= 4.67.0 | | [random](#requirement\_random) | >= 3.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.17, <=4.67.0 | +| [aws](#provider\_aws) | >= 4.17, <= 4.67.0 | | [random](#provider\_random) | >= 3.0 | ## Modules diff --git a/modules/redshift/versions.tf b/modules/redshift/versions.tf index f04baf043..4eeaaf38e 100644 --- a/modules/redshift/versions.tf +++ b/modules/redshift/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.17, <=4.67.0" + version = ">= 4.17, <= 4.67.0" } random = { source = "hashicorp/random" From ffd1fb6ad965094da12b225a227b41544eb343ab Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 31 Jul 2024 17:26:32 -0400 Subject: [PATCH 457/501] feat: Auth0 Components (#1086) --- modules/auth0/app/README.md | 138 ++++++++++ modules/auth0/app/context.tf | 279 +++++++++++++++++++++ modules/auth0/app/main.tf | 25 ++ modules/auth0/app/outputs.tf | 4 + modules/auth0/app/provider-auth0-client.tf | 101 ++++++++ modules/auth0/app/providers.tf | 19 ++ modules/auth0/app/variables.tf | 64 +++++ modules/auth0/app/versions.tf | 14 ++ modules/auth0/tenant/README.md | 169 +++++++++++++ modules/auth0/tenant/context.tf | 279 +++++++++++++++++++++ modules/auth0/tenant/main.tf | 89 +++++++ modules/auth0/tenant/outputs.tf | 19 ++ modules/auth0/tenant/provider-auth0.tf | 36 +++ modules/auth0/tenant/providers.tf | 19 ++ modules/auth0/tenant/remote-state.tf | 9 + modules/auth0/tenant/variables.tf | 110 ++++++++ modules/auth0/tenant/versions.tf | 14 ++ 17 files changed, 1388 insertions(+) create mode 100644 modules/auth0/app/README.md create mode 100644 modules/auth0/app/context.tf create mode 100644 modules/auth0/app/main.tf create mode 100644 modules/auth0/app/outputs.tf create mode 100644 modules/auth0/app/provider-auth0-client.tf create mode 100644 modules/auth0/app/providers.tf create mode 100644 modules/auth0/app/variables.tf create mode 100644 modules/auth0/app/versions.tf create mode 100644 modules/auth0/tenant/README.md create mode 100644 modules/auth0/tenant/context.tf create mode 100644 modules/auth0/tenant/main.tf create mode 100644 modules/auth0/tenant/outputs.tf create mode 100644 modules/auth0/tenant/provider-auth0.tf create mode 100644 modules/auth0/tenant/providers.tf create mode 100644 modules/auth0/tenant/remote-state.tf create mode 100644 modules/auth0/tenant/variables.tf create mode 100644 modules/auth0/tenant/versions.tf diff --git a/modules/auth0/app/README.md b/modules/auth0/app/README.md new file mode 100644 index 000000000..929b4be2d --- /dev/null +++ b/modules/auth0/app/README.md @@ -0,0 +1,138 @@ +# Component: `auth0/app` + +Auth0 Application component. [Auth0](https://auth0.com/docs/) is a third-party service that provides authentication and +authorization as a service. It is typically used to to authenticate users. + +An Auth0 application is a client that can request authentication and authorization from an Auth0 server. Auth0 +applications can be of different types, such as regular web applications, single-page applications, machine-to-machine +applications, and others. Each application has a set of allowed origins, allowed callback URLs, and allowed web origins. + +## Usage + +Before deploying this component, you need to deploy the `auth0/tenant` component. This components with authenticate with +the [Auth0 Terraform provider](https://registry.terraform.io/providers/auth0/auth0/latest/) using the Auth0 tenant's +client ID and client secret configured with the `auth0/tenant` component. + +**Stack Level**: Global + +Here's an example snippet for how to use this component. + +```yaml +# stacks/catalog/auth0/app.yaml +components: + terraform: + auth0/app: + vars: + enabled: true + name: "auth0" + + # We can centralize plat-sandbox, plat-dev, and plat-staging all use a "nonprod" Auth0 tenant, which is deployed in plat-staging. + auth0_tenant_stage_name: "plat-staging" + + # Common client configuration + grant_types: + - "authorization_code" + - "refresh_token" + - "implicit" + - "client_credentials" + + # Stage-specific client configuration + callbacks: + - "https://auth.acme-dev.com/login/auth0/callback" + allowed_origins: + - "https://*.acme-dev.com" + web_origins: + - "https://portal.acme-dev.com" + - "https://auth.acme-dev.com" +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [auth0](#requirement\_auth0) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [auth0](#provider\_auth0) | >= 1.0.0 | +| [aws.auth0\_provider](#provider\_aws.auth0\_provider) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [auth0\_tenant](#module\_auth0\_tenant) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [iam\_roles\_auth0\_provider](#module\_iam\_roles\_auth0\_provider) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [auth0_client.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/client) | resource | +| [aws_ssm_parameter.auth0_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.auth0_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.auth0_domain](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [allowed\_origins](#input\_allowed\_origins) | Allowed Origins | `list(string)` | `[]` | no | +| [app\_type](#input\_app\_type) | Auth0 Application Type | `string` | `"regular_web"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [auth0\_debug](#input\_auth0\_debug) | Enable debug mode for the Auth0 provider | `bool` | `true` | no | +| [auth0\_tenant\_component\_name](#input\_auth0\_tenant\_component\_name) | The name of the component | `string` | `"auth0/tenant"` | no | +| [auth0\_tenant\_environment\_name](#input\_auth0\_tenant\_environment\_name) | The name of the environment where the Auth0 tenant component is deployed. Defaults to the environment of the current stack. | `string` | `""` | no | +| [auth0\_tenant\_stage\_name](#input\_auth0\_tenant\_stage\_name) | The name of the stage where the Auth0 tenant component is deployed. Defaults to the stage of the current stack. | `string` | `""` | no | +| [auth0\_tenant\_tenant\_name](#input\_auth0\_tenant\_tenant\_name) | The name of the tenant where the Auth0 tenant component is deployed. Yes this is a bit redundant, since Auth0 also calls this resource a tenant. Defaults to the tenant of the current stack. | `string` | `""` | no | +| [callbacks](#input\_callbacks) | Allowed Callback URLs | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [grant\_types](#input\_grant\_types) | Allowed Grant Types | `list(string)` | `[]` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [jwt\_alg](#input\_jwt\_alg) | JWT Algorithm | `string` | `"RS256"` | no | +| [jwt\_lifetime\_in\_seconds](#input\_jwt\_lifetime\_in\_seconds) | JWT Lifetime in Seconds | `number` | `36000` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [logo\_uri](#input\_logo\_uri) | Logo URI | `string` | `"https://cloudposse.com/wp-content/uploads/2017/07/CloudPosse2-TRANSAPRENT.png"` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [oidc\_conformant](#input\_oidc\_conformant) | OIDC Conformant | `bool` | `true` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [sso](#input\_sso) | Single Sign-On for the Auth0 app | `bool` | `true` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [web\_origins](#input\_web\_origins) | Allowed Web Origins | `list(string)` | `[]` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [auth0\_client\_id](#output\_auth0\_client\_id) | The Auth0 Application Client ID | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/auth0) - + Cloud Posse's upstream component +- [Auth0 Terraform Provider](https://registry.terraform.io/providers/auth0/auth0/latest/) +- [Auth0 Documentation](https://auth0.com/docs/) + +[](https://cpco.io/component) diff --git a/modules/auth0/app/context.tf b/modules/auth0/app/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/auth0/app/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/auth0/app/main.tf b/modules/auth0/app/main.tf new file mode 100644 index 000000000..9a098c365 --- /dev/null +++ b/modules/auth0/app/main.tf @@ -0,0 +1,25 @@ +locals { + enabled = module.this.enabled +} + +resource "auth0_client" "this" { + count = local.enabled ? 1 : 0 + + name = module.this.id + + app_type = var.app_type + oidc_conformant = var.oidc_conformant + sso = var.sso + + jwt_configuration { + lifetime_in_seconds = var.jwt_lifetime_in_seconds + alg = var.jwt_alg + } + + callbacks = var.callbacks + allowed_origins = var.allowed_origins + web_origins = var.web_origins + grant_types = var.grant_types + logo_uri = var.logo_uri + +} diff --git a/modules/auth0/app/outputs.tf b/modules/auth0/app/outputs.tf new file mode 100644 index 000000000..f169d9e94 --- /dev/null +++ b/modules/auth0/app/outputs.tf @@ -0,0 +1,4 @@ +output "auth0_client_id" { + value = auth0_client.this[0].client_id + description = "The Auth0 Application Client ID" +} diff --git a/modules/auth0/app/provider-auth0-client.tf b/modules/auth0/app/provider-auth0-client.tf new file mode 100644 index 000000000..b6cbb4ff1 --- /dev/null +++ b/modules/auth0/app/provider-auth0-client.tf @@ -0,0 +1,101 @@ +# +# Fetch the Auth0 tenant component deployment in some stack +# +variable "auth0_tenant_component_name" { + description = "The name of the component" + type = string + default = "auth0/tenant" +} + +variable "auth0_tenant_environment_name" { + description = "The name of the environment where the Auth0 tenant component is deployed. Defaults to the environment of the current stack." + type = string + default = "" +} + +variable "auth0_tenant_stage_name" { + description = "The name of the stage where the Auth0 tenant component is deployed. Defaults to the stage of the current stack." + type = string + default = "" +} + +variable "auth0_tenant_tenant_name" { + description = "The name of the tenant where the Auth0 tenant component is deployed. Yes this is a bit redundant, since Auth0 also calls this resource a tenant. Defaults to the tenant of the current stack." + type = string + default = "" +} + +module "auth0_tenant" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.enabled ? 1 : 0 + + component = var.auth0_tenant_component_name + + environment = length(var.auth0_tenant_environment_name) > 0 ? var.auth0_tenant_environment_name : module.this.environment + stage = length(var.auth0_tenant_stage_name) > 0 ? var.auth0_tenant_stage_name : module.this.stage + tenant = length(var.auth0_tenant_tenant_name) > 0 ? var.auth0_tenant_tenant_name : module.this.tenant +} + +# +# Set up the AWS provider to access AWS SSM parameters in the same account as the Auth0 tenant +# + +provider "aws" { + alias = "auth0_provider" + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles_auth0_provider.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles_auth0_provider.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles_auth0_provider.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles_auth0_provider" { + source = "../../account-map/modules/iam-roles" + + environment = length(var.auth0_tenant_environment_name) > 0 ? var.auth0_tenant_environment_name : module.this.environment + stage = length(var.auth0_tenant_stage_name) > 0 ? var.auth0_tenant_stage_name : module.this.stage + tenant = length(var.auth0_tenant_tenant_name) > 0 ? var.auth0_tenant_tenant_name : module.this.tenant + + context = module.this.context +} + +data "aws_ssm_parameter" "auth0_domain" { + provider = aws.auth0_provider + name = module.auth0_tenant[0].outputs.domain_ssm_path +} + +data "aws_ssm_parameter" "auth0_client_id" { + provider = aws.auth0_provider + name = module.auth0_tenant[0].outputs.client_id_ssm_path +} + +data "aws_ssm_parameter" "auth0_client_secret" { + provider = aws.auth0_provider + name = module.auth0_tenant[0].outputs.client_secret_ssm_path +} + +# +# Initialize the Auth0 provider with the Auth0 domain, client ID, and client secret from that deployment +# + +variable "auth0_debug" { + type = bool + description = "Enable debug mode for the Auth0 provider" + default = true +} + +provider "auth0" { + domain = data.aws_ssm_parameter.auth0_domain.value + client_id = data.aws_ssm_parameter.auth0_client_id.value + client_secret = data.aws_ssm_parameter.auth0_client_secret.value + debug = var.auth0_debug +} diff --git a/modules/auth0/app/providers.tf b/modules/auth0/app/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/auth0/app/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/auth0/app/variables.tf b/modules/auth0/app/variables.tf new file mode 100644 index 000000000..bacbf38c0 --- /dev/null +++ b/modules/auth0/app/variables.tf @@ -0,0 +1,64 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "callbacks" { + type = list(string) + description = "Allowed Callback URLs" + default = [] +} + +variable "allowed_origins" { + type = list(string) + description = "Allowed Origins" + default = [] +} + +variable "web_origins" { + type = list(string) + description = "Allowed Web Origins" + default = [] +} + +variable "grant_types" { + type = list(string) + description = "Allowed Grant Types" + default = [] +} + +variable "logo_uri" { + type = string + description = "Logo URI" + default = "https://cloudposse.com/wp-content/uploads/2017/07/CloudPosse2-TRANSAPRENT.png" +} + +variable "app_type" { + type = string + description = "Auth0 Application Type" + default = "regular_web" +} + +variable "oidc_conformant" { + type = bool + description = "OIDC Conformant" + default = true +} + +variable "sso" { + type = bool + description = "Single Sign-On for the Auth0 app" + default = true +} + +variable "jwt_lifetime_in_seconds" { + type = number + description = "JWT Lifetime in Seconds" + default = 36000 +} + +variable "jwt_alg" { + type = string + description = "JWT Algorithm" + default = "RS256" +} diff --git a/modules/auth0/app/versions.tf b/modules/auth0/app/versions.tf new file mode 100644 index 000000000..3894f08a9 --- /dev/null +++ b/modules/auth0/app/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + auth0 = { + source = "auth0/auth0" + version = ">= 1.0.0" + } + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} diff --git a/modules/auth0/tenant/README.md b/modules/auth0/tenant/README.md new file mode 100644 index 000000000..2562725f6 --- /dev/null +++ b/modules/auth0/tenant/README.md @@ -0,0 +1,169 @@ +# Component: `auth0/tenant` + +This component configures an [Auth0](https://auth0.com/docs/) tenant. This component is used to configure authentication +for the Terraform provider for Auth0 and to configure the Auth0 tenant itself. + +## Usage + +**Stack Level**: Global + +Here's an example snippet for how to use this component. + +```yaml +# catalog/auth0/tenant.yaml +components: + terraform: + auth0/tenant: + vars: + enabled: true + name: auth0 + support_email: "tech@acme.com" + support_url: "https://acme.com" +``` + +### Auth0 Tenant Creation + +Chicken before the egg... + +The Auth0 tenant must exist before we can manage it with Terraform. In order to create the Auth0 application used by the +[Auth0 Terraform provider](https://registry.terraform.io/providers/auth0/auth0/latest/), we must first create the Auth0 +tenant. Then once we have the Auth0 provider configured, we can import the tenant into Terraform. However, the tenant is +not a resource identifiable by an ID within the Auth0 Management API! We can nevertheless import it using a random +string. On first run, we import the existing tenant using a random string. It does not matter what this value is. +Terraform will use the same tenant as the Auth0 application for the Terraform Auth0 Provider. + +Create the Auth0 tenant now using the Auth0 Management API or the Auth0 Dashboard following +[the Auth0 create tenants documentation](https://auth0.com/docs/get-started/auth0-overview/create-tenants). + +### Provider Pre-requisites + +Once the Auth0 tenant is created or you've been given access to an existing tenant, you can configure the Auth0 provider +in Terraform. Follow the +[Auth0 provider documentation](https://registry.terraform.io/providers/auth0/auth0/latest/docs/guides/quickstart) to +create a Machine to Machine application. + +:::tip Machine to Machine App Name + +Use the Context Label format for the machine name for consistency. For example, `acme-plat-gbl-prod-auth0-provider`. + +::: + +After creating the Machine to Machine application, add the app's domain, client ID, and client secret to AWS Systems +Manager Parameter Store in the same account and region as this component deployment. The path for the parameters are +defined by the component deployment's Null Label context ID as follows: + +```hcl +auth0_domain_ssm_path = "/${module.this.id}/domain" +auth0_client_id_ssm_path = "/${module.this.id}/client_id" +auth0_client_secret_ssm_path = "/${module.this.id}/client_secret" +``` + +For example, if we're deploying `auth0/tenant` into `plat-gbl-prod` and my default region is `us-west-2`, then I would +add the following parameters to the `plat-prod` account in `us-west-2`: + +``` +/acme-plat-gbl-prod-auth0/domain +/acme-plat-gbl-prod-auth0/client_id +/acme-plat-gbl-prod-auth0/client_secret +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [auth0](#requirement\_auth0) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [auth0](#provider\_auth0) | >= 1.0.0 | +| [aws](#provider\_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [auth0_custom_domain.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/custom_domain) | resource | +| [auth0_custom_domain_verification.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/custom_domain_verification) | resource | +| [auth0_tenant.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/tenant) | resource | +| [aws_route53_record.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | +| [aws_ssm_parameter.auth0_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.auth0_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.auth0_domain](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [allowed\_logout\_urls](#input\_allowed\_logout\_urls) | The URLs that Auth0 can redirect to after logout. | `list(string)` | `[]` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [auth0\_debug](#input\_auth0\_debug) | Enable debug mode for the Auth0 provider | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [default\_redirection\_uri](#input\_default\_redirection\_uri) | The default redirection URI. | `string` | `""` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [disable\_clickjack\_protection\_headers](#input\_disable\_clickjack\_protection\_headers) | Whether to disable clickjack protection headers. | `bool` | `true` | no | +| [disable\_fields\_map\_fix](#input\_disable\_fields\_map\_fix) | Whether to disable fields map fix. | `bool` | `false` | no | +| [disable\_management\_api\_sms\_obfuscation](#input\_disable\_management\_api\_sms\_obfuscation) | Whether to disable management API SMS obfuscation. | `bool` | `false` | no | +| [enable\_public\_signup\_user\_exists\_error](#input\_enable\_public\_signup\_user\_exists\_error) | Whether to enable public signup user exists error. | `bool` | `true` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [enabled\_locales](#input\_enabled\_locales) | The enabled locales. | `list(string)` |
[
"en"
]
| no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [friendly\_name](#input\_friendly\_name) | The friendly name of the Auth0 tenant. If not provided, the module context ID will be used. | `string` | `""` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [idle\_session\_lifetime](#input\_idle\_session\_lifetime) | The idle session lifetime in hours. | `number` | `72` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [no\_disclose\_enterprise\_connections](#input\_no\_disclose\_enterprise\_connections) | Whether to disclose enterprise connections. | `bool` | `false` | no | +| [oidc\_logout\_prompt\_enabled](#input\_oidc\_logout\_prompt\_enabled) | Whether the OIDC logout prompt is enabled. | `bool` | `false` | no | +| [picture\_url](#input\_picture\_url) | The URL of the picture to be displayed in the Auth0 Universal Login page. | `string` | `"https://cloudposse.com/wp-content/uploads/2017/07/CloudPosse2-TRANSAPRENT.png"` | no | +| [provider\_ssm\_base\_path](#input\_provider\_ssm\_base\_path) | The base path for the SSM parameters. If not defined, this is set to the module context ID. This is also required when `var.enabled` is set to `false` | `string` | `""` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [sandbox\_version](#input\_sandbox\_version) | The sandbox version. | `string` | `"18"` | no | +| [session\_cookie\_mode](#input\_session\_cookie\_mode) | The session cookie mode. | `string` | `"persistent"` | no | +| [session\_lifetime](#input\_session\_lifetime) | The session lifetime in hours. | `number` | `168` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [support\_email](#input\_support\_email) | The email address to be displayed in the Auth0 Universal Login page. | `string` | n/a | yes | +| [support\_url](#input\_support\_url) | The URL to be displayed in the Auth0 Universal Login page. | `string` | n/a | yes | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [use\_scope\_descriptions\_for\_consent](#input\_use\_scope\_descriptions\_for\_consent) | Whether to use scope descriptions for consent. | `bool` | `false` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [auth0\_domain](#output\_auth0\_domain) | The Auth0 custom domain | +| [client\_id\_ssm\_path](#output\_client\_id\_ssm\_path) | The SSM parameter path for the Auth0 client ID | +| [client\_secret\_ssm\_path](#output\_client\_secret\_ssm\_path) | The SSM parameter path for the Auth0 client secret | +| [domain\_ssm\_path](#output\_domain\_ssm\_path) | The SSM parameter path for the Auth0 domain | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/auth0) - + Cloud Posse's upstream component +- [Auth0 Terraform Provider](https://registry.terraform.io/providers/auth0/auth0/latest/) +- [Auth0 Documentation](https://auth0.com/docs/) + +[](https://cpco.io/component) diff --git a/modules/auth0/tenant/context.tf b/modules/auth0/tenant/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/auth0/tenant/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/auth0/tenant/main.tf b/modules/auth0/tenant/main.tf new file mode 100644 index 000000000..570cff8dc --- /dev/null +++ b/modules/auth0/tenant/main.tf @@ -0,0 +1,89 @@ +locals { + enabled = module.this.enabled + + name = length(module.this.name) > 0 ? module.this.name : "auth0" + domain_name = format("%s.%s", local.name, module.dns_gbl_delegated.outputs.default_domain_name) + + friendly_name = length(var.friendly_name) > 0 ? var.friendly_name : module.this.id +} + +# Chicken before the egg... +# +# The tenant must exist before we can manage Auth0 with Terraform, +# but the tenant is not a resource identifiable by an ID within the Auth0 Management API! +# +# However, we can import it using a random string. On first run, we import the existing tenant +# using a random string. It does not matter what this value is. Terraform will use the same +# tenant as the Auth0 application for the Terraform Auth0 Provider. +# +# https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/tenant#import +import { + to = auth0_tenant.this[0] + id = "f6615002-81ff-49f7-afd3-8814d07af4fa" +} + +resource "auth0_tenant" "this" { + count = local.enabled ? 1 : 0 + + friendly_name = local.friendly_name + picture_url = var.picture_url + support_email = var.support_email + support_url = var.support_url + + allowed_logout_urls = var.allowed_logout_urls + idle_session_lifetime = var.idle_session_lifetime + session_lifetime = var.session_lifetime + sandbox_version = var.sandbox_version + enabled_locales = var.enabled_locales + default_redirection_uri = var.default_redirection_uri + + flags { + disable_clickjack_protection_headers = var.disable_clickjack_protection_headers + enable_public_signup_user_exists_error = var.enable_public_signup_user_exists_error + use_scope_descriptions_for_consent = var.use_scope_descriptions_for_consent + no_disclose_enterprise_connections = var.no_disclose_enterprise_connections + disable_management_api_sms_obfuscation = var.disable_management_api_sms_obfuscation + disable_fields_map_fix = var.disable_fields_map_fix + } + + session_cookie { + mode = var.session_cookie_mode + } + + sessions { + oidc_logout_prompt_enabled = var.oidc_logout_prompt_enabled + } +} + +resource "auth0_custom_domain" "this" { + count = local.enabled ? 1 : 0 + + domain = local.domain_name + type = "auth0_managed_certs" +} + +resource "aws_route53_record" "this" { + count = local.enabled ? 1 : 0 + + zone_id = module.dns_gbl_delegated.outputs.default_dns_zone_id + name = local.domain_name + type = try(upper(auth0_custom_domain.this[0].verification[0].methods[0].name), null) + ttl = "300" + records = local.enabled ? [ + auth0_custom_domain.this[0].verification[0].methods[0].record + ] : [] +} + +resource "auth0_custom_domain_verification" "this" { + count = local.enabled ? 1 : 0 + + custom_domain_id = auth0_custom_domain.this[0].id + + timeouts { + create = "15m" + } + + depends_on = [ + aws_route53_record.this, + ] +} diff --git a/modules/auth0/tenant/outputs.tf b/modules/auth0/tenant/outputs.tf new file mode 100644 index 000000000..b51187dc9 --- /dev/null +++ b/modules/auth0/tenant/outputs.tf @@ -0,0 +1,19 @@ +output "domain_ssm_path" { + value = local.auth0_domain_ssm_path + description = "The SSM parameter path for the Auth0 domain" +} + +output "client_id_ssm_path" { + value = local.auth0_client_id_ssm_path + description = "The SSM parameter path for the Auth0 client ID" +} + +output "client_secret_ssm_path" { + value = local.auth0_client_secret_ssm_path + description = "The SSM parameter path for the Auth0 client secret" +} + +output "auth0_domain" { + value = local.domain_name + description = "The Auth0 custom domain" +} diff --git a/modules/auth0/tenant/provider-auth0.tf b/modules/auth0/tenant/provider-auth0.tf new file mode 100644 index 000000000..161849353 --- /dev/null +++ b/modules/auth0/tenant/provider-auth0.tf @@ -0,0 +1,36 @@ +locals { + auth0_domain_ssm_path = local.enabled && length(var.provider_ssm_base_path) == 0 ? "/${module.this.id}/domain" : "/${var.provider_ssm_base_path}/domain" + auth0_client_id_ssm_path = local.enabled && length(var.provider_ssm_base_path) == 0 ? "/${module.this.id}/client_id" : "/${var.provider_ssm_base_path}/client_id" + auth0_client_secret_ssm_path = local.enabled && length(var.provider_ssm_base_path) == 0 ? "/${module.this.id}/client_secret" : "/${var.provider_ssm_base_path}/client_secret" +} + +variable "provider_ssm_base_path" { + type = string + description = "The base path for the SSM parameters. If not defined, this is set to the module context ID. This is also required when `var.enabled` is set to `false`" + default = "" +} + +variable "auth0_debug" { + type = bool + description = "Enable debug mode for the Auth0 provider" + default = true +} + +data "aws_ssm_parameter" "auth0_domain" { + name = local.auth0_domain_ssm_path +} + +data "aws_ssm_parameter" "auth0_client_id" { + name = local.auth0_client_id_ssm_path +} + +data "aws_ssm_parameter" "auth0_client_secret" { + name = local.auth0_client_secret_ssm_path +} + +provider "auth0" { + domain = data.aws_ssm_parameter.auth0_domain.value + client_id = data.aws_ssm_parameter.auth0_client_id.value + client_secret = data.aws_ssm_parameter.auth0_client_secret.value + debug = var.auth0_debug +} diff --git a/modules/auth0/tenant/providers.tf b/modules/auth0/tenant/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/auth0/tenant/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/auth0/tenant/remote-state.tf b/modules/auth0/tenant/remote-state.tf new file mode 100644 index 000000000..1921826eb --- /dev/null +++ b/modules/auth0/tenant/remote-state.tf @@ -0,0 +1,9 @@ +module "dns_gbl_delegated" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + environment = "gbl" + component = "dns-delegated" + + context = module.this.context +} diff --git a/modules/auth0/tenant/variables.tf b/modules/auth0/tenant/variables.tf new file mode 100644 index 000000000..24a9c2696 --- /dev/null +++ b/modules/auth0/tenant/variables.tf @@ -0,0 +1,110 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "friendly_name" { + type = string + description = "The friendly name of the Auth0 tenant. If not provided, the module context ID will be used." + default = "" +} + +variable "picture_url" { + type = string + description = "The URL of the picture to be displayed in the Auth0 Universal Login page." + default = "https://cloudposse.com/wp-content/uploads/2017/07/CloudPosse2-TRANSAPRENT.png" +} + +variable "support_email" { + type = string + description = "The email address to be displayed in the Auth0 Universal Login page." +} + +variable "support_url" { + type = string + description = "The URL to be displayed in the Auth0 Universal Login page." +} + +variable "allowed_logout_urls" { + type = list(string) + description = "The URLs that Auth0 can redirect to after logout." + default = [] +} + +variable "idle_session_lifetime" { + type = number + description = "The idle session lifetime in hours." + default = 72 +} + +variable "session_lifetime" { + type = number + description = "The session lifetime in hours." + default = 168 +} + +variable "sandbox_version" { + type = string + description = "The sandbox version." + default = "18" +} + +variable "enabled_locales" { + type = list(string) + description = "The enabled locales." + default = ["en"] +} + +variable "default_redirection_uri" { + type = string + description = "The default redirection URI." + default = "" +} + +variable "disable_clickjack_protection_headers" { + type = bool + description = "Whether to disable clickjack protection headers." + default = true +} + +variable "enable_public_signup_user_exists_error" { + type = bool + description = "Whether to enable public signup user exists error." + default = true +} + +variable "use_scope_descriptions_for_consent" { + type = bool + description = "Whether to use scope descriptions for consent." + default = false +} + +variable "no_disclose_enterprise_connections" { + type = bool + description = "Whether to disclose enterprise connections." + default = false +} + +variable "disable_management_api_sms_obfuscation" { + type = bool + description = "Whether to disable management API SMS obfuscation." + default = false +} + +variable "disable_fields_map_fix" { + type = bool + description = "Whether to disable fields map fix." + default = false +} + +variable "session_cookie_mode" { + type = string + description = "The session cookie mode." + default = "persistent" +} + +variable "oidc_logout_prompt_enabled" { + type = bool + description = "Whether the OIDC logout prompt is enabled." + default = false +} diff --git a/modules/auth0/tenant/versions.tf b/modules/auth0/tenant/versions.tf new file mode 100644 index 000000000..3894f08a9 --- /dev/null +++ b/modules/auth0/tenant/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + auth0 = { + source = "auth0/auth0" + version = ">= 1.0.0" + } + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From 2198e8e27899695befe37405681bcfb808d25fef Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Fri, 2 Aug 2024 00:25:11 +0200 Subject: [PATCH 458/501] EKS IDP roles added reader (#1089) --- .../eks/idp-roles/charts/idp-roles/Chart.yaml | 4 +- .../templates/clusterrole-reader-extra.yaml | 42 +++++++++++++++++++ .../templates/clusterrole-reader.yaml | 12 ++++++ .../templates/clusterrolebinding-reader.yaml | 15 +++++++ .../idp-roles/charts/idp-roles/values.yaml | 5 +++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 modules/eks/idp-roles/charts/idp-roles/templates/clusterrole-reader-extra.yaml create mode 100644 modules/eks/idp-roles/charts/idp-roles/templates/clusterrole-reader.yaml create mode 100644 modules/eks/idp-roles/charts/idp-roles/templates/clusterrolebinding-reader.yaml diff --git a/modules/eks/idp-roles/charts/idp-roles/Chart.yaml b/modules/eks/idp-roles/charts/idp-roles/Chart.yaml index 35b5bbfae..19b759c5d 100644 --- a/modules/eks/idp-roles/charts/idp-roles/Chart.yaml +++ b/modules/eks/idp-roles/charts/idp-roles/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.1.0" +appVersion: "0.2.0" diff --git a/modules/eks/idp-roles/charts/idp-roles/templates/clusterrole-reader-extra.yaml b/modules/eks/idp-roles/charts/idp-roles/templates/clusterrole-reader-extra.yaml new file mode 100644 index 000000000..2e7d454db --- /dev/null +++ b/modules/eks/idp-roles/charts/idp-roles/templates/clusterrole-reader-extra.yaml @@ -0,0 +1,42 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: "{{ .Values.reader_cluster_role }}-extra" + labels: + rbac.authorization.k8s.io/aggregate-to-reader: "true" +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - list + - get + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - list + - get + - apiGroups: + - storage.k8s.io + resources: + - storageclasses + verbs: + - list + - get + - apiGroups: + - karpenter.k8s.aws + resources: + - ec2nodeclasses + verbs: + - list + - get + - apiGroups: + - karpenter.sh + resources: + - nodepools + verbs: + - list + - get diff --git a/modules/eks/idp-roles/charts/idp-roles/templates/clusterrole-reader.yaml b/modules/eks/idp-roles/charts/idp-roles/templates/clusterrole-reader.yaml new file mode 100644 index 000000000..2e536dfb2 --- /dev/null +++ b/modules/eks/idp-roles/charts/idp-roles/templates/clusterrole-reader.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Values.reader_cluster_role | quote }} +aggregationRule: + clusterRoleSelectors: + - matchLabels: + rbac.authorization.k8s.io/aggregate-to-view: "true" + - matchLabels: + rbac.authorization.k8s.io/aggregate-to-observer: "true" + - matchLabels: + rbac.authorization.k8s.io/aggregate-to-reader: "true" diff --git a/modules/eks/idp-roles/charts/idp-roles/templates/clusterrolebinding-reader.yaml b/modules/eks/idp-roles/charts/idp-roles/templates/clusterrolebinding-reader.yaml new file mode 100644 index 000000000..2723b9d7e --- /dev/null +++ b/modules/eks/idp-roles/charts/idp-roles/templates/clusterrolebinding-reader.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.reader_crb_name | quote }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Values.reader_cluster_role | quote }} +subjects: +- apiGroup: rbac.authorization.k8s.io + kind: Group + name: {{ .Values.reader_client_role | quote }} +- apiGroup: rbac.authorization.k8s.io + kind: User + name: {{ .Values.reader_client_role | quote }} diff --git a/modules/eks/idp-roles/charts/idp-roles/values.yaml b/modules/eks/idp-roles/charts/idp-roles/values.yaml index 6d4ef2192..af8066ecc 100644 --- a/modules/eks/idp-roles/charts/idp-roles/values.yaml +++ b/modules/eks/idp-roles/charts/idp-roles/values.yaml @@ -27,3 +27,8 @@ poweruser_client_role: "idp:poweruser" observer_crb_name: "idp-observer" observer_cluster_role: "idp-observer" observer_client_role: "idp:observer" + +# Reader +reader_crb_name: "idp-reader" +reader_cluster_role: "idp-reader" +reader_client_role: "idp:reader" From 2f7135abd564410f888955849a6d16065991cf96 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Fri, 2 Aug 2024 00:26:03 +0200 Subject: [PATCH 459/501] Fix account map special accounts like dns, identity support dynamic roles (#1087) --- modules/account-map/dynamic-roles.tf | 4 +- modules/account-map/modules/iam-roles/main.tf | 74 +++++++++++-------- .../account-map/modules/iam-roles/outputs.tf | 12 +-- 3 files changed, 51 insertions(+), 39 deletions(-) diff --git a/modules/account-map/dynamic-roles.tf b/modules/account-map/dynamic-roles.tf index 75d84271c..fe0d6c993 100644 --- a/modules/account-map/dynamic-roles.tf +++ b/modules/account-map/dynamic-roles.tf @@ -63,7 +63,7 @@ locals { for k, v in yamldecode(data.utils_describe_stacks.teams[0].output) : k => v if !local.stack_has_namespace || try(split(module.this.delimiter, k)[local.stack_namespace_index] == module.this.namespace, false) } : local.empty - teams_vars = { for k, v in local.teams_stacks : k => v.components.terraform.aws-teams.vars } + teams_vars = { for k, v in local.teams_stacks : k => v.components.terraform.aws-teams.vars if try(v.components.terraform.aws-teams.vars, null) != null } teams_config = local.dynamic_role_enabled ? values(local.teams_vars)[0].teams_config : local.empty team_names = [for k, v in local.teams_config : k if try(v.enabled, true)] team_arns = { for team_name in local.team_names : team_name => format(local.iam_role_arn_templates[local.account_role_map.identity], team_name) } @@ -72,7 +72,7 @@ locals { for k, v in yamldecode(data.utils_describe_stacks.team_roles[0].output) : k => v if !local.stack_has_namespace || try(split(module.this.delimiter, k)[local.stack_namespace_index] == module.this.namespace, false) } : local.empty - team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform.aws-team-roles.vars } + team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform.aws-team-roles.vars if try(v.components.terraform.aws-team-roles.vars, null) != null } all_team_vars = merge(local.teams_vars, local.team_roles_vars) diff --git a/modules/account-map/modules/iam-roles/main.tf b/modules/account-map/modules/iam-roles/main.tf index 0e17d4f18..3e69a1de3 100644 --- a/modules/account-map/modules/iam-roles/main.tf +++ b/modules/account-map/modules/iam-roles/main.tf @@ -31,42 +31,54 @@ module "account_map" { locals { profiles_enabled = coalesce(var.profiles_enabled, local.account_map.profiles_enabled) - account_map = module.account_map.outputs - account_name = lookup(module.always.descriptors, "account_name", module.always.stage) - account_org_role_arn = local.account_name == local.account_map.root_account_account_name ? null : format( - "arn:%s:iam::%s:role/OrganizationAccountAccessRole", local.account_map.aws_partition, - local.account_map.full_account_map[local.account_name] - ) - dynamic_terraform_role_enabled = try(local.account_map.terraform_dynamic_role_enabled, false) - static_terraform_role = local.account_map.terraform_roles[local.account_name] - dynamic_terraform_role = try(local.dynamic_terraform_role_map[local.dynamic_terraform_role_type], null) + account_map = module.account_map.outputs + account_name = lookup(module.always.descriptors, "account_name", module.always.stage) + root_account_name = local.account_map.root_account_account_name - current_user_role_arn = coalesce(one(data.awsutils_caller_identity.current[*].eks_role_arn), one(data.awsutils_caller_identity.current[*].arn), "disabled") - dynamic_terraform_role_type = try(local.account_map.terraform_access_map[local.current_user_role_arn][local.account_name], "none") + current_user_role_arn = coalesce(one(data.awsutils_caller_identity.current[*].eks_role_arn), one(data.awsutils_caller_identity.current[*].arn), "disabled") current_identity_account = local.dynamic_terraform_role_enabled ? split(":", local.current_user_role_arn)[4] : "" - is_root_user = local.current_identity_account == local.account_map.full_account_map[local.account_map.root_account_account_name] - is_target_user = local.current_identity_account == local.account_map.full_account_map[local.account_name] - - dynamic_terraform_role_map = local.dynamic_terraform_role_enabled ? { - apply = format(local.account_map.iam_role_arn_templates[local.account_name], local.account_map.terraform_role_name_map["apply"]) - plan = format(local.account_map.iam_role_arn_templates[local.account_name], local.account_map.terraform_role_name_map["plan"]) - # For user without explicit permissions: - # If the current user is a user in the `root` account, assume the `OrganizationAccountAccessRole` role in the target account. - # If the current user is a user in the target account, do not assume a role at all, let them do what their role allows. - # Otherwise, force them into the static Terraform role for the target account, - # to prevent users from accidentally running Terraform in the wrong account. - none = local.is_root_user ? local.account_org_role_arn : ( - # null means use current user's role - local.is_target_user ? null : local.static_terraform_role - ) - } : {} - final_terraform_role_arn = local.profiles_enabled ? null : ( - local.dynamic_terraform_role_enabled ? local.dynamic_terraform_role : local.static_terraform_role - ) + terraform_access_map = try(local.account_map.terraform_access_map[local.current_user_role_arn], {}) - final_terraform_profile_name = local.profiles_enabled ? local.account_map.profiles[local.account_name] : null + is_root_user = local.current_identity_account == local.account_map.full_account_map[local.root_account_name] + is_target_user = local.current_identity_account == local.account_map.full_account_map[local.account_name] + + account_org_role_arns = { for name, id in local.account_map.full_account_map : name => + name == local.root_account_name ? null : format( + "arn:%s:iam::%s:role/OrganizationAccountAccessRole", local.account_map.aws_partition, id + ) + } + + static_terraform_roles = local.account_map.terraform_roles + + dynamic_terraform_role_maps = { + for account_name in local.account_map.all_accounts : account_name => { + apply = format(local.account_map.iam_role_arn_templates[account_name], local.account_map.terraform_role_name_map["apply"]) + plan = format(local.account_map.iam_role_arn_templates[account_name], local.account_map.terraform_role_name_map["plan"]) + # For user without explicit permissions: + # If the current user is a user in the `root` account, assume the `OrganizationAccountAccessRole` role in the target account. + # If the current user is a user in the target account, do not assume a role at all, let them do what their role allows. + # Otherwise, force them into the static Terraform role for the target account, + # to prevent users from accidentally running Terraform in the wrong account. + none = local.is_root_user ? local.account_org_role_arns[account_name] : ( + # null means use current user's role + local.is_target_user ? null : local.static_terraform_roles[account_name] + ) + } + } + + dynamic_terraform_role_types = { for account_name in local.account_map.all_accounts : + account_name => try(local.terraform_access_map[account_name], "none") + } + + dynamic_terraform_roles = { for account_name in local.account_map.all_accounts : + account_name => local.dynamic_terraform_role_maps[account_name][local.dynamic_terraform_role_types[account_name]] + } + + final_terraform_role_arns = { for account_name in local.account_map.all_accounts : account_name => + local.dynamic_terraform_role_enabled ? local.dynamic_terraform_roles[account_name] : local.static_terraform_roles[account_name] + } } diff --git a/modules/account-map/modules/iam-roles/outputs.tf b/modules/account-map/modules/iam-roles/outputs.tf index 380f4d89a..049380636 100644 --- a/modules/account-map/modules/iam-roles/outputs.tf +++ b/modules/account-map/modules/iam-roles/outputs.tf @@ -1,5 +1,5 @@ output "terraform_role_arn" { - value = local.profiles_enabled ? null : local.final_terraform_role_arn + value = local.profiles_enabled ? null : local.final_terraform_role_arns[local.account_name] description = "The AWS Role ARN for Terraform to use when provisioning resources in the account, when Role ARNs are in use" } @@ -9,7 +9,7 @@ output "terraform_role_arns" { } output "terraform_profile_name" { - value = local.profiles_enabled ? local.final_terraform_profile_name : null + value = local.profiles_enabled ? local.account_map.profiles[local.account_name] : null description = "The AWS config profile name for Terraform to use when provisioning resources in the account, when profiles are in use" } @@ -19,7 +19,7 @@ output "aws_partition" { } output "org_role_arn" { - value = local.account_org_role_arn + value = local.account_org_role_arns[local.account_name] description = "The AWS Role ARN for Terraform to use when SuperAdmin is provisioning resources in the account" } @@ -47,7 +47,7 @@ output "current_account_account_name" { } output "dns_terraform_role_arn" { - value = local.profiles_enabled ? null : local.account_map.terraform_roles[local.account_map.dns_account_account_name] + value = local.profiles_enabled ? null : local.final_terraform_role_arns[local.account_map.dns_account_account_name] description = "The AWS Role ARN for Terraform to use to provision DNS Zone delegations, when Role ARNs are in use" } @@ -57,7 +57,7 @@ output "dns_terraform_profile_name" { } output "audit_terraform_role_arn" { - value = local.profiles_enabled ? null : local.account_map.terraform_roles[local.account_map.audit_account_account_name] + value = local.profiles_enabled ? null : local.final_terraform_role_arns[local.account_map.audit_account_account_name] description = "The AWS Role ARN for Terraform to use to provision resources in the \"audit\" role account, when Role ARNs are in use" } @@ -72,7 +72,7 @@ output "identity_account_account_name" { } output "identity_terraform_role_arn" { - value = local.profiles_enabled ? null : local.account_map.terraform_roles[local.account_map.identity_account_account_name] + value = local.profiles_enabled ? null : local.final_terraform_role_arns[local.account_map.identity_account_account_name] description = "The AWS Role ARN for Terraform to use to provision resources in the \"identity\" role account, when Role ARNs are in use" } From 199c6701ae6a3794fb4d68376cfc43bdb04d3006 Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Fri, 2 Aug 2024 22:45:54 +0200 Subject: [PATCH 460/501] Added additional polices for vpn and kms - required by planner (#1088) Co-authored-by: Nuru --- modules/aws-team-roles/main.tf | 4 +- modules/aws-team-roles/policy-kms-planner.tf | 48 ++++++++++++++++++++ modules/aws-team-roles/policy-vpn-planner.tf | 36 +++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 modules/aws-team-roles/policy-kms-planner.tf create mode 100644 modules/aws-team-roles/policy-vpn-planner.tf diff --git a/modules/aws-team-roles/main.tf b/modules/aws-team-roles/main.tf index 5d4fc2ff2..ff68d4191 100644 --- a/modules/aws-team-roles/main.tf +++ b/modules/aws-team-roles/main.tf @@ -11,7 +11,9 @@ locals { # using an aws_iam_policy resource and then map it to the name you want to use in the # YAML configuration by adding an entry in `custom_policy_map`. supplied_custom_policy_map = { - eks_viewer = try(aws_iam_policy.eks_viewer[0].arn, null) + eks_viewer = try(aws_iam_policy.eks_viewer[0].arn, null) + vpn_planner = try(aws_iam_policy.vpn_planner[0].arn, null) + kms_planner = try(aws_iam_policy.kms_planner[0].arn, null) } custom_policy_map = merge(local.supplied_custom_policy_map, local.overridable_additional_custom_policy_map) diff --git a/modules/aws-team-roles/policy-kms-planner.tf b/modules/aws-team-roles/policy-kms-planner.tf new file mode 100644 index 000000000..45080b183 --- /dev/null +++ b/modules/aws-team-roles/policy-kms-planner.tf @@ -0,0 +1,48 @@ +locals { + kms_planner_enabled = contains(local.configured_policies, "kms_planner") +} + +data "aws_iam_policy_document" "kms_planner_access" { + count = local.kms_planner_enabled ? 1 : 0 + + statement { + sid = "AllowKMSDecrypt" + effect = "Allow" + + actions = [ + "kms:Decrypt", + ] + + # Only allow decryption of SSM parameters. + # To further restrict to specific parameters, add conditions on the value of + # kms:EncryptionContext:PARAMETER_ARN + # See https://docs.aws.amazon.com/kms/latest/developerguide/services-parameter-store.html#parameter-store-encryption-context + condition { + test = "Null" + variable = "kms:EncryptionContext:PARAMETER_ARN" + values = ["false"] + } + + resources = [ + "*" + ] + } + +} + +data "aws_iam_policy_document" "kms_planner_access_aggregated" { + count = local.kms_planner_enabled ? 1 : 0 + + source_policy_documents = [ + data.aws_iam_policy_document.kms_planner_access[0].json, + ] +} + +resource "aws_iam_policy" "kms_planner" { + count = local.kms_planner_enabled ? 1 : 0 + + name = format("%s-kms_planner", module.this.id) + policy = data.aws_iam_policy_document.kms_planner_access_aggregated[0].json + + tags = module.this.tags +} diff --git a/modules/aws-team-roles/policy-vpn-planner.tf b/modules/aws-team-roles/policy-vpn-planner.tf new file mode 100644 index 000000000..09a4c8c11 --- /dev/null +++ b/modules/aws-team-roles/policy-vpn-planner.tf @@ -0,0 +1,36 @@ +locals { + vpn_planner_enabled = contains(local.configured_policies, "vpn_planner") +} + +data "aws_iam_policy_document" "vpn_planner_access" { + count = local.vpn_planner_enabled ? 1 : 0 + + statement { + sid = "AllowVPNReader" + effect = "Allow" + actions = [ + "ec2:ExportClientVpnClientConfiguration", + ] + resources = [ + "*" + ] + } + +} + +data "aws_iam_policy_document" "vpn_planner_access_aggregated" { + count = local.vpn_planner_enabled ? 1 : 0 + + source_policy_documents = [ + data.aws_iam_policy_document.vpn_planner_access[0].json, + ] +} + +resource "aws_iam_policy" "vpn_planner" { + count = local.vpn_planner_enabled ? 1 : 0 + + name = format("%s-vpn_planner", module.this.id) + policy = data.aws_iam_policy_document.vpn_planner_access_aggregated[0].json + + tags = module.this.tags +} From 27be807d020821ec1e72b39ae729b9c38f639b42 Mon Sep 17 00:00:00 2001 From: yangci Date: Mon, 5 Aug 2024 11:08:57 -0400 Subject: [PATCH 461/501] feat(spacelift): support for local files for policies (#1091) --- modules/spacelift/spaces/README.md | 4 ++-- modules/spacelift/spaces/main.tf | 4 +++- modules/spacelift/spaces/variables.tf | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index f8773f8ed..17e110d86 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -82,7 +82,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.6.0 | +| [policy](#module\_policy) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy | 1.7.0 | | [space](#module\_space) | cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-space | 1.6.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -109,7 +109,7 @@ No resources. | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [spaces](#input\_spaces) | A map of all Spaces to create in Spacelift |
map(object({
parent_space_id = string,
description = optional(string),
inherit_entities = optional(bool, false),
labels = optional(set(string), []),
policies = optional(map(object({
body = optional(string),
body_url = optional(string),
body_url_version = optional(string, "master"),
type = optional(string),
labels = optional(set(string), []),
})), {}),
}))
| n/a | yes | +| [spaces](#input\_spaces) | A map of all Spaces to create in Spacelift |
map(object({
parent_space_id = string,
description = optional(string),
inherit_entities = optional(bool, false),
labels = optional(set(string), []),
policies = optional(map(object({
body = optional(string),
body_url = optional(string),
body_url_version = optional(string, "master"),
body_file_path = optional(string),
type = optional(string),
labels = optional(set(string), []),
})), {}),
}))
| n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/spacelift/spaces/main.tf b/modules/spacelift/spaces/main.tf index 99a641663..bcaa6ffe6 100644 --- a/modules/spacelift/spaces/main.tf +++ b/modules/spacelift/spaces/main.tf @@ -25,6 +25,7 @@ locals { body = p.body body_url = p.body_url body_url_version = p.body_url_version + body_file_path = p.body_file_path labels = setunion(toset(v.labels), toset(p.labels)) name = pn space_id = k == "root" ? "root" : module.space[k].space_id @@ -53,7 +54,7 @@ module "space" { module "policy" { source = "cloudposse/cloud-infrastructure-automation/spacelift//modules/spacelift-policy" - version = "1.6.0" + version = "1.7.0" for_each = local.all_policies_inputs @@ -61,6 +62,7 @@ module "policy" { body = each.value.body body_url = each.value.body_url body_url_version = each.value.body_url_version + body_file_path = each.value.body_file_path type = each.value.type labels = each.value.labels space_id = each.value.space_id diff --git a/modules/spacelift/spaces/variables.tf b/modules/spacelift/spaces/variables.tf index 534bb86c4..f11aed45c 100644 --- a/modules/spacelift/spaces/variables.tf +++ b/modules/spacelift/spaces/variables.tf @@ -8,6 +8,7 @@ variable "spaces" { body = optional(string), body_url = optional(string), body_url_version = optional(string, "master"), + body_file_path = optional(string), type = optional(string), labels = optional(set(string), []), })), {}), From fec510659239264540ecbecaeebdc194cd1ff209 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 7 Aug 2024 14:04:22 -0400 Subject: [PATCH 462/501] Replace Admonition Style (#1092) Co-authored-by: Erik Osterman (CEO @ Cloud Posse) --- .../eks/karpenter-provisioner/README.md | 16 ++--- modules/account/README.md | 56 +++++++-------- modules/auth0/tenant/README.md | 10 +-- modules/aws-config/README.md | 69 ++++++++++--------- modules/aws-sso/README.md | 14 ++-- modules/dns-primary/README.md | 10 ++- modules/ecr/README.md | 10 ++- .../actions-runner-controller/CHANGELOG.md | 12 ++-- modules/eks/cluster/CHANGELOG.md | 48 +++++++------ modules/eks/cluster/README.md | 64 ++++++++--------- modules/eks/datadog-agent/README.md | 10 ++- modules/eks/karpenter/CHANGELOG.md | 54 +++++++-------- modules/eks/karpenter/README.md | 46 ++++++------- modules/github-runners/README.md | 30 ++++---- modules/network-firewall/README.md | 38 +++++----- modules/spacelift/README.md | 18 ++--- modules/tfstate-backend/README.md | 20 +++--- 17 files changed, 242 insertions(+), 283 deletions(-) diff --git a/deprecated/eks/karpenter-provisioner/README.md b/deprecated/eks/karpenter-provisioner/README.md index 9f0ce2010..5b79ab02d 100644 --- a/deprecated/eks/karpenter-provisioner/README.md +++ b/deprecated/eks/karpenter-provisioner/README.md @@ -1,13 +1,13 @@ # Component: `eks/karpenter-provisioner` -:::warning This component is DEPRECATED - -With v1beta1 of Karpenter, the `provisioner` component is deprecated. -Please use the `eks/karpenter-node-group` component instead. - -For more details, see the [Karpenter v1beta1 release notes](/modules/eks/karpenter/CHANGELOG.md). - -::: +> [!WARNING] +> +> #### This component is DEPRECATED +> +> With v1beta1 of Karpenter, the `provisioner` component is deprecated. +> Please use the `eks/karpenter-node-group` component instead. +> +> For more details, see the [Karpenter v1beta1 release notes](/modules/eks/karpenter/CHANGELOG.md). This component deploys [Karpenter provisioners](https://karpenter.sh/v0.18.0/aws/provisioning) on an EKS cluster. diff --git a/modules/account/README.md b/modules/account/README.md index 9e0613bfb..1f35f30b8 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -4,13 +4,11 @@ This component is responsible for provisioning the full account hierarchy along includes the ability to associate Service Control Policies (SCPs) to the Organization, each Organizational Unit and account. -:::info - -Part of a -[cold start](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) -so it has to be initially run with `SuperAdmin` role. - -::: +> [!NOTE] +> +> Part of a +> [cold start](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) +> so it has to be initially run with `SuperAdmin` role. In addition, it enables [AWS IAM Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html), which helps @@ -178,15 +176,13 @@ SuperAdmin) credentials you have saved in 1Password. #### Request an increase in the maximum number of accounts allowed -:::caution - -Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is necessary -to expedite the quota increase requests, which could take several days on a basic support plan. Without it, AWS support -will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the requests. -AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to your AWS TAM. -See [AWS](https://docs.cloudposse.com/reference-architecture/reference/aws). - -::: +> [!WARNING] +> +> Make sure your support plan for the _root_ account was upgraded to the "Business" level (or Higher). This is necessary +> to expedite the quota increase requests, which could take several days on a basic support plan. Without it, AWS +> support will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the +> requests. AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to +> your AWS TAM. See [AWS](https://docs.cloudposse.com/reference-architecture/reference/aws). 1. From the region list, select "US East (N. Virginia) us-east-1". @@ -318,21 +314,19 @@ atmos terraform import account --stack core-gbl-root 'aws_organizations_organiza AWS accounts and organizational units are generated dynamically by the `terraform/account` component using the configuration in the `gbl-root` stack. -:::info _**Special note:**_ - -In the rare case where you will need to be enabling non-default AWS Regions, temporarily comment out the -`DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore it later, after enabling the -optional Regions. See related: -[Decide on Opting Into Non-default Regions](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) - -::: - -:::caution You must wait until your quota increase request has been granted - -If you try to create the accounts before the quota increase is granted, you can expect to see failures like -`ACCOUNT_NUMBER_LIMIT_EXCEEDED`. - -::: +> [!IMPORTANT] +> +> In the rare case where you will need to be enabling non-default AWS Regions, temporarily comment out the +> `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore it later, after enabling +> the optional Regions. See related: +> [Decide on Opting Into Non-default Regions](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) + +> [!TIP] +> +> #### You must wait until your quota increase request has been granted +> +> If you try to create the accounts before the quota increase is granted, you can expect to see failures like +> `ACCOUNT_NUMBER_LIMIT_EXCEEDED`. In the Geodesic shell, execute the following commands to provision AWS Organizational Units and AWS accounts: diff --git a/modules/auth0/tenant/README.md b/modules/auth0/tenant/README.md index 2562725f6..e165ca834 100644 --- a/modules/auth0/tenant/README.md +++ b/modules/auth0/tenant/README.md @@ -42,11 +42,11 @@ in Terraform. Follow the [Auth0 provider documentation](https://registry.terraform.io/providers/auth0/auth0/latest/docs/guides/quickstart) to create a Machine to Machine application. -:::tip Machine to Machine App Name - -Use the Context Label format for the machine name for consistency. For example, `acme-plat-gbl-prod-auth0-provider`. - -::: +> [!TIP] +> +> #### Machine to Machine App Name +> +> Use the Context Label format for the machine name for consistency. For example, `acme-plat-gbl-prod-auth0-provider`. After creating the Machine to Machine application, add the app's domain, client ID, and client secret to AWS Systems Manager Parameter Store in the same account and region as this component deployment. The path for the parameters are diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index c8f35b94c..c280c627b 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -20,25 +20,25 @@ Some of the key features of AWS Config include: - Notifications and alerts: AWS Config can send notifications and alerts when changes are made to your AWS resources that could impact their compliance or security posture. -:::caution AWS Config Limitations - -You'll also want to be aware of some limitations with AWS Config: - -- The maximum number of AWS Config rules that can be evaluated in a single account is 1000. - - This can be mitigated by removing rules that are duplicated across packs. You'll have to manually search for these - duplicates. - - You can also look for rules that do not apply to any resources and remove those. You'll have to manually click - through rules in the AWS Config interface to see which rules are not being evaluated. - - If you end up still needing more than 1000 rules, one recommendation is to only run packs on a schedule with a - lambda that removes the pack after results are collected. If you had different schedule for each day of the week, - that would mean 7000 rules over the week. The aggregators would not be able to handle this, so you would need to - make sure to store them somewhere else (i.e. S3) so the findings are not lost. - - See the - [Audit Manager docs](https://aws.amazon.com/blogs/mt/integrate-across-the-three-lines-model-part-2-transform-aws-config-conformance-packs-into-aws-audit-manager-assessments/) - if you think you would like to convert conformance packs to custom Audit Manager assessments. -- The maximum number of AWS Config conformance packs that can be created in a single account is 50. - -::: +> [!WARNING] +> +> #### AWS Config Limitations +> +> You'll also want to be aware of some limitations with AWS Config: +> +> - The maximum number of AWS Config rules that can be evaluated in a single account is 1000. +> - This can be mitigated by removing rules that are duplicated across packs. You'll have to manually search for these +> duplicates. +> - You can also look for rules that do not apply to any resources and remove those. You'll have to manually click +> through rules in the AWS Config interface to see which rules are not being evaluated. +> - If you end up still needing more than 1000 rules, one recommendation is to only run packs on a schedule with a +> lambda that removes the pack after results are collected. If you had different schedule for each day of the week, +> that would mean 7000 rules over the week. The aggregators would not be able to handle this, so you would need to +> make sure to store them somewhere else (i.e. S3) so the findings are not lost. +> - See the +> [Audit Manager docs](https://aws.amazon.com/blogs/mt/integrate-across-the-three-lines-model-part-2-transform-aws-config-conformance-packs-into-aws-audit-manager-assessments/) +> if you think you would like to convert conformance packs to custom Audit Manager assessments. +> - The maximum number of AWS Config conformance packs that can be created in a single account is 50. Overall, AWS Config provides you with a powerful toolset to help you monitor and manage the configurations of your AWS resources, ensuring that they remain compliant, secure, and properly configured over time. @@ -79,21 +79,22 @@ Before deploying this AWS Config component `config-bucket` and `cloudtrail-bucke This component has a `default_scope` variable for configuring if it will be an organization-wide or account-level component by default. Note that this can be overridden by the `scope` variable in the `conformance_packs` items. -:::info Using the account default_scope - -If default_scope == `account`, AWS Config is regional AWS service, so this component needs to be deployed to all -regions. If an individual `conformance_packs` item has `scope` set to `organization`, that particular pack will be -deployed to the organization level. - -::: - -:::info Using the organization default_scope - -If default_scope == `organization`, AWS Config is global unless overriden in the `conformance_packs` items. You will -need to update your org to allow the `config-multiaccountsetup.amazonaws.com` service access principal for this to work. -If you are using our `account` component, just add that principal to the `aws_service_access_principals` variable. - -::: +> [!TIP] +> +> #### Using the account default_scope +> +> If default_scope == `account`, AWS Config is regional AWS service, so this component needs to be deployed to all +> regions. If an individual `conformance_packs` item has `scope` set to `organization`, that particular pack will be +> deployed to the organization level. + +> [!TIP] +> +> #### Using the organization default_scope +> +> If default_scope == `organization`, AWS Config is global unless overriden in the `conformance_packs` items. You will +> need to update your org to allow the `config-multiaccountsetup.amazonaws.com` service access principal for this to +> work. If you are using our `account` component, just add that principal to the `aws_service_access_principals` +> variable. At the AWS Organizational level, the Components designate an account to be the `central collection account` and a single region to be the `central collection region` so that compliance information can be aggregated into a central location. diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index e351537be..d51fa0db4 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -32,14 +32,12 @@ recommended `gbl-root` stack. ### Google Workspace -:::important - -> Your identity source is currently configured as 'External identity provider'. To add new groups or edit their -> memberships, you must do this using your external identity provider. - -Groups _cannot_ be created with ClickOps in the AWS console and instead must be created with AWS API. - -::: +> [!IMPORTANT] +> +> > Your identity source is currently configured as 'External identity provider'. To add new groups or edit their +> > memberships, you must do this using your external identity provider. +> +> Groups _cannot_ be created with ClickOps in the AWS console and instead must be created with AWS API. Google Workspace is now supported by AWS Identity Center, but Group creation is not automatically handled. After [configuring SAML and SCIM with Google Workspace and IAM Identity Center following the AWS documentation](https://docs.aws.amazon.com/singlesignon/latest/userguide/gs-gwp.html), diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index 9d6d0df29..b53c42776 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -93,12 +93,10 @@ components: YourVeryLongStringGoesHere ``` -:::info - -Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component for more advanced certificate -requirements. - -::: +> [!TIP] +> +> Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component for more advanced certificate +> requirements. diff --git a/modules/ecr/README.md b/modules/ecr/README.md index ea55966cf..7ee7c4396 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -6,12 +6,10 @@ This utilizes to assign accounts to various roles. It is also compatible with the [GitHub Actions IAM Role mixin](https://github.com/cloudposse/terraform-aws-components/blob/master/mixins/github-actions-iam-role/README-github-action-iam-role.md). -:::caution - -Older versions of our reference architecture have an`eks-iam` component that needs to be updated to provide sufficient -IAM roles to allow pods to pull from ECR repos - -::: +> [!WARNING] +> +> Older versions of our reference architecture have an`eks-iam` component that needs to be updated to provide sufficient +> IAM roles to allow pods to pull from ECR repos ## Usage diff --git a/modules/eks/actions-runner-controller/CHANGELOG.md b/modules/eks/actions-runner-controller/CHANGELOG.md index d3c2cc338..5fa8bdc77 100644 --- a/modules/eks/actions-runner-controller/CHANGELOG.md +++ b/modules/eks/actions-runner-controller/CHANGELOG.md @@ -76,12 +76,12 @@ of memory allocated to the runner Pod to account for this. This is generally not small enough amount of disk space that it can be reasonably stored in the RAM allocated to a single CPU in an EC2 instance, so it is the CPU that remains the limiting factor in how many Runners can be run on an instance. -:::warning You must configure a memory request for the runner Pod - -When using `tmpfs_enabled`, you must configure a memory request for the runner Pod. If you do not, a single Pod would be -allowed to consume half the Node's memory just for its disk storage. - -::: +> [!WARNING] +> +> #### You must configure a memory request for the runner Pod +> +> When using `tmpfs_enabled`, you must configure a memory request for the runner Pod. If you do not, a single Pod would +> be allowed to consume half the Node's memory just for its disk storage. #### Configure startup timeout via `wait_for_docker_seconds` diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index bef5b7e2f..ae11e83df 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -49,13 +49,13 @@ Components PR [#1033](https://github.com/cloudposse/terraform-aws-components/pul ### Major Breaking Changes -:::warning Major Breaking Changes, Manual Intervention Required - -This release includes a major breaking change that requires manual intervention to migrate existing clusters. The change -is necessary to support the new AWS Access Control API, which is more secure and more reliable than the old `aws-auth` -ConfigMap. - -::: +> [!WARNING] +> +> #### Major Breaking Changes, Manual Intervention Required +> +> This release includes a major breaking change that requires manual intervention to migrate existing clusters. The +> change is necessary to support the new AWS Access Control API, which is more secure and more reliable than the old +> `aws-auth` ConfigMap. This release drops support for the `aws-auth` ConfigMap and switches to managing access control with the new AWS Access Control API. This change allows for more secure and reliable access control, and removes the requirement that Terraform @@ -65,18 +65,18 @@ In this release, this component only supports assigning "team roles" to Kubernet Access Policies is not yet implemented. However, if you specify `system:masters` as a group, that will be translated into assigning the `AmazonEKSClusterAdminPolicy` to the role. Any other `system:*` group will cause an error. -:::tip Network Access Considerations - -Previously, this component required network access to the EKS control plane to manage the `aws-auth` ConfigMap. This -meant having the EKS control plane accessible from the public internet, or using a bastion host or VPN to access the -control plane. With the new AWS Access Control API, Terraform operations on the EKS cluster no longer require network -access to the EKS control plane. - -This may seem like it makes it easier to secure the EKS control plane, but Terraform users will still require network -access to the EKS control plane to manage any deployments or other Kubernetes resources in the cluster. This means that -this upgrade does not substantially change the need for network access. - -::: +> [!TIP] +> +> #### Network Access Considerations +> +> Previously, this component required network access to the EKS control plane to manage the `aws-auth` ConfigMap. This +> meant having the EKS control plane accessible from the public internet, or using a bastion host or VPN to access the +> control plane. With the new AWS Access Control API, Terraform operations on the EKS cluster no longer require network +> access to the EKS control plane. +> +> This may seem like it makes it easier to secure the EKS control plane, but Terraform users will still require network +> access to the EKS control plane to manage any deployments or other Kubernetes resources in the cluster. This means +> that this upgrade does not substantially change the need for network access. ### Minor Changes @@ -94,12 +94,10 @@ Full details of the migration process can be found in the `cloudposse/terraform- [migration document](https://github.com/cloudposse/terraform-aws-eks-cluster/blob/main/docs/migration-v3-v4.md). This section is a streamlined version for users of this `eks/cluster` component. -:::important - -The commands below assume the component is named "eks/cluster". If you are using a different name, replace "eks/cluster" -with the correct component name. - -::: +> [!IMPORTANT] +> +> The commands below assume the component is named "eks/cluster". If you are using a different name, replace +> "eks/cluster" with the correct component name. #### Prepare for Migration diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index f8e6c29ed..cb86484ad 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -3,14 +3,14 @@ This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups and Fargate profiles. -:::note Windows not supported - -This component has not been tested with Windows worker nodes of any launch type. Although upstream modules support -Windows nodes, there are likely issues around incorrect or insufficient IAM permissions or other configuration that -would need to be resolved for this component to properly configure the upstream modules for Windows nodes. If you need -Windows nodes, please experiment and be on the lookout for issues, and then report any issues to Cloud Posse. - -::: +> [!NOTE] +> +> #### Windows not supported +> +> This component has not been tested with Windows worker nodes of any launch type. Although upstream modules support +> Windows nodes, there are likely issues around incorrect or insufficient IAM permissions or other configuration that +> would need to be resolved for this component to properly configure the upstream modules for Windows nodes. If you need +> Windows nodes, please experiment and be on the lookout for issues, and then report any issues to Cloud Posse. ## Usage @@ -191,9 +191,9 @@ components: # Also, it is only supported for AL2 and some Windows AMIs, not BottleRocket or AL2023. # Kubernetes docs: https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/ kubelet_extra_args: >- - --kube-reserved cpu=100m,memory=0.6Gi,ephemeral-storage=1Gi - --system-reserved cpu=100m,memory=0.2Gi,ephemeral-storage=1Gi - --eviction-hard memory.available<200Mi,nodefs.available<10%,imagefs.available<15% + --kube-reserved cpu=100m,memory=0.6Gi,ephemeral-storage=1Gi --system-reserved + cpu=100m,memory=0.2Gi,ephemeral-storage=1Gi --eviction-hard + memory.available<200Mi,nodefs.available<10%,imagefs.available<15% block_device_map: # EBS volume for local ephemeral storage # IGNORED if legacy `disk_encryption_enabled` or `disk_size` are set! @@ -294,14 +294,12 @@ You can also view the release and support timeline for EKS clusters support “Addons” that can be automatically installed on a cluster. Install these addons with the [`var.addons` input](https://docs.cloudposse.com/components/library/aws/eks/cluster/#input_addons). -:::info - -Run the following command to see all available addons, their type, and their publisher. You can also see the URL for -addons that are available through the AWS Marketplace. Replace 1.27 with the version of your cluster. See -[Creating an addon](https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on) for more -details. - -::: +> [!TIP] +> +> Run the following command to see all available addons, their type, and their publisher. You can also see the URL for +> addons that are available through the AWS Marketplace. Replace 1.27 with the version of your cluster. See +> [Creating an addon](https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on) for +> more details. ```shell EKS_K8S_VERSION=1.29 # replace with your cluster version @@ -309,12 +307,10 @@ aws eks describe-addon-versions --kubernetes-version $EKS_K8S_VERSION \ --query 'addons[].{MarketplaceProductUrl: marketplaceInformation.productUrl, Name: addonName, Owner: owner Publisher: publisher, Type: type}' --output table ``` -:::info - -You can see which versions are available for each addon by executing the following commands. Replace 1.29 with the -version of your cluster. - -::: +> [!TIP] +> +> You can see which versions are available for each addon by executing the following commands. Replace 1.29 with the +> version of your cluster. ```shell EKS_K8S_VERSION=1.29 # replace with your cluster version @@ -394,16 +390,14 @@ addons: addon_version: "v1.8.7-eksbuild.1" ``` -:::warning - -Addons may not be suitable for all use-cases! For example, if you are deploying Karpenter to Fargate and using Karpenter -to provision all nodes, these nodes will never be available before the cluster component is deployed if you are using -the CoreDNS addon (for example). - -This is one of the reasons we recommend deploying a managed node group: to ensure that the addons will become fully -functional during deployment of the cluster. - -::: +> [!WARNING] +> +> Addons may not be suitable for all use-cases! For example, if you are deploying Karpenter to Fargate and using +> Karpenter to provision all nodes, these nodes will never be available before the cluster component is deployed if you +> are using the CoreDNS addon (for example). +> +> This is one of the reasons we recommend deploying a managed node group: to ensure that the addons will become fully +> functional during deployment of the cluster. For more information on upgrading EKS Addons, see ["How to Upgrade EKS Cluster Addons"](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons/) diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index c4373bab5..58791fa45 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -105,12 +105,10 @@ for `.yaml`. #### Sample Yaml -:::caution - -The key of a filename must match datadog docs, which is `.yaml` -[Datadog Cluster Checks](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) - -::: +> [!WARNING] +> +> The key of a filename must match datadog docs, which is `.yaml` > +> [Datadog Cluster Checks](https://docs.datadoghq.com/agent/cluster_agent/clusterchecks/?tab=helm#configuration-from-static-configuration-files) Cluster Checks **can** be used for external URL testing (loadbalancer endpoints), whereas annotations **must** be used for kubernetes services. diff --git a/modules/eks/karpenter/CHANGELOG.md b/modules/eks/karpenter/CHANGELOG.md index bd5b3bc24..55e5d90f3 100644 --- a/modules/eks/karpenter/CHANGELOG.md +++ b/modules/eks/karpenter/CHANGELOG.md @@ -22,27 +22,27 @@ Policy. This has also been fixed by making the `v1alpha` policy a separate manag controller's role, rather than merging the statements into the `v1beta` policy. This change also avoids potential conflicts with policy SIDs. -:::note Innocuous Changes - -Terraform will show IAM Policy changes, including deletion of statements from the existing policy and creation of a new -policy. This is expected and innocuous. The IAM Policy has been split into 2 to avoid exceeding length limits, but the -current (`v1beta`) policy remains the same and the now separate (`v1alpha`) policy has been corrected. - -::: +> [!NOTE] +> +> #### Innocuous Changes +> +> Terraform will show IAM Policy changes, including deletion of statements from the existing policy and creation of a +> new policy. This is expected and innocuous. The IAM Policy has been split into 2 to avoid exceeding length limits, but +> the current (`v1beta`) policy remains the same and the now separate (`v1alpha`) policy has been corrected. ## Version 1.445.0 Components [PR #1039](https://github.com/cloudposse/terraform-aws-components/pull/1039) -:::warning Major Breaking Changes - -Karpenter at version v0.33.0 transitioned from the `v1alpha` API to the `v1beta` API with many breaking changes. This -component (`eks/karpenter`) changed as well, dropping support for the `v1alpha` API and adding support for the `v1beta` -API. At the same time, the corresponding `eks/karpenter-provisioner` component was replaced with the -`eks/karpenter-node-pool` component. The old components remain available under the -[`deprecated/`](https://github.com/cloudposse/terraform-aws-components/tree/main/deprecated) directory. - -::: +> [!WARNING] +> +> #### Major Breaking Changes +> +> Karpenter at version v0.33.0 transitioned from the `v1alpha` API to the `v1beta` API with many breaking changes. This +> component (`eks/karpenter`) changed as well, dropping support for the `v1alpha` API and adding support for the +> `v1beta` API. At the same time, the corresponding `eks/karpenter-provisioner` component was replaced with the +> `eks/karpenter-node-pool` component. The old components remain available under the +> [`deprecated/`](https://github.com/cloudposse/terraform-aws-components/tree/main/deprecated) directory. The full list of changes in Karpenter is too extensive to repeat here. See the [Karpenter v1beta Migration Guide](https://karpenter.sh/v0.32/upgrading/v1beta1-migration/) and the @@ -106,18 +106,16 @@ kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.s kubectl annotate crd awsnodetemplates.karpenter.k8s.aws provisioners.karpenter.sh meta.helm.sh/release-namespace=karpenter --overwrite ``` -:::info - -Previously the `karpenter-crd-upgrade` script included deploying the `karpenter-crd` chart. Now that this chart is moved -to Terraform, that helm deployment is no longer necessary. - -For reference, the `karpenter-crd` chart can be installed with helm with the following: - -```bash -helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version "$VERSION" --namespace karpenter -``` - -::: +> [!NOTE] +> +> Previously the `karpenter-crd-upgrade` script included deploying the `karpenter-crd` chart. Now that this chart is +> moved to Terraform, that helm deployment is no longer necessary. +> +> For reference, the `karpenter-crd` chart can be installed with helm with the following: +> +> ```bash +> helm upgrade --install karpenter-crd oci://public.ecr.aws/karpenter/karpenter-crd --version "$VERSION" --namespace karpenter +> ``` Now that the CRDs are upgraded, the component is ready to be applied. Apply the `eks/karpenter` component and then apply `eks/karpenter-provisioner`. diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index 5732ce94c..f13cbcfaa 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -94,12 +94,12 @@ The process of provisioning Karpenter on an EKS cluster consists of 3 steps. ### 1. Provision EKS IAM Role for Nodes Launched by Karpenter -:::note VPC assumptions being made - -We assume you've already created a VPC using our [VPC component](/modules/vpc) and have private subnets already set up. -The Karpenter node pools will be launched in the private subnets. - -::: +> [!NOTE] +> +> #### VPC assumptions being made +> +> We assume you've already created a VPC using our [VPC component](/modules/vpc) and have private subnets already set +> up. The Karpenter node pools will be launched in the private subnets. EKS IAM Role for Nodes launched by Karpenter are provisioned by the `eks/cluster` component. (EKS can also provision a Fargate Profile for Karpenter, but deploying Karpenter to Fargate is not recommended.): @@ -116,11 +116,9 @@ components: karpenter_iam_role_enabled: true ``` -:::note Authorization - -- The AWS Auth API for EKS is used to authorize the Karpenter controller to interact with the EKS cluster. - -::: +> [!NOTE] +> +> The AWS Auth API for EKS is used to authorize the Karpenter controller to interact with the EKS cluster. Karpenter is installed using a Helm chart. The Helm chart installs the Karpenter controller and a webhook pod as a Deployment that needs to run before the controller can be used for scaling your cluster. We recommend a minimum of one @@ -189,12 +187,12 @@ In this step, we provision the `components/terraform/eks/karpenter-node-pool` co [NodePools](https://karpenter.sh/v0.36/getting-started/getting-started-with-karpenter/#5-create-nodepool) using the `kubernetes_manifest` resource. -:::note Why use a separate component for NodePools? - -We create the NodePools as a separate component since the CRDs for the NodePools are created by the Karpenter component. -This helps manage dependencies. - -::: +> [!TIP] +> +> #### Why use a separate component for NodePools? +> +> We create the NodePools as a separate component since the CRDs for the NodePools are created by the Karpenter +> component. This helps manage dependencies. First, create an abstract component for the `eks/karpenter-node-pool` component: @@ -287,13 +285,13 @@ interruption events include: - Instance Terminating Events - Instance Stopping Events -:::info Interruption Handler vs. Termination Handler - -The Node Interruption Handler is not the same as the Node Termination Handler. The latter is always enabled and cleanly -shuts down the node in 2 minutes in response to a Node Termination event. The former gets advance notice that a node -will soon be terminated, so it can have 5-10 minutes to shut down a node. - -::: +> [!TIP] +> +> #### Interruption Handler vs. Termination Handler +> +> The Node Interruption Handler is not the same as the Node Termination Handler. The latter is always enabled and +> cleanly shuts down the node in 2 minutes in response to a Node Termination event. The former gets advance notice that +> a node will soon be terminated, so it can have 5-10 minutes to shut down a node. For more details, see refer to the [Karpenter docs](https://karpenter.sh/v0.32/concepts/disruption/#interruption) and [FAQ](https://karpenter.sh/v0.32/faq/#interruption-handling) diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 8cd4fa30a..e36a78b29 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -2,12 +2,10 @@ This component is responsible for provisioning EC2 instances for GitHub runners. -:::info - -We also have a similar component based on -[actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) for Kubernetes. - -::: +> [!TIP] +> +> We also have a similar component based on +> [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) for Kubernetes. ## Requirements @@ -179,13 +177,11 @@ permissions “mode” for Self-hosted runners to Read-Only. The instructions fo ### Creating Registration Token -:::info - -We highly recommend using a GitHub Application with the github-action-token-rotator module to generate the Registration -Token. This will ensure that the token is rotated and that the token is stored in SSM Parameter Store encrypted with -KMS. - -::: +> [!TIP] +> +> We highly recommend using a GitHub Application with the github-action-token-rotator module to generate the +> Registration Token. This will ensure that the token is rotated and that the token is stored in SSM Parameter Store +> encrypted with KMS. #### GitHub Application @@ -224,11 +220,9 @@ and skip the rest. Otherwise, complete the private key setup in `core- [!TIP] +> +> If you change the Private Key saved in SSM, redeploy `github-action-token-rotator` #### (ClickOps) Obtain the Runner Registration Token diff --git a/modules/network-firewall/README.md b/modules/network-firewall/README.md index a7fe6c867..4d2b122a8 100644 --- a/modules/network-firewall/README.md +++ b/modules/network-firewall/README.md @@ -9,16 +9,14 @@ including Network Firewall, firewall policy, rule groups, and logging configurat Example of a Network Firewall with stateful 5-tuple rules: -:::info - -The "5-tuple" means the five items (columns) that each rule (row, or tuple) in a firewall policy uses to define whether -to block or allow traffic: source and destination IP, source and destination port, and protocol. - -Refer to -[Standard stateful rule groups in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-basic.html) -for more details. - -::: +> [!TIP] +> +> The "5-tuple" means the five items (columns) that each rule (row, or tuple) in a firewall policy uses to define +> whether to block or allow traffic: source and destination IP, source and destination port, and protocol. +> +> Refer to +> [Standard stateful rule groups in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-basic.html) +> for more details. ```yaml components: @@ -89,17 +87,15 @@ components: Example of a Network Firewall with [Suricata](https://suricata.readthedocs.io/en/suricata-6.0.0/rules/) rules: -:::info - -For [Suricata](https://suricata.io/) rule group type, you provide match and action settings in a string, in a Suricata -compatible specification. The specification fully defines what the stateful rules engine looks for in a traffic flow and -the action to take on the packets in a flow that matches the inspection criteria. - -Refer to -[Suricata compatible rule strings in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-suricata.html) -for more details. - -::: +> [!TIP] +> +> For [Suricata](https://suricata.io/) rule group type, you provide match and action settings in a string, in a Suricata +> compatible specification. The specification fully defines what the stateful rules engine looks for in a traffic flow +> and the action to take on the packets in a flow that matches the inspection criteria. +> +> Refer to +> [Suricata compatible rule strings in AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/stateful-rule-groups-suricata.html) +> for more details. ```yaml components: diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index 4adc6c1d4..864cdbbb7 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -122,11 +122,9 @@ components: #### Deployment -:::info - -The following steps assume that you've already authenticated with Spacelift locally. - -::: +> [!TIP] +> +> The following steps assume that you've already authenticated with Spacelift locally. First deploy Spaces and policies with the `spaces` component: @@ -153,12 +151,10 @@ following: + core-ue1-auto-spacelift-worker-pool ``` -:::info - -The `spacelift/worker-pool` component is deployed to a specific tenant, stage, and region but is still deployed by the -root administrator stack. Verify the administrator stack by checking the `managed-by:` label. - -::: +> [!TIP] +> +> The `spacelift/worker-pool` component is deployed to a specific tenant, stage, and region but is still deployed by the +> root administrator stack. Verify the administrator stack by checking the `managed-by:` label. Finally, deploy the Spacelift Worker Pool (change the stack-slug to match your configuration): diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 70da2f2ba..03aaa0818 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -10,14 +10,12 @@ wish to restrict who can read the production Terraform state backend S3 bucket. all Terraform users require read access to the most sensitive accounts, such as `root` and `audit`, in order to read security configuration information, so careful planning is required when architecting backend splits. -:::info - -Part of cold start, so it has to initially be run with `SuperAdmin`, multiple -times: to create the S3 bucket and then to move the state into it. Follow -the guide **[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** -to get started. - -::: +> [!TIP] +> +> Part of cold start, so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and then +> to move the state into it. Follow the guide +> **[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** +> to get started. ### Access Control @@ -58,9 +56,9 @@ access. You can configure who is allowed to assume these roles. - For convenience, the component automatically grants access to the backend to the user deploying it. This is helpful because it allows that user, presumably SuperAdmin, to deploy the normal components that expect the user does not have - direct access to Terraform state, without requiring custom configuration. However, you may want to explicitly - grant SuperAdmin access to the backend in the `allowed_principal_arns` configuration, to ensure that SuperAdmin - can always access the backend, even if the component is later updated by the `root-admin` role. + direct access to Terraform state, without requiring custom configuration. However, you may want to explicitly grant + SuperAdmin access to the backend in the `allowed_principal_arns` configuration, to ensure that SuperAdmin can always + access the backend, even if the component is later updated by the `root-admin` role. ### Quotas From 020dc81394f21e505b65743761773f815c79b77c Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Thu, 8 Aug 2024 14:51:51 -0400 Subject: [PATCH 463/501] chore: update argocd-repo to use 6.0+ github provider (#1031) Co-authored-by: Dan Miller --- modules/argocd-repo/README.md | 4 ++-- modules/argocd-repo/main.tf | 8 +++++--- modules/argocd-repo/versions.tf | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index c5342a2de..2e5638192 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -83,7 +83,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.0 | | [aws](#requirement\_aws) | >= 4.0 | -| [github](#requirement\_github) | >= 4.0 | +| [github](#requirement\_github) | >= 6.0 | | [random](#requirement\_random) | >= 2.3 | | [tls](#requirement\_tls) | >= 3.0 | @@ -92,7 +92,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.0 | -| [github](#provider\_github) | >= 4.0 | +| [github](#provider\_github) | >= 6.0 | | [tls](#provider\_tls) | >= 3.0 | ## Modules diff --git a/modules/argocd-repo/main.tf b/modules/argocd-repo/main.tf index ede85f1bd..12f2facd3 100644 --- a/modules/argocd-repo/main.tf +++ b/modules/argocd-repo/main.tf @@ -85,9 +85,11 @@ resource "github_branch_protection" "default" { } } - push_restrictions = var.push_restrictions_enabled ? [ - join("", data.github_user.automation_user[*].node_id), - ] : [] + restrict_pushes { + push_allowances = var.push_restrictions_enabled ? [ + join("", data.github_user.automation_user[*].node_id), + ] : [] + } } data "github_team" "default" { diff --git a/modules/argocd-repo/versions.tf b/modules/argocd-repo/versions.tf index 2c76e7b55..5cc992f47 100644 --- a/modules/argocd-repo/versions.tf +++ b/modules/argocd-repo/versions.tf @@ -8,7 +8,7 @@ terraform { } github = { source = "integrations/github" - version = ">= 4.0" + version = ">= 6.0" } tls = { source = "hashicorp/tls" From 6a4eff31b06bd8fc05837a701ff443d469e68be1 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Fri, 9 Aug 2024 14:14:14 -0400 Subject: [PATCH 464/501] Add Variable for dynamic dns component lookup (#1094) --- modules/eks/external-dns/README.md | 9 +++++++++ modules/eks/external-dns/main.tf | 3 ++- modules/eks/external-dns/remote-state.tf | 11 +++++++++++ modules/eks/external-dns/variables.tf | 10 ++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index 3a8df4f93..0949dfc99 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -45,6 +45,13 @@ components: # aws: # batchChangeSize: 1000 chart_values: {} + # Extra hosted zones to lookup and support by component name + dns_components: + - component: dns-primary + - component: dns-delegated + - component: dns-delegated/abc + - component: dns-delegated/123 + environment: "gbl" # Optional (default "gbl") ``` @@ -68,6 +75,7 @@ components: | Name | Source | Version | |------|--------|---------| +| [additional\_dns\_components](#module\_additional\_dns\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [dns\_gbl\_delegated](#module\_dns\_gbl\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [dns\_gbl\_primary](#module\_dns\_gbl\_primary) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | @@ -100,6 +108,7 @@ components: | [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_components](#input\_dns\_components) | A list of additional DNS components to search for ZoneIDs |
list(object({
component = string,
environment = optional(string)
}))
| `[]` | no | | [dns\_gbl\_delegated\_environment\_name](#input\_dns\_gbl\_delegated\_environment\_name) | The name of the environment where global `dns_delegated` is provisioned | `string` | `"gbl"` | no | | [dns\_gbl\_primary\_environment\_name](#input\_dns\_gbl\_primary\_environment\_name) | The name of the environment where global `dns_primary` is provisioned | `string` | `"gbl"` | no | | [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | diff --git a/modules/eks/external-dns/main.tf b/modules/eks/external-dns/main.tf index 082b3ad0b..5c053cd4c 100644 --- a/modules/eks/external-dns/main.tf +++ b/modules/eks/external-dns/main.tf @@ -9,7 +9,8 @@ locals { txt_prefix = var.txt_prefix != "" ? format("%s-", local.txt_owner) : "" zone_ids = compact(concat( values(module.dns_gbl_delegated.outputs.zones)[*].zone_id, - values(module.dns_gbl_primary.outputs.zones)[*].zone_id + values(module.dns_gbl_primary.outputs.zones)[*].zone_id, + flatten([for k, v in module.additional_dns_components : [for i, j in v.outputs.zones : j.zone_id]]) )) } diff --git a/modules/eks/external-dns/remote-state.tf b/modules/eks/external-dns/remote-state.tf index d499f78c7..9f15458c7 100644 --- a/modules/eks/external-dns/remote-state.tf +++ b/modules/eks/external-dns/remote-state.tf @@ -36,3 +36,14 @@ module "dns_gbl_primary" { zones = {} } } + +module "additional_dns_components" { + for_each = { for obj in var.dns_components : obj.component => obj } + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = each.value.component + environment = coalesce(each.value.environment, "gbl") + + context = module.this.context +} diff --git a/modules/eks/external-dns/variables.tf b/modules/eks/external-dns/variables.tf index 68e9091a7..8689b5530 100644 --- a/modules/eks/external-dns/variables.tf +++ b/modules/eks/external-dns/variables.tf @@ -132,6 +132,16 @@ variable "dns_gbl_primary_environment_name" { default = "gbl" } + +variable "dns_components" { + type = list(object({ + component = string, + environment = optional(string) + })) + description = "A list of additional DNS components to search for ZoneIDs" + default = [] +} + variable "publish_internal_services" { type = bool description = "Allow external-dns to publish DNS records for ClusterIP services" From c7b9050cda25d7435dc68a86fdf1280ee7051ef3 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 9 Aug 2024 15:55:22 -0400 Subject: [PATCH 465/501] fix: tfstate-backend cold-start regression (#1093) Co-authored-by: Nuru --- modules/tfstate-backend/README.md | 19 +++++--- modules/tfstate-backend/iam.tf | 69 ++++++++++++++++++++++++++-- modules/tfstate-backend/variables.tf | 6 ++- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 03aaa0818..870561472 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -10,12 +10,14 @@ wish to restrict who can read the production Terraform state backend S3 bucket. all Terraform users require read access to the most sensitive accounts, such as `root` and `audit`, in order to read security configuration information, so careful planning is required when architecting backend splits. -> [!TIP] -> -> Part of cold start, so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and then -> to move the state into it. Follow the guide -> **[here](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start/#provision-tfstate-backend-component)** -> to get started. +## Prerequisites + +- This component assumes you are using the `aws-teams` and `aws-team-roles` components. +- Before the `account` and `account-map` components are deployed for the first time, you'll want to run this component with `access_roles_enabled` set to `false` to + prevent errors due to missing IAM Role ARNs. + This will enable only enough access to the Terraform state for you to finish provisioning accounts and roles. + After those components have been deployed, you will want to + run this component again with `access_roles_enabled` set to `true` to provide the complete access as configured in the stacks. ### Access Control @@ -141,7 +143,10 @@ terraform: | Name | Type | |------|------| | [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_arn.cold_start_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source | +| [aws_iam_policy_document.cold_start_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.tfstate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | | [awsutils_caller_identity.current](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/data-sources/caller_identity) | data source | ## Inputs @@ -149,7 +154,7 @@ terraform: | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [access\_roles](#input\_access\_roles) | Map of access roles to create (key is role name, use "default" for same as component). See iam-assume-role-policy module for details. |
map(object({
write_enabled = bool
allowed_roles = map(list(string))
denied_roles = map(list(string))
allowed_principal_arns = list(string)
denied_principal_arns = list(string)
allowed_permission_sets = map(list(string))
denied_permission_sets = map(list(string))
}))
| `{}` | no | -| [access\_roles\_enabled](#input\_access\_roles\_enabled) | Enable creation of access roles. Set false for cold start (before account-map has been created). | `bool` | `true` | no | +| [access\_roles\_enabled](#input\_access\_roles\_enabled) | Enable access roles to be assumed. Set `false` for cold start (before account-map has been created),
because the role to ARN mapping has not yet been created.
Note that the current caller and any `allowed_principal_arns` will always be allowed to assume the role. | `bool` | `true` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | diff --git a/modules/tfstate-backend/iam.tf b/modules/tfstate-backend/iam.tf index 5881b3998..84ee92865 100644 --- a/modules/tfstate-backend/iam.tf +++ b/modules/tfstate-backend/iam.tf @@ -1,19 +1,28 @@ locals { - access_roles = local.enabled && var.access_roles_enabled ? { + access_roles = local.enabled ? { for k, v in var.access_roles : ( length(split(module.this.delimiter, k)) > 1 ? k : module.label[k].id ) => v } : {} - access_roles_enabled = module.this.enabled && length(keys(local.access_roles)) > 0 + access_roles_enabled = local.enabled && var.access_roles_enabled + cold_start_access_enabled = local.enabled && !var.access_roles_enabled + + # Technically, `eks_role_arn` is incorrect, becuse it strips any path from the ARN, + # but since we do not expect there to be a path in the role ARN (as opposed to perhaps an attached IAM policy), + # it is OK. The advantage of using `eks_role_arn` is that it converts and Assumed Role ARN from STS, like + # arn:aws:sts::123456789012:assumed-role/acme-core-gbl-root-admin/aws-go-sdk-1722029959251053170 + # to the IAM Role ARN, like + # arn:aws:iam::123456789012:role/acme-core-gbl-root-admin caller_arn = coalesce(data.awsutils_caller_identity.current.eks_role_arn, data.awsutils_caller_identity.current.arn) } data "awsutils_caller_identity" "current" {} +data "aws_partition" "current" {} module "label" { - for_each = var.access_roles + for_each = local.enabled ? var.access_roles : {} source = "cloudposse/label/null" version = "0.25.0" # requires Terraform >= 0.13.0 @@ -28,7 +37,7 @@ module "label" { } module "assume_role" { - for_each = local.access_roles + for_each = local.access_roles_enabled ? local.access_roles : {} source = "../account-map/modules/team-assume-role-policy" allowed_roles = each.value.allowed_roles @@ -74,7 +83,7 @@ resource "aws_iam_role" "default" { name = each.key description = "${each.value.write_enabled ? "Access" : "Read-only access"} role for ${module.this.id}" - assume_role_policy = module.assume_role[each.key].policy_document + assume_role_policy = var.access_roles_enabled ? module.assume_role[each.key].policy_document : data.aws_iam_policy_document.cold_start_assume_role[each.key].json tags = merge(module.this.tags, { Name = each.key }) inline_policy { @@ -83,3 +92,53 @@ resource "aws_iam_role" "default" { } managed_policy_arns = [] } + +locals { + all_cold_start_access_principals = local.cold_start_access_enabled ? toset(concat([local.caller_arn], + flatten([for k, v in local.access_roles : v.allowed_principal_arns]))) : toset([]) + cold_start_access_principal_arns = local.cold_start_access_enabled ? { for k, v in local.access_roles : k => distinct(concat( + [local.caller_arn], v.allowed_principal_arns + )) } : {} + cold_start_access_principals = local.cold_start_access_enabled ? { + for k, v in local.cold_start_access_principal_arns : k => formatlist("arn:%v:iam::%v:root", data.aws_partition.current.partition, distinct([ + for arn in v : data.aws_arn.cold_start_access[arn].account + ])) + } : {} + +} + +data "aws_arn" "cold_start_access" { + for_each = local.all_cold_start_access_principals + arn = each.value +} + +# This is a basic policy that allows the caller and explicitly allowed principals to assume the role +# during the period roles are being set up (cold start). +data "aws_iam_policy_document" "cold_start_assume_role" { + for_each = local.cold_start_access_enabled ? local.access_roles : {} + + statement { + sid = "ColdStartRoleAssumeRole" + + effect = "Allow" + # These actions need to be kept in sync with the actions in the assume_role module + actions = [ + "sts:AssumeRole", + "sts:SetSourceIdentity", + "sts:TagSession", + ] + + condition { + test = "ArnLike" + variable = "aws:PrincipalArn" + values = local.cold_start_access_principal_arns[each.key] + } + + principals { + type = "AWS" + # Principals is a required field, so we allow any principal in any of the accounts, restricted by the assumed Role ARN in the condition clauses. + # This allows us to allow non-existent (yet to be created) roles, which would not be allowed if directly specified in `principals`. + identifiers = local.cold_start_access_principals[each.key] + } + } +} diff --git a/modules/tfstate-backend/variables.tf b/modules/tfstate-backend/variables.tf index 541cbf855..47a25cd97 100644 --- a/modules/tfstate-backend/variables.tf +++ b/modules/tfstate-backend/variables.tf @@ -43,6 +43,10 @@ variable "access_roles" { variable "access_roles_enabled" { type = bool - description = "Enable creation of access roles. Set false for cold start (before account-map has been created)." + description = <<-EOT + Enable access roles to be assumed. Set `false` for cold start (before account-map has been created), + because the role to ARN mapping has not yet been created. + Note that the current caller and any `allowed_principal_arns` will always be allowed to assume the role. + EOT default = true } From 32d120d64b7336ebbef8371dae99e37a8fac203c Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 12 Aug 2024 12:34:26 -0400 Subject: [PATCH 466/501] Upstream `eks/actions-runner-controller` with `var.auto_update_enabled` (#1095) --- .../eks/actions-runner-controller/README.md | 18 +++++------- .../charts/actions-runner/Chart.yaml | 2 +- .../templates/runnerdeployment.yaml | 24 +++++++++++++-- modules/eks/actions-runner-controller/main.tf | 1 + .../actions-runner-controller/variables.tf | 29 ++++++++++++------- 5 files changed, 49 insertions(+), 25 deletions(-) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index ea1a144b3..29543a965 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -384,15 +384,13 @@ scale down to zero before finishing all the jobs, leaving some waiting indefinit the `max_duration` to a time long enough to cover the full time a job may have to wait between the time it is queued and the time it finishes, assuming that the HRA scales up the pool by 1 and runs the job on the new runner. -:::info - -If there are more jobs queued than there are runners allowed by `maxReplicas`, the timeout timer does not start on the -capacity reservation until enough reservations ahead of it are removed for it to be considered as representing and -active job. Although there are some edge cases regarding `max_duration` that seem not to be covered properly (see -[actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)), they only -merit adding a few extra minutes to the timeout. - -::: +> [!TIP] +> +> If there are more jobs queued than there are runners allowed by `maxReplicas`, the timeout timer does not start on the +> capacity reservation until enough reservations ahead of it are removed for it to be considered as representing and +> active job. Although there are some edge cases regarding `max_duration` that seem not to be covered properly (see +> [actions-runner-controller issue #2466](https://github.com/actions/actions-runner-controller/issues/2466)), they only +> merit adding a few extra minutes to the timeout. ### Recommended `max_duration` Duration @@ -570,7 +568,7 @@ documentation for further details. | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | | [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
| n/a | yes | -| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: true # A Docker daemon will be started in the runner Pod
image: summerwind/actions-runner-dind # If dind_enabled=false, set this to 'summerwind/actions-runner'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "summerwind/actions-runner-dind")
dind_enabled = optional(bool, true)
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})

# running_pod_annotations are only applied to the pods once they start running a job
running_pod_annotations = optional(map(string), {})

# affinity is too complex to model. Whatever you assigned affinity will be copied
# to the runner Pod spec.
affinity = optional(any)

tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = optional(number, 300)
min_replicas = number
max_replicas = number
# Scheduled overrides. See https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides
# Order is important. The earlier entry is prioritized higher than later entries. So you usually define
# one-time overrides at the top of your list, then yearly, monthly, weekly, and lastly daily overrides.
scheduled_overrides = optional(list(object({
start_time = string # ISO 8601 format, eg, "2021-06-01T00:00:00+09:00"
end_time = string # ISO 8601 format, eg, "2021-06-01T00:00:00+09:00"
min_replicas = optional(number)
max_replicas = optional(number)
recurrence_rule = optional(object({
frequency = string # One of Daily, Weekly, Monthly, Yearly
until_time = optional(string) # ISO 8601 format time after which the schedule will no longer apply
}))
})), [])
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = optional(bool, true)
# max_duration is the duration after which a job will be considered completed,
# even if the webhook has not received a "job completed" event.
# This is to ensure that if an event is missed, it does not leave the runner running forever.
# Set it long enough to cover the longest job you expect to run and then some.
# See https://github.com/actions/actions-runner-controller/blob/9afd93065fa8b1f87296f0dcdf0c2753a0548cb7/docs/automatically-scaling-runners.md?plain=1#L264-L268
# Defaults to 1 hour programmatically (to be able to detect if both max_duration and webhook_startup_timeout are set).
max_duration = optional(string)
# The name `webhook_startup_timeout` was misleading and has been deprecated.
# It has been renamed `max_duration`.
webhook_startup_timeout = optional(string)
# Adjust the time (in seconds) to wait for the Docker in Docker daemon to become responsive.
wait_for_docker_seconds = optional(string, "")
pull_driven_scaling_enabled = optional(bool, false)
labels = optional(list(string), [])
# If not null, `docker_storage` specifies the size (as `go` string) of
# an ephemeral (default storage class) Persistent Volume to allocate for the Docker daemon.
# Takes precedence over `tmpfs_enabled` for the Docker daemon storage.
docker_storage = optional(string, null)
# storage is deprecated in favor of docker_storage, since it is only storage for the Docker daemon
storage = optional(string, null)
# If `pvc_enabled` is true, a Persistent Volume Claim will be created for the runner
# and mounted at /home/runner/work/shared. This is useful for sharing data between runners.
pvc_enabled = optional(bool, false)
# If `tmpfs_enabled` is `true`, both the runner and the docker daemon will use a tmpfs volume,
# meaning that all data will be stored in RAM rather than on disk, bypassing disk I/O limitations,
# but what would have been disk usage is now additional memory usage. You must specify memory
# requests and limits when using tmpfs or else the Pod will likely crash the Node.
tmpfs_enabled = optional(bool)
resources = optional(object({
limits = optional(object({
cpu = optional(string, "1")
memory = optional(string, "1Gi")
ephemeral_storage = optional(string, "10Gi")
}), {})
requests = optional(object({
cpu = optional(string, "500m")
memory = optional(string, "256Mi")
ephemeral_storage = optional(string, "1Gi")
}), {})
}), {})
}))
| n/a | yes | +| [runners](#input\_runners) | Map of Action Runner configurations, with the key being the name of the runner. Please note that the name must be in
kebab-case.

For example:
hcl
organization_runner = {
type = "organization" # can be either 'organization' or 'repository'
dind_enabled: true # A Docker daemon will be started in the runner Pod
image: summerwind/actions-runner-dind # If dind_enabled=false, set this to 'summerwind/actions-runner'
scope = "ACME" # org name for Organization runners, repo name for Repository runners
group = "core-automation" # Optional. Assigns the runners to a runner group, for access control.
scale_down_delay_seconds = 300
min_replicas = 1
max_replicas = 5
labels = [
"Ubuntu",
"core-automation",
]
}
|
map(object({
type = string
scope = string
group = optional(string, null)
image = optional(string, "summerwind/actions-runner-dind")
auto_update_enabled = optional(bool, true)
dind_enabled = optional(bool, true)
node_selector = optional(map(string), {})
pod_annotations = optional(map(string), {})

# running_pod_annotations are only applied to the pods once they start running a job
running_pod_annotations = optional(map(string), {})

# affinity is too complex to model. Whatever you assigned affinity will be copied
# to the runner Pod spec.
affinity = optional(any)

tolerations = optional(list(object({
key = string
operator = string
value = optional(string, null)
effect = string
})), [])
scale_down_delay_seconds = optional(number, 300)
min_replicas = number
max_replicas = number
# Scheduled overrides. See https://github.com/actions/actions-runner-controller/blob/master/docs/automatically-scaling-runners.md#scheduled-overrides
# Order is important. The earlier entry is prioritized higher than later entries. So you usually define
# one-time overrides at the top of your list, then yearly, monthly, weekly, and lastly daily overrides.
scheduled_overrides = optional(list(object({
start_time = string # ISO 8601 format, eg, "2021-06-01T00:00:00+09:00"
end_time = string # ISO 8601 format, eg, "2021-06-01T00:00:00+09:00"
min_replicas = optional(number)
max_replicas = optional(number)
recurrence_rule = optional(object({
frequency = string # One of Daily, Weekly, Monthly, Yearly
until_time = optional(string) # ISO 8601 format time after which the schedule will no longer apply
}))
})), [])
busy_metrics = optional(object({
scale_up_threshold = string
scale_down_threshold = string
scale_up_adjustment = optional(string)
scale_down_adjustment = optional(string)
scale_up_factor = optional(string)
scale_down_factor = optional(string)
}))
webhook_driven_scaling_enabled = optional(bool, true)
# max_duration is the duration after which a job will be considered completed,
# even if the webhook has not received a "job completed" event.
# This is to ensure that if an event is missed, it does not leave the runner running forever.
# Set it long enough to cover the longest job you expect to run and then some.
# See https://github.com/actions/actions-runner-controller/blob/9afd93065fa8b1f87296f0dcdf0c2753a0548cb7/docs/automatically-scaling-runners.md?plain=1#L264-L268
# Defaults to 1 hour programmatically (to be able to detect if both max_duration and webhook_startup_timeout are set).
max_duration = optional(string)
# The name `webhook_startup_timeout` was misleading and has been deprecated.
# It has been renamed `max_duration`.
webhook_startup_timeout = optional(string)
# Adjust the time (in seconds) to wait for the Docker in Docker daemon to become responsive.
wait_for_docker_seconds = optional(string, "")
pull_driven_scaling_enabled = optional(bool, false)
labels = optional(list(string), [])
# If not null, `docker_storage` specifies the size (as `go` string) of
# an ephemeral (default storage class) Persistent Volume to allocate for the Docker daemon.
# Takes precedence over `tmpfs_enabled` for the Docker daemon storage.
docker_storage = optional(string, null)
# storage is deprecated in favor of docker_storage, since it is only storage for the Docker daemon
storage = optional(string, null)
# If `pvc_enabled` is true, a Persistent Volume Claim will be created for the runner
# and mounted at /home/runner/work/shared. This is useful for sharing data between runners.
pvc_enabled = optional(bool, false)
# If `tmpfs_enabled` is `true`, both the runner and the docker daemon will use a tmpfs volume,
# meaning that all data will be stored in RAM rather than on disk, bypassing disk I/O limitations,
# but what would have been disk usage is now additional memory usage. You must specify memory
# requests and limits when using tmpfs or else the Pod will likely crash the Node.
tmpfs_enabled = optional(bool)
resources = optional(object({
limits = optional(object({
cpu = optional(string, "1")
memory = optional(string, "1Gi")
# ephemeral-storage is the Kubernetes name, but `ephemeral_storage` is the gomplate name,
# so allow either. If both are specified, `ephemeral-storage` takes precedence.
ephemeral-storage = optional(string)
ephemeral_storage = optional(string, "10Gi")
}), {})
requests = optional(object({
cpu = optional(string, "500m")
memory = optional(string, "256Mi")
# ephemeral-storage is the Kubernetes name, but `ephemeral_storage` is the gomplate name,
# so allow either. If both are specified, `ephemeral-storage` takes precedence.
ephemeral-storage = optional(string)
ephemeral_storage = optional(string, "1Gi")
}), {})
}), {})
}))
| n/a | yes | | [s3\_bucket\_arns](#input\_s3\_bucket\_arns) | List of ARNs of S3 Buckets to which the runners will have read-write access to. | `list(string)` | `[]` | no | | [ssm\_docker\_config\_json\_path](#input\_ssm\_docker\_config\_json\_path) | SSM path to the Docker config JSON | `string` | `null` | no | | [ssm\_github\_secret\_path](#input\_ssm\_github\_secret\_path) | The path in SSM to the GitHub app private key file contents or GitHub PAT token. | `string` | `""` | no | diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml index d4a340fa3..1bfa1968d 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.3.1 +version: 0.3.2 # This chart only deploys Resources for actions-runner-controller, so app version does not really apply. # We use Resource API version instead. diff --git a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml index dbdfd1a84..97382feda 100644 --- a/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml +++ b/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml @@ -139,12 +139,21 @@ spec: # to report its status and deregister itself from the runner pool. - name: RUNNER_GRACEFUL_STOP_TIMEOUT value: "80" + - name: DISABLE_RUNNER_UPDATE + value: "{{ printf "%v" (not .Values.auto_update_enabled) }}" {{- with .Values.wait_for_docker_seconds }} # If Docker is taking too long to start (which is likely due to some other performance issue), # increase the timeout from the default of 120 seconds. - name: WAIT_FOR_DOCKER_SECONDS value: "{{ . }}" {{- end }} + {{- if $use_tmpfs }} + - name: RUNNER_HOME + value: "/runner-tmpfs" + - name: RUNNER_WORKDIR + value: "/runner-tmpfs/_work" + {{- end }} + # You could reserve nodes for runners by labeling and tainting nodes with # node-role.kubernetes.io/actions-runner # and then adding the following to this RunnerDeployment @@ -206,6 +215,7 @@ spec: {{- end }} # dockerdWithinRunnerContainer = false means access to a Docker daemon is provided by a sidecar container. dockerdWithinRunnerContainer: {{ $use_dind_in_runner }} + dockerEnabled: {{ $use_dind }} image: {{ .Values.image | quote }} imagePullPolicy: IfNotPresent {{- if $use_dockerconfig }} @@ -217,15 +227,23 @@ spec: limits: cpu: {{ .Values.resources.limits.cpu }} memory: {{ .Values.resources.limits.memory }} + {{- if index .Values.resources.limits "ephemeral-storage" }} + ephemeral-storage: {{ index .Values.resources.limits "ephemeral-storage" }} + {{- else }} {{- if index .Values.resources.limits "ephemeral_storage" }} ephemeral-storage: {{ .Values.resources.limits.ephemeral_storage }} {{- end }} + {{- end }} requests: cpu: {{ .Values.resources.requests.cpu }} memory: {{ .Values.resources.requests.memory }} + {{- if index .Values.resources.requests "ephemeral-storage" }} + ephemeral-storage: {{ index .Values.resources.requests "ephemeral-storage" }} + {{- else }} {{- if index .Values.resources.requests "ephemeral_storage" }} ephemeral-storage: {{ .Values.resources.requests.ephemeral_storage }} {{- end }} + {{- end }} {{- if and (not $use_dind_in_runner) (or .Values.docker_storage $use_tmpfs) }} {{- /* dockerVolumeMounts are mounted into the docker sidecar, and ignored if running with dockerdWithinRunnerContainer */}} dockerVolumeMounts: @@ -251,14 +269,14 @@ spec: {{- if $use_tmpfs }} - mountPath: /tmp name: tmp - - mountPath: /runner/_work - name: work + - mountPath: /runner-tmpfs + name: runner-tmpfs {{- end }} {{- end }}{{/* End of volumeMounts */}} {{- if or (and $use_dind (or .Values.docker_storage $use_tmpfs)) $use_pvc $use_dockerconfig (not (empty .Values.running_pod_annotations)) }} volumes: {{- if $use_tmpfs }} - - name: work + - name: runner-tmpfs emptyDir: medium: Memory - name: tmp diff --git a/modules/eks/actions-runner-controller/main.tf b/modules/eks/actions-runner-controller/main.tf index e7b7257e5..39684c171 100644 --- a/modules/eks/actions-runner-controller/main.tf +++ b/modules/eks/actions-runner-controller/main.tf @@ -225,6 +225,7 @@ module "actions_runner" { type = each.value.type scope = each.value.scope image = each.value.image + auto_update_enabled = each.value.auto_update_enabled dind_enabled = each.value.dind_enabled service_account_role_arn = module.actions_runner_controller.service_account_role_arn resources = each.value.resources diff --git a/modules/eks/actions-runner-controller/variables.tf b/modules/eks/actions-runner-controller/variables.tf index 9e7efe793..a9c7f16c4 100644 --- a/modules/eks/actions-runner-controller/variables.tf +++ b/modules/eks/actions-runner-controller/variables.tf @@ -148,13 +148,14 @@ variable "runners" { EOT type = map(object({ - type = string - scope = string - group = optional(string, null) - image = optional(string, "summerwind/actions-runner-dind") - dind_enabled = optional(bool, true) - node_selector = optional(map(string), {}) - pod_annotations = optional(map(string), {}) + type = string + scope = string + group = optional(string, null) + image = optional(string, "summerwind/actions-runner-dind") + auto_update_enabled = optional(bool, true) + dind_enabled = optional(bool, true) + node_selector = optional(map(string), {}) + pod_annotations = optional(map(string), {}) # running_pod_annotations are only applied to the pods once they start running a job running_pod_annotations = optional(map(string), {}) @@ -224,13 +225,19 @@ variable "runners" { tmpfs_enabled = optional(bool) resources = optional(object({ limits = optional(object({ - cpu = optional(string, "1") - memory = optional(string, "1Gi") + cpu = optional(string, "1") + memory = optional(string, "1Gi") + # ephemeral-storage is the Kubernetes name, but `ephemeral_storage` is the gomplate name, + # so allow either. If both are specified, `ephemeral-storage` takes precedence. + ephemeral-storage = optional(string) ephemeral_storage = optional(string, "10Gi") }), {}) requests = optional(object({ - cpu = optional(string, "500m") - memory = optional(string, "256Mi") + cpu = optional(string, "500m") + memory = optional(string, "256Mi") + # ephemeral-storage is the Kubernetes name, but `ephemeral_storage` is the gomplate name, + # so allow either. If both are specified, `ephemeral-storage` takes precedence. + ephemeral-storage = optional(string) ephemeral_storage = optional(string, "1Gi") }), {}) }), {}) From 4cfa96c36810247420d33dd170a256e18469e117 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 13 Aug 2024 12:45:57 -0400 Subject: [PATCH 467/501] DEV-2439: Update all docs.cloudposse links (#1096) --- modules/account/README.md | 9 +++---- modules/aws-backup/README.md | 2 +- modules/datadog-monitor/README.md | 5 +--- modules/ecr/README.md | 4 +-- modules/eks/cluster/README.md | 26 +++++++------------ .../eks/external-secrets-operator/README.md | 2 +- modules/opsgenie-team/README.md | 11 +++----- 7 files changed, 21 insertions(+), 38 deletions(-) diff --git a/modules/account/README.md b/modules/account/README.md index 1f35f30b8..a22e3c1be 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -6,9 +6,8 @@ account. > [!NOTE] > -> Part of a -> [cold start](https://docs.cloudposse.com/reference-architecture/how-to-guides/implementation/enterprise/implement-aws-cold-start) -> so it has to be initially run with `SuperAdmin` role. +> Part of a [cold start](https://docs.cloudposse.com/layers/accounts/prepare-aws-organization/) so it has to be +> initially run with `SuperAdmin` role. In addition, it enables [AWS IAM Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html), which helps @@ -182,7 +181,7 @@ SuperAdmin) credentials you have saved in 1Password. > to expedite the quota increase requests, which could take several days on a basic support plan. Without it, AWS > support will claim that since we’re not currently utilizing any of the resources, so they do not want to approve the > requests. AWS support is not aware of your other organization. If AWS still gives you problems, please escalate to -> your AWS TAM. See [AWS](https://docs.cloudposse.com/reference-architecture/reference/aws). +> your AWS TAM. 1. From the region list, select "US East (N. Virginia) us-east-1". @@ -319,7 +318,7 @@ configuration in the `gbl-root` stack. > In the rare case where you will need to be enabling non-default AWS Regions, temporarily comment out the > `DenyRootAccountAccess` service control policy setting in `gbl-root.yaml`. You will restore it later, after enabling > the optional Regions. See related: -> [Decide on Opting Into Non-default Regions](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-opting-into-non-default-regions) +> [Decide on Opting Into Non-default Regions](https://docs.cloudposse.com/layers/network/design-decisions/decide-on-opting-into-non-default-regions/) > [!TIP] > diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index cf6c29e3e..75d4ebfcc 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -335,4 +335,4 @@ No resources. ## Related How-to Guides -- [How to Enable Cross-Region Backups in AWS-Backup](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-enable-cross-region-backups-in-aws-backup) +- [How to Enable Cross-Region Backups in AWS-Backup](https://docs.cloudposse.com/layers/data/tutorials/how-to-enable-cross-region-backups-in-aws-backup/) diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 631402515..7caa2b21c 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -259,10 +259,7 @@ No resources. ## Related How-to Guides -- [How to Onboard a New Service with Datadog and OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-onboard-a-new-service-with-datadog-and-opsgenie) -- [How to Sign Up for Datadog?](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-sign-up-for-datadog) -- [How to use Datadog Metrics for Horizontal Pod Autoscaling (HPA)](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-use-datadog-metrics-for-horizontal-pod-autoscaling-hpa) -- [How to Implement SRE with Datadog](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-sre-with-datadog) +- [How to Monitor Everything with Datadog](https://docs.cloudposse.com/layers/monitoring/datadog/) ## Component Dependencies diff --git a/modules/ecr/README.md b/modules/ecr/README.md index 7ee7c4396..e31b4a8ff 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -134,9 +134,9 @@ components: ## Related -- [Decide How to distribute Docker Images](https://docs.cloudposse.com/reference-architecture/design-decisions/foundational-platform/decide-how-to-distribute-docker-images) +- [Decide How to distribute Docker Images](https://docs.cloudposse.com/layers/software-delivery/design-decisions/decide-how-to-distribute-docker-images/) -- [Decide on ECR Strategy](https://docs.cloudposse.com/reference-architecture/design-decisions/foundational-platform/decide-on-ecr-strategy) +- [Decide on ECR Strategy](https://docs.cloudposse.com/layers/project/design-decisions/decide-on-ecr-strategy/) ## References diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index cb86484ad..dcf8c48e0 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -18,16 +18,14 @@ profiles. Here's an example snippet for how to use this component. -This example expects the [Cloud Posse Reference Architecture](https://docs.cloudposse.com/reference-architecture/) -Identity and Network designs deployed for mapping users to EKS service roles and granting access in a private network. -In addition, this example has the GitHub OIDC integration added and makes use of Karpenter to dynamically scale cluster -nodes. +This example expects the [Cloud Posse Reference Architecture](https://docs.cloudposse.com/) Identity and Network designs +deployed for mapping users to EKS service roles and granting access in a private network. In addition, this example has +the GitHub OIDC integration added and makes use of Karpenter to dynamically scale cluster nodes. -For more on these requirements, see -[Identity Reference Architecture](https://docs.cloudposse.com/reference-architecture/quickstart/iam-identity/), -[Network Reference Architecture](https://docs.cloudposse.com/reference-architecture/scaffolding/setup/network/), the -[GitHub OIDC component](https://docs.cloudposse.com/components/catalog/aws/github-oidc-provider/), and the -[Karpenter component](https://docs.cloudposse.com/components/catalog/aws/eks/karpenter/). +For more on these requirements, see [Identity Reference Architecture](https://docs.cloudposse.com/layers/identity/), +[Network Reference Architecture](https://docs.cloudposse.com/layers/network/), the +[GitHub OIDC component](https://docs.cloudposse.com/components/library/aws/github-oidc-provider/), and the +[Karpenter component](https://docs.cloudposse.com/components/library/aws/eks/karpenter/). ### Mixin pattern for Kubernetes version @@ -400,7 +398,7 @@ addons: > functional during deployment of the cluster. For more information on upgrading EKS Addons, see -["How to Upgrade EKS Cluster Addons"](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons/) +["How to Upgrade EKS Cluster Addons"](https://docs.cloudposse.com/learn/maintenance/upgrades/how-to-upgrade-eks-cluster-addons/) ### Adding and Configuring a new EKS Addon @@ -630,13 +628,7 @@ If the new addon requires an EKS IAM Role for Kubernetes Service Account, perfor ## Related How-to Guides -- [How to Load Test in AWS](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-load-test-in-aws) -- [How to Tune EKS with AWS Managed Node Groups](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-tune-eks-with-aws-managed-node-groups) -- [How to Keep Everything Up to Date](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-keep-everything-up-to-date) -- [How to Tune SpotInst Parameters for EKS](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-tune-spotinst-parameters-for-eks) -- [How to Upgrade EKS Cluster Addons](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks-cluster-addons) -- [How to Upgrade EKS](https://docs.cloudposse.com/reference-architecture/how-to-guides/upgrades/how-to-upgrade-eks) -- [EBS CSI Migration FAQ](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi-migration-faq.html) +- [EKS Foundational Platform](https://docs.cloudposse.com/layers/eks/) ## References diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index 44053c5fd..ad75b70aa 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -194,6 +194,6 @@ components: ## References -- [Secrets Management Strategy](https://docs.cloudposse.com/reference-architecture/design-decisions/cold-start/decide-on-secrets-management-strategy-for-terraform/) +- [Secrets Management Strategy](https://docs.cloudposse.com/layers/project/design-decisions/decide-on-secrets-management-strategy-for-terraform) - https://external-secrets.io/v0.5.9/ - https://external-secrets.io/v0.5.9/provider-aws-parameter-store/ diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index 06ee77000..4b3c38e96 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -387,14 +387,9 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ ## Related How-to Guides -- [How to Add Users to a Team in OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-add-users-to-a-team-in-opsgenie) -- [How to Pass Tags Along to Datadog](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-pass-tags-along-to-datadog) -- [How to Onboard a New Service with Datadog and OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-onboard-a-new-service-with-datadog-and-opsgenie) -- [How to Create Escalation Rules in OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-escalation-rules-in-opsgenie) -- [How to Setup Rotations in OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-setup-rotations-in-opsgenie) -- [How to Create New Teams in OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-create-new-teams-in-opsgenie) -- [How to Sign Up for OpsGenie?](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie/how-to-sign-up-for-opsgenie/) -- [How to Implement Incident Management with OpsGenie](https://docs.cloudposse.com/reference-architecture/how-to-guides/tutorials/how-to-implement-incident-management-with-opsgenie) +[See OpsGenie in the Reference Architecture](https://docs.cloudposse.com/layers/alerting/opsgenie/) + + ## References From 8354d70e9d62cc9f275b8fb9802cda322fa88462 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Wed, 14 Aug 2024 09:23:22 -0700 Subject: [PATCH 468/501] update `vpc-peering` with requester params (#1097) --- modules/vpc-peering/README.md | 2 ++ modules/vpc-peering/main.tf | 4 ++-- modules/vpc-peering/variables.tf | 12 ++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/modules/vpc-peering/README.md b/modules/vpc-peering/README.md index 3238f64d2..c4a2eadfc 100644 --- a/modules/vpc-peering/README.md +++ b/modules/vpc-peering/README.md @@ -230,7 +230,9 @@ atmos terraform apply vpc-peering -s ue1-prod | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [requester\_allow\_remote\_vpc\_dns\_resolution](#input\_requester\_allow\_remote\_vpc\_dns\_resolution) | Allow requester VPC to resolve public DNS hostnames to private IP addresses when queried from instances in the accepter VPC | `bool` | `true` | no | +| [requester\_role\_arn](#input\_requester\_role\_arn) | Requestor AWS assume role ARN, if not provided it will be assumed to be the current terraform role. | `string` | `null` | no | | [requester\_vpc\_component\_name](#input\_requester\_vpc\_component\_name) | Requestor vpc component name | `string` | `"vpc"` | no | +| [requester\_vpc\_id](#input\_requester\_vpc\_id) | Requestor VPC ID, if not provided, it will be looked up by component using variable `requester_vpc_component_name` | `string` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/vpc-peering/main.tf b/modules/vpc-peering/main.tf index 292dfe062..c189b773e 100644 --- a/modules/vpc-peering/main.tf +++ b/modules/vpc-peering/main.tf @@ -1,7 +1,7 @@ locals { enabled = module.this.enabled - requester_vpc_id = module.requester_vpc.outputs.vpc_id + requester_vpc_id = coalesce(var.requester_vpc_id, module.requester_vpc.outputs.vpc_id) accepter_aws_assume_role_arn = var.accepter_stage_name != null ? module.iam_roles.terraform_role_arns[var.accepter_stage_name] : var.accepter_aws_assume_role_arn } @@ -24,7 +24,7 @@ module "vpc_peering" { auto_accept = var.auto_accept requester_allow_remote_vpc_dns_resolution = var.requester_allow_remote_vpc_dns_resolution - requester_aws_assume_role_arn = module.iam_roles.terraform_role_arn + requester_aws_assume_role_arn = coalesce(var.requester_role_arn, module.iam_roles.terraform_role_arn) requester_region = var.region requester_vpc_id = local.requester_vpc_id diff --git a/modules/vpc-peering/variables.tf b/modules/vpc-peering/variables.tf index b88a5383d..50fd53818 100644 --- a/modules/vpc-peering/variables.tf +++ b/modules/vpc-peering/variables.tf @@ -37,6 +37,18 @@ variable "accepter_stage_name" { default = null } +variable "requester_vpc_id" { + type = string + description = "Requestor VPC ID, if not provided, it will be looked up by component using variable `requester_vpc_component_name`" + default = null +} + +variable "requester_role_arn" { + type = string + description = "Requestor AWS assume role ARN, if not provided it will be assumed to be the current terraform role." + default = null +} + variable "requester_allow_remote_vpc_dns_resolution" { type = bool description = "Allow requester VPC to resolve public DNS hostnames to private IP addresses when queried from instances in the accepter VPC" From 711575ab10187909b9a39b308bc66cd121c04294 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Wed, 14 Aug 2024 13:17:17 -0400 Subject: [PATCH 469/501] Update `eks/external-dns` component to support Istio `istio-gateway` resources (#1098) --- modules/eks/external-dns/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/eks/external-dns/main.tf b/modules/eks/external-dns/main.tf index 5c053cd4c..058aa7ccf 100644 --- a/modules/eks/external-dns/main.tf +++ b/modules/eks/external-dns/main.tf @@ -98,7 +98,7 @@ module "external_dns" { publishInternalServices = var.publish_internal_services txtOwnerId = local.txt_owner txtPrefix = local.txt_prefix - source = local.sources + sources = local.sources }), # hardcoded values file("${path.module}/resources/values.yaml"), From fedec4f458c059445172a09bdd0d454fea527754 Mon Sep 17 00:00:00 2001 From: "Erik Osterman (CEO @ Cloud Posse)" Date: Thu, 15 Aug 2024 08:39:25 -0500 Subject: [PATCH 470/501] Add frontmatter (#1085) Co-authored-by: milldr Co-authored-by: Dan Miller --- .../eks/platform/README.md | 0 .../eks/platform/context.tf | 0 {modules => deprecated}/eks/platform/main.tf | 0 .../eks/platform/outputs.tf | 0 .../eks/platform/providers.tf | 0 .../eks/platform/remote-state.tf | 0 .../eks/platform/variables.tf | 0 .../eks/platform/versions.tf | 0 {modules => deprecated}/gitops/README.md | 0 {modules => deprecated}/gitops/context.tf | 0 .../gitops/github-actions-iam-policy.tf | 0 .../gitops/github-actions-iam-role.mixin.tf | 0 {modules => deprecated}/gitops/providers.tf | 0 .../gitops/remote-state.tf | 0 {modules => deprecated}/gitops/variables.tf | 0 {modules => deprecated}/gitops/versions.tf | 0 modules/account-map/README.md | 8 ++++++ modules/account-quotas/README.md | 7 ++++++ modules/account-settings/README.md | 8 ++++++ modules/account/README.md | 8 ++++++ modules/acm/README.md | 7 ++++++ modules/alb/README.md | 7 ++++++ modules/amplify/README.md | 7 ++++++ .../api-gateway-account-settings/README.md | 7 ++++++ modules/api-gateway-rest-api/README.md | 7 ++++++ modules/argocd-repo/README.md | 8 ++++++ modules/athena/README.md | 7 ++++++ modules/aurora-mysql-resources/README.md | 7 ++++++ modules/aurora-mysql/README.md | 7 ++++++ modules/aurora-postgres-resources/README.md | 7 ++++++ modules/aurora-postgres/README.md | 13 +++++++--- modules/aws-backup/README.md | 7 ++++++ modules/aws-config/README.md | 7 ++++++ modules/aws-inspector/README.md | 7 ++++++ modules/aws-inspector2/README.md | 7 ++++++ modules/aws-saml/README.md | 8 ++++++ modules/aws-shield/README.md | 7 ++++++ modules/aws-sso/README.md | 8 ++++++ modules/aws-ssosync/README.md | 7 ++++++ modules/aws-team-roles/README.md | 8 ++++++ modules/aws-teams/README.md | 8 ++++++ modules/bastion/README.md | 7 ++++++ modules/cloudtrail-bucket/README.md | 7 ++++++ modules/cloudtrail/README.md | 7 ++++++ modules/cloudwatch-logs/README.md | 8 ++++++ modules/cognito/README.md | 7 ++++++ modules/config-bucket/README.md | 7 ++++++ modules/datadog-configuration/README.md | 8 ++++++ .../modules/datadog_keys/README.md | 10 +++++++- modules/datadog-integration/README.md | 8 ++++++ modules/datadog-lambda-forwarder/README.md | 8 ++++++ modules/datadog-logs-archive/README.md | 8 ++++++ modules/datadog-monitor/README.md | 8 ++++++ .../datadog-private-location-ecs/README.md | 8 ++++++ .../README.md | 8 ++++++ modules/datadog-synthetics/README.md | 8 ++++++ modules/dms/endpoint/README.md | 7 ++++++ modules/dms/iam/README.md | 7 ++++++ modules/dms/replication-instance/README.md | 7 ++++++ modules/dms/replication-task/README.md | 7 ++++++ modules/dns-delegated/README.md | 7 ++++++ modules/dns-primary/README.md | 7 ++++++ modules/documentdb/README.md | 7 ++++++ modules/dynamodb/README.md | 8 ++++++ modules/ec2-client-vpn/README.md | 7 ++++++ modules/ec2-instance/README.md | 7 ++++++ modules/ecr/README.md | 7 ++++++ modules/ecs-service/README.md | 7 ++++++ modules/ecs/README.md | 7 ++++++ modules/efs/README.md | 8 ++++++ .../eks/actions-runner-controller/README.md | 10 +++++++- .../alb-controller-ingress-class/README.md | 8 ++++++ .../alb-controller-ingress-group/README.md | 10 +++++++- modules/eks/alb-controller/README.md | 8 ++++++ modules/eks/argocd/README.md | 10 +++++++- .../aws-node-termination-handler/README.md | 10 +++++++- modules/eks/cert-manager/README.md | 8 ++++++ modules/eks/cluster/README.md | 7 ++++++ modules/eks/datadog-agent/README.md | 11 +++++++- modules/eks/echo-server/README.md | 8 ++++++ modules/eks/external-dns/README.md | 8 ++++++ .../eks/external-secrets-operator/README.md | 10 +++++++- modules/eks/github-actions-runner/README.md | 10 +++++++- modules/eks/idp-roles/README.md | 8 ++++++ modules/eks/karpenter-node-pool/README.md | 8 ++++++ modules/eks/karpenter/README.md | 8 ++++++ modules/eks/keda/README.md | 10 +++++++- modules/eks/loki/README.md | 8 ++++++ modules/eks/metrics-server/README.md | 11 ++++++-- modules/eks/prometheus-scraper/README.md | 8 ++++++ modules/eks/promtail/README.md | 8 ++++++ modules/eks/redis-operator/README.md | 9 +++++++ modules/eks/redis/README.md | 8 ++++++ modules/eks/reloader/README.md | 8 ++++++ modules/eks/storage-class/README.md | 9 +++++++ modules/elasticache-redis/README.md | 7 ++++++ modules/elasticsearch/README.md | 7 ++++++ modules/eventbridge/README.md | 7 ++++++ modules/github-action-token-rotator/README.md | 7 ++++++ modules/github-oidc-provider/README.md | 8 ++++++ modules/github-oidc-role/README.md | 8 ++++++ modules/github-runners/README.md | 7 ++++++ modules/github-webhook/README.md | 7 ++++++ .../README.md | 9 ++++++- modules/global-accelerator/README.md | 7 ++++++ modules/glue/catalog-database/README.md | 7 ++++++ modules/glue/catalog-table/README.md | 7 ++++++ modules/glue/connection/README.md | 7 ++++++ modules/glue/crawler/README.md | 7 ++++++ modules/glue/iam/README.md | 7 ++++++ modules/glue/job/README.md | 7 ++++++ modules/glue/registry/README.md | 7 ++++++ modules/glue/schema/README.md | 7 ++++++ modules/glue/trigger/README.md | 7 ++++++ modules/glue/workflow/README.md | 7 ++++++ modules/guardduty/README.md | 7 ++++++ modules/iam-role/README.md | 7 ++++++ modules/iam-service-linked-roles/README.md | 7 ++++++ modules/ipam/README.md | 7 ++++++ modules/kinesis-stream/README.md | 7 ++++++ modules/kms/README.md | 7 ++++++ modules/lakeformation/README.md | 7 ++++++ modules/lambda/README.md | 9 ++++++- modules/macie/README.md | 7 ++++++ modules/managed-grafana/api-key/README.md | 8 ++++++ modules/managed-grafana/dashboard/README.md | 8 ++++++ .../data-source/loki/README.md | 8 ++++++ .../data-source/managed-prometheus/README.md | 8 ++++++ modules/managed-grafana/workspace/README.md | 10 +++++++- .../managed-prometheus/workspace/README.md | 7 ++++++ modules/mq-broker/README.md | 7 ++++++ modules/msk/README.md | 9 ++++++- modules/mwaa/README.md | 7 ++++++ modules/network-firewall/README.md | 7 ++++++ modules/opsgenie-team/README.md | 9 +++++-- modules/philips-labs-github-runners/README.md | 7 ++++++ modules/rds/README.md | 7 ++++++ modules/redshift/README.md | 7 ++++++ .../route53-resolver-dns-firewall/README.md | 7 ++++++ modules/s3-bucket/README.md | 8 ++++++ modules/security-hub/README.md | 7 ++++++ modules/ses/README.md | 7 ++++++ modules/sftp/README.md | 7 ++++++ modules/snowflake-account/README.md | 8 ++++++ modules/snowflake-database/README.md | 8 ++++++ modules/sns-topic/README.md | 7 ++++++ modules/spa-s3-cloudfront/README.md | 7 ++++++ modules/spacelift/README.md | 9 ++++++- modules/spacelift/admin-stack/README.md | 8 ++++++ modules/spacelift/spaces/README.md | 8 ++++++ modules/spacelift/worker-pool/README.md | 8 ++++++ modules/sqs-queue/README.md | 7 ++++++ modules/ssm-parameters/README.md | 7 ++++++ modules/sso-saml-provider/README.md | 7 ++++++ modules/strongdm/README.md | 7 ++++++ modules/tfstate-backend/README.md | 25 +++++++++++++++---- modules/tgw/README.md | 9 ++++++- .../tgw/cross-region-hub-connector/README.md | 9 ++++++- modules/tgw/hub/README.md | 7 ++++++ modules/tgw/spoke/README.md | 7 ++++++ modules/vpc-flow-logs-bucket/README.md | 7 ++++++ modules/vpc-peering/README.md | 7 ++++++ modules/vpc/README.md | 9 ++++++- modules/waf/README.md | 9 ++++++- modules/zscaler/README.md | 7 ++++++ 165 files changed, 1138 insertions(+), 30 deletions(-) rename {modules => deprecated}/eks/platform/README.md (100%) rename {modules => deprecated}/eks/platform/context.tf (100%) rename {modules => deprecated}/eks/platform/main.tf (100%) rename {modules => deprecated}/eks/platform/outputs.tf (100%) rename {modules => deprecated}/eks/platform/providers.tf (100%) rename {modules => deprecated}/eks/platform/remote-state.tf (100%) rename {modules => deprecated}/eks/platform/variables.tf (100%) rename {modules => deprecated}/eks/platform/versions.tf (100%) rename {modules => deprecated}/gitops/README.md (100%) rename {modules => deprecated}/gitops/context.tf (100%) rename {modules => deprecated}/gitops/github-actions-iam-policy.tf (100%) rename {modules => deprecated}/gitops/github-actions-iam-role.mixin.tf (100%) rename {modules => deprecated}/gitops/providers.tf (100%) rename {modules => deprecated}/gitops/remote-state.tf (100%) rename {modules => deprecated}/gitops/variables.tf (100%) rename {modules => deprecated}/gitops/versions.tf (100%) diff --git a/modules/eks/platform/README.md b/deprecated/eks/platform/README.md similarity index 100% rename from modules/eks/platform/README.md rename to deprecated/eks/platform/README.md diff --git a/modules/eks/platform/context.tf b/deprecated/eks/platform/context.tf similarity index 100% rename from modules/eks/platform/context.tf rename to deprecated/eks/platform/context.tf diff --git a/modules/eks/platform/main.tf b/deprecated/eks/platform/main.tf similarity index 100% rename from modules/eks/platform/main.tf rename to deprecated/eks/platform/main.tf diff --git a/modules/eks/platform/outputs.tf b/deprecated/eks/platform/outputs.tf similarity index 100% rename from modules/eks/platform/outputs.tf rename to deprecated/eks/platform/outputs.tf diff --git a/modules/eks/platform/providers.tf b/deprecated/eks/platform/providers.tf similarity index 100% rename from modules/eks/platform/providers.tf rename to deprecated/eks/platform/providers.tf diff --git a/modules/eks/platform/remote-state.tf b/deprecated/eks/platform/remote-state.tf similarity index 100% rename from modules/eks/platform/remote-state.tf rename to deprecated/eks/platform/remote-state.tf diff --git a/modules/eks/platform/variables.tf b/deprecated/eks/platform/variables.tf similarity index 100% rename from modules/eks/platform/variables.tf rename to deprecated/eks/platform/variables.tf diff --git a/modules/eks/platform/versions.tf b/deprecated/eks/platform/versions.tf similarity index 100% rename from modules/eks/platform/versions.tf rename to deprecated/eks/platform/versions.tf diff --git a/modules/gitops/README.md b/deprecated/gitops/README.md similarity index 100% rename from modules/gitops/README.md rename to deprecated/gitops/README.md diff --git a/modules/gitops/context.tf b/deprecated/gitops/context.tf similarity index 100% rename from modules/gitops/context.tf rename to deprecated/gitops/context.tf diff --git a/modules/gitops/github-actions-iam-policy.tf b/deprecated/gitops/github-actions-iam-policy.tf similarity index 100% rename from modules/gitops/github-actions-iam-policy.tf rename to deprecated/gitops/github-actions-iam-policy.tf diff --git a/modules/gitops/github-actions-iam-role.mixin.tf b/deprecated/gitops/github-actions-iam-role.mixin.tf similarity index 100% rename from modules/gitops/github-actions-iam-role.mixin.tf rename to deprecated/gitops/github-actions-iam-role.mixin.tf diff --git a/modules/gitops/providers.tf b/deprecated/gitops/providers.tf similarity index 100% rename from modules/gitops/providers.tf rename to deprecated/gitops/providers.tf diff --git a/modules/gitops/remote-state.tf b/deprecated/gitops/remote-state.tf similarity index 100% rename from modules/gitops/remote-state.tf rename to deprecated/gitops/remote-state.tf diff --git a/modules/gitops/variables.tf b/deprecated/gitops/variables.tf similarity index 100% rename from modules/gitops/variables.tf rename to deprecated/gitops/variables.tf diff --git a/modules/gitops/versions.tf b/deprecated/gitops/versions.tf similarity index 100% rename from modules/gitops/versions.tf rename to deprecated/gitops/versions.tf diff --git a/modules/account-map/README.md b/modules/account-map/README.md index 893586de3..f8ce68877 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/account-map + - layer/accounts + - provider/aws + - privileged +--- + # Component: `account-map` This component is responsible for provisioning information only: it simply populates Terraform state with data (account diff --git a/modules/account-quotas/README.md b/modules/account-quotas/README.md index a442dddac..92db56f36 100644 --- a/modules/account-quotas/README.md +++ b/modules/account-quotas/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/account-quotas + - layer/foundation + - provider/aws +--- + # Component: `account-quotas` This component is responsible for requesting service quota increases. We recommend making requests here rather than in diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index a7e6a0d5e..e5fee8198 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/account-settings + - layer/accounts + - provider/aws + - privileged +--- + # Component: `account-settings` This component is responsible for provisioning account level settings: IAM password policy, AWS Account Alias, EBS diff --git a/modules/account/README.md b/modules/account/README.md index a22e3c1be..446bac0b1 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/account + - layer/accounts + - provider/aws + - privileged +--- + # Component: `account` This component is responsible for provisioning the full account hierarchy along with Organizational Units (OUs). It diff --git a/modules/acm/README.md b/modules/acm/README.md index bdd7d25a0..47891fc0b 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/acm + - layer/network + - provider/aws +--- + # Component: `acm` This component is responsible for requesting an ACM certificate for a domain and adding a CNAME record to the DNS zone diff --git a/modules/alb/README.md b/modules/alb/README.md index 25e47e977..cedac8a82 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/alb + - layer/ecs + - provider/aws +--- + # Component: `alb` This component is responsible for provisioning a generic Application Load Balancer. It depends on the `vpc` and diff --git a/modules/amplify/README.md b/modules/amplify/README.md index b64597941..c53819b76 100644 --- a/modules/amplify/README.md +++ b/modules/amplify/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/amplify + - layer/unassigned + - provider/aws +--- + # Component: `amplify` This component is responsible for provisioning AWS Amplify apps, backend environments, branches, domain associations, diff --git a/modules/api-gateway-account-settings/README.md b/modules/api-gateway-account-settings/README.md index 70a4a008b..8e5bd4b07 100644 --- a/modules/api-gateway-account-settings/README.md +++ b/modules/api-gateway-account-settings/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/api-gateway-account-settings + - layer/unassigned + - provider/aws +--- + # Component: `api-gateway-account-settings` This component is responsible for setting the global, regional settings required to allow API Gateway to write to diff --git a/modules/api-gateway-rest-api/README.md b/modules/api-gateway-rest-api/README.md index 7f347f130..0e7e44e72 100644 --- a/modules/api-gateway-rest-api/README.md +++ b/modules/api-gateway-rest-api/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/api-gateway-rest-api + - layer/addons + - provider/aws +--- + # Component: `api-gateway-rest-api` This component is responsible for deploying an API Gateway REST API. diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index 2e5638192..b3c8073c7 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/argocd-repo + - layer/software-delivery + - provider/aws + - provider/github +--- + # Component: `argocd-repo` This component is responsible for creating and managing an ArgoCD desired state repository. diff --git a/modules/athena/README.md b/modules/athena/README.md index 3e6ee7ed9..1575f7234 100644 --- a/modules/athena/README.md +++ b/modules/athena/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/athena + - layer/data + - provider/aws +--- + # Component: `athena` This component is responsible for provisioning an Amazon Athena workgroup, databases, and related resources. diff --git a/modules/aurora-mysql-resources/README.md b/modules/aurora-mysql-resources/README.md index c8e8c5ad1..7c4b848cd 100644 --- a/modules/aurora-mysql-resources/README.md +++ b/modules/aurora-mysql-resources/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aurora-mysql-resources + - layer/data + - provider/aws +--- + # Component: `aurora-mysql-resources` This component is responsible for provisioning Aurora MySQL resources: additional databases, users, permissions, grants, diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index ff8ad6570..08688cffd 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aurora-mysql + - layer/data + - provider/aws +--- + # Component: `aurora-mysql` This component is responsible for provisioning Aurora MySQL RDS clusters. It seeds relevant database information diff --git a/modules/aurora-postgres-resources/README.md b/modules/aurora-postgres-resources/README.md index 1c6bc4a95..8c6bef69b 100644 --- a/modules/aurora-postgres-resources/README.md +++ b/modules/aurora-postgres-resources/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aurora-postgres-resources + - layer/data + - provider/aws +--- + # Component: `aurora-postgres-resources` This component is responsible for provisioning Aurora Postgres resources: additional databases, users, permissions, diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index 7d6205c60..fb524bfee 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aurora-postgres + - layer/data + - provider/aws +--- + # Component: `aurora-postgres` This component is responsible for provisioning Aurora Postgres RDS clusters. It seeds relevant database information @@ -302,10 +309,12 @@ components: | [autoscaling\_scale\_out\_cooldown](#input\_autoscaling\_scale\_out\_cooldown) | The amount of time, in seconds, after a scaling activity completes and before the next scaling up activity can start. Default is 300s | `number` | `300` | no | | [autoscaling\_target\_metrics](#input\_autoscaling\_target\_metrics) | The metrics type to use. If this value isn't provided the default is CPU utilization | `string` | `"RDSReaderAverageCPUUtilization"` | no | | [autoscaling\_target\_value](#input\_autoscaling\_target\_value) | The target value to scale with respect to target metrics | `number` | `75` | no | +| [backup\_window](#input\_backup\_window) | Daily time range during which the backups happen, UTC | `string` | `"07:00-09:00"` | no | | [ca\_cert\_identifier](#input\_ca\_cert\_identifier) | The identifier of the CA certificate for the DB instance | `string` | `null` | no | | [cluster\_dns\_name\_part](#input\_cluster\_dns\_name\_part) | Part of DNS name added to module and cluster name for DNS for cluster endpoint | `string` | `"writer"` | no | | [cluster\_family](#input\_cluster\_family) | Family of the DB parameter group. Valid values for Aurora PostgreSQL: `aurora-postgresql9.6`, `aurora-postgresql10`, `aurora-postgresql11`, `aurora-postgresql12` | `string` | `"aurora-postgresql13"` | no | | [cluster\_name](#input\_cluster\_name) | Short name for this cluster | `string` | n/a | yes | +| [cluster\_parameters](#input\_cluster\_parameters) | List of DB cluster parameters to apply |
list(object({
apply_method = string
name = string
value = string
}))
| `[]` | no | | [cluster\_size](#input\_cluster\_size) | Postgres cluster size | `number` | n/a | yes | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [database\_name](#input\_database\_name) | Name for an automatically created database on cluster creation. An empty name will generate a db name. | `string` | `""` | no | @@ -341,6 +350,7 @@ components: | [reader\_dns\_name\_part](#input\_reader\_dns\_name\_part) | Part of DNS name added to module and cluster name for DNS for cluster reader | `string` | `"reader"` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [retention\_period](#input\_retention\_period) | Number of days to retain backups for | `number` | `5` | no | | [scaling\_configuration](#input\_scaling\_configuration) | List of nested attributes with scaling properties. Only valid when `engine_mode` is set to `serverless`. This is required for Serverless v1 |
list(object({
auto_pause = bool
max_capacity = number
min_capacity = number
seconds_until_auto_pause = number
timeout_action = string
}))
| `[]` | no | | [serverlessv2\_scaling\_configuration](#input\_serverlessv2\_scaling\_configuration) | Nested attribute with scaling properties for ServerlessV2. Only valid when `engine_mode` is set to `provisioned.` This is required for Serverless v2 |
object({
min_capacity = number
max_capacity = number
})
| `null` | no | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Normally AWS makes a snapshot of the database before deleting it. Set this to `true` in order to skip this.
NOTE: The final snapshot has a name derived from the cluster name. If you delete a cluster, get a final snapshot,
then create a cluster of the same name, its final snapshot will fail with a name collision unless you delete
the previous final snapshot first. | `bool` | `false` | no | @@ -351,9 +361,6 @@ components: | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | | [vpc\_component\_name](#input\_vpc\_component\_name) | The name of the VPC component | `string` | `"vpc"` | no | -| [retention\_period](#input\_retention\_period) | Number of days to retain backups for | `number` | `5` | no | -| [backup\_window](#input\_backup\_window) | Daily time range during which the backups happen, UTC | `string` | `"07:00-09:00"` | no | - ## Outputs diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index 75d4ebfcc..3eed64d8c 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aws-backup + - layer/data + - provider/aws +--- + # Component: `aws-backup` This component is responsible for provisioning an AWS Backup Plan. It creates a schedule for backing up given ARNs. diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index c280c627b..20fbd35f3 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aws-config + - layer/security-and-compliance + - provider/aws +--- + # Component: `aws-config` This component is responsible for configuring AWS Config. diff --git a/modules/aws-inspector/README.md b/modules/aws-inspector/README.md index 679d122d5..ec1bc6084 100644 --- a/modules/aws-inspector/README.md +++ b/modules/aws-inspector/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aws-inspector + - layer/security-and-compliance + - provider/aws +--- + # Component: `aws-inspector` This component is responsible for provisioning an diff --git a/modules/aws-inspector2/README.md b/modules/aws-inspector2/README.md index e40a45838..280f363ce 100644 --- a/modules/aws-inspector2/README.md +++ b/modules/aws-inspector2/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aws-inspector2 + - layer/security-and-compliance + - provider/aws +--- + # Component: `aws-inspector2` This component is responsible for configuring Inspector V2 within an AWS Organization. diff --git a/modules/aws-saml/README.md b/modules/aws-saml/README.md index 94f2ccece..94733ace7 100644 --- a/modules/aws-saml/README.md +++ b/modules/aws-saml/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/aws-saml + - layer/identity + - provider/aws + - priviliged +--- + # Component: `aws-saml` This component is responsible for provisioning SAML metadata into AWS IAM as new SAML providers. Additionally, for an diff --git a/modules/aws-shield/README.md b/modules/aws-shield/README.md index 500f9785b..e3114ef40 100644 --- a/modules/aws-shield/README.md +++ b/modules/aws-shield/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aws-shield + - layer/security-and-compliance + - provider/aws +--- + # Component: `aws-shield` This component is responsible for enabling AWS Shield Advanced Protection for the following resources: diff --git a/modules/aws-sso/README.md b/modules/aws-sso/README.md index d51fa0db4..dc29fcd7c 100644 --- a/modules/aws-sso/README.md +++ b/modules/aws-sso/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/aws-sso + - layer/identity + - provider/aws + - privileged +--- + # Component: `aws-sso` This component is responsible for creating [AWS SSO Permission Sets][1] and creating AWS SSO Account Assignments, that diff --git a/modules/aws-ssosync/README.md b/modules/aws-ssosync/README.md index d4bc7384a..c31ab93ce 100644 --- a/modules/aws-ssosync/README.md +++ b/modules/aws-ssosync/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/aws-ssosync + - layer/identity + - provider/aws +--- + # Component: `aws-ssosync` Deploys [AWS ssosync](https://github.com/awslabs/ssosync) to sync Google Groups with AWS SSO. diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index 4e9feda9a..0109b2307 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/aws-team-roles + - layer/identity + - provider/aws + - privileged +--- + # Component: `aws-team-roles` This component is responsible for provisioning user and system IAM roles outside the `identity` account. It sets them up diff --git a/modules/aws-teams/README.md b/modules/aws-teams/README.md index 38b71abf1..fb99606a9 100644 --- a/modules/aws-teams/README.md +++ b/modules/aws-teams/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/aws-teams + - layer/identity + - provider/aws + - privileged +--- + # Component: `aws-teams` This component is responsible for provisioning all primary user and system roles into the centralized identity account. diff --git a/modules/bastion/README.md b/modules/bastion/README.md index 4af775ebf..529d84dfe 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/bastion + - layer/network + - provider/aws +--- + # Component: `bastion` This component is responsible for provisioning a generic Bastion host within an ASG with parameterized `user_data` and diff --git a/modules/cloudtrail-bucket/README.md b/modules/cloudtrail-bucket/README.md index a38604932..817b955b1 100644 --- a/modules/cloudtrail-bucket/README.md +++ b/modules/cloudtrail-bucket/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/cloudtrail-bucket + - layer/foundation + - provider/aws +--- + # Component: `cloudtrail-bucket` This component is responsible for provisioning a bucket for storing cloudtrail logs for auditing purposes. It's expected diff --git a/modules/cloudtrail/README.md b/modules/cloudtrail/README.md index 715cdc696..b42770ebf 100644 --- a/modules/cloudtrail/README.md +++ b/modules/cloudtrail/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/cloudtrail + - layer/foundation + - provider/aws +--- + # Component: `cloudtrail` This component is responsible for provisioning cloudtrail auditing in an individual account. It's expected to be used diff --git a/modules/cloudwatch-logs/README.md b/modules/cloudwatch-logs/README.md index 9a78855af..e1244f1b9 100644 --- a/modules/cloudwatch-logs/README.md +++ b/modules/cloudwatch-logs/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/cloudwatch-logs + - layer/baseline + - layer/security-and-compliance + - provider/aws +--- + # Component: `cloudwatch-logs` This component is responsible for creation of CloudWatch Log Streams and Log Groups. diff --git a/modules/cognito/README.md b/modules/cognito/README.md index a9219970b..0b29c148b 100644 --- a/modules/cognito/README.md +++ b/modules/cognito/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/cognito + - layer/addons + - provider/aws +--- + # Component: `cognito` This component is responsible for provisioning and managing AWS Cognito resources. diff --git a/modules/config-bucket/README.md b/modules/config-bucket/README.md index 72f36c015..0c3371a86 100644 --- a/modules/config-bucket/README.md +++ b/modules/config-bucket/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/config-bucket + - layer/security-and-compliance + - provider/aws +--- + # Component: `config-bucket` This module creates an S3 bucket suitable for storing `AWS Config` data. diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index 90736c474..a9673333b 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/datadog-configuration + - layer/datadog + - provider/datadog + - provider/aws +--- + # Component: `datadog-configuration` This component is responsible for provisioning SSM or ASM entries for Datadog API keys. diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index 56325b4e0..4ffc16018 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -1,4 +1,12 @@ -# Submodule `datadog_keys` +--- +tags: + - component/datadog_keys + - layer/datadog + - provider/datadog + - provider/aws +--- + +# Component: `datadog_keys` Useful submodule for other modules to quickly configure the datadog provider diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index d27d078ae..e182591e3 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/datadog-integration + - layer/datadog + - provider/aws + - provider/datadog +--- + # Component: `datadog-integration` This component is responsible for provisioning Datadog AWS integrations. It depends on the `datadog-configuration` diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index a80caa4c8..75251a11f 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/datadog-lambda-forwarder + - layer/datadog + - provider/aws + - provider/datadog +--- + # Component: `datadog-lambda-forwarder` This component is responsible for provision all the necessary infrastructure to deploy diff --git a/modules/datadog-logs-archive/README.md b/modules/datadog-logs-archive/README.md index 8eb8ffcdb..cf2a92e6b 100644 --- a/modules/datadog-logs-archive/README.md +++ b/modules/datadog-logs-archive/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/datadog-logs-archive + - layer/datadog + - provider/aws + - provider/datadog +--- + # Component: `datadog-logs-archive` This component is responsible for provisioning Datadog Log Archives. It creates a single log archive pipeline for each diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 7caa2b21c..2a0543abe 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/datadog-monitor + - layer/datadog + - provider/aws + - provider/datadog +--- + # Component: `datadog-monitor` This component is responsible for provisioning Datadog monitors and assigning Datadog roles to the monitors. diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index 3d75f2286..4970e455f 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/datadog-private-location-ecs + - layer/datadog + - provider/aws + - provider/datadog +--- + # Component: `datadog-private-location-ecs` This component is responsible for creating a datadog private location and deploying it to ECS (EC2 / Fargate) diff --git a/modules/datadog-synthetics-private-location/README.md b/modules/datadog-synthetics-private-location/README.md index 54209576a..0d78ced6f 100644 --- a/modules/datadog-synthetics-private-location/README.md +++ b/modules/datadog-synthetics-private-location/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/datadog-synthetics-private-location + - layer/datadog + - provider/aws + - provider/datadog +--- + # Component: `datadog-synthetics-private-location` This component provisions a Datadog synthetics private location on Datadog and a private location agent on EKS cluster. diff --git a/modules/datadog-synthetics/README.md b/modules/datadog-synthetics/README.md index a18461c1e..aba1801e7 100644 --- a/modules/datadog-synthetics/README.md +++ b/modules/datadog-synthetics/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/datadog-synthetics + - layer/datadog + - provider/aws + - provider/datadog +--- + # Component: `datadog-synthetics` This component provides the ability to implement diff --git a/modules/dms/endpoint/README.md b/modules/dms/endpoint/README.md index b65dfc542..a395212ff 100644 --- a/modules/dms/endpoint/README.md +++ b/modules/dms/endpoint/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/dms/endpoint + - layer/unassigned + - provider/aws +--- + # Component: `dms/endpoint` This component provisions DMS endpoints. diff --git a/modules/dms/iam/README.md b/modules/dms/iam/README.md index 021da144d..b1d5ec321 100644 --- a/modules/dms/iam/README.md +++ b/modules/dms/iam/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/dms/iam + - layer/unassigned + - provider/aws +--- + # Component: `dms/iam` This component provisions IAM roles required for DMS. diff --git a/modules/dms/replication-instance/README.md b/modules/dms/replication-instance/README.md index fade7e38c..b48146975 100644 --- a/modules/dms/replication-instance/README.md +++ b/modules/dms/replication-instance/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/dms/replication-instance + - layer/unassigned + - provider/aws +--- + # Component: `dms/replication-instance` This component provisions DMS replication instances. diff --git a/modules/dms/replication-task/README.md b/modules/dms/replication-task/README.md index 4732e9072..294345780 100644 --- a/modules/dms/replication-task/README.md +++ b/modules/dms/replication-task/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/dms/replication-task + - layer/unassigned + - provider/aws +--- + # Component: `dms/replication-task` This component provisions DMS replication tasks. diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index ed73c60a6..dedfa2577 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/dns-delegated + - layer/network + - provider/aws +--- + # Component: `dns-delegated` This component is responsible for provisioning a DNS zone which delegates nameservers to the DNS zone in the primary DNS diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index b53c42776..d8b64a66b 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/dns-primary + - layer/network + - provider/aws +--- + # Component: `dns-primary` This component is responsible for provisioning the primary DNS zones into an AWS account. By convention, we typically diff --git a/modules/documentdb/README.md b/modules/documentdb/README.md index cdea391ee..ebc5ee1ed 100644 --- a/modules/documentdb/README.md +++ b/modules/documentdb/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/documentdb + - layer/data + - provider/aws +--- + # Component: `documentdb` This component is responsible for provisioning DocumentDB clusters. diff --git a/modules/dynamodb/README.md b/modules/dynamodb/README.md index efef2584b..ff68e2682 100644 --- a/modules/dynamodb/README.md +++ b/modules/dynamodb/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/dynamodb + - layer/data + - layer/gitops + - provider/aws +--- + # Component: `dynamodb` This component is responsible for provisioning a DynamoDB table. diff --git a/modules/ec2-client-vpn/README.md b/modules/ec2-client-vpn/README.md index 04b6de797..c4ac715b9 100644 --- a/modules/ec2-client-vpn/README.md +++ b/modules/ec2-client-vpn/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/ec2-client-vpn + - layer/network + - provider/aws +--- + # Component: `ec2-client-vpn` This component is responsible for provisioning VPN Client Endpoints. diff --git a/modules/ec2-instance/README.md b/modules/ec2-instance/README.md index 6959a329f..26502b724 100644 --- a/modules/ec2-instance/README.md +++ b/modules/ec2-instance/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/ec2-instance + - layer/addons + - provider/aws +--- + # Component: `ec2-instance` This component is responsible for provisioning a single EC2 instance. diff --git a/modules/ecr/README.md b/modules/ecr/README.md index e31b4a8ff..78d9ae4e4 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/ecr + - layer/baseline + - provider/aws +--- + # Component: `ecr` This component is responsible for provisioning repositories, lifecycle rules, and permissions for streamlined ECR usage. diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 0df29a8a9..eb65e229e 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/ecs-service + - layer/ecs + - provider/aws +--- + # Component: `ecs-service` This component is responsible for creating an ECS service. diff --git a/modules/ecs/README.md b/modules/ecs/README.md index aeb446def..751d7bb83 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/ecs + - layer/ecs + - provider/aws +--- + # Component: `ecs` This component is responsible for provisioning an ECS Cluster and associated load balancer. diff --git a/modules/efs/README.md b/modules/efs/README.md index 34981e144..72d289bb6 100644 --- a/modules/efs/README.md +++ b/modules/efs/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/efs + - layer/data + - layer/eks + - provider/aws +--- + # Component: `efs` This component is responsible for provisioning an [EFS](https://aws.amazon.com/efs/) Network File System with KMS diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 29543a965..31a64319f 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -1,4 +1,12 @@ -# Component: `actions-runner-controller` +--- +tags: + - component/eks/actions-runner-controller + - layer/github + - provider/aws + - provider/helm +--- + +# Component: `eks/actions-runner-controller` This component creates a Helm release for [actions-runner-controller](https://github.com/actions-runner-controller/actions-runner-controller) on an EKS cluster. diff --git a/modules/eks/alb-controller-ingress-class/README.md b/modules/eks/alb-controller-ingress-class/README.md index cb821739e..d7856b6d2 100644 --- a/modules/eks/alb-controller-ingress-class/README.md +++ b/modules/eks/alb-controller-ingress-class/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/alb-controller-ingress-class + - layer/eks + - provider/aws + - provider/helm +--- + # Component: `eks/alb-controller-ingress-class` This component deploys a Kubernetes `IngressClass` resource for the AWS Load Balancer Controller. This is not often diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index 80066889c..cee06ff06 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -1,4 +1,12 @@ -# Component `eks/alb-controller-ingress-group` +--- +tags: + - component/eks/alb-controller-ingress-group + - layer/eks + - provider/aws + - provider/helm +--- + +# Component: `eks/alb-controller-ingress-group` This component provisions a Kubernetes Service that creates an ALB for a specific [IngressGroup]. diff --git a/modules/eks/alb-controller/README.md b/modules/eks/alb-controller/README.md index 6887162d5..ccf4e616b 100644 --- a/modules/eks/alb-controller/README.md +++ b/modules/eks/alb-controller/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/alb-controller + - layer/eks + - provider/aws + - provider/helm +--- + # Component: `eks/alb-controller` This component creates a Helm release for diff --git a/modules/eks/argocd/README.md b/modules/eks/argocd/README.md index 6013062b3..26b28cbd0 100644 --- a/modules/eks/argocd/README.md +++ b/modules/eks/argocd/README.md @@ -1,4 +1,12 @@ -# Component: `argocd` +--- +tags: + - component/eks/argocd + - layer/software-delivery + - provider/aws + - provider/helm +--- + +# Component: `eks/argocd` This component is responsible for provisioning [Argo CD](https://argoproj.github.io/cd/). diff --git a/modules/eks/aws-node-termination-handler/README.md b/modules/eks/aws-node-termination-handler/README.md index 11acd350a..d6505fb97 100644 --- a/modules/eks/aws-node-termination-handler/README.md +++ b/modules/eks/aws-node-termination-handler/README.md @@ -1,4 +1,12 @@ -# Component: `aws-node-termination-handler` +--- +tags: + - component/eks/aws-node-termination-handler + - layer/eks + - provider/aws + - provider/helm +--- + +# Component: `eks/aws-node-termination-handler` This component creates a Helm release for [aws-node-termination-handler](https://github.com/aws/aws-node-termination-handler) on a Kubernetes cluster. diff --git a/modules/eks/cert-manager/README.md b/modules/eks/cert-manager/README.md index 0fab762d9..6d1bf2f87 100644 --- a/modules/eks/cert-manager/README.md +++ b/modules/eks/cert-manager/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/cert-manager + - layer/eks + - provider/aws + - provider/helm +--- + # Component: `eks/cert-manager` This component creates a Helm release for [cert-manager](https://github.com/jetstack/cert-manager) on a Kubernetes diff --git a/modules/eks/cluster/README.md b/modules/eks/cluster/README.md index dcf8c48e0..b4c723e2d 100644 --- a/modules/eks/cluster/README.md +++ b/modules/eks/cluster/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/eks/cluster + - layer/eks + - provider/aws +--- + # Component: `eks/cluster` This component is responsible for provisioning an end-to-end EKS Cluster, including managed node groups and Fargate diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index 58791fa45..23a4d2419 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -1,4 +1,13 @@ -# Component: `datadog-agent` +--- +tags: + - component/eks/datadog-agent + - layer/datadog + - provider/aws + - provider/helm + - provider/datadog +--- + +# Component: `eks/datadog-agent` This component installs the `datadog-agent` for EKS clusters. diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index 15867e75d..8ad731f57 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/echo-server + - layer/eks + - provider/aws + - provider/echo-server +--- + # Component: `eks/echo-server` This is copied from diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index 0949dfc99..77433eee3 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/external-dns + - layer/eks + - provider/aws + - provider/helm +--- + # Component: `eks/external-dns` This component creates a Helm deployment for [external-dns](https://github.com/bitnami/bitnami-docker-external-dns) on a diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index ad75b70aa..a2c82edd4 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -1,4 +1,12 @@ -# Component: `external-secrets-operator` +--- +tags: + - component/eks/external-secrets-operator + - layer/eks + - provider/aws + - provider/helm +--- + +# Component: `eks/external-secrets-operator` This component (ESO) is used to create an external `SecretStore` configured to synchronize secrets from AWS SSM Parameter store as Kubernetes Secrets within the cluster. Per the operator pattern, the `external-secret-operator` pods diff --git a/modules/eks/github-actions-runner/README.md b/modules/eks/github-actions-runner/README.md index ab149c1e8..0c511f62d 100644 --- a/modules/eks/github-actions-runner/README.md +++ b/modules/eks/github-actions-runner/README.md @@ -1,4 +1,12 @@ -# Component: `github-actions-runner` +--- +tags: + - component/eks/github-actions-runner + - layer/github + - provider/aws + - provider/helm +--- + +# Component: `eks/github-actions-runner` This component deploys self-hosted GitHub Actions Runners and a [Controller](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/quickstart-for-actions-runner-controller#introduction) diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index a5cf79006..6eff24902 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/idp-roles + - layer/eks + - provider/aws + - provider/helm +--- + # Component: `eks/idp-roles` This component installs the `idp-roles` for EKS clusters. These identity provider roles specify severl pre-determined diff --git a/modules/eks/karpenter-node-pool/README.md b/modules/eks/karpenter-node-pool/README.md index 449fb589d..8bfefb308 100644 --- a/modules/eks/karpenter-node-pool/README.md +++ b/modules/eks/karpenter-node-pool/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/karpenter-node-pool + - layer/eks + - provider/aws + - provider/helm +--- + # Component: `eks/karpenter-node-pool` This component deploys [Karpenter NodePools](https://karpenter.sh/docs/concepts/nodepools/) to an EKS cluster. diff --git a/modules/eks/karpenter/README.md b/modules/eks/karpenter/README.md index f13cbcfaa..4234e3cff 100644 --- a/modules/eks/karpenter/README.md +++ b/modules/eks/karpenter/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/karpenter + - layer/eks + - provider/aws + - provider/helm +--- + # Component: `eks/karpenter` This component provisions [Karpenter](https://karpenter.sh) on an EKS cluster. It requires at least version 0.32.0 of diff --git a/modules/eks/keda/README.md b/modules/eks/keda/README.md index 1546853ca..eb5b207ad 100644 --- a/modules/eks/keda/README.md +++ b/modules/eks/keda/README.md @@ -1,4 +1,12 @@ -# Component: `keda` +--- +tags: + - component/eks/keda + - layer/eks + - provider/aws + - provider/helm +--- + +# Component: `eks/keda` This component is used to install the KEDA operator. diff --git a/modules/eks/loki/README.md b/modules/eks/loki/README.md index 60f7fef1d..3b96994cf 100644 --- a/modules/eks/loki/README.md +++ b/modules/eks/loki/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/loki + - layer/grafana + - provider/aws + - provider/helm +--- + # Component: `eks/loki` Grafana Loki is a set of resources that can be combined into a fully featured logging stack. Unlike other logging diff --git a/modules/eks/metrics-server/README.md b/modules/eks/metrics-server/README.md index 90c4d4f31..743edc51e 100644 --- a/modules/eks/metrics-server/README.md +++ b/modules/eks/metrics-server/README.md @@ -1,4 +1,12 @@ -# Component: `metrics-server` +--- +tags: + - component/eks/metrics-server + - layer/eks + - provider/aws + - provider/helm +--- + +# Component: `eks/metrics-server` This component creates a Helm release for [metrics-server](https://github.com/kubernetes-sigs/metrics-server) is a Kubernetes addon that provides resource usage metrics used in particular by other addons such Horizontal Pod Autoscaler. @@ -54,7 +62,6 @@ components: | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 4.9.0 | -| [kubernetes](#provider\_kubernetes) | >= 2.14.0, != 2.21.0 | ## Modules diff --git a/modules/eks/prometheus-scraper/README.md b/modules/eks/prometheus-scraper/README.md index 20c7ce7b8..fc6754aa8 100644 --- a/modules/eks/prometheus-scraper/README.md +++ b/modules/eks/prometheus-scraper/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/prometheus-scraper + - layer/grafana + - provider/aws + - provider/helm +--- + # Component: `eks/prometheus-scraper` This component provisions the an Amazon Managed collector or scraper to connect Amazon Managed Prometheus (AMP) with an diff --git a/modules/eks/promtail/README.md b/modules/eks/promtail/README.md index 5ecafa3b4..ecefac8bd 100644 --- a/modules/eks/promtail/README.md +++ b/modules/eks/promtail/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/promtail + - layer/grafana + - provider/aws + - provider/helm +--- + # Component: `eks/promtail` Promtail is an agent which ships the contents of local logs to a Loki instance. diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index a2e51ed93..0504d982b 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -1,3 +1,12 @@ +--- +tags: + - component/eks/redis-operator + - layer/eks + - layer/data + - provider/aws + - provider/helm +--- + # Component: `eks/redis-operator` This component installs `redis-operator` for EKS clusters. Redis Operator creates/configures/manages high availability diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index d488ba944..6bf7feac3 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/redis + - layer/data + - provider/aws + - provider/helm +--- + # Component: `eks/redis` This component installs `redis` for EKS clusters. This is a Self Hosted Redis Cluster installed on EKS. diff --git a/modules/eks/reloader/README.md b/modules/eks/reloader/README.md index 3119f1f3b..9e720e55f 100644 --- a/modules/eks/reloader/README.md +++ b/modules/eks/reloader/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/eks/reloader + - layer/eks + - provider/aws + - provider/helm +--- + # Component: `eks/reloader` This component installs the [Stakater Reloader](https://github.com/stakater/Reloader) for EKS clusters. `reloader` can diff --git a/modules/eks/storage-class/README.md b/modules/eks/storage-class/README.md index 4ada75221..a9c64d06e 100644 --- a/modules/eks/storage-class/README.md +++ b/modules/eks/storage-class/README.md @@ -1,3 +1,12 @@ +--- +tags: + - component/eks + - layer/eks + - layer/data + - provider/aws + - provider/helm +--- + # Component: `eks/storage-class` This component is responsible for provisioning `StorageClasses` in an EKS cluster. See the list of guides and references diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md index 0088fa0e8..eaec1c2ae 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/elasticache-redis + - layer/data + - provider/aws +--- + # Component: `elasticache-redis` This component is responsible for provisioning [ElastiCache Redis](https://aws.amazon.com/elasticache/redis/) clusters. diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index 0458a7433..710e244eb 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/elasticsearch + - layer/data + - provider/aws +--- + # Component: `elasticsearch` This component is responsible for provisioning an Elasticsearch cluster with built-in integrations with Kibana and diff --git a/modules/eventbridge/README.md b/modules/eventbridge/README.md index bbd1c0a95..a406e4d7e 100644 --- a/modules/eventbridge/README.md +++ b/modules/eventbridge/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/eventbridge + - layer/unassigned + - provider/aws +--- + # Component: `eventbridge` The `eventbridge` component is a Terraform module that defines a CloudWatch EventBridge rule. The rule is pointed at diff --git a/modules/github-action-token-rotator/README.md b/modules/github-action-token-rotator/README.md index cdff9ec74..dd566a83c 100644 --- a/modules/github-action-token-rotator/README.md +++ b/modules/github-action-token-rotator/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/github-action-token-rotator + - layer/github + - provider/aws +--- + # Component: `github-action-token-rotator` This component is responsible for provisioning diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index 59c17f515..e2d38fa7a 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/github-oidc-provider + - layer/github + - provider/aws + - privileged +--- + # Component: `github-oidc-provider` This component is responsible for authorizing the GitHub OIDC provider as an Identity provider for an AWS account. It is diff --git a/modules/github-oidc-role/README.md b/modules/github-oidc-role/README.md index 4c003e77d..e4bce3939 100644 --- a/modules/github-oidc-role/README.md +++ b/modules/github-oidc-role/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/github-oidc-role + - layer/github + - provider/aws + - privileged +--- + # Component: `github-oidc-role` This component is responsible for creating IAM roles for GitHub Actions to assume. diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index e36a78b29..0cadce033 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/github-runners + - layer/github + - provider/aws +--- + # Component: `github-runners` This component is responsible for provisioning EC2 instances for GitHub runners. diff --git a/modules/github-webhook/README.md b/modules/github-webhook/README.md index 74b5e13f1..578ae47e9 100644 --- a/modules/github-webhook/README.md +++ b/modules/github-webhook/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/github-webhook + - layer/software-delivery + - provider/aws +--- + # Component: `github-webhook` This component provisions a GitHub webhook for a single GitHub repository. diff --git a/modules/global-accelerator-endpoint-group/README.md b/modules/global-accelerator-endpoint-group/README.md index fcbedd063..8e434bfcd 100644 --- a/modules/global-accelerator-endpoint-group/README.md +++ b/modules/global-accelerator-endpoint-group/README.md @@ -1,4 +1,11 @@ -# Component: `global-accelerator` +--- +tags: + - component/global-accelerator-endpoint-group + - layer/unassigned + - provider/aws +--- + +# Component: `global-accelerator-endpoint-group` This component is responsible for provisioning a Global Accelerator Endpoint Group. diff --git a/modules/global-accelerator/README.md b/modules/global-accelerator/README.md index f76093e40..7fd22f2f7 100644 --- a/modules/global-accelerator/README.md +++ b/modules/global-accelerator/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/global-accelerator + - layer/unassigned + - provider/aws +--- + # Component: `global-accelerator` This component is responsible for provisioning AWS Global Accelerator and its listeners. diff --git a/modules/glue/catalog-database/README.md b/modules/glue/catalog-database/README.md index 9ed139442..1d5230a51 100644 --- a/modules/glue/catalog-database/README.md +++ b/modules/glue/catalog-database/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/catalog-database + - layer/unassigned + - provider/aws +--- + # Component: `glue/catalog-database` This component provisions Glue catalog databases. diff --git a/modules/glue/catalog-table/README.md b/modules/glue/catalog-table/README.md index 2dbff5cf5..8c7407714 100644 --- a/modules/glue/catalog-table/README.md +++ b/modules/glue/catalog-table/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/catalog-table + - layer/unassigned + - provider/aws +--- + # Component: `glue/catalog-table` This component provisions Glue catalog tables. diff --git a/modules/glue/connection/README.md b/modules/glue/connection/README.md index 082197fd3..d6015a422 100644 --- a/modules/glue/connection/README.md +++ b/modules/glue/connection/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/connection + - layer/unassigned + - provider/aws +--- + # Component: `glue/connection` This component provisions Glue connections. diff --git a/modules/glue/crawler/README.md b/modules/glue/crawler/README.md index 9395b5eb1..a06fd003c 100644 --- a/modules/glue/crawler/README.md +++ b/modules/glue/crawler/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/crawler + - layer/unassigned + - provider/aws +--- + # Component: `glue/crawler` This component provisions Glue crawlers. diff --git a/modules/glue/iam/README.md b/modules/glue/iam/README.md index 6de843fc5..ce4020405 100644 --- a/modules/glue/iam/README.md +++ b/modules/glue/iam/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/iam + - layer/unassigned + - provider/aws +--- + # Component: `glue/iam` This component provisions IAM roles for AWS Glue. diff --git a/modules/glue/job/README.md b/modules/glue/job/README.md index edfe7f946..5d0a2081c 100644 --- a/modules/glue/job/README.md +++ b/modules/glue/job/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/job + - layer/unassigned + - provider/aws +--- + # Component: `glue/job` This component provisions Glue jobs. diff --git a/modules/glue/registry/README.md b/modules/glue/registry/README.md index 0fa49a243..0ad49a19d 100644 --- a/modules/glue/registry/README.md +++ b/modules/glue/registry/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/registry + - layer/unassigned + - provider/aws +--- + # Component: `glue/registry` This component provisions Glue registries. diff --git a/modules/glue/schema/README.md b/modules/glue/schema/README.md index 82a58c1fe..d0bdb857e 100644 --- a/modules/glue/schema/README.md +++ b/modules/glue/schema/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/schema + - layer/unassigned + - provider/aws +--- + # Component: `glue/schema` This component provisions Glue schemas. diff --git a/modules/glue/trigger/README.md b/modules/glue/trigger/README.md index e692e2aa5..c9ba1b6ee 100644 --- a/modules/glue/trigger/README.md +++ b/modules/glue/trigger/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/trigger + - layer/unassigned + - provider/aws +--- + # Component: `glue/trigger` This component provisions Glue triggers. diff --git a/modules/glue/workflow/README.md b/modules/glue/workflow/README.md index d6adadd7a..576ed5c3b 100644 --- a/modules/glue/workflow/README.md +++ b/modules/glue/workflow/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/glue/workflow + - layer/unassigned + - provider/aws +--- + # Component: `glue/workflow` This component provisions Glue workflows. diff --git a/modules/guardduty/README.md b/modules/guardduty/README.md index 199691f33..ce4e3163d 100644 --- a/modules/guardduty/README.md +++ b/modules/guardduty/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/guardduty + - layer/security-and-compliance + - provider/aws +--- + # Component: `guardduty` This component is responsible for configuring GuardDuty within an AWS Organization. diff --git a/modules/iam-role/README.md b/modules/iam-role/README.md index 9976affcf..ea332bd17 100644 --- a/modules/iam-role/README.md +++ b/modules/iam-role/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/iam-role + - layer/addons + - provider/aws +--- + # Component: `iam-role` This component is responsible for provisioning simple IAM roles. If a more complicated IAM role and policy are desired diff --git a/modules/iam-service-linked-roles/README.md b/modules/iam-service-linked-roles/README.md index b36f0f4f0..5d7f38029 100644 --- a/modules/iam-service-linked-roles/README.md +++ b/modules/iam-service-linked-roles/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/iam-service-linked-roles + - layer/eks + - provider/aws +--- + # Component: `iam-service-linked-roles` This component is responsible for provisioning diff --git a/modules/ipam/README.md b/modules/ipam/README.md index a9b590df5..b185706df 100644 --- a/modules/ipam/README.md +++ b/modules/ipam/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/ipam + - layer/unassigned + - provider/aws +--- + # Component: `ipam` This component is responsible for provisioning IPAM per region in a centralized account. diff --git a/modules/kinesis-stream/README.md b/modules/kinesis-stream/README.md index 495403606..98d5758b4 100644 --- a/modules/kinesis-stream/README.md +++ b/modules/kinesis-stream/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/kinesis-stream + - layer/addons + - provider/aws +--- + # Component: `kinesis-stream` This component is responsible for provisioning an Amazon Kinesis data stream. diff --git a/modules/kms/README.md b/modules/kms/README.md index 4be480599..0d754d9fe 100644 --- a/modules/kms/README.md +++ b/modules/kms/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/kms + - layer/addons + - provider/aws +--- + # Component: `kms` This component is responsible for provisioning a KMS Key. diff --git a/modules/lakeformation/README.md b/modules/lakeformation/README.md index 2c43b8d5a..83807cb20 100644 --- a/modules/lakeformation/README.md +++ b/modules/lakeformation/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/lakeformation + - layer/unassigned + - provider/aws +--- + # Component: `lakeformation` This component is responsible for provisioning Amazon Lake Formation resources. diff --git a/modules/lambda/README.md b/modules/lambda/README.md index 34eaf24cf..c8adb2505 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -1,4 +1,11 @@ -# Component: `lambda` +--- +tags: + - component/sso-saml-provider + - layer/software-delivery + - provider/aws +--- + +# Component: `sso-saml-provider` This component is responsible for provisioning Lambda functions. diff --git a/modules/macie/README.md b/modules/macie/README.md index e5ab09ff9..497e14d7e 100644 --- a/modules/macie/README.md +++ b/modules/macie/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/macie + - layer/security-and-compliance + - provider/aws +--- + # Component: `macie` This component is responsible for configuring Macie within an AWS Organization. diff --git a/modules/managed-grafana/api-key/README.md b/modules/managed-grafana/api-key/README.md index fbbad996c..612ddcaac 100644 --- a/modules/managed-grafana/api-key/README.md +++ b/modules/managed-grafana/api-key/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/managed-grafana/api-key + - layer/grafana + - provider/aws + - provider/grafana +--- + # Component: `managed-grafana/api-key` This component is responsible for provisioning an API Key for an Amazon Managed Grafana workspace. diff --git a/modules/managed-grafana/dashboard/README.md b/modules/managed-grafana/dashboard/README.md index 834b81f35..170b5941a 100644 --- a/modules/managed-grafana/dashboard/README.md +++ b/modules/managed-grafana/dashboard/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/managed-grafana/dashboard + - layer/grafana + - provider/aws + - provider/grafana +--- + # Component: `managed-grafana/dashboard` This component is responsible for provisioning a dashboard an Amazon Managed Grafana workspace. diff --git a/modules/managed-grafana/data-source/loki/README.md b/modules/managed-grafana/data-source/loki/README.md index 52816afe6..248b9239d 100644 --- a/modules/managed-grafana/data-source/loki/README.md +++ b/modules/managed-grafana/data-source/loki/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/managed-grafana/data-source/loki + - layer/grafana + - provider/aws + - provider/grafana +--- + # Component: `managed-grafana/data-source/loki` This component is responsible for provisioning a Loki data source for an Amazon Managed Grafana workspace. diff --git a/modules/managed-grafana/data-source/managed-prometheus/README.md b/modules/managed-grafana/data-source/managed-prometheus/README.md index f261ef614..2f3ae9bd9 100644 --- a/modules/managed-grafana/data-source/managed-prometheus/README.md +++ b/modules/managed-grafana/data-source/managed-prometheus/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/managed-grafana/data-source/managed-prometheus + - layer/grafana + - provider/aws + - provider/grafana +--- + # Component: `managed-grafana/data-source/managed-prometheus` This component is responsible for provisioning an Amazon Managed Prometheus data source for an Amazon Managed Grafana diff --git a/modules/managed-grafana/workspace/README.md b/modules/managed-grafana/workspace/README.md index 27e92e1c0..3c2a31de1 100644 --- a/modules/managed-grafana/workspace/README.md +++ b/modules/managed-grafana/workspace/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/managed-grafana/workspace + - layer/grafana + - provider/aws + - provider/grafana +--- + # Component: `managed-grafana/workspace` This component is responsible for provisioning an Amazon Managed Grafana workspace. @@ -36,7 +44,7 @@ components: stage: dev ``` -> [!NOTE] +> [!NOTE] > > We would prefer to have a custom URL for the provisioned Grafana workspace, but at the moment it's not supported > natively and implementation would be non-trivial. We will continue to monitor that Issue and consider alternatives, diff --git a/modules/managed-prometheus/workspace/README.md b/modules/managed-prometheus/workspace/README.md index 9f270b9ae..44f391d12 100644 --- a/modules/managed-prometheus/workspace/README.md +++ b/modules/managed-prometheus/workspace/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/managed-prometheus/workspace + - layer/grafana + - provider/aws +--- + # Component: `managed-prometheus/workspace` This component is responsible for provisioning a workspace for Amazon Managed Service for Prometheus, also known as diff --git a/modules/mq-broker/README.md b/modules/mq-broker/README.md index bd763ca48..56466728b 100644 --- a/modules/mq-broker/README.md +++ b/modules/mq-broker/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/mq-broker + - layer/unassigned + - provider/aws +--- + # Component: `mq-broker` This component is responsible for provisioning an AmazonMQ broker and corresponding security group. diff --git a/modules/msk/README.md b/modules/msk/README.md index 6c4d8424a..e18148bd9 100644 --- a/modules/msk/README.md +++ b/modules/msk/README.md @@ -1,4 +1,11 @@ -# Component: `msk/cluster` +--- +tags: + - component/msk + - layer/unassigned + - provider/aws +--- + +# Component: `msk` This component is responsible for provisioning [Amazon Managed Streaming](https://aws.amazon.com/msk/) clusters for [Apache Kafka](https://aws.amazon.com/msk/what-is-kafka/). diff --git a/modules/mwaa/README.md b/modules/mwaa/README.md index e8c816d16..2c3d90e8d 100644 --- a/modules/mwaa/README.md +++ b/modules/mwaa/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/mwaa + - layer/unassigned + - provider/aws +--- + # Component: `mwaa` This component provisions Amazon managed workflows for Apache Airflow. diff --git a/modules/network-firewall/README.md b/modules/network-firewall/README.md index 4d2b122a8..b95a0e63e 100644 --- a/modules/network-firewall/README.md +++ b/modules/network-firewall/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/network-firewall + - layer/unassigned + - provider/aws +--- + # Component: `network-firewall` This component is responsible for provisioning [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, diff --git a/modules/opsgenie-team/README.md b/modules/opsgenie-team/README.md index 4b3c38e96..74bd1dc15 100644 --- a/modules/opsgenie-team/README.md +++ b/modules/opsgenie-team/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/opsgenie-team + - layer/unassigned + - provider/aws +--- + # Component: `opsgenie-team` This component is responsible for provisioning Opsgenie teams and related services, rules, schedules. @@ -389,8 +396,6 @@ Track the issue: https://github.com/opsgenie/terraform-provider-opsgenie/issues/ [See OpsGenie in the Reference Architecture](https://docs.cloudposse.com/layers/alerting/opsgenie/) - - ## References - [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/opsgenie-team) - diff --git a/modules/philips-labs-github-runners/README.md b/modules/philips-labs-github-runners/README.md index a1c4e7bce..9a13c7806 100644 --- a/modules/philips-labs-github-runners/README.md +++ b/modules/philips-labs-github-runners/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/philips-labs-github-runners + - layer/github + - provider/aws +--- + # Component: `philips-labs-github-runners` This component is responsible for provisioning the surrounding infrastructure for the github runners. diff --git a/modules/rds/README.md b/modules/rds/README.md index 69341a92b..841a441c4 100644 --- a/modules/rds/README.md +++ b/modules/rds/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/rds + - layer/data + - provider/aws +--- + # Component: `rds` This component is responsible for provisioning an RDS instance. It seeds relevant database information (hostnames, diff --git a/modules/redshift/README.md b/modules/redshift/README.md index 52d0fd6a0..7e5445b11 100644 --- a/modules/redshift/README.md +++ b/modules/redshift/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/redshift + - layer/data + - provider/aws +--- + # Component: `redshift` This component is responsible for provisioning a RedShift instance. It seeds relevant database information (hostnames, diff --git a/modules/route53-resolver-dns-firewall/README.md b/modules/route53-resolver-dns-firewall/README.md index 63519378f..8b8ba8a58 100644 --- a/modules/route53-resolver-dns-firewall/README.md +++ b/modules/route53-resolver-dns-firewall/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/route53-resolver-dns-firewall + - layer/unassigned + - provider/aws +--- + # Component: `route53-resolver-dns-firewall` This component is responsible for provisioning diff --git a/modules/s3-bucket/README.md b/modules/s3-bucket/README.md index 7e35bf2ed..218d3b4a2 100644 --- a/modules/s3-bucket/README.md +++ b/modules/s3-bucket/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/s3-bucket + - layer/addons + - layer/gitops + - provider/aws +--- + # Component: `s3-bucket` This component is responsible for provisioning S3 buckets. diff --git a/modules/security-hub/README.md b/modules/security-hub/README.md index 43bf853ca..e962f3118 100644 --- a/modules/security-hub/README.md +++ b/modules/security-hub/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/security-hub + - layer/security-and-compliance + - provider/aws +--- + # Component: `security-hub` This component is responsible for configuring Security Hub within an AWS Organization. diff --git a/modules/ses/README.md b/modules/ses/README.md index 5d99a0c07..0bbfdf52b 100644 --- a/modules/ses/README.md +++ b/modules/ses/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/ses + - layer/addons + - provider/aws +--- + # Component: `ses` This component is responsible for provisioning SES to act as an SMTP gateway. The credentials used for sending email can diff --git a/modules/sftp/README.md b/modules/sftp/README.md index 7aad36556..460ba8f2c 100644 --- a/modules/sftp/README.md +++ b/modules/sftp/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/sftp + - layer/unassigned + - provider/aws +--- + # Component: `sftp` This component is responsible for provisioning SFTP Endpoints. diff --git a/modules/snowflake-account/README.md b/modules/snowflake-account/README.md index 3ad2093f1..e290c5231 100644 --- a/modules/snowflake-account/README.md +++ b/modules/snowflake-account/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/snowflake-account + - layer/unassigned + - provider/aws + - provider/snowflake +--- + # Component: `snowflake-account` This component sets up the requirements for all other Snowflake components, including creating the Terraform service diff --git a/modules/snowflake-database/README.md b/modules/snowflake-database/README.md index 70e340027..a96776606 100644 --- a/modules/snowflake-database/README.md +++ b/modules/snowflake-database/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/snowflake-database + - layer/unassigned + - provider/aws + - provider/snowflake +--- + # Component: `snowflake-database` All data in Snowflake is stored in database tables, logically structured as collections of columns and rows. This diff --git a/modules/sns-topic/README.md b/modules/sns-topic/README.md index 1c5eee12d..97fdb4665 100644 --- a/modules/sns-topic/README.md +++ b/modules/sns-topic/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/sns-topic + - layer/addons + - provider/aws +--- + # Component: `sns-topic` This component is responsible for provisioning an SNS topic. diff --git a/modules/spa-s3-cloudfront/README.md b/modules/spa-s3-cloudfront/README.md index 2a8f8e034..173f9a2b7 100644 --- a/modules/spa-s3-cloudfront/README.md +++ b/modules/spa-s3-cloudfront/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/spa-s3-cloudfront + - layer/addons + - provider/aws +--- + # Component: `spa-s3-cloudfront` This component is responsible for provisioning: diff --git a/modules/spacelift/README.md b/modules/spacelift/README.md index 864cdbbb7..1dedcf8ed 100644 --- a/modules/spacelift/README.md +++ b/modules/spacelift/README.md @@ -1,4 +1,11 @@ -# Spacelift +--- +tags: + - layer/spacelift + - provider/aws + - provider/spacelift +--- + +# Component: `spacelift` These components are responsible for setting up Spacelift and include three components: `spacelift/admin-stack`, `spacelift/spaces`, and `spacelift/worker-pool`. diff --git a/modules/spacelift/admin-stack/README.md b/modules/spacelift/admin-stack/README.md index ce23def69..e48e910b3 100644 --- a/modules/spacelift/admin-stack/README.md +++ b/modules/spacelift/admin-stack/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/spacelift/admin-stack + - layer/spacelift + - provider/aws + - provider/spacelift +--- + # Component: `spacelift/admin-stack` This component is responsible for creating an administrative [stack](https://docs.spacelift.io/concepts/stack/) and its diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index 17e110d86..37f43cb73 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/spacelift/spaces + - layer/spacelift + - provider/aws + - provider/spacelift +--- + # Component: `spacelift/spaces` This component is responsible for creating and managing the [spaces](https://docs.spacelift.io/concepts/spaces/) in the diff --git a/modules/spacelift/worker-pool/README.md b/modules/spacelift/worker-pool/README.md index bf2ad4acc..073e41011 100644 --- a/modules/spacelift/worker-pool/README.md +++ b/modules/spacelift/worker-pool/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/spacelift/worker-pool + - layer/spacelift + - provider/aws + - provider/spacelift +--- + # Component: `spacelift/worker-pool` This component is responsible for provisioning Spacelift worker pools. diff --git a/modules/sqs-queue/README.md b/modules/sqs-queue/README.md index 4c0f1b786..ee5b7a583 100644 --- a/modules/sqs-queue/README.md +++ b/modules/sqs-queue/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/sqs-queue + - layer/addons + - provider/aws +--- + # Component: `sqs-queue` This component is responsible for creating an SQS queue. diff --git a/modules/ssm-parameters/README.md b/modules/ssm-parameters/README.md index 911755472..3c5c374bf 100644 --- a/modules/ssm-parameters/README.md +++ b/modules/ssm-parameters/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/ssm-parameters + - layer/addons + - provider/aws +--- + # Component: `ssm-parameters` This component is responsible for provisioning Parameter Store resources against AWS SSM. It supports normal parameter diff --git a/modules/sso-saml-provider/README.md b/modules/sso-saml-provider/README.md index 008892277..cf3b076a7 100644 --- a/modules/sso-saml-provider/README.md +++ b/modules/sso-saml-provider/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/sso-saml-provider + - layer/software-delivery + - provider/aws +--- + # Component: `sso-saml-provider` This component reads sso credentials from SSM Parameter store and provides them as outputs diff --git a/modules/strongdm/README.md b/modules/strongdm/README.md index 20aa20f0d..91f0a6941 100644 --- a/modules/strongdm/README.md +++ b/modules/strongdm/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/strongdm + - layer/unassigned + - provider/aws +--- + # Component: `strongdm` This component provisions [strongDM](https://www.strongdm.com/) gateway, relay and roles diff --git a/modules/tfstate-backend/README.md b/modules/tfstate-backend/README.md index 870561472..cdd0e2a5a 100644 --- a/modules/tfstate-backend/README.md +++ b/modules/tfstate-backend/README.md @@ -1,3 +1,11 @@ +--- +tags: + - component/tfstate-backend + - layer/foundation + - provider/aws + - privileged +--- + # Component: `tfstate-backend` This component is responsible for provisioning an S3 Bucket and DynamoDB table that follow security best practices for @@ -12,12 +20,19 @@ security configuration information, so careful planning is required when archite ## Prerequisites +> [!TIP] +> +> Part of cold start, so it has to initially be run with `SuperAdmin`, multiple times: to create the S3 bucket and then +> to move the state into it. Follow the guide +> **[here](https://docs.cloudposse.com/layers/accounts/tutorials/manual-configuration/#provision-tfstate-backend-component)** +> to get started. + - This component assumes you are using the `aws-teams` and `aws-team-roles` components. -- Before the `account` and `account-map` components are deployed for the first time, you'll want to run this component with `access_roles_enabled` set to `false` to - prevent errors due to missing IAM Role ARNs. - This will enable only enough access to the Terraform state for you to finish provisioning accounts and roles. - After those components have been deployed, you will want to - run this component again with `access_roles_enabled` set to `true` to provide the complete access as configured in the stacks. +- Before the `account` and `account-map` components are deployed for the first time, you'll want to run this component + with `access_roles_enabled` set to `false` to prevent errors due to missing IAM Role ARNs. This will enable only + enough access to the Terraform state for you to finish provisioning accounts and roles. After those components have + been deployed, you will want to run this component again with `access_roles_enabled` set to `true` to provide the + complete access as configured in the stacks. ### Access Control diff --git a/modules/tgw/README.md b/modules/tgw/README.md index 8d191c74d..386a1f4ed 100644 --- a/modules/tgw/README.md +++ b/modules/tgw/README.md @@ -1,4 +1,11 @@ -# Transit Gateway: `tgw` +--- +tags: + - component/tgw + - layer/network + - provider/aws +--- + +# Component: `tgw` AWS Transit Gateway connects your Amazon Virtual Private Clouds (VPCs) and on-premises networks through a central hub. This connection simplifies your network and puts an end to complex peering relationships. Transit Gateway acts as a diff --git a/modules/tgw/cross-region-hub-connector/README.md b/modules/tgw/cross-region-hub-connector/README.md index 1de5a593a..8d2d7af97 100644 --- a/modules/tgw/cross-region-hub-connector/README.md +++ b/modules/tgw/cross-region-hub-connector/README.md @@ -1,4 +1,11 @@ -# Component: `cross-region-hub-connector` +--- +tags: + - component/tgw/cross-region-hub-connector + - layer/network + - provider/aws +--- + +# Component: `tgw/cross-region-hub-connector` This component is responsible for provisioning an [AWS Transit Gateway Peering Connection](https://aws.amazon.com/transit-gateway) to connect TGWs from different accounts diff --git a/modules/tgw/hub/README.md b/modules/tgw/hub/README.md index 1ada2debe..08a17a0fd 100644 --- a/modules/tgw/hub/README.md +++ b/modules/tgw/hub/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/tgw/hub + - layer/network + - provider/aws +--- + # Component: `tgw/hub` This component is responsible for provisioning an [AWS Transit Gateway](https://aws.amazon.com/transit-gateway) `hub` diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index acc6ce0ba..816339c22 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/tgw/spoke + - layer/network + - provider/aws +--- + # Component: `tgw/spoke` This component is responsible for provisioning [AWS Transit Gateway](https://aws.amazon.com/transit-gateway) attachments diff --git a/modules/vpc-flow-logs-bucket/README.md b/modules/vpc-flow-logs-bucket/README.md index 143e0d10c..f831f858c 100644 --- a/modules/vpc-flow-logs-bucket/README.md +++ b/modules/vpc-flow-logs-bucket/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/vpc-flow-logs-bucket + - layer/network + - provider/aws +--- + # Component: `vpc-flow-logs-bucket` This component is responsible for provisioning an encrypted S3 bucket which is configured to receive VPC Flow Logs. diff --git a/modules/vpc-peering/README.md b/modules/vpc-peering/README.md index c4a2eadfc..53c9d6f18 100644 --- a/modules/vpc-peering/README.md +++ b/modules/vpc-peering/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/vpc-peering + - layer/network + - provider/aws +--- + # Component: `vpc-peering` This component is responsible for creating a peering connection between two VPCs existing in different AWS accounts. diff --git a/modules/vpc/README.md b/modules/vpc/README.md index 2d0030d9b..cc9d93b7a 100644 --- a/modules/vpc/README.md +++ b/modules/vpc/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/vpc + - layer/network + - provider/aws +--- + # Component: `vpc` This component is responsible for provisioning a VPC and corresponding Subnets. Additionally, VPC Flow Logs can @@ -75,7 +82,7 @@ components: |------|--------|---------| | [endpoint\_security\_groups](#module\_endpoint\_security\_groups) | cloudposse/security-group/aws | 2.2.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [subnets](#module\_subnets) | cloudposse/dynamic-subnets/aws | 2.3.0 | +| [subnets](#module\_subnets) | cloudposse/dynamic-subnets/aws | 2.4.2 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | | [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | | [vpc](#module\_vpc) | cloudposse/vpc/aws | 2.1.0 | diff --git a/modules/waf/README.md b/modules/waf/README.md index 3538f19fc..9b41f4367 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -1,4 +1,11 @@ -# Component: `aws-waf-acl` +--- +tags: + - component/waf + - layer/addons + - provider/aws +--- + +# Component: `waf` This component is responsible for provisioning an AWS Web Application Firewall (WAF) with an associated managed rule group. diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index c736f0109..4cd5bf6fd 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -1,3 +1,10 @@ +--- +tags: + - component/zscaler + - layer/unassigned + - provider/aws +--- + # Component: `zscaler` This component is responsible for provisioning ZScaler Private Access Connector instances on Amazon Linux 2 AMIs. From 482be0b564f0830a506b721186251cbd8168b883 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 15 Aug 2024 08:30:51 -0700 Subject: [PATCH 471/501] make requester vpc lookup optional if vpc is passed (#1099) --- modules/vpc-peering/main.tf | 2 +- modules/vpc-peering/remote-state.tf | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/vpc-peering/main.tf b/modules/vpc-peering/main.tf index c189b773e..00bccce7c 100644 --- a/modules/vpc-peering/main.tf +++ b/modules/vpc-peering/main.tf @@ -1,7 +1,7 @@ locals { enabled = module.this.enabled - requester_vpc_id = coalesce(var.requester_vpc_id, module.requester_vpc.outputs.vpc_id) + requester_vpc_id = coalesce(var.requester_vpc_id, one(module.requester_vpc[*].outputs.vpc_id)) accepter_aws_assume_role_arn = var.accepter_stage_name != null ? module.iam_roles.terraform_role_arns[var.accepter_stage_name] : var.accepter_aws_assume_role_arn } diff --git a/modules/vpc-peering/remote-state.tf b/modules/vpc-peering/remote-state.tf index 17a9d24ec..41ed05d07 100644 --- a/modules/vpc-peering/remote-state.tf +++ b/modules/vpc-peering/remote-state.tf @@ -1,4 +1,6 @@ module "requester_vpc" { + count = var.requester_vpc_id == null ? 1 : 0 + source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" From 6bbc490127d89b803a4ed81edf7a5d5239f51a68 Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Mon, 19 Aug 2024 07:46:17 -0500 Subject: [PATCH 472/501] feat: IAM Access Analyzer Component (#1066) Co-authored-by: Andriy Knysh --- modules/access-analyzer/README.md | 195 +++++++++++++++++ modules/access-analyzer/context.tf | 279 ++++++++++++++++++++++++ modules/access-analyzer/main.tf | 37 ++++ modules/access-analyzer/outputs.tf | 19 ++ modules/access-analyzer/providers.tf | 19 ++ modules/access-analyzer/remote-state.tf | 11 + modules/access-analyzer/variables.tf | 62 ++++++ modules/access-analyzer/versions.tf | 10 + 8 files changed, 632 insertions(+) create mode 100644 modules/access-analyzer/README.md create mode 100644 modules/access-analyzer/context.tf create mode 100644 modules/access-analyzer/main.tf create mode 100644 modules/access-analyzer/outputs.tf create mode 100644 modules/access-analyzer/providers.tf create mode 100644 modules/access-analyzer/remote-state.tf create mode 100644 modules/access-analyzer/variables.tf create mode 100644 modules/access-analyzer/versions.tf diff --git a/modules/access-analyzer/README.md b/modules/access-analyzer/README.md new file mode 100644 index 000000000..86203888b --- /dev/null +++ b/modules/access-analyzer/README.md @@ -0,0 +1,195 @@ +# Component: `access-analyzer` + +This component is responsible for configuring AWS Identity and Access Management Access Analyzer within an AWS +Organization. + +IAM Access Analyzer helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM +roles, shared with an external entity. This lets you identify unintended access to your resources and data, which is a +security risk. IAM Access Analyzer identifies resources shared with external principals by using logic-based reasoning +to analyze the resource-based policies in your AWS environment. For each instance of a resource shared outside of your +account, IAM Access Analyzer generates a finding. Findings include information about the access and the external +principal granted to it. You can review findings to determine if the access is intended and safe or if the access is +unintended and a security risk. In addition to helping you identify resources shared with an external entity, you can +use IAM Access Analyzer findings to preview how your policy affects public and cross-account access to your resource +before deploying resource permissions. The findings are organized in a visual summary dashboard. The dashboard +highlights the split between public and cross-account access findings, and provides a breakdown of findings by resource +type. + +IAM Access Analyzer analyzes only policies applied to resources in the same AWS Region where it's enabled. To monitor +all resources in your AWS environment, you must create an analyzer to enable IAM Access Analyzer in each Region where +you're using supported AWS resources. + +AWS Identity and Access Management Access Analyzer provides the following capabilities: + +- IAM Access Analyzer external access analyzers help identify resources in your organization and accounts that are + shared with an external entity. + +- IAM Access Analyzer unused access analyzers help identify unused access in your organization and accounts. + +- IAM Access Analyzer validates IAM policies against policy grammar and AWS best practices. + +- IAM Access Analyzer custom policy checks help validate IAM policies against your specified security standards. + +- IAM Access Analyzer generates IAM policies based on access activity in your AWS CloudTrail logs. + +Here's a typical workflow: + +**Delegate Access Analyzer to another account**: From the Organization management (root) account, delegate +administration to a specific AWS account within your organization (usually the security account). + +**Create Access Analyzers in the Delegated Administrator Account**: Enable the Access Analyzers for external access and +unused access in the delegated administrator account. + +## Deployment Overview + +```yaml +components: + terraform: + access-analyzer/defaults: + metadata: + component: access-analyzer + type: abstract + vars: + enabled: true + global_environment: gbl + account_map_tenant: core + root_account_stage: root + delegated_administrator_account_name: core-mgt + accessanalyzer_service_principal: "access-analyzer.amazonaws.com" + accessanalyzer_organization_enabled: false + accessanalyzer_organization_unused_access_enabled: false + organizations_delegated_administrator_enabled: false +``` + +```yaml +import: + - catalog/access-analyzer/defaults + +components: + terraform: + access-analyzer/root: + metadata: + component: access-analyzer + inherits: + - access-analyzer/defaults + vars: + organizations_delegated_administrator_enabled: true +``` + +```yaml +import: + - catalog/access-analyzer/defaults + +components: + terraform: + access-analyzer/delegated-administrator: + metadata: + component: access-analyzer + inherits: + - access-analyzer/defaults + vars: + accessanalyzer_organization_enabled: true + accessanalyzer_organization_unused_access_enabled: true + unused_access_age: 30 +``` + +### Provisioning + +Delegate Access Analyzer to the security account: + +```bash +atmos terraform apply access-analyzer/root -s plat-dev-gbl-root +``` + +Provision Access Analyzers for external access and unused access in the delegated administrator (security) account in +each region: + +```bash +atmos terraform apply access-analyzer/delegated-administrator -s plat-dev-use1-mgt +``` + + + +## Requirements + +| Name | Version | +| ------------------------------------------------------------------------ | -------- | +| [terraform](#requirement_terraform) | >= 1.3.0 | +| [aws](#requirement_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +| ------------------------------------------------ | -------- | +| [aws](#provider_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +| -------------------------------------------------------------------- | -------------------------------------------------- | ------- | +| [account_map](#module_account_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam_roles](#module_iam_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| [aws_accessanalyzer_analyzer.organization](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/accessanalyzer_analyzer) | resource | +| [aws_accessanalyzer_analyzer.organization_unused_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/accessanalyzer_analyzer) | resource | +| [aws_organizations_delegated_administrator.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_delegated_administrator) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | +| [accessanalyzer_organization_enabled](#input_accessanalyzer_organization_enabled) | Flag to enable the Organization Access Analyzer | `bool` | n/a | yes | +| [accessanalyzer_organization_unused_access_enabled](#input_accessanalyzer_organization_unused_access_enabled) | Flag to enable the Organization unused access Access Analyzer | `bool` | n/a | yes | +| [accessanalyzer_service_principal](#input_accessanalyzer_service_principal) | The Access Analyzer service principal for which you want to make the member account a delegated administrator | `string` | `"access-analyzer.amazonaws.com"` | no | +| [account_map_tenant](#input_account_map_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | n/a | yes | +| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delegated_administrator_account_name](#input_delegated_administrator_account_name) | The name of the account that is the AWS Organization Delegated Administrator account | `string` | n/a | yes | +| [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global_environment](#input_global_environment) | Global environment name | `string` | `"gbl"` | no | +| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [organization_management_account_name](#input_organization_management_account_name) | The name of the AWS Organization management account | `string` | `null` | no | +| [organizations_delegated_administrator_enabled](#input_organizations_delegated_administrator_enabled) | Flag to enable the Organization delegated administrator | `bool` | n/a | yes | +| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input_region) | AWS Region | `string` | n/a | yes | +| [root_account_stage](#input_root_account_stage) | The stage name for the Organization root (management) account. This is used to lookup account IDs from account names
using the `account-map` component. | `string` | `"root"` | no | +| [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [unused_access_age](#input_unused_access_age) | The specified access age in days for which to generate findings for unused access | `number` | `30` | no | + +## Outputs + +| Name | Description | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| [aws_organizations_delegated_administrator_id](#output_aws_organizations_delegated_administrator_id) | AWS Organizations Delegated Administrator ID | +| [aws_organizations_delegated_administrator_status](#output_aws_organizations_delegated_administrator_status) | AWS Organizations Delegated Administrator status | +| [organization_accessanalyzer_id](#output_organization_accessanalyzer_id) | Organization Access Analyzer ID | +| [organization_unused_access_accessanalyzer_id](#output_organization_unused_access_accessanalyzer_id) | Organization unused access Access Analyzer ID | + + + +## References + +- https://aws.amazon.com/iam/access-analyzer/ +- https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html +- https://repost.aws/knowledge-center/iam-access-analyzer-organization +- https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/accessanalyzer_analyzer +- https://github.com/hashicorp/terraform-provider-aws/issues/19312 +- https://github.com/hashicorp/terraform-provider-aws/pull/19389 +- https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_delegated_administrator diff --git a/modules/access-analyzer/context.tf b/modules/access-analyzer/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/access-analyzer/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/access-analyzer/main.tf b/modules/access-analyzer/main.tf new file mode 100644 index 000000000..db6f89e38 --- /dev/null +++ b/modules/access-analyzer/main.tf @@ -0,0 +1,37 @@ +locals { + enabled = module.this.enabled + account_map = module.account_map.outputs.full_account_map + org_delegated_administrator_account_id = local.account_map[var.delegated_administrator_account_name] +} + +resource "aws_accessanalyzer_analyzer" "organization" { + count = local.enabled && var.accessanalyzer_organization_enabled ? 1 : 0 + + analyzer_name = format("%s-organization", module.this.id) + type = "ORGANIZATION" + + tags = module.this.tags +} + +resource "aws_accessanalyzer_analyzer" "organization_unused_access" { + count = local.enabled && var.accessanalyzer_organization_unused_access_enabled ? 1 : 0 + + analyzer_name = format("%s-organization-unused-access", module.this.id) + type = "ORGANIZATION_UNUSED_ACCESS" + + configuration { + unused_access { + unused_access_age = var.unused_access_age + } + } + + tags = module.this.tags +} + +# Delegate Access Analyzer to the administrator account (usually the security account) +resource "aws_organizations_delegated_administrator" "default" { + count = local.enabled && var.organizations_delegated_administrator_enabled ? 1 : 0 + + account_id = local.org_delegated_administrator_account_id + service_principal = var.accessanalyzer_service_principal +} diff --git a/modules/access-analyzer/outputs.tf b/modules/access-analyzer/outputs.tf new file mode 100644 index 000000000..a7e70d6c7 --- /dev/null +++ b/modules/access-analyzer/outputs.tf @@ -0,0 +1,19 @@ +output "organization_accessanalyzer_id" { + value = one(aws_accessanalyzer_analyzer.organization[*].id) + description = "Organization Access Analyzer ID" +} + +output "organization_unused_access_accessanalyzer_id" { + value = one(aws_accessanalyzer_analyzer.organization_unused_access[*].id) + description = "Organization unused access Access Analyzer ID" +} + +output "aws_organizations_delegated_administrator_id" { + value = one(aws_organizations_delegated_administrator.default[*].id) + description = "AWS Organizations Delegated Administrator ID" +} + +output "aws_organizations_delegated_administrator_status" { + value = one(aws_organizations_delegated_administrator.default[*].status) + description = "AWS Organizations Delegated Administrator status" +} diff --git a/modules/access-analyzer/providers.tf b/modules/access-analyzer/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/access-analyzer/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/access-analyzer/remote-state.tf b/modules/access-analyzer/remote-state.tf new file mode 100644 index 000000000..ba717f1ab --- /dev/null +++ b/modules/access-analyzer/remote-state.tf @@ -0,0 +1,11 @@ +module "account_map" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = "account-map" + tenant = (var.account_map_tenant != "") ? var.account_map_tenant : module.this.tenant + stage = var.root_account_stage + environment = var.global_environment + + context = module.this.context +} diff --git a/modules/access-analyzer/variables.tf b/modules/access-analyzer/variables.tf new file mode 100644 index 000000000..f6244ecd1 --- /dev/null +++ b/modules/access-analyzer/variables.tf @@ -0,0 +1,62 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "account_map_tenant" { + type = string + description = "The tenant where the `account_map` component required by remote-state is deployed" +} + +variable "delegated_administrator_account_name" { + type = string + description = "The name of the account that is the AWS Organization Delegated Administrator account" +} + +variable "global_environment" { + type = string + default = "gbl" + description = "Global environment name" +} + +variable "organization_management_account_name" { + type = string + default = null + description = "The name of the AWS Organization management account" +} + +variable "root_account_stage" { + type = string + default = "root" + description = <<-DOC + The stage name for the Organization root (management) account. This is used to lookup account IDs from account names + using the `account-map` component. + DOC +} + +variable "accessanalyzer_organization_enabled" { + type = bool + description = "Flag to enable the Organization Access Analyzer" +} + +variable "accessanalyzer_organization_unused_access_enabled" { + type = bool + description = "Flag to enable the Organization unused access Access Analyzer" +} + +variable "unused_access_age" { + type = number + description = "The specified access age in days for which to generate findings for unused access" + default = 30 +} + +variable "organizations_delegated_administrator_enabled" { + type = bool + description = "Flag to enable the Organization delegated administrator" +} + +variable "accessanalyzer_service_principal" { + type = string + description = "The Access Analyzer service principal for which you want to make the member account a delegated administrator" + default = "access-analyzer.amazonaws.com" +} diff --git a/modules/access-analyzer/versions.tf b/modules/access-analyzer/versions.tf new file mode 100644 index 000000000..b5920b7b1 --- /dev/null +++ b/modules/access-analyzer/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} From f5c5a6f5b7410efe88b0339dfb388e0627db34da Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:47:07 -0500 Subject: [PATCH 473/501] Add Scoped Rate Limits and Bot Control Machine Learning to WAF Component (#1103) --- modules/waf/README.md | 8 +++---- modules/waf/main.tf | 2 +- modules/waf/variables.tf | 49 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/modules/waf/README.md b/modules/waf/README.md index 9b41f4367..0e09e4014 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -66,7 +66,7 @@ components: | Name | Source | Version | |------|--------|---------| | [association\_resource\_components](#module\_association\_resource\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 1.3.0 | +| [aws\_waf](#module\_aws\_waf) | cloudposse/waf/aws | 1.8.0 | | [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | | [log\_destination\_components](#module\_log\_destination\_components) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | @@ -96,7 +96,7 @@ components: | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [geo\_allowlist\_statement\_rules](#input\_geo\_allowlist\_statement\_rules) | A rule statement used to identify a list of allowed countries which should not be blocked by the WAF.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | +| [geo\_allowlist\_statement\_rules](#input\_geo\_allowlist\_statement\_rules) | A rule statement used to identify a list of allowed countries which should not be blocked by the WAF.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [geo\_match\_statement\_rules](#input\_geo\_match\_statement\_rules) | A rule statement used to identify web requests based on country of origin.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
country\_codes:
A list of two-character country codes.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | | [ip\_set\_reference\_statement\_rules](#input\_ip\_set\_reference\_statement\_rules) | A rule statement used to detect web requests coming from particular IP addresses or address ranges.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The ARN of the IP Set that this statement references.
ip\_set:
Defines a new IP Set

description:
A friendly description of the IP Set
addresses:
Contains an array of strings that specifies zero or more IP addresses or blocks of IP addresses.
All addresses must be specified using Classless Inter-Domain Routing (CIDR) notation.
ip\_address\_version:
Specify `IPV4` or `IPV6`
ip\_set\_forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
position:
The position in the header to search for the IP address.
Possible values include: `FIRST`, `LAST`, or `ANY`.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | @@ -107,10 +107,10 @@ components: | [log\_destination\_component\_selectors](#input\_log\_destination\_component\_selectors) | A list of Atmos component selectors to get from the remote state and associate their names/ARNs with the WAF logs.
The components must be Amazon Kinesis Data Firehose, CloudWatch Log Group, or S3 bucket.

component:
Atmos component name
component\_output:
The component output that defines the component name or ARN

Set `tenant`, `environment` and `stage` if the components are in different OUs, regions or accounts.

Note: data firehose, log group, or bucket name must be prefixed with `aws-waf-logs-`,
e.g. `aws-waf-logs-example-firehose`, `aws-waf-logs-example-log-group`, or `aws-waf-logs-example-bucket`. |
list(object({
component = string
namespace = optional(string, null)
tenant = optional(string, null)
environment = optional(string, null)
stage = optional(string, null)
component_output = string
}))
| `[]` | no | | [log\_destination\_configs](#input\_log\_destination\_configs) | A list of resource names/ARNs to associate Amazon Kinesis Data Firehose, Cloudwatch Log log group, or S3 bucket with the WAF logs.
Note: data firehose, log group, or bucket name must be prefixed with `aws-waf-logs-`,
e.g. `aws-waf-logs-example-firehose`, `aws-waf-logs-example-log-group`, or `aws-waf-logs-example-bucket`. | `list(string)` | `[]` | no | | [logging\_filter](#input\_logging\_filter) | A configuration block that specifies which web requests are kept in the logs and which are dropped.
You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation. |
object({
default_behavior = string
filter = list(object({
behavior = string
requirement = string
condition = list(object({
action_condition = optional(object({
action = string
}), null)
label_name_condition = optional(object({
label_name = string
}), null)
}))
}))
})
| `null` | no | -| [managed\_rule\_group\_statement\_rules](#input\_managed\_rule\_group\_statement\_rules) | A rule statement used to run the rules that are defined in a managed rule group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
name:
The name of the managed rule group.
vendor\_name:
The name of the managed rule group vendor.
version:
The version of the managed rule group.
You can set `Version_1.0` or `Version_1.1` etc. If you want to use the default version, do not set anything.
rule\_action\_override:
Action settings to use in the place of the rule actions that are configured inside the rule group.
You specify one override for each rule whose action you want to change.
managed\_rule\_group\_configs:
Additional information that's used by a managed rule group. Only one rule attribute is allowed in each config.
Refer to https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html for more details.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
override_action = optional(string)
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = object({
name = string
vendor_name = string
version = optional(string)
rule_action_override = optional(map(object({
action = string
custom_request_handling = optional(object({
insert_header = object({
name = string
value = string
})
}), null)
custom_response = optional(object({
response_code = string
response_header = optional(object({
name = string
value = string
}), null)
}), null)
})), null)
managed_rule_group_configs = optional(list(object({
aws_managed_rules_bot_control_rule_set = optional(object({
inspection_level = string
}), null)
aws_managed_rules_atp_rule_set = optional(object({
enable_regex_in_path = optional(bool)
login_path = string
request_inspection = optional(object({
payload_type = string
password_field = object({
identifier = string
})
username_field = object({
identifier = string
})
}), null)
response_inspection = optional(object({
body_contains = optional(object({
success_strings = list(string)
failure_strings = list(string)
}), null)
header = optional(object({
name = string
success_values = list(string)
failure_values = list(string)
}), null)
json = optional(object({

identifier = string
success_strings = list(string)
failure_strings = list(string)
}), null)
status_code = optional(object({
success_codes = list(string)
failure_codes = list(string)
}), null)
}), null)
}), null)
})), null)
})
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | +| [managed\_rule\_group\_statement\_rules](#input\_managed\_rule\_group\_statement\_rules) | A rule statement used to run the rules that are defined in a managed rule group.

name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

override\_action:
The override action to apply to the rules in a rule group.
Possible values: `count`, `none`

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
name:
The name of the managed rule group.
vendor\_name:
The name of the managed rule group vendor.
version:
The version of the managed rule group.
You can set `Version_1.0` or `Version_1.1` etc. If you want to use the default version, do not set anything.
rule\_action\_override:
Action settings to use in the place of the rule actions that are configured inside the rule group.
You specify one override for each rule whose action you want to change.
managed\_rule\_group\_configs:
Additional information that's used by a managed rule group. Only one rule attribute is allowed in each config.
Refer to https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html for more details.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
override_action = optional(string)
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = object({
name = string
vendor_name = string
version = optional(string)
rule_action_override = optional(map(object({
action = string
custom_request_handling = optional(object({
insert_header = object({
name = string
value = string
})
}), null)
custom_response = optional(object({
response_code = string
response_header = optional(object({
name = string
value = string
}), null)
}), null)
})), null)
managed_rule_group_configs = optional(list(object({
aws_managed_rules_bot_control_rule_set = optional(object({
inspection_level = string
enable_machine_learning = optional(bool, true)
}), null)
aws_managed_rules_atp_rule_set = optional(object({
enable_regex_in_path = optional(bool)
login_path = string
request_inspection = optional(object({
payload_type = string
password_field = object({
identifier = string
})
username_field = object({
identifier = string
})
}), null)
response_inspection = optional(object({
body_contains = optional(object({
success_strings = list(string)
failure_strings = list(string)
}), null)
header = optional(object({
name = string
success_values = list(string)
failure_values = list(string)
}), null)
json = optional(object({

identifier = string
success_strings = list(string)
failure_strings = list(string)
}), null)
status_code = optional(object({
success_codes = list(string)
failure_codes = list(string)
}), null)
}), null)
}), null)
})), null)
})
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [rate\_based\_statement\_rules](#input\_rate\_based\_statement\_rules) | A rate-based rule tracks the rate of requests for each originating IP address,
and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
aggregate\_key\_type:
Setting that indicates how to aggregate the request counts.
Possible values include: `FORWARDED_IP` or `IP`
limit:
The limit on requests per 5-minute period for a single originating IP address.
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | +| [rate\_based\_statement\_rules](#input\_rate\_based\_statement\_rules) | A rate-based rule tracks the rate of requests for each originating IP address,
and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
aggregate\_key\_type:
Setting that indicates how to aggregate the request counts.
Possible values include: `FORWARDED_IP` or `IP`
limit:
The limit on requests per 5-minute period for a single originating IP address.
evaluation\_window\_sec:
The amount of time, in seconds, that AWS WAF should include in its request counts, looking back from the current time.
Valid values are 60, 120, 300, and 600. Defaults to 300 (5 minutes).
forwarded\_ip\_config:
fallback\_behavior:
The match status to assign to the web request if the request doesn't have a valid IP address in the specified position.
Possible values: `MATCH`, `NO_MATCH`
header\_name:
The name of the HTTP header to use for the IP address.
byte\_match\_statement:
field\_to\_match:
Part of a web request that you want AWS WAF to inspect.
positional\_constraint:
Area within the portion of a web request that you want AWS WAF to search for search\_string.
Valid values include the following: `EXACTLY`, `STARTS_WITH`, `ENDS_WITH`, `CONTAINS`, `CONTAINS_WORD`.
search\_string:
String value that you want AWS WAF to search for.
AWS WAF searches only in the part of web requests that you designate for inspection in `field_to_match`.
The maximum length of the value is 50 bytes.
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = object({
limit = number
aggregate_key_type = string
evaluation_window_sec = optional(number)
forwarded_ip_config = optional(object({
fallback_behavior = string
header_name = string
}), null)
scope_down_statement = optional(object({
byte_match_statement = object({
positional_constraint = string
search_string = string
field_to_match = object({
all_query_arguments = optional(bool)
body = optional(bool)
method = optional(bool)
query_string = optional(bool)
single_header = optional(object({ name = string }))
single_query_argument = optional(object({ name = string }))
uri_path = optional(bool)
})
text_transformation = list(object({
priority = number
type = string
}))
})
}), null)
})
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [redacted\_fields](#input\_redacted\_fields) | The parts of the request that you want to keep out of the logs.
You can only specify one of the following: `method`, `query_string`, `single_header`, or `uri_path`

method:
Whether to enable redaction of the HTTP method.
The method indicates the type of operation that the request is asking the origin to perform.
uri\_path:
Whether to enable redaction of the URI path.
This is the part of a web request that identifies a resource.
query\_string:
Whether to enable redaction of the query string.
This is the part of a URL that appears after a `?` character, if any.
single\_header:
The list of names of the query headers to redact. |
map(object({
method = optional(bool, false)
uri_path = optional(bool, false)
query_string = optional(bool, false)
single_header = optional(list(string), null)
}))
| `{}` | no | | [regex\_match\_statement\_rules](#input\_regex\_match\_statement\_rules) | A rule statement used to search web request components for a match against a single regular expression.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
regex\_string:
String representing the regular expression. Minimum of 1 and maximum of 512 characters.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl.html#field_to_match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection. At least one required.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | | [regex\_pattern\_set\_reference\_statement\_rules](#input\_regex\_pattern\_set\_reference\_statement\_rules) | A rule statement used to search web request components for matches with regular expressions.

action:
The action that AWS WAF should take on a web request when it matches the rule's statement.
name:
A friendly name of the rule.
priority:
If you define more than one Rule in a WebACL,
AWS WAF evaluates each request against the rules in order based on the value of priority.
AWS WAF processes rules with lower priority first.

captcha\_config:
Specifies how AWS WAF should handle CAPTCHA evaluations.

immunity\_time\_property:
Defines custom immunity time.

immunity\_time:
The amount of time, in seconds, that a CAPTCHA or challenge timestamp is considered valid by AWS WAF. The default setting is 300.

rule\_label:
A List of labels to apply to web requests that match the rule match statement

statement:
arn:
The Amazon Resource Name (ARN) of the Regex Pattern Set that this statement references.
field\_to\_match:
The part of a web request that you want AWS WAF to inspect.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#field-to-match
text\_transformation:
Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection.
See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation

visibility\_config:
Defines and enables Amazon CloudWatch metrics and web request sample collection.

cloudwatch\_metrics\_enabled:
Whether the associated resource sends metrics to CloudWatch.
metric\_name:
A friendly name of the CloudWatch metric.
sampled\_requests\_enabled:
Whether AWS WAF should store a sampling of the web requests that match the rules. |
list(object({
name = string
priority = number
action = string
captcha_config = optional(object({
immunity_time_property = object({
immunity_time = number
})
}), null)
rule_label = optional(list(string), null)
statement = any
visibility_config = optional(object({
cloudwatch_metrics_enabled = optional(bool)
metric_name = string
sampled_requests_enabled = optional(bool)
}), null)
}))
| `null` | no | diff --git a/modules/waf/main.tf b/modules/waf/main.tf index d1e5340b8..4f05ce854 100644 --- a/modules/waf/main.tf +++ b/modules/waf/main.tf @@ -18,7 +18,7 @@ locals { module "aws_waf" { source = "cloudposse/waf/aws" - version = "1.3.0" + version = "1.8.0" description = var.description default_action = var.default_action diff --git a/modules/waf/variables.tf b/modules/waf/variables.tf index fc165a555..11a46a60c 100644 --- a/modules/waf/variables.tf +++ b/modules/waf/variables.tf @@ -179,6 +179,7 @@ variable "geo_allowlist_statement_rules" { type = list(object({ name = string priority = number + action = string captcha_config = optional(object({ immunity_time_property = object({ immunity_time = number @@ -413,7 +414,8 @@ variable "managed_rule_group_statement_rules" { })), null) managed_rule_group_configs = optional(list(object({ aws_managed_rules_bot_control_rule_set = optional(object({ - inspection_level = string + inspection_level = string + enable_machine_learning = optional(bool, true) }), null) aws_managed_rules_atp_rule_set = optional(object({ enable_regex_in_path = optional(bool) @@ -522,7 +524,34 @@ variable "rate_based_statement_rules" { }) }), null) rule_label = optional(list(string), null) - statement = any + statement = object({ + limit = number + aggregate_key_type = string + evaluation_window_sec = optional(number) + forwarded_ip_config = optional(object({ + fallback_behavior = string + header_name = string + }), null) + scope_down_statement = optional(object({ + byte_match_statement = object({ + positional_constraint = string + search_string = string + field_to_match = object({ + all_query_arguments = optional(bool) + body = optional(bool) + method = optional(bool) + query_string = optional(bool) + single_header = optional(object({ name = string })) + single_query_argument = optional(object({ name = string })) + uri_path = optional(bool) + }) + text_transformation = list(object({ + priority = number + type = string + })) + }) + }), null) + }) visibility_config = optional(object({ cloudwatch_metrics_enabled = optional(bool) metric_name = string @@ -561,12 +590,28 @@ variable "rate_based_statement_rules" { Possible values include: `FORWARDED_IP` or `IP` limit: The limit on requests per 5-minute period for a single originating IP address. + evaluation_window_sec: + The amount of time, in seconds, that AWS WAF should include in its request counts, looking back from the current time. + Valid values are 60, 120, 300, and 600. Defaults to 300 (5 minutes). forwarded_ip_config: fallback_behavior: The match status to assign to the web request if the request doesn't have a valid IP address in the specified position. Possible values: `MATCH`, `NO_MATCH` header_name: The name of the HTTP header to use for the IP address. + byte_match_statement: + field_to_match: + Part of a web request that you want AWS WAF to inspect. + positional_constraint: + Area within the portion of a web request that you want AWS WAF to search for search_string. + Valid values include the following: `EXACTLY`, `STARTS_WITH`, `ENDS_WITH`, `CONTAINS`, `CONTAINS_WORD`. + search_string: + String value that you want AWS WAF to search for. + AWS WAF searches only in the part of web requests that you designate for inspection in `field_to_match`. + The maximum length of the value is 50 bytes. + text_transformation: + Text transformations eliminate some of the unusual formatting that attackers use in web requests in an effort to bypass detection. + See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#text-transformation visibility_config: Defines and enables Amazon CloudWatch metrics and web request sample collection. From e3c4ffd7559b1db9c06cae0131ac7c332fca06f0 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 27 Aug 2024 16:30:55 -0400 Subject: [PATCH 474/501] feat: Improvements for Auth0 Components (#1104) --- modules/auth0/app/README.md | 2 + modules/auth0/app/provider-auth0-client.tf | 60 +++- modules/auth0/connection/README.md | 154 ++++++++++ modules/auth0/connection/context.tf | 279 ++++++++++++++++++ modules/auth0/connection/main.tf | 41 +++ modules/auth0/connection/outputs.tf | 4 + .../auth0/connection/provider-auth0-client.tf | 149 ++++++++++ modules/auth0/connection/providers.tf | 19 ++ modules/auth0/connection/remote-state.tf | 11 + modules/auth0/connection/templates/.gitkeep | 0 modules/auth0/connection/variables.tf | 105 +++++++ modules/auth0/connection/versions.tf | 14 + modules/auth0/tenant/README.md | 7 + modules/auth0/tenant/main.tf | 30 +- modules/auth0/tenant/variables.tf | 24 ++ 15 files changed, 892 insertions(+), 7 deletions(-) create mode 100644 modules/auth0/connection/README.md create mode 100644 modules/auth0/connection/context.tf create mode 100644 modules/auth0/connection/main.tf create mode 100644 modules/auth0/connection/outputs.tf create mode 100644 modules/auth0/connection/provider-auth0-client.tf create mode 100644 modules/auth0/connection/providers.tf create mode 100644 modules/auth0/connection/remote-state.tf create mode 100644 modules/auth0/connection/templates/.gitkeep create mode 100644 modules/auth0/connection/variables.tf create mode 100644 modules/auth0/connection/versions.tf diff --git a/modules/auth0/app/README.md b/modules/auth0/app/README.md index 929b4be2d..fa48107c0 100644 --- a/modules/auth0/app/README.md +++ b/modules/auth0/app/README.md @@ -67,6 +67,7 @@ components: | Name | Source | Version | |------|--------|---------| +| [auth0\_ssm\_parameters](#module\_auth0\_ssm\_parameters) | cloudposse/ssm-parameter-store/aws | 0.13.0 | | [auth0\_tenant](#module\_auth0\_tenant) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles\_auth0\_provider](#module\_iam\_roles\_auth0\_provider) | ../../account-map/modules/iam-roles | n/a | @@ -96,6 +97,7 @@ components: | [auth0\_tenant\_tenant\_name](#input\_auth0\_tenant\_tenant\_name) | The name of the tenant where the Auth0 tenant component is deployed. Yes this is a bit redundant, since Auth0 also calls this resource a tenant. Defaults to the tenant of the current stack. | `string` | `""` | no | | [callbacks](#input\_callbacks) | Allowed Callback URLs | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_auth0\_ssm\_parameters\_enabled](#input\_create\_auth0\_ssm\_parameters\_enabled) | Whether or not to create a duplicate of the AWS SSM parameter for the Auth0 domain, client ID, and client secret in this account. | `bool` | `false` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | diff --git a/modules/auth0/app/provider-auth0-client.tf b/modules/auth0/app/provider-auth0-client.tf index b6cbb4ff1..1b35cf9b5 100644 --- a/modules/auth0/app/provider-auth0-client.tf +++ b/modules/auth0/app/provider-auth0-client.tf @@ -25,6 +25,12 @@ variable "auth0_tenant_tenant_name" { default = "" } +locals { + auth0_tenant_environment_name = length(var.auth0_tenant_environment_name) > 0 ? var.auth0_tenant_environment_name : module.this.environment + auth0_tenant_stage_name = length(var.auth0_tenant_stage_name) > 0 ? var.auth0_tenant_stage_name : module.this.stage + auth0_tenant_tenant_name = length(var.auth0_tenant_tenant_name) > 0 ? var.auth0_tenant_tenant_name : module.this.tenant +} + module "auth0_tenant" { source = "cloudposse/stack-config/yaml//modules/remote-state" version = "1.5.0" @@ -33,9 +39,9 @@ module "auth0_tenant" { component = var.auth0_tenant_component_name - environment = length(var.auth0_tenant_environment_name) > 0 ? var.auth0_tenant_environment_name : module.this.environment - stage = length(var.auth0_tenant_stage_name) > 0 ? var.auth0_tenant_stage_name : module.this.stage - tenant = length(var.auth0_tenant_tenant_name) > 0 ? var.auth0_tenant_tenant_name : module.this.tenant + environment = local.auth0_tenant_environment_name + stage = local.auth0_tenant_stage_name + tenant = local.auth0_tenant_tenant_name } # @@ -61,9 +67,9 @@ provider "aws" { module "iam_roles_auth0_provider" { source = "../../account-map/modules/iam-roles" - environment = length(var.auth0_tenant_environment_name) > 0 ? var.auth0_tenant_environment_name : module.this.environment - stage = length(var.auth0_tenant_stage_name) > 0 ? var.auth0_tenant_stage_name : module.this.stage - tenant = length(var.auth0_tenant_tenant_name) > 0 ? var.auth0_tenant_tenant_name : module.this.tenant + environment = local.auth0_tenant_environment_name + stage = local.auth0_tenant_stage_name + tenant = local.auth0_tenant_tenant_name context = module.this.context } @@ -99,3 +105,45 @@ provider "auth0" { client_secret = data.aws_ssm_parameter.auth0_client_secret.value debug = var.auth0_debug } + +# +# Finally if enabled, create a duplicate of the AWS SSM parameters for Auth0 in this account. +# +variable "create_auth0_ssm_parameters_enabled" { + description = "Whether or not to create a duplicate of the AWS SSM parameter for the Auth0 domain, client ID, and client secret in this account." + type = bool + default = false +} + +module "auth0_ssm_parameters" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.13.0" + + enabled = local.enabled && var.create_auth0_ssm_parameters_enabled + + parameter_write = [ + { + name = module.auth0_tenant[0].outputs.domain_ssm_path + value = data.aws_ssm_parameter.auth0_domain.value + type = "SecureString" + overwrite = "true" + description = "Auth0 domain value for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" + }, + { + name = module.auth0_tenant[0].outputs.client_id_ssm_path + value = data.aws_ssm_parameter.auth0_client_id.value + type = "SecureString" + overwrite = "true" + description = "Auth0 client ID for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" + }, + { + name = module.auth0_tenant[0].outputs.client_secret_ssm_path + value = data.aws_ssm_parameter.auth0_client_secret.value + type = "SecureString" + overwrite = "true" + description = "Auth0 client secret for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" + }, + ] + + context = module.this.context +} diff --git a/modules/auth0/connection/README.md b/modules/auth0/connection/README.md new file mode 100644 index 000000000..89d4305a3 --- /dev/null +++ b/modules/auth0/connection/README.md @@ -0,0 +1,154 @@ +# Component: `auth0/connection` + +Auth 0 Connection component. [Auth0](https://auth0.com/docs/) is a third-party service that provides authentication and +authorization as a service. It is typically used to to authenticate users. + +An Auth0 connection is a bridge between Auth0 and an identity provider (IdP) that allows your application to +authenticate users. Auth0 supports many types of connections, including social identity providers such as Google, +Facebook, and Twitter, enterprise identity providers such as Microsoft Azure AD, and passwordless authentication methods +such as email and SMS. + +## Usage + +Before deploying this component, you need to deploy the `auth0/tenant` component. This components with authenticate with +the [Auth0 Terraform provider](https://registry.terraform.io/providers/auth0/auth0/latest/) using the Auth0 tenant's +client ID and client secret configured with the `auth0/tenant` component. + +**Stack Level**: Global + +Here's an example snippet for how to use this component. + +```yaml +# stacks/catalog/auth0/connection.yaml +components: + terraform: + auth0/connection: + vars: + enabled: true + name: "auth0" + + # These must all be specified for the connection to be created + strategy: "email" + connection_name: "email" + options_name: "email" + + email_from: "{{`{{ application.name }}`}} " + email_subject: "Welcome to {{`{{ application.name }}`}}" + syntax: "liquid" + + auth_params: + scope: "openid profile" + response_type: "code" + + totp: + time_step: 895 + length: 6 + + template_file: "templates/email.html" + + # Stage-specific configuration + auth0_app_connections: + - stage: sandbox + - stage: dev + - stage: staging +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [auth0](#requirement\_auth0) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | + +## Providers + +| Name | Version | +|------|---------| +| [auth0](#provider\_auth0) | >= 1.0.0 | +| [aws.auth0\_provider](#provider\_aws.auth0\_provider) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [auth0\_apps](#module\_auth0\_apps) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [auth0\_ssm\_parameters](#module\_auth0\_ssm\_parameters) | cloudposse/ssm-parameter-store/aws | 0.13.0 | +| [auth0\_tenant](#module\_auth0\_tenant) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [iam\_roles\_auth0\_provider](#module\_iam\_roles\_auth0\_provider) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [auth0_connection.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/connection) | resource | +| [auth0_connection_clients.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/connection_clients) | resource | +| [aws_ssm_parameter.auth0_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.auth0_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.auth0_domain](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [auth0\_app\_connections](#input\_auth0\_app\_connections) | The list of Auth0 apps to add to this connection |
list(object({
component = optional(string, "auth0/app")
environment = optional(string, "")
stage = optional(string, "")
tenant = optional(string, "")
}))
| `[]` | no | +| [auth0\_debug](#input\_auth0\_debug) | Enable debug mode for the Auth0 provider | `bool` | `true` | no | +| [auth0\_tenant\_component\_name](#input\_auth0\_tenant\_component\_name) | The name of the component | `string` | `"auth0/tenant"` | no | +| [auth0\_tenant\_environment\_name](#input\_auth0\_tenant\_environment\_name) | The name of the environment where the Auth0 tenant component is deployed. Defaults to the environment of the current stack. | `string` | `""` | no | +| [auth0\_tenant\_stage\_name](#input\_auth0\_tenant\_stage\_name) | The name of the stage where the Auth0 tenant component is deployed. Defaults to the stage of the current stack. | `string` | `""` | no | +| [auth0\_tenant\_tenant\_name](#input\_auth0\_tenant\_tenant\_name) | The name of the tenant where the Auth0 tenant component is deployed. Yes this is a bit redundant, since Auth0 also calls this resource a tenant. Defaults to the tenant of the current stack. | `string` | `""` | no | +| [auth\_params](#input\_auth\_params) | Query string parameters to be included as part of the generated passwordless email link. |
object({
scope = optional(string, null)
response_type = optional(string, null)
})
| `{}` | no | +| [brute\_force\_protection](#input\_brute\_force\_protection) | Indicates whether to enable brute force protection, which will limit the number of signups and failed logins from a suspicious IP address. | `bool` | `true` | no | +| [connection\_name](#input\_connection\_name) | The name of the connection | `string` | `""` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_auth0\_ssm\_parameters\_enabled](#input\_create\_auth0\_ssm\_parameters\_enabled) | Whether or not to create a duplicate of the AWS SSM parameter for the Auth0 domain, client ID, and client secret in this account. | `bool` | `false` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [disable\_signup](#input\_disable\_signup) | Indicates whether to allow user sign-ups to your application. | `bool` | `false` | no | +| [email\_from](#input\_email\_from) | When using an email strategy, the address to use as the sender | `string` | `null` | no | +| [email\_subject](#input\_email\_subject) | When using an email strategy, the subject of the email | `string` | `null` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [non\_persistent\_attrs](#input\_non\_persistent\_attrs) | If there are user fields that should not be stored in Auth0 databases due to privacy reasons, you can add them to the DenyList here. | `list(string)` | `[]` | no | +| [options\_name](#input\_options\_name) | The name of the connection options. Required for the email strategy. | `string` | `""` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [set\_user\_root\_attributes](#input\_set\_user\_root\_attributes) | Determines whether to sync user profile attributes at each login or only on the first login. Options include: `on_each_login`, `on_first_login`. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [strategy](#input\_strategy) | The strategy to use for the connection | `string` | `"auth0"` | no | +| [syntax](#input\_syntax) | The syntax of the template body | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [template](#input\_template) | The template to use for the connection. If not provided, the `template_file` variable must be set. | `string` | `""` | no | +| [template\_file](#input\_template\_file) | The path to the template file. If not provided, the `template` variable must be set. | `string` | `""` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [totp](#input\_totp) | The TOTP settings for the connection |
object({
time_step = optional(number, 900)
length = optional(number, 6)
})
| `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [auth0\_connection\_id](#output\_auth0\_connection\_id) | The Auth0 Connection ID | + + + +## References + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/auth0/connection) - + Cloud Posse's upstream component +- [Auth0 Terraform Provider](https://registry.terraform.io/providers/auth0/auth0/latest/) +- [Auth0 Documentation](https://auth0.com/docs/) + +[](https://cpco.io/component) diff --git a/modules/auth0/connection/context.tf b/modules/auth0/connection/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/auth0/connection/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/auth0/connection/main.tf b/modules/auth0/connection/main.tf new file mode 100644 index 000000000..2451cf70d --- /dev/null +++ b/modules/auth0/connection/main.tf @@ -0,0 +1,41 @@ +locals { + enabled = module.this.enabled + + # If var.template is set, use it. + # Otherwise, if var.template_file is set, use that file content. + # Otherwise, use null. + template = length(var.template) > 0 ? var.template : length(var.template_file) > 0 ? file("${path.module}/${var.template_file}") : null +} + +resource "auth0_connection" "this" { + count = local.enabled ? 1 : 0 + + strategy = var.strategy + name = length(var.connection_name) > 0 ? var.connection_name : module.this.name + + options { + name = var.options_name + from = var.email_from + subject = var.email_subject + syntax = var.syntax + template = local.template + + disable_signup = var.disable_signup + brute_force_protection = var.brute_force_protection + set_user_root_attributes = var.set_user_root_attributes + non_persistent_attrs = var.non_persistent_attrs + auth_params = var.auth_params + + totp { + time_step = var.totp.time_step + length = var.totp.length + } + } +} + +resource "auth0_connection_clients" "this" { + count = local.enabled ? 1 : 0 + + connection_id = auth0_connection.this[0].id + enabled_clients = length(module.auth0_apps) > 0 ? [for auth0_app in module.auth0_apps : auth0_app.outputs.auth0_client_id] : [] +} diff --git a/modules/auth0/connection/outputs.tf b/modules/auth0/connection/outputs.tf new file mode 100644 index 000000000..e1a0f06f1 --- /dev/null +++ b/modules/auth0/connection/outputs.tf @@ -0,0 +1,4 @@ +output "auth0_connection_id" { + value = auth0_connection.this[0].id + description = "The Auth0 Connection ID" +} diff --git a/modules/auth0/connection/provider-auth0-client.tf b/modules/auth0/connection/provider-auth0-client.tf new file mode 100644 index 000000000..1b35cf9b5 --- /dev/null +++ b/modules/auth0/connection/provider-auth0-client.tf @@ -0,0 +1,149 @@ +# +# Fetch the Auth0 tenant component deployment in some stack +# +variable "auth0_tenant_component_name" { + description = "The name of the component" + type = string + default = "auth0/tenant" +} + +variable "auth0_tenant_environment_name" { + description = "The name of the environment where the Auth0 tenant component is deployed. Defaults to the environment of the current stack." + type = string + default = "" +} + +variable "auth0_tenant_stage_name" { + description = "The name of the stage where the Auth0 tenant component is deployed. Defaults to the stage of the current stack." + type = string + default = "" +} + +variable "auth0_tenant_tenant_name" { + description = "The name of the tenant where the Auth0 tenant component is deployed. Yes this is a bit redundant, since Auth0 also calls this resource a tenant. Defaults to the tenant of the current stack." + type = string + default = "" +} + +locals { + auth0_tenant_environment_name = length(var.auth0_tenant_environment_name) > 0 ? var.auth0_tenant_environment_name : module.this.environment + auth0_tenant_stage_name = length(var.auth0_tenant_stage_name) > 0 ? var.auth0_tenant_stage_name : module.this.stage + auth0_tenant_tenant_name = length(var.auth0_tenant_tenant_name) > 0 ? var.auth0_tenant_tenant_name : module.this.tenant +} + +module "auth0_tenant" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + count = local.enabled ? 1 : 0 + + component = var.auth0_tenant_component_name + + environment = local.auth0_tenant_environment_name + stage = local.auth0_tenant_stage_name + tenant = local.auth0_tenant_tenant_name +} + +# +# Set up the AWS provider to access AWS SSM parameters in the same account as the Auth0 tenant +# + +provider "aws" { + alias = "auth0_provider" + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles_auth0_provider.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles_auth0_provider.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles_auth0_provider.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles_auth0_provider" { + source = "../../account-map/modules/iam-roles" + + environment = local.auth0_tenant_environment_name + stage = local.auth0_tenant_stage_name + tenant = local.auth0_tenant_tenant_name + + context = module.this.context +} + +data "aws_ssm_parameter" "auth0_domain" { + provider = aws.auth0_provider + name = module.auth0_tenant[0].outputs.domain_ssm_path +} + +data "aws_ssm_parameter" "auth0_client_id" { + provider = aws.auth0_provider + name = module.auth0_tenant[0].outputs.client_id_ssm_path +} + +data "aws_ssm_parameter" "auth0_client_secret" { + provider = aws.auth0_provider + name = module.auth0_tenant[0].outputs.client_secret_ssm_path +} + +# +# Initialize the Auth0 provider with the Auth0 domain, client ID, and client secret from that deployment +# + +variable "auth0_debug" { + type = bool + description = "Enable debug mode for the Auth0 provider" + default = true +} + +provider "auth0" { + domain = data.aws_ssm_parameter.auth0_domain.value + client_id = data.aws_ssm_parameter.auth0_client_id.value + client_secret = data.aws_ssm_parameter.auth0_client_secret.value + debug = var.auth0_debug +} + +# +# Finally if enabled, create a duplicate of the AWS SSM parameters for Auth0 in this account. +# +variable "create_auth0_ssm_parameters_enabled" { + description = "Whether or not to create a duplicate of the AWS SSM parameter for the Auth0 domain, client ID, and client secret in this account." + type = bool + default = false +} + +module "auth0_ssm_parameters" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.13.0" + + enabled = local.enabled && var.create_auth0_ssm_parameters_enabled + + parameter_write = [ + { + name = module.auth0_tenant[0].outputs.domain_ssm_path + value = data.aws_ssm_parameter.auth0_domain.value + type = "SecureString" + overwrite = "true" + description = "Auth0 domain value for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" + }, + { + name = module.auth0_tenant[0].outputs.client_id_ssm_path + value = data.aws_ssm_parameter.auth0_client_id.value + type = "SecureString" + overwrite = "true" + description = "Auth0 client ID for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" + }, + { + name = module.auth0_tenant[0].outputs.client_secret_ssm_path + value = data.aws_ssm_parameter.auth0_client_secret.value + type = "SecureString" + overwrite = "true" + description = "Auth0 client secret for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" + }, + ] + + context = module.this.context +} diff --git a/modules/auth0/connection/providers.tf b/modules/auth0/connection/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/auth0/connection/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/auth0/connection/remote-state.tf b/modules/auth0/connection/remote-state.tf new file mode 100644 index 000000000..068ecc6b7 --- /dev/null +++ b/modules/auth0/connection/remote-state.tf @@ -0,0 +1,11 @@ +module "auth0_apps" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + for_each = local.enabled ? { for app in var.auth0_app_connections : "${app.tenant}-${app.environment}-${app.stage}-${app.component}" => app } : {} + + component = each.value.component + tenant = length(each.value.tenant) > 0 ? each.value.tenant : module.this.tenant + environment = length(each.value.environment) > 0 ? each.value.environment : module.this.environment + stage = length(each.value.stage) > 0 ? each.value.stage : module.this.stage +} diff --git a/modules/auth0/connection/templates/.gitkeep b/modules/auth0/connection/templates/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/modules/auth0/connection/variables.tf b/modules/auth0/connection/variables.tf new file mode 100644 index 000000000..748d3bb19 --- /dev/null +++ b/modules/auth0/connection/variables.tf @@ -0,0 +1,105 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "auth0_app_connections" { + type = list(object({ + component = optional(string, "auth0/app") + environment = optional(string, "") + stage = optional(string, "") + tenant = optional(string, "") + })) + default = [] + description = "The list of Auth0 apps to add to this connection" +} + +variable "strategy" { + type = string + description = "The strategy to use for the connection" + default = "auth0" +} + +variable "connection_name" { + type = string + description = "The name of the connection" + default = "" +} + +variable "options_name" { + type = string + description = "The name of the connection options. Required for the email strategy." + default = "" +} + +variable "email_from" { + type = string + description = "When using an email strategy, the address to use as the sender" + default = null +} + +variable "email_subject" { + type = string + description = "When using an email strategy, the subject of the email" + default = null +} + +variable "syntax" { + type = string + description = "The syntax of the template body" + default = null +} + +variable "disable_signup" { + type = bool + description = "Indicates whether to allow user sign-ups to your application." + default = false +} + +variable "brute_force_protection" { + type = bool + description = "Indicates whether to enable brute force protection, which will limit the number of signups and failed logins from a suspicious IP address." + default = true +} + +variable "set_user_root_attributes" { + type = string + description = "Determines whether to sync user profile attributes at each login or only on the first login. Options include: `on_each_login`, `on_first_login`." + default = null +} + +variable "non_persistent_attrs" { + type = list(string) + description = "If there are user fields that should not be stored in Auth0 databases due to privacy reasons, you can add them to the DenyList here." + default = [] +} + +variable "auth_params" { + type = object({ + scope = optional(string, null) + response_type = optional(string, null) + }) + description = "Query string parameters to be included as part of the generated passwordless email link." + default = {} +} + +variable "totp" { + type = object({ + time_step = optional(number, 900) + length = optional(number, 6) + }) + description = "The TOTP settings for the connection" + default = {} +} + +variable "template_file" { + type = string + description = "The path to the template file. If not provided, the `template` variable must be set." + default = "" +} + +variable "template" { + type = string + description = "The template to use for the connection. If not provided, the `template_file` variable must be set." + default = "" +} diff --git a/modules/auth0/connection/versions.tf b/modules/auth0/connection/versions.tf new file mode 100644 index 000000000..3894f08a9 --- /dev/null +++ b/modules/auth0/connection/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + auth0 = { + source = "auth0/auth0" + version = ">= 1.0.0" + } + aws = { + source = "hashicorp/aws" + version = ">= 4.9.0" + } + } +} diff --git a/modules/auth0/tenant/README.md b/modules/auth0/tenant/README.md index e165ca834..afc9fffd3 100644 --- a/modules/auth0/tenant/README.md +++ b/modules/auth0/tenant/README.md @@ -98,11 +98,14 @@ add the following parameters to the `plat-prod` account in `us-west-2`: |------|------| | [auth0_custom_domain.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/custom_domain) | resource | | [auth0_custom_domain_verification.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/custom_domain_verification) | resource | +| [auth0_email_provider.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/email_provider) | resource | +| [auth0_prompt.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/prompt) | resource | | [auth0_tenant.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/tenant) | resource | | [aws_route53_record.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | | [aws_ssm_parameter.auth0_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.auth0_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.auth0_domain](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.sendgrid_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs @@ -112,6 +115,7 @@ add the following parameters to the `plat-prod` account in `us-west-2`: | [allowed\_logout\_urls](#input\_allowed\_logout\_urls) | The URLs that Auth0 can redirect to after logout. | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [auth0\_debug](#input\_auth0\_debug) | Enable debug mode for the Auth0 provider | `bool` | `true` | no | +| [auth0\_prompt\_experience](#input\_auth0\_prompt\_experience) | Which prompt login experience to use. Options include classic and new. | `string` | `"new"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [default\_redirection\_uri](#input\_default\_redirection\_uri) | The default redirection URI. | `string` | `""` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | @@ -119,6 +123,8 @@ add the following parameters to the `plat-prod` account in `us-west-2`: | [disable\_clickjack\_protection\_headers](#input\_disable\_clickjack\_protection\_headers) | Whether to disable clickjack protection headers. | `bool` | `true` | no | | [disable\_fields\_map\_fix](#input\_disable\_fields\_map\_fix) | Whether to disable fields map fix. | `bool` | `false` | no | | [disable\_management\_api\_sms\_obfuscation](#input\_disable\_management\_api\_sms\_obfuscation) | Whether to disable management API SMS obfuscation. | `bool` | `false` | no | +| [email\_provider\_default\_from\_address](#input\_email\_provider\_default\_from\_address) | The default from address for the email provider. | `string` | `""` | no | +| [email\_provider\_name](#input\_email\_provider\_name) | The name of the email provider. If not defined, no email provider will be created. | `string` | `""` | no | | [enable\_public\_signup\_user\_exists\_error](#input\_enable\_public\_signup\_user\_exists\_error) | Whether to enable public signup user exists error. | `bool` | `true` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [enabled\_locales](#input\_enabled\_locales) | The enabled locales. | `list(string)` |
[
"en"
]
| no | @@ -139,6 +145,7 @@ add the following parameters to the `plat-prod` account in `us-west-2`: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [sandbox\_version](#input\_sandbox\_version) | The sandbox version. | `string` | `"18"` | no | +| [sendgrid\_api\_key\_ssm\_path](#input\_sendgrid\_api\_key\_ssm\_path) | The SSM path to the SendGrid API key. Only required if `email_provider_name` is `sendgrid`. | `string` | `""` | no | | [session\_cookie\_mode](#input\_session\_cookie\_mode) | The session cookie mode. | `string` | `"persistent"` | no | | [session\_lifetime](#input\_session\_lifetime) | The session lifetime in hours. | `number` | `168` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | diff --git a/modules/auth0/tenant/main.tf b/modules/auth0/tenant/main.tf index 570cff8dc..79687d1c8 100644 --- a/modules/auth0/tenant/main.tf +++ b/modules/auth0/tenant/main.tf @@ -1,5 +1,6 @@ locals { - enabled = module.this.enabled + enabled = module.this.enabled + email_provider_enabled = length(var.email_provider_name) > 0 && local.enabled name = length(module.this.name) > 0 ? module.this.name : "auth0" domain_name = format("%s.%s", local.name, module.dns_gbl_delegated.outputs.default_domain_name) @@ -87,3 +88,30 @@ resource "auth0_custom_domain_verification" "this" { aws_route53_record.this, ] } + +resource "auth0_prompt" "this" { + count = local.enabled ? 1 : 0 + + universal_login_experience = var.auth0_prompt_experience +} + +data "aws_ssm_parameter" "sendgrid_api_key" { + count = local.email_provider_enabled ? 1 : 0 + + name = var.sendgrid_api_key_ssm_path +} + +resource "auth0_email_provider" "this" { + count = local.email_provider_enabled ? 1 : 0 + + name = var.email_provider_name + enabled = local.email_provider_enabled + default_from_address = var.email_provider_default_from_address + + dynamic "credentials" { + for_each = var.email_provider_name == "sendgrid" ? ["1"] : [] + content { + api_key = data.aws_ssm_parameter.sendgrid_api_key[0].value + } + } +} diff --git a/modules/auth0/tenant/variables.tf b/modules/auth0/tenant/variables.tf index 24a9c2696..960647ab6 100644 --- a/modules/auth0/tenant/variables.tf +++ b/modules/auth0/tenant/variables.tf @@ -108,3 +108,27 @@ variable "oidc_logout_prompt_enabled" { description = "Whether the OIDC logout prompt is enabled." default = false } + +variable "email_provider_name" { + type = string + description = "The name of the email provider. If not defined, no email provider will be created." + default = "" +} + +variable "email_provider_default_from_address" { + type = string + description = "The default from address for the email provider." + default = "" +} + +variable "sendgrid_api_key_ssm_path" { + type = string + description = "The SSM path to the SendGrid API key. Only required if `email_provider_name` is `sendgrid`." + default = "" +} + +variable "auth0_prompt_experience" { + type = string + description = "Which prompt login experience to use. Options include classic and new." + default = "new" +} From 7f71c2ecd5743b633ee1f825885521878ca56aac Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 3 Sep 2024 10:43:07 -0400 Subject: [PATCH 475/501] feat: memorydb component (#1105) --- modules/memorydb/README.md | 128 ++++++++++++++ modules/memorydb/context.tf | 279 +++++++++++++++++++++++++++++++ modules/memorydb/main.tf | 37 ++++ modules/memorydb/outputs.tf | 64 +++++++ modules/memorydb/providers.tf | 19 +++ modules/memorydb/remote-state.tf | 8 + modules/memorydb/variables.tf | 132 +++++++++++++++ modules/memorydb/versions.tf | 10 ++ 8 files changed, 677 insertions(+) create mode 100644 modules/memorydb/README.md create mode 100644 modules/memorydb/context.tf create mode 100644 modules/memorydb/main.tf create mode 100644 modules/memorydb/outputs.tf create mode 100644 modules/memorydb/providers.tf create mode 100644 modules/memorydb/remote-state.tf create mode 100644 modules/memorydb/variables.tf create mode 100644 modules/memorydb/versions.tf diff --git a/modules/memorydb/README.md b/modules/memorydb/README.md new file mode 100644 index 000000000..b0bdbd661 --- /dev/null +++ b/modules/memorydb/README.md @@ -0,0 +1,128 @@ +# Component: `memorydb` + +This component provisions an AWS MemoryDB cluster. MemoryDB is a fully managed, Redis-compatible, in-memory database +service. + +While Redis is commonly used as a cache, MemoryDB is designed to also function well as a +[vector database](https://docs.aws.amazon.com/memorydb/latest/devguide/vector-search.html). This makes it appropriate +for AI model backends. + +## Usage + +**Stack Level**: Regional + +### Example + +Here's an example snippet for how to use this component: + +```yaml +components: + terraform: + vpc: + vars: + availability_zones: + - "a" + - "b" + - "c" + ipv4_primary_cidr_block: "10.111.0.0/18" + memorydb: + vars: {} +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 5.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [memorydb](#module\_memorydb) | cloudposse/memorydb/aws | 0.1.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_username](#input\_admin\_username) | The username for the MemoryDB admin | `string` | `"admin"` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [auto\_minor\_version\_upgrade](#input\_auto\_minor\_version\_upgrade) | Indicates that minor engine upgrades will be applied automatically to the cluster during the maintenance window | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [engine\_version](#input\_engine\_version) | The version of the Redis engine to use | `string` | `"6.2"` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [maintenance\_window](#input\_maintenance\_window) | The weekly time range during which system maintenance can occur | `string` | `null` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [node\_type](#input\_node\_type) | The compute and memory capacity of the nodes in the cluster | `string` | `"db.r6g.large"` | no | +| [num\_replicas\_per\_shard](#input\_num\_replicas\_per\_shard) | The number of replicas per shard | `number` | `1` | no | +| [num\_shards](#input\_num\_shards) | The number of shards in the cluster | `number` | `1` | no | +| [parameter\_group\_family](#input\_parameter\_group\_family) | The name of the parameter group family | `string` | `"memorydb_redis6"` | no | +| [parameters](#input\_parameters) | Key-value mapping of parameters to apply to the parameter group | `map(string)` | `{}` | no | +| [port](#input\_port) | The port on which the cluster accepts connections | `number` | `6379` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [security\_group\_ids](#input\_security\_group\_ids) | List of security group IDs to associate with the MemoryDB cluster | `list(string)` | `[]` | no | +| [snapshot\_arns](#input\_snapshot\_arns) | List of ARNs for the snapshots to be restored. NOTE: destroys the existing cluster. Use for restoring. | `list(string)` | `[]` | no | +| [snapshot\_retention\_limit](#input\_snapshot\_retention\_limit) | The number of days for which MemoryDB retains automatic snapshots before deleting them | `number` | `null` | no | +| [snapshot\_window](#input\_snapshot\_window) | The daily time range during which MemoryDB begins taking daily snapshots | `string` | `null` | no | +| [sns\_topic\_arn](#input\_sns\_topic\_arn) | The ARN of the SNS topic to send notifications to | `string` | `null` | no | +| [ssm\_kms\_key\_id](#input\_ssm\_kms\_key\_id) | The KMS key ID to use for SSM parameter encryption. If not specified, the default key will be used. | `string` | `null` | no | +| [ssm\_parameter\_name](#input\_ssm\_parameter\_name) | The name of the SSM parameter to store the password in. If not specified, the password will be stored in `/{context.id}/admin_password` | `string` | `""` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [tls\_enabled](#input\_tls\_enabled) | Indicates whether Transport Layer Security (TLS) encryption is enabled for the cluster | `bool` | `true` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | The name of the VPC component. This is used to pick out subnets for the MemoryDB cluster | `string` | `"vpc"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [admin\_acl\_arn](#output\_admin\_acl\_arn) | The ARN of the MemoryDB user's ACL | +| [admin\_arn](#output\_admin\_arn) | The ARN of the MemoryDB user | +| [admin\_password\_ssm\_parameter\_name](#output\_admin\_password\_ssm\_parameter\_name) | The name of the SSM parameter storing the password for the MemoryDB user | +| [admin\_username](#output\_admin\_username) | The username for the MemoryDB user | +| [arn](#output\_arn) | The ARN of the MemoryDB cluster | +| [cluster\_endpoint](#output\_cluster\_endpoint) | The endpoint of the MemoryDB cluster | +| [engine\_patch\_version](#output\_engine\_patch\_version) | The Redis engine version | +| [id](#output\_id) | The name of the MemoryDB cluster | +| [parameter\_group\_arn](#output\_parameter\_group\_arn) | The ARN of the MemoryDB parameter group | +| [parameter\_group\_id](#output\_parameter\_group\_id) | The name of the MemoryDB parameter group | +| [shards](#output\_shards) | The shard details for the MemoryDB cluster | +| [subnet\_group\_arn](#output\_subnet\_group\_arn) | The ARN of the MemoryDB subnet group | +| [subnet\_group\_id](#output\_subnet\_group\_id) | The name of the MemoryDB subnet group | + + + +## References + +- [MemoryDB Documentation](https://docs.aws.amazon.com/memorydb/latest/devguide/what-is-memorydb.html) +- [Vector Searches with MemoryDB](https://docs.aws.amazon.com/memorydb/latest/devguide/vector-search.html) +- AWS CLI + [command to list MemoryDB engine versions](https://docs.aws.amazon.com/cli/latest/reference/memorydb/describe-engine-versions.html): + `aws memorydb describe-engine-versions`. + +[](https://cpco.io/component) diff --git a/modules/memorydb/context.tf b/modules/memorydb/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/memorydb/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/memorydb/main.tf b/modules/memorydb/main.tf new file mode 100644 index 000000000..30d93ad5c --- /dev/null +++ b/modules/memorydb/main.tf @@ -0,0 +1,37 @@ +locals { + vpc = module.vpc.outputs + private_subnet_ids = local.vpc.private_subnet_ids + + default_ssm_parameter_name = "/${module.this.id}/admin_password" + ssm_parameter_name = var.ssm_parameter_name == "" ? local.default_ssm_parameter_name : var.ssm_parameter_name +} + +module "memorydb" { + source = "cloudposse/memorydb/aws" + version = "0.1.0" + + node_type = var.node_type + num_shards = var.num_shards + num_replicas_per_shard = var.num_replicas_per_shard + tls_enabled = var.tls_enabled + engine_version = var.engine_version + auto_minor_version_upgrade = var.auto_minor_version_upgrade + subnet_ids = local.private_subnet_ids + security_group_ids = var.security_group_ids + port = var.port + maintenance_window = var.maintenance_window + + snapshot_window = var.snapshot_window + snapshot_retention_limit = var.snapshot_retention_limit + snapshot_arns = var.snapshot_arns + sns_topic_arn = var.sns_topic_arn + + admin_username = var.admin_username + + ssm_parameter_name = local.ssm_parameter_name + + parameter_group_family = var.parameter_group_family + parameters = var.parameters + + context = module.this.context +} diff --git a/modules/memorydb/outputs.tf b/modules/memorydb/outputs.tf new file mode 100644 index 000000000..383c1f818 --- /dev/null +++ b/modules/memorydb/outputs.tf @@ -0,0 +1,64 @@ +output "id" { + description = "The name of the MemoryDB cluster" + value = module.memorydb.id +} + +output "arn" { + description = "The ARN of the MemoryDB cluster" + value = module.memorydb.arn +} + +output "cluster_endpoint" { + description = "The endpoint of the MemoryDB cluster" + value = module.memorydb.cluster_endpoint +} + +output "engine_patch_version" { + description = "The Redis engine version" + value = module.memorydb.engine_patch_version +} + +output "parameter_group_id" { + description = "The name of the MemoryDB parameter group" + value = module.memorydb.id +} + +output "parameter_group_arn" { + description = "The ARN of the MemoryDB parameter group" + value = module.memorydb.arn +} + +output "subnet_group_id" { + description = "The name of the MemoryDB subnet group" + value = module.memorydb.id +} + +output "subnet_group_arn" { + description = "The ARN of the MemoryDB subnet group" + value = module.memorydb.arn +} + +output "shards" { + description = "The shard details for the MemoryDB cluster" + value = module.memorydb.shards +} + +output "admin_username" { + description = "The username for the MemoryDB user" + value = module.memorydb.admin_username +} + +output "admin_arn" { + description = "The ARN of the MemoryDB user" + value = module.memorydb.admin_arn +} + +output "admin_acl_arn" { + description = "The ARN of the MemoryDB user's ACL" + value = module.memorydb.admin_acl_arn +} + +output "admin_password_ssm_parameter_name" { + description = "The name of the SSM parameter storing the password for the MemoryDB user" + value = module.memorydb.admin_password_ssm_parameter_name +} diff --git a/modules/memorydb/providers.tf b/modules/memorydb/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/memorydb/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/memorydb/remote-state.tf b/modules/memorydb/remote-state.tf new file mode 100644 index 000000000..4e2391525 --- /dev/null +++ b/modules/memorydb/remote-state.tf @@ -0,0 +1,8 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.vpc_component_name + + context = module.this.context +} diff --git a/modules/memorydb/variables.tf b/modules/memorydb/variables.tf new file mode 100644 index 000000000..32515a6c6 --- /dev/null +++ b/modules/memorydb/variables.tf @@ -0,0 +1,132 @@ +variable "node_type" { + description = "The compute and memory capacity of the nodes in the cluster" + type = string + default = "db.r6g.large" + nullable = false +} + +variable "num_shards" { + description = "The number of shards in the cluster" + type = number + default = 1 + nullable = false +} + +variable "num_replicas_per_shard" { + description = "The number of replicas per shard" + type = number + default = 1 + nullable = false +} + +variable "tls_enabled" { + description = "Indicates whether Transport Layer Security (TLS) encryption is enabled for the cluster" + type = bool + default = true + nullable = false +} + +variable "engine_version" { + description = "The version of the Redis engine to use" + type = string + default = "6.2" + nullable = false +} + +variable "auto_minor_version_upgrade" { + description = "Indicates that minor engine upgrades will be applied automatically to the cluster during the maintenance window" + type = bool + default = true + nullable = false +} + +variable "security_group_ids" { + description = "List of security group IDs to associate with the MemoryDB cluster" + type = list(string) + default = [] + nullable = false +} + +variable "port" { + description = "The port on which the cluster accepts connections" + type = number + default = 6379 + nullable = false +} + +variable "maintenance_window" { + description = "The weekly time range during which system maintenance can occur" + type = string + default = null + nullable = true +} + +variable "snapshot_window" { + description = "The daily time range during which MemoryDB begins taking daily snapshots" + type = string + default = null + nullable = true +} + +variable "snapshot_retention_limit" { + description = "The number of days for which MemoryDB retains automatic snapshots before deleting them" + type = number + default = null + nullable = true +} + +variable "snapshot_arns" { + description = "List of ARNs for the snapshots to be restored. NOTE: destroys the existing cluster. Use for restoring." + type = list(string) + default = [] + nullable = false +} + +variable "admin_username" { + description = "The username for the MemoryDB admin" + type = string + default = "admin" + nullable = false +} + +variable "ssm_kms_key_id" { + description = "The KMS key ID to use for SSM parameter encryption. If not specified, the default key will be used." + type = string + default = null + nullable = true +} + +variable "ssm_parameter_name" { + description = "The name of the SSM parameter to store the password in. If not specified, the password will be stored in `/{context.id}/admin_password`" + type = string + default = "" + nullable = false +} + +variable "parameter_group_family" { + description = "The name of the parameter group family" + type = string + default = "memorydb_redis6" + nullable = false +} + +variable "parameters" { + description = "Key-value mapping of parameters to apply to the parameter group" + type = map(string) + default = {} + nullable = false +} + +variable "sns_topic_arn" { + description = "The ARN of the SNS topic to send notifications to" + type = string + default = null + nullable = true +} + +variable "vpc_component_name" { + description = "The name of the VPC component. This is used to pick out subnets for the MemoryDB cluster" + type = string + default = "vpc" + nullable = false +} diff --git a/modules/memorydb/versions.tf b/modules/memorydb/versions.tf new file mode 100644 index 000000000..6d7861d01 --- /dev/null +++ b/modules/memorydb/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} From 4cc206e2be5f43ffad58b2a2274897ce429d7e32 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Tue, 3 Sep 2024 11:02:10 -0400 Subject: [PATCH 476/501] Add `site-to-site-vpn` Terraform/OpenTofu component (#1106) --- modules/access-analyzer/README.md | 108 ++++----- modules/aws-config/README.md | 140 ++++++----- modules/aws-team-roles/README.md | 6 + modules/site-to-site-vpn/README.md | 236 ++++++++++++++++++ modules/site-to-site-vpn/context.tf | 279 ++++++++++++++++++++++ modules/site-to-site-vpn/main.tf | 84 +++++++ modules/site-to-site-vpn/outputs.tf | 50 ++++ modules/site-to-site-vpn/providers.tf | 19 ++ modules/site-to-site-vpn/remote-state.tf | 8 + modules/site-to-site-vpn/ssm.tf | 25 ++ modules/site-to-site-vpn/variables.tf | 292 +++++++++++++++++++++++ modules/site-to-site-vpn/versions.tf | 14 ++ modules/spacelift/spaces/README.md | 2 +- 13 files changed, 1136 insertions(+), 127 deletions(-) create mode 100644 modules/site-to-site-vpn/README.md create mode 100644 modules/site-to-site-vpn/context.tf create mode 100644 modules/site-to-site-vpn/main.tf create mode 100644 modules/site-to-site-vpn/outputs.tf create mode 100644 modules/site-to-site-vpn/providers.tf create mode 100644 modules/site-to-site-vpn/remote-state.tf create mode 100644 modules/site-to-site-vpn/ssm.tf create mode 100644 modules/site-to-site-vpn/variables.tf create mode 100644 modules/site-to-site-vpn/versions.tf diff --git a/modules/access-analyzer/README.md b/modules/access-analyzer/README.md index 86203888b..896e06095 100644 --- a/modules/access-analyzer/README.md +++ b/modules/access-analyzer/README.md @@ -109,79 +109,77 @@ atmos terraform apply access-analyzer/delegated-administrator -s plat-dev-use1-m ``` - ## Requirements -| Name | Version | -| ------------------------------------------------------------------------ | -------- | -| [terraform](#requirement_terraform) | >= 1.3.0 | -| [aws](#requirement_aws) | >= 4.9.0 | +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | ## Providers -| Name | Version | -| ------------------------------------------------ | -------- | -| [aws](#provider_aws) | >= 4.9.0 | +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | ## Modules -| Name | Source | Version | -| -------------------------------------------------------------------- | -------------------------------------------------- | ------- | -| [account_map](#module_account_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [iam_roles](#module_iam_roles) | ../../account-map/modules/iam-roles | n/a | -| [this](#module_this) | cloudposse/label/null | 0.25.0 | +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Resources -| Name | Type | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| [aws_accessanalyzer_analyzer.organization](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/accessanalyzer_analyzer) | resource | -| [aws_accessanalyzer_analyzer.organization_unused_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/accessanalyzer_analyzer) | resource | +| Name | Type | +|------|------| +| [aws_accessanalyzer_analyzer.organization](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/accessanalyzer_analyzer) | resource | +| [aws_accessanalyzer_analyzer.organization_unused_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/accessanalyzer_analyzer) | resource | | [aws_organizations_delegated_administrator.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_delegated_administrator) | resource | ## Inputs -| Name | Description | Type | Default | Required | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | -| [accessanalyzer_organization_enabled](#input_accessanalyzer_organization_enabled) | Flag to enable the Organization Access Analyzer | `bool` | n/a | yes | -| [accessanalyzer_organization_unused_access_enabled](#input_accessanalyzer_organization_unused_access_enabled) | Flag to enable the Organization unused access Access Analyzer | `bool` | n/a | yes | -| [accessanalyzer_service_principal](#input_accessanalyzer_service_principal) | The Access Analyzer service principal for which you want to make the member account a delegated administrator | `string` | `"access-analyzer.amazonaws.com"` | no | -| [account_map_tenant](#input_account_map_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | n/a | yes | -| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [context](#input_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [delegated_administrator_account_name](#input_delegated_administrator_account_name) | The name of the account that is the AWS Organization Delegated Administrator account | `string` | n/a | yes | -| [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [global_environment](#input_global_environment) | Global environment name | `string` | `"gbl"` | no | -| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | -| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | -| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | -| [namespace](#input_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [organization_management_account_name](#input_organization_management_account_name) | The name of the AWS Organization management account | `string` | `null` | no | -| [organizations_delegated_administrator_enabled](#input_organizations_delegated_administrator_enabled) | Flag to enable the Organization delegated administrator | `bool` | n/a | yes | -| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input_region) | AWS Region | `string` | n/a | yes | -| [root_account_stage](#input_root_account_stage) | The stage name for the Organization root (management) account. This is used to lookup account IDs from account names
using the `account-map` component. | `string` | `"root"` | no | -| [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [tenant](#input_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [unused_access_age](#input_unused_access_age) | The specified access age in days for which to generate findings for unused access | `number` | `30` | no | +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [accessanalyzer\_organization\_enabled](#input\_accessanalyzer\_organization\_enabled) | Flag to enable the Organization Access Analyzer | `bool` | n/a | yes | +| [accessanalyzer\_organization\_unused\_access\_enabled](#input\_accessanalyzer\_organization\_unused\_access\_enabled) | Flag to enable the Organization unused access Access Analyzer | `bool` | n/a | yes | +| [accessanalyzer\_service\_principal](#input\_accessanalyzer\_service\_principal) | The Access Analyzer service principal for which you want to make the member account a delegated administrator | `string` | `"access-analyzer.amazonaws.com"` | no | +| [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | n/a | yes | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delegated\_administrator\_account\_name](#input\_delegated\_administrator\_account\_name) | The name of the account that is the AWS Organization Delegated Administrator account | `string` | n/a | yes | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [organization\_management\_account\_name](#input\_organization\_management\_account\_name) | The name of the AWS Organization management account | `string` | `null` | no | +| [organizations\_delegated\_administrator\_enabled](#input\_organizations\_delegated\_administrator\_enabled) | Flag to enable the Organization delegated administrator | `bool` | n/a | yes | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (management) account. This is used to lookup account IDs from account names
using the `account-map` component. | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [unused\_access\_age](#input\_unused\_access\_age) | The specified access age in days for which to generate findings for unused access | `number` | `30` | no | ## Outputs -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -| [aws_organizations_delegated_administrator_id](#output_aws_organizations_delegated_administrator_id) | AWS Organizations Delegated Administrator ID | -| [aws_organizations_delegated_administrator_status](#output_aws_organizations_delegated_administrator_status) | AWS Organizations Delegated Administrator status | -| [organization_accessanalyzer_id](#output_organization_accessanalyzer_id) | Organization Access Analyzer ID | -| [organization_unused_access_accessanalyzer_id](#output_organization_unused_access_accessanalyzer_id) | Organization unused access Access Analyzer ID | - +| Name | Description | +|------|-------------| +| [aws\_organizations\_delegated\_administrator\_id](#output\_aws\_organizations\_delegated\_administrator\_id) | AWS Organizations Delegated Administrator ID | +| [aws\_organizations\_delegated\_administrator\_status](#output\_aws\_organizations\_delegated\_administrator\_status) | AWS Organizations Delegated Administrator status | +| [organization\_accessanalyzer\_id](#output\_organization\_accessanalyzer\_id) | Organization Access Analyzer ID | +| [organization\_unused\_access\_accessanalyzer\_id](#output\_organization\_unused\_access\_accessanalyzer\_id) | Organization unused access Access Analyzer ID | ## References diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index 20fbd35f3..9878ca4ef 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -168,95 +168,93 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} ``` - ## Requirements -| Name | Version | -| ------------------------------------------------------------------------ | --------- | -| [terraform](#requirement_terraform) | >= 1.0.0 | -| [aws](#requirement_aws) | >= 4.0 | -| [awsutils](#requirement_awsutils) | >= 0.16.0 | +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [awsutils](#requirement\_awsutils) | >= 0.16.0 | ## Providers -| Name | Version | -| ------------------------------------------------ | ------- | -| [aws](#provider_aws) | >= 4.0 | +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | ## Modules -| Name | Source | Version | -| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------- | -| [account_map](#module_account_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [aws_config](#module_aws_config) | cloudposse/config/aws | 1.1.0 | -| [aws_config_label](#module_aws_config_label) | cloudposse/label/null | 0.25.0 | -| [aws_team_roles](#module_aws_team_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [config_bucket](#module_config_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [conformance_pack](#module_conformance_pack) | cloudposse/config/aws//modules/conformance-pack | 1.1.0 | -| [global_collector_region](#module_global_collector_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [iam_roles](#module_iam_roles) | ../account-map/modules/iam-roles | n/a | -| [org_conformance_pack](#module_org_conformance_pack) | ./modules/org-conformance-pack | n/a | -| [this](#module_this) | cloudposse/label/null | 0.25.0 | -| [utils](#module_utils) | cloudposse/utils/aws | 1.3.0 | +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [aws\_config](#module\_aws\_config) | cloudposse/config/aws | 1.1.0 | +| [aws\_config\_label](#module\_aws\_config\_label) | cloudposse/label/null | 0.25.0 | +| [aws\_team\_roles](#module\_aws\_team\_roles) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [config\_bucket](#module\_config\_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [conformance\_pack](#module\_conformance\_pack) | cloudposse/config/aws//modules/conformance-pack | 1.1.0 | +| [global\_collector\_region](#module\_global\_collector\_region) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [org\_conformance\_pack](#module\_org\_conformance\_pack) | ./modules/org-conformance-pack | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 | ## Resources -| Name | Type | -| -------------------------------------------------------------------------------------------------------------------------- | ----------- | +| Name | Type | +|------|------| | [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | -| [aws_partition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | -| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [aws_partition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs -| Name | Description | Type | Default | Required | -| --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | -| [account_map_tenant](#input_account_map_tenant) | (Optional) The tenant where the account_map component required by remote-state is deployed. | `string` | `""` | no | -| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [az_abbreviation_type](#input_az_abbreviation_type) | AZ abbreviation type, `fixed` or `short` | `string` | `"fixed"` | no | -| [central_resource_collector_account](#input_central_resource_collector_account) | The name of the account that is the centralized aggregation account. | `string` | n/a | yes | -| [config_bucket_env](#input_config_bucket_env) | The environment of the AWS Config S3 Bucket | `string` | n/a | yes | -| [config_bucket_stage](#input_config_bucket_stage) | The stage of the AWS Config S3 Bucket | `string` | n/a | yes | -| [config_bucket_tenant](#input_config_bucket_tenant) | (Optional) The tenant of the AWS Config S3 Bucket | `string` | `""` | no | -| [conformance_packs](#input_conformance_packs) | List of conformance packs. Each conformance pack is a map with the following keys: name, conformance_pack, parameter_overrides.

For example:
conformance_packs = [
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level1"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level1.yaml"
parameter\_overrides = {
"AccessKeysRotatedParamMaxAccessKeyAge" = "45"
}
},
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level2"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml"
parameter\_overrides = {
"IamPasswordPolicyParamMaxPasswordAge" = "45"
}
}
]

Complete list of AWS Conformance Packs managed by AWSLabs can be found here:
https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs |
list(object({
name = string
conformance_pack = string
parameter_overrides = map(string)
scope = optional(string, null)
}))
| `[]` | no | -| [context](#input_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create_iam_role](#input_create_iam_role) | Flag to indicate whether an IAM Role should be created to grant the proper permissions for AWS Config | `bool` | `false` | no | -| [default_scope](#input_default_scope) | The default scope of the conformance pack. Valid values are `account` and `organization`. | `string` | `"account"` | no | -| [delegated_accounts](#input_delegated_accounts) | The account IDs of other accounts that will send their AWS Configuration or Security Hub data to this account | `set(string)` | `null` | no | -| [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [global_environment](#input_global_environment) | Global environment name | `string` | `"gbl"` | no | -| [global_resource_collector_region](#input_global_resource_collector_region) | The region that collects AWS Config data for global resources such as IAM | `string` | n/a | yes | -| [iam_role_arn](#input_iam_role_arn) | The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the
AWS resources associated with the account. This is only used if create_iam_role is false.

If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create_iam_role to `false`.

See the AWS Docs for further information:
http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html | `string` | `null` | no | -| [iam_roles_environment_name](#input_iam_roles_environment_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | -| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | -| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | -| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [managed_rules](#input_managed_rules) | A list of AWS Managed Rules that should be enabled on the account.

See the following for a list of possible rules to enable:
https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html

Example:
managed_rules = {
access-keys-rotated = {
identifier = "ACCESS_KEYS_ROTATED"
description = "Checks whether the active access keys are rotated within the number of days specified in maxAccessKeyAge. The rule is NON_COMPLIANT if the access keys have not been rotated for more than maxAccessKeyAge number of days."
input_parameters = {
maxAccessKeyAge : "90"
}
enabled = true
tags = {}
}
}
|
map(object({
description = string
identifier = string
input_parameters = any
tags = map(string)
enabled = bool
}))
| `{}` | no | -| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | -| [namespace](#input_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [privileged](#input_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | -| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input_region) | AWS Region | `string` | n/a | yes | -| [root_account_stage](#input_root_account_stage) | The stage name for the Organization root (master) account | `string` | `"root"` | no | -| [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [tenant](#input_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_map\_tenant](#input\_account\_map\_tenant) | (Optional) The tenant where the account\_map component required by remote-state is deployed. | `string` | `""` | no | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [az\_abbreviation\_type](#input\_az\_abbreviation\_type) | AZ abbreviation type, `fixed` or `short` | `string` | `"fixed"` | no | +| [central\_resource\_collector\_account](#input\_central\_resource\_collector\_account) | The name of the account that is the centralized aggregation account. | `string` | n/a | yes | +| [config\_bucket\_env](#input\_config\_bucket\_env) | The environment of the AWS Config S3 Bucket | `string` | n/a | yes | +| [config\_bucket\_stage](#input\_config\_bucket\_stage) | The stage of the AWS Config S3 Bucket | `string` | n/a | yes | +| [config\_bucket\_tenant](#input\_config\_bucket\_tenant) | (Optional) The tenant of the AWS Config S3 Bucket | `string` | `""` | no | +| [conformance\_packs](#input\_conformance\_packs) | List of conformance packs. Each conformance pack is a map with the following keys: name, conformance\_pack, parameter\_overrides.

For example:
conformance\_packs = [
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level1"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level1.yaml"
parameter\_overrides = {
"AccessKeysRotatedParamMaxAccessKeyAge" = "45"
}
},
{
name = "Operational-Best-Practices-for-CIS-AWS-v1.4-Level2"
conformance\_pack = "https://raw.githubusercontent.com/awslabs/aws-config-rules/master/aws-config-conformance-packs/Operational-Best-Practices-for-CIS-AWS-v1.4-Level2.yaml"
parameter\_overrides = {
"IamPasswordPolicyParamMaxPasswordAge" = "45"
}
}
]

Complete list of AWS Conformance Packs managed by AWSLabs can be found here:
https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs |
list(object({
name = string
conformance_pack = string
parameter_overrides = map(string)
scope = optional(string, null)
}))
| `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_iam\_role](#input\_create\_iam\_role) | Flag to indicate whether an IAM Role should be created to grant the proper permissions for AWS Config | `bool` | `false` | no | +| [default\_scope](#input\_default\_scope) | The default scope of the conformance pack. Valid values are `account` and `organization`. | `string` | `"account"` | no | +| [delegated\_accounts](#input\_delegated\_accounts) | The account IDs of other accounts that will send their AWS Configuration or Security Hub data to this account | `set(string)` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [global\_environment](#input\_global\_environment) | Global environment name | `string` | `"gbl"` | no | +| [global\_resource\_collector\_region](#input\_global\_resource\_collector\_region) | The region that collects AWS Config data for global resources such as IAM | `string` | n/a | yes | +| [iam\_role\_arn](#input\_iam\_role\_arn) | The ARN for an IAM Role AWS Config uses to make read or write requests to the delivery channel and to describe the
AWS resources associated with the account. This is only used if create\_iam\_role is false.

If you want to use an existing IAM Role, set the variable to the ARN of the existing role and set create\_iam\_role to `false`.

See the AWS Docs for further information:
http://docs.aws.amazon.com/config/latest/developerguide/iamrole-permissions.html | `string` | `null` | no | +| [iam\_roles\_environment\_name](#input\_iam\_roles\_environment\_name) | The name of the environment where the IAM roles are provisioned | `string` | `"gbl"` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [managed\_rules](#input\_managed\_rules) | A list of AWS Managed Rules that should be enabled on the account.

See the following for a list of possible rules to enable:
https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html

Example:
managed_rules = {
access-keys-rotated = {
identifier = "ACCESS_KEYS_ROTATED"
description = "Checks whether the active access keys are rotated within the number of days specified in maxAccessKeyAge. The rule is NON_COMPLIANT if the access keys have not been rotated for more than maxAccessKeyAge number of days."
input_parameters = {
maxAccessKeyAge : "90"
}
enabled = true
tags = {}
}
}
|
map(object({
description = string
identifier = string
input_parameters = any
tags = map(string)
enabled = bool
}))
| `{}` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [privileged](#input\_privileged) | True if the default provider already has access to the backend | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [root\_account\_stage](#input\_root\_account\_stage) | The stage name for the Organization root (master) account | `string` | `"root"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | ## Outputs -| Name | Description | -| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -| [aws_config_configuration_recorder_id](#output_aws_config_configuration_recorder_id) | The ID of the AWS Config Recorder | -| [aws_config_iam_role](#output_aws_config_iam_role) | The ARN of the IAM Role used for AWS Config | -| [storage_bucket_arn](#output_storage_bucket_arn) | Storage Config bucket ARN | -| [storage_bucket_id](#output_storage_bucket_id) | Storage Config bucket ID | - +| Name | Description | +|------|-------------| +| [aws\_config\_configuration\_recorder\_id](#output\_aws\_config\_configuration\_recorder\_id) | The ID of the AWS Config Recorder | +| [aws\_config\_iam\_role](#output\_aws\_config\_iam\_role) | The ARN of the IAM Role used for AWS Config | +| [storage\_bucket\_arn](#output\_storage\_bucket\_arn) | Storage Config bucket ARN | +| [storage\_bucket\_id](#output\_storage\_bucket\_id) | Storage Config bucket ID | ## References diff --git a/modules/aws-team-roles/README.md b/modules/aws-team-roles/README.md index 0109b2307..61a2b5c0b 100644 --- a/modules/aws-team-roles/README.md +++ b/modules/aws-team-roles/README.md @@ -197,12 +197,18 @@ components: | Name | Type | |------|------| | [aws_iam_policy.eks_viewer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.kms_planner](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.vpn_planner](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [local_file.account_info](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource | | [aws_iam_policy_document.assume_role_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.eks_view_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.eks_viewer_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.kms_planner_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.kms_planner_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.vpn_planner_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.vpn_planner_access_aggregated](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs diff --git a/modules/site-to-site-vpn/README.md b/modules/site-to-site-vpn/README.md new file mode 100644 index 000000000..3e432621c --- /dev/null +++ b/modules/site-to-site-vpn/README.md @@ -0,0 +1,236 @@ +--- +tags: + - component/site-to-site-vpn + - layer/network + - provider/aws +--- + +# Component: `site-to-site-vpn` + +This component provisions a [Site-To-Site VPN](https://aws.amazon.com/vpn/site-to-site-vpn/) with a +target AWS VPC on one side of the tunnel. +The other (customer) side can be any VPN gateway endpoint, e.g. a hardware device, other cloud VPN, etc. + +AWS Site-to-Site VPN is a fully-managed service that creates a secure connection between your data center or branch +office and your AWS resources using IP Security (IPSec) tunnels. When using Site-to-Site VPN, you can connect to both +your Amazon Virtual Private Clouds (VPC) and AWS Transit Gateway, and two tunnels per connection are used for +increased redundancy. + +The component provisions the following resources: + +- AWS Virtual Private Gateway (a representation of the AWS side of the tunnel) + +- AWS Customer Gateway (a representation of the other (remote) side of the tunnel). It requires: + - The gateway's Border Gateway Protocol (BGP) Autonomous System Number (ASN) + - `/32` IP of the VPN endpoint + +- AWS Site-To-Site VPN connection. It creates two VPN tunnels for redundancy and requires: + - The IP CIDR ranges on each side of the tunnel + - Pre-shared Keys for each tunnel (can be auto-generated if not provided and saved into SSM Parameter Store) + - (Optional) IP CIDR ranges to be used inside each VPN tunnel + +- Route table entries to direct the appropriate traffic from the local VPC to the other side of the tunnel + +## Post-tunnel creation requirements + +Once the site-to-site VPN resources are deployed, you need to send the VPN configuration +from AWS side to the administrator of the remote side of the VPN connection. To do this: + +1. Determine the infrastructure that will be used for the remote side, specifically: + +- Vendor +- Platform +- Software Version +- IKE version + +1. Log into the target AWS account +1. Go to the "VPC" console +1. On the left navigation manu, go to `Virtual Private Network` > `Site-to-Site VPN Connections` +1. Select the VPN connection that was created via this component +1. On the top right, click the `Download Configuration` button +1. Enter the information you obtained and click `Download` +1. Send the configuration file to the administrator of the remote side of the tunnel + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + site-to-site-vpn: + metadata: + component: site-to-site-vpn + vars: + enabled: true + name: "site-to-site-vpn" + vpc_component_name: vpc + customer_gateway_bgp_asn: 65000 + customer_gateway_ip_address: 20.200.30.0 + vpn_gateway_amazon_side_asn: 64512 + vpn_connection_static_routes_only: true + vpn_connection_tunnel1_inside_cidr: 169.254.20.0/30 + vpn_connection_tunnel2_inside_cidr: 169.254.21.0/30 + vpn_connection_local_ipv4_network_cidr: 10.100.128.0/24 + vpn_connection_remote_ipv4_network_cidr: 10.10.80.0/24 + vpn_connection_static_routes_destinations: + - 10.100.128.0/24 + vpn_connection_tunnel1_startup_action: add + vpn_connection_tunnel2_startup_action: add + transit_gateway_enabled: false + vpn_connection_tunnel1_cloudwatch_log_enabled: false + vpn_connection_tunnel2_cloudwatch_log_enabled: false + preshared_key_enabled: true + ssm_enabled: true + ssm_path_prefix: "/site-to-site-vpn" +````` + +## Amazon side Autonomous System Number (ASN) + +The variable `vpn_gateway_amazon_side_asn` (Amazon side Autonomous System Number) is not strictly required when creating +an AWS VPN Gateway. If you do not specify Amazon side ASN during the creation of the VPN Gateway, AWS will automatically +assign a default ASN (which is 7224 for the Amazon side of the VPN). + +However, specifying Amazon side ASN can be important if you need to integrate the VPN with an on-premises network that +uses Border Gateway Protocol (BGP) and you want to avoid ASN conflicts or require a specific ASN for routing policies. + +If your use case involves BGP peering, and you need a specific ASN for the Amazon side, then you should explicitly set +the `vpn_gateway_amazon_side_asn`. Otherwise, it can be omitted (set to `null`), and AWS will handle it automatically. + +## Provisioning + +Provision the `site-to-site-vpn` component by executing the following commands: + +```sh +atmos terraform plan site-to-site-vpn -s +atmos terraform apply site-to-site-vpn -s +``` + +## References + +- https://aws.amazon.com/vpn/site-to-site-vpn +- https://docs.aws.amazon.com/vpn/latest/s2svpn/VPC_VPN.html +- https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_VpnTunnelOptionsSpecification.html + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [random](#requirement\_random) | >= 2.2 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [random](#provider\_random) | >= 2.2 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [site\_to\_site\_vpn](#module\_site\_to\_site\_vpn) | cloudposse/vpn-connection/aws | 1.3.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ssm_parameter.tunnel1_preshared_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.tunnel2_preshared_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [random_password.tunnel1_preshared_key](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | +| [random_password.tunnel2_preshared_key](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [customer\_gateway\_bgp\_asn](#input\_customer\_gateway\_bgp\_asn) | The Customer Gateway's Border Gateway Protocol (BGP) Autonomous System Number (ASN) | `number` | n/a | yes | +| [customer\_gateway\_ip\_address](#input\_customer\_gateway\_ip\_address) | The IPv4 address for the Customer Gateway device's outside interface. Set to `null` to not create the Customer Gateway | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [existing\_transit\_gateway\_id](#input\_existing\_transit\_gateway\_id) | Existing Transit Gateway ID. If provided, the module will not create a Virtual Private Gateway but instead will use the transit\_gateway. For setting up transit gateway we can use the cloudposse/transit-gateway/aws module and pass the output transit\_gateway\_id to this variable | `string` | `""` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [preshared\_key\_enabled](#input\_preshared\_key\_enabled) | Flag to enable adding the preshared keys to the VPN connection | `bool` | `true` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [ssm\_enabled](#input\_ssm\_enabled) | Flag to enable saving the `tunnel1_preshared_key` and `tunnel2_preshared_key` in the SSM Parameter Store | `bool` | `false` | no | +| [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | SSM Key path prefix for the associated SSM parameters | `string` | `""` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [transit\_gateway\_enabled](#input\_transit\_gateway\_enabled) | Set to true to enable VPN connection to transit gateway and then pass in the existing\_transit\_gateway\_id | `bool` | `false` | no | +| [transit\_gateway\_route\_table\_id](#input\_transit\_gateway\_route\_table\_id) | The ID of the route table for the transit gateway that you want to associate + propagate the VPN connection's TGW attachment | `string` | `null` | no | +| [transit\_gateway\_routes](#input\_transit\_gateway\_routes) | A map of transit gateway routes to create on the given TGW route table (via `transit_gateway_route_table_id`) for the created VPN Attachment. Use the key in the map to describe the route |
map(object({
blackhole = optional(bool, false)
destination_cidr_block = string
}))
| `{}` | no | +| [vpc\_component\_name](#input\_vpc\_component\_name) | Atmos VPC component name | `string` | `"vpc"` | no | +| [vpn\_connection\_local\_ipv4\_network\_cidr](#input\_vpn\_connection\_local\_ipv4\_network\_cidr) | The IPv4 CIDR on the Customer Gateway (on-premises) side of the VPN connection | `string` | `"0.0.0.0/0"` | no | +| [vpn\_connection\_log\_retention\_in\_days](#input\_vpn\_connection\_log\_retention\_in\_days) | Specifies the number of days you want to retain log events | `number` | `30` | no | +| [vpn\_connection\_remote\_ipv4\_network\_cidr](#input\_vpn\_connection\_remote\_ipv4\_network\_cidr) | The IPv4 CIDR on the AWS side of the VPN connection | `string` | `"0.0.0.0/0"` | no | +| [vpn\_connection\_static\_routes\_destinations](#input\_vpn\_connection\_static\_routes\_destinations) | List of CIDR blocks to be used as destination for static routes. Routes to destinations will be propagated to the VPC route tables | `list(string)` | `[]` | no | +| [vpn\_connection\_static\_routes\_only](#input\_vpn\_connection\_static\_routes\_only) | If set to `true`, the VPN connection will use static routes exclusively. Static routes must be used for devices that don't support BGP | `bool` | `false` | no | +| [vpn\_connection\_tunnel1\_cloudwatch\_log\_enabled](#input\_vpn\_connection\_tunnel1\_cloudwatch\_log\_enabled) | Enable or disable VPN tunnel logging feature for the tunnel | `bool` | `false` | no | +| [vpn\_connection\_tunnel1\_cloudwatch\_log\_output\_format](#input\_vpn\_connection\_tunnel1\_cloudwatch\_log\_output\_format) | Set log format for the tunnel. Default format is json. Possible values are `json` and `text` | `string` | `"json"` | no | +| [vpn\_connection\_tunnel1\_dpd\_timeout\_action](#input\_vpn\_connection\_tunnel1\_dpd\_timeout\_action) | The action to take after DPD timeout occurs for the first VPN tunnel. Specify restart to restart the IKE initiation. Specify `clear` to end the IKE session. Valid values are `clear` \| `none` \| `restart` | `string` | `"clear"` | no | +| [vpn\_connection\_tunnel1\_ike\_versions](#input\_vpn\_connection\_tunnel1\_ike\_versions) | The IKE versions that are permitted for the first VPN tunnel. Valid values are ikev1 \| ikev2 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel1\_inside\_cidr](#input\_vpn\_connection\_tunnel1\_inside\_cidr) | The CIDR block of the inside IP addresses for the first VPN tunnel | `string` | `null` | no | +| [vpn\_connection\_tunnel1\_phase1\_dh\_group\_numbers](#input\_vpn\_connection\_tunnel1\_phase1\_dh\_group\_numbers) | List of one or more Diffie-Hellman group numbers that are permitted for the first VPN tunnel for phase 1 IKE negotiations. Valid values are 2 \| 5 \| 14 \| 15 \| 16 \| 17 \| 18 \| 19 \| 20 \| 21 \| 22 \| 23 \| 24 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel1\_phase1\_encryption\_algorithms](#input\_vpn\_connection\_tunnel1\_phase1\_encryption\_algorithms) | List of one or more encryption algorithms that are permitted for the first VPN tunnel for phase 1 IKE negotiations. Valid values are AES128 \| AES256 \| AES128-GCM-16 \| AES256-GCM-16 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel1\_phase1\_integrity\_algorithms](#input\_vpn\_connection\_tunnel1\_phase1\_integrity\_algorithms) | One or more integrity algorithms that are permitted for the first VPN tunnel for phase 1 IKE negotiations. Valid values are SHA1 \| SHA2-256 \| SHA2-384 \| SHA2-512 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel1\_phase2\_dh\_group\_numbers](#input\_vpn\_connection\_tunnel1\_phase2\_dh\_group\_numbers) | List of one or more Diffie-Hellman group numbers that are permitted for the first VPN tunnel for phase 2 IKE negotiations. Valid values are 2 \| 5 \| 14 \| 15 \| 16 \| 17 \| 18 \| 19 \| 20 \| 21 \| 22 \| 23 \| 24 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel1\_phase2\_encryption\_algorithms](#input\_vpn\_connection\_tunnel1\_phase2\_encryption\_algorithms) | List of one or more encryption algorithms that are permitted for the first VPN tunnel for phase 2 IKE negotiations. Valid values are AES128 \| AES256 \| AES128-GCM-16 \| AES256-GCM-16 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel1\_phase2\_integrity\_algorithms](#input\_vpn\_connection\_tunnel1\_phase2\_integrity\_algorithms) | One or more integrity algorithms that are permitted for the first VPN tunnel for phase 2 IKE negotiations. Valid values are SHA1 \| SHA2-256 \| SHA2-384 \| SHA2-512 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel1\_preshared\_key](#input\_vpn\_connection\_tunnel1\_preshared\_key) | The preshared key of the first VPN tunnel. The preshared key must be between 8 and 64 characters in length and cannot start with zero. Allowed characters are alphanumeric characters, periods(.) and underscores(\_) | `string` | `null` | no | +| [vpn\_connection\_tunnel1\_startup\_action](#input\_vpn\_connection\_tunnel1\_startup\_action) | The action to take when the establishing the tunnel for the first VPN connection. By default, your customer gateway device must initiate the IKE negotiation and bring up the tunnel. Specify `start` for AWS to initiate the IKE negotiation. Valid values are `add` \| `start` | `string` | `"add"` | no | +| [vpn\_connection\_tunnel2\_cloudwatch\_log\_enabled](#input\_vpn\_connection\_tunnel2\_cloudwatch\_log\_enabled) | Enable or disable VPN tunnel logging feature for the tunnel | `bool` | `false` | no | +| [vpn\_connection\_tunnel2\_cloudwatch\_log\_output\_format](#input\_vpn\_connection\_tunnel2\_cloudwatch\_log\_output\_format) | Set log format for the tunnel. Default format is json. Possible values are `json` and `text` | `string` | `"json"` | no | +| [vpn\_connection\_tunnel2\_dpd\_timeout\_action](#input\_vpn\_connection\_tunnel2\_dpd\_timeout\_action) | The action to take after DPD timeout occurs for the second VPN tunnel. Specify restart to restart the IKE initiation. Specify clear to end the IKE session. Valid values are `clear` \| `none` \| `restart` | `string` | `"clear"` | no | +| [vpn\_connection\_tunnel2\_ike\_versions](#input\_vpn\_connection\_tunnel2\_ike\_versions) | The IKE versions that are permitted for the second VPN tunnel. Valid values are ikev1 \| ikev2 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel2\_inside\_cidr](#input\_vpn\_connection\_tunnel2\_inside\_cidr) | The CIDR block of the inside IP addresses for the second VPN tunnel | `string` | `null` | no | +| [vpn\_connection\_tunnel2\_phase1\_dh\_group\_numbers](#input\_vpn\_connection\_tunnel2\_phase1\_dh\_group\_numbers) | List of one or more Diffie-Hellman group numbers that are permitted for the second VPN tunnel for phase 1 IKE negotiations. Valid values are 2 \| 5 \| 14 \| 15 \| 16 \| 17 \| 18 \| 19 \| 20 \| 21 \| 22 \| 23 \| 24 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel2\_phase1\_encryption\_algorithms](#input\_vpn\_connection\_tunnel2\_phase1\_encryption\_algorithms) | List of one or more encryption algorithms that are permitted for the second VPN tunnel for phase 1 IKE negotiations. Valid values are AES128 \| AES256 \| AES128-GCM-16 \| AES256-GCM-16 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel2\_phase1\_integrity\_algorithms](#input\_vpn\_connection\_tunnel2\_phase1\_integrity\_algorithms) | One or more integrity algorithms that are permitted for the second VPN tunnel for phase 1 IKE negotiations. Valid values are SHA1 \| SHA2-256 \| SHA2-384 \| SHA2-512 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel2\_phase2\_dh\_group\_numbers](#input\_vpn\_connection\_tunnel2\_phase2\_dh\_group\_numbers) | List of one or more Diffie-Hellman group numbers that are permitted for the second VPN tunnel for phase 2 IKE negotiations. Valid values are 2 \| 5 \| 14 \| 15 \| 16 \| 17 \| 18 \| 19 \| 20 \| 21 \| 22 \| 23 \| 24 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel2\_phase2\_encryption\_algorithms](#input\_vpn\_connection\_tunnel2\_phase2\_encryption\_algorithms) | List of one or more encryption algorithms that are permitted for the second VPN tunnel for phase 2 IKE negotiations. Valid values are AES128 \| AES256 \| AES128-GCM-16 \| AES256-GCM-16 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel2\_phase2\_integrity\_algorithms](#input\_vpn\_connection\_tunnel2\_phase2\_integrity\_algorithms) | One or more integrity algorithms that are permitted for the second VPN tunnel for phase 2 IKE negotiations. Valid values are SHA1 \| SHA2-256 \| SHA2-384 \| SHA2-512 | `list(string)` | `[]` | no | +| [vpn\_connection\_tunnel2\_preshared\_key](#input\_vpn\_connection\_tunnel2\_preshared\_key) | The preshared key of the second VPN tunnel. The preshared key must be between 8 and 64 characters in length and cannot start with zero. Allowed characters are alphanumeric characters, periods(.) and underscores(\_) | `string` | `null` | no | +| [vpn\_connection\_tunnel2\_startup\_action](#input\_vpn\_connection\_tunnel2\_startup\_action) | The action to take when the establishing the tunnel for the second VPN connection. By default, your customer gateway device must initiate the IKE negotiation and bring up the tunnel. Specify `start` for AWS to initiate the IKE negotiation. Valid values are `add` \| `start` | `string` | `"add"` | no | +| [vpn\_gateway\_amazon\_side\_asn](#input\_vpn\_gateway\_amazon\_side\_asn) | The Autonomous System Number (ASN) for the Amazon side of the VPN Gateway. If you don't specify an ASN, the Virtual Private Gateway is created with the default ASN | `number` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [customer\_gateway\_id](#output\_customer\_gateway\_id) | Customer Gateway ID | +| [vpn\_connection\_customer\_gateway\_configuration](#output\_vpn\_connection\_customer\_gateway\_configuration) | The configuration information for the VPN connection's Customer Gateway (in the native XML format) | +| [vpn\_connection\_id](#output\_vpn\_connection\_id) | VPN Connection ID | +| [vpn\_connection\_tunnel1\_address](#output\_vpn\_connection\_tunnel1\_address) | The public IP address of the first VPN tunnel | +| [vpn\_connection\_tunnel1\_cgw\_inside\_address](#output\_vpn\_connection\_tunnel1\_cgw\_inside\_address) | The RFC 6890 link-local address of the first VPN tunnel (Customer Gateway side) | +| [vpn\_connection\_tunnel1\_vgw\_inside\_address](#output\_vpn\_connection\_tunnel1\_vgw\_inside\_address) | The RFC 6890 link-local address of the first VPN tunnel (Virtual Private Gateway side) | +| [vpn\_connection\_tunnel2\_address](#output\_vpn\_connection\_tunnel2\_address) | The public IP address of the second VPN tunnel | +| [vpn\_connection\_tunnel2\_cgw\_inside\_address](#output\_vpn\_connection\_tunnel2\_cgw\_inside\_address) | The RFC 6890 link-local address of the second VPN tunnel (Customer Gateway side) | +| [vpn\_connection\_tunnel2\_vgw\_inside\_address](#output\_vpn\_connection\_tunnel2\_vgw\_inside\_address) | The RFC 6890 link-local address of the second VPN tunnel (Virtual Private Gateway side) | +| [vpn\_gateway\_id](#output\_vpn\_gateway\_id) | Virtual Private Gateway ID | + + + +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/site-to-site-vpn) - + Cloud Posse's upstream component + +[](https://cpco.io/component) diff --git a/modules/site-to-site-vpn/context.tf b/modules/site-to-site-vpn/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/site-to-site-vpn/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/site-to-site-vpn/main.tf b/modules/site-to-site-vpn/main.tf new file mode 100644 index 000000000..e930bb5af --- /dev/null +++ b/modules/site-to-site-vpn/main.tf @@ -0,0 +1,84 @@ +locals { + enabled = module.this.enabled + vpc_outputs = module.vpc.outputs + + preshared_key_enabled = local.enabled && var.preshared_key_enabled + + tunnel1_preshared_key = local.preshared_key_enabled ? ( + length(var.vpn_connection_tunnel1_preshared_key) > 0 ? var.vpn_connection_tunnel1_preshared_key : + one(random_password.tunnel1_preshared_key[*].result) + ) : null + + tunnel2_preshared_key = local.preshared_key_enabled ? ( + length(var.vpn_connection_tunnel2_preshared_key) > 0 ? var.vpn_connection_tunnel2_preshared_key : + one(random_password.tunnel2_preshared_key[*].result) + ) : null +} + +module "site_to_site_vpn" { + source = "cloudposse/vpn-connection/aws" + version = "1.3.0" + + vpc_id = local.vpc_outputs.vpc_id + vpn_gateway_amazon_side_asn = var.vpn_gateway_amazon_side_asn + customer_gateway_bgp_asn = var.customer_gateway_bgp_asn + customer_gateway_ip_address = var.customer_gateway_ip_address + route_table_ids = local.vpc_outputs.private_route_table_ids + vpn_connection_static_routes_only = var.vpn_connection_static_routes_only + vpn_connection_static_routes_destinations = var.vpn_connection_static_routes_destinations + vpn_connection_tunnel1_inside_cidr = var.vpn_connection_tunnel1_inside_cidr + vpn_connection_tunnel2_inside_cidr = var.vpn_connection_tunnel2_inside_cidr + vpn_connection_tunnel1_preshared_key = local.tunnel1_preshared_key + vpn_connection_tunnel2_preshared_key = local.tunnel2_preshared_key + vpn_connection_local_ipv4_network_cidr = var.vpn_connection_local_ipv4_network_cidr + vpn_connection_remote_ipv4_network_cidr = var.vpn_connection_remote_ipv4_network_cidr + vpn_connection_tunnel1_ike_versions = var.vpn_connection_tunnel1_ike_versions + vpn_connection_tunnel2_ike_versions = var.vpn_connection_tunnel2_ike_versions + vpn_connection_tunnel1_phase1_encryption_algorithms = var.vpn_connection_tunnel1_phase1_encryption_algorithms + vpn_connection_tunnel1_phase2_encryption_algorithms = var.vpn_connection_tunnel1_phase2_encryption_algorithms + vpn_connection_tunnel1_phase1_integrity_algorithms = var.vpn_connection_tunnel1_phase1_integrity_algorithms + vpn_connection_tunnel1_phase2_integrity_algorithms = var.vpn_connection_tunnel1_phase2_integrity_algorithms + vpn_connection_tunnel2_phase1_encryption_algorithms = var.vpn_connection_tunnel2_phase1_encryption_algorithms + vpn_connection_tunnel2_phase2_encryption_algorithms = var.vpn_connection_tunnel2_phase2_encryption_algorithms + vpn_connection_tunnel2_phase1_integrity_algorithms = var.vpn_connection_tunnel2_phase1_integrity_algorithms + vpn_connection_tunnel2_phase2_integrity_algorithms = var.vpn_connection_tunnel2_phase2_integrity_algorithms + vpn_connection_tunnel1_phase1_dh_group_numbers = var.vpn_connection_tunnel1_phase1_dh_group_numbers + vpn_connection_tunnel1_phase2_dh_group_numbers = var.vpn_connection_tunnel1_phase2_dh_group_numbers + vpn_connection_tunnel2_phase1_dh_group_numbers = var.vpn_connection_tunnel2_phase1_dh_group_numbers + vpn_connection_tunnel2_phase2_dh_group_numbers = var.vpn_connection_tunnel2_phase2_dh_group_numbers + vpn_connection_tunnel1_startup_action = var.vpn_connection_tunnel1_startup_action + vpn_connection_tunnel2_startup_action = var.vpn_connection_tunnel2_startup_action + vpn_connection_log_retention_in_days = var.vpn_connection_log_retention_in_days + vpn_connection_tunnel1_dpd_timeout_action = var.vpn_connection_tunnel1_dpd_timeout_action + vpn_connection_tunnel2_dpd_timeout_action = var.vpn_connection_tunnel2_dpd_timeout_action + vpn_connection_tunnel1_cloudwatch_log_enabled = var.vpn_connection_tunnel1_cloudwatch_log_enabled + vpn_connection_tunnel2_cloudwatch_log_enabled = var.vpn_connection_tunnel2_cloudwatch_log_enabled + vpn_connection_tunnel1_cloudwatch_log_output_format = var.vpn_connection_tunnel1_cloudwatch_log_output_format + vpn_connection_tunnel2_cloudwatch_log_output_format = var.vpn_connection_tunnel2_cloudwatch_log_output_format + transit_gateway_enabled = var.transit_gateway_enabled + existing_transit_gateway_id = var.existing_transit_gateway_id + transit_gateway_route_table_id = var.transit_gateway_route_table_id + transit_gateway_routes = var.transit_gateway_routes + + context = module.this.context +} + +resource "random_password" "tunnel1_preshared_key" { + count = local.preshared_key_enabled && length(var.vpn_connection_tunnel1_preshared_key) == 0 ? 1 : 0 + + length = 60 + # Leave special characters out to avoid quoting and other issues. + # Special characters have no additional security compared to increasing length. + special = false + override_special = "!#$%^&*()<>-_" +} + +resource "random_password" "tunnel2_preshared_key" { + count = local.preshared_key_enabled && length(var.vpn_connection_tunnel2_preshared_key) == 0 ? 1 : 0 + + length = 60 + # Leave special characters out to avoid quoting and other issues. + # Special characters have no additional security compared to increasing length. + special = false + override_special = "!#$%^&*()<>-_" +} diff --git a/modules/site-to-site-vpn/outputs.tf b/modules/site-to-site-vpn/outputs.tf new file mode 100644 index 000000000..ab2251f39 --- /dev/null +++ b/modules/site-to-site-vpn/outputs.tf @@ -0,0 +1,50 @@ +output "vpn_gateway_id" { + description = "Virtual Private Gateway ID" + value = module.vpn_connection.vpn_connection_id +} + +output "customer_gateway_id" { + description = "Customer Gateway ID" + value = module.vpn_connection.customer_gateway_id +} + +output "vpn_connection_id" { + description = "VPN Connection ID" + value = module.vpn_connection.vpn_connection_id +} + +output "vpn_connection_customer_gateway_configuration" { + description = "The configuration information for the VPN connection's Customer Gateway (in the native XML format)" + sensitive = true + value = module.vpn_connection.vpn_connection_customer_gateway_configuration +} + +output "vpn_connection_tunnel1_address" { + description = "The public IP address of the first VPN tunnel" + value = module.vpn_connection.vpn_connection_tunnel1_address +} + +output "vpn_connection_tunnel1_cgw_inside_address" { + description = "The RFC 6890 link-local address of the first VPN tunnel (Customer Gateway side)" + value = module.vpn_connection.vpn_connection_tunnel1_cgw_inside_address +} + +output "vpn_connection_tunnel1_vgw_inside_address" { + description = "The RFC 6890 link-local address of the first VPN tunnel (Virtual Private Gateway side)" + value = module.vpn_connection.vpn_connection_tunnel1_vgw_inside_address +} + +output "vpn_connection_tunnel2_address" { + description = "The public IP address of the second VPN tunnel" + value = module.vpn_connection.vpn_connection_tunnel2_address +} + +output "vpn_connection_tunnel2_cgw_inside_address" { + description = "The RFC 6890 link-local address of the second VPN tunnel (Customer Gateway side)" + value = module.vpn_connection.vpn_connection_tunnel2_cgw_inside_address +} + +output "vpn_connection_tunnel2_vgw_inside_address" { + description = "The RFC 6890 link-local address of the second VPN tunnel (Virtual Private Gateway side)" + value = module.vpn_connection.vpn_connection_tunnel2_vgw_inside_address +} diff --git a/modules/site-to-site-vpn/providers.tf b/modules/site-to-site-vpn/providers.tf new file mode 100644 index 000000000..ef923e10a --- /dev/null +++ b/modules/site-to-site-vpn/providers.tf @@ -0,0 +1,19 @@ +provider "aws" { + region = var.region + + # Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null. + profile = module.iam_roles.terraform_profile_name + + dynamic "assume_role" { + # module.iam_roles.terraform_role_arn may be null, in which case do not assume a role. + for_each = compact([module.iam_roles.terraform_role_arn]) + content { + role_arn = assume_role.value + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} diff --git a/modules/site-to-site-vpn/remote-state.tf b/modules/site-to-site-vpn/remote-state.tf new file mode 100644 index 000000000..4e2391525 --- /dev/null +++ b/modules/site-to-site-vpn/remote-state.tf @@ -0,0 +1,8 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.vpc_component_name + + context = module.this.context +} diff --git a/modules/site-to-site-vpn/ssm.tf b/modules/site-to-site-vpn/ssm.tf new file mode 100644 index 000000000..281dfee89 --- /dev/null +++ b/modules/site-to-site-vpn/ssm.tf @@ -0,0 +1,25 @@ +locals { + ssm_enabled = local.enabled && var.ssm_enabled +} + +resource "aws_ssm_parameter" "tunnel1_preshared_key" { + count = local.ssm_enabled && local.preshared_key_enabled ? 0 : 1 + + name = format("%s/%s", var.ssm_path_prefix, "tunnel1_preshared_key") + value = local.tunnel1_preshared_key + description = format("Preshared Key for Tunnel1 in the %s Site-to-Site VPN connection", module.this.id) + type = "SecureString" + + tags = module.this.tags +} + +resource "aws_ssm_parameter" "tunnel2_preshared_key" { + count = local.ssm_enabled && local.preshared_key_enabled ? 0 : 1 + + name = format("%s/%s", var.ssm_path_prefix, "tunnel2_preshared_key") + value = local.tunnel2_preshared_key + description = format("Preshared Key for Tunnel2 in the %s Site-to-Site VPN connection", module.this.id) + type = "SecureString" + + tags = module.this.tags +} diff --git a/modules/site-to-site-vpn/variables.tf b/modules/site-to-site-vpn/variables.tf new file mode 100644 index 000000000..6ef29e23e --- /dev/null +++ b/modules/site-to-site-vpn/variables.tf @@ -0,0 +1,292 @@ +variable "region" { + type = string + description = "AWS Region" + nullable = false +} + +variable "vpc_component_name" { + type = string + description = "Atmos VPC component name" + default = "vpc" + nullable = false +} + +variable "customer_gateway_bgp_asn" { + type = number + description = "The Customer Gateway's Border Gateway Protocol (BGP) Autonomous System Number (ASN)" + nullable = false +} + +variable "customer_gateway_ip_address" { + type = string + description = "The IPv4 address for the Customer Gateway device's outside interface. Set to `null` to not create the Customer Gateway" + default = null +} + +variable "vpn_gateway_amazon_side_asn" { + type = number + description = "The Autonomous System Number (ASN) for the Amazon side of the VPN Gateway. If you don't specify an ASN, the Virtual Private Gateway is created with the default ASN" + default = null + nullable = false +} + +variable "vpn_connection_static_routes_only" { + type = bool + description = "If set to `true`, the VPN connection will use static routes exclusively. Static routes must be used for devices that don't support BGP" + default = false + nullable = false +} + +variable "vpn_connection_static_routes_destinations" { + type = list(string) + description = "List of CIDR blocks to be used as destination for static routes. Routes to destinations will be propagated to the VPC route tables" + default = [] + nullable = false +} + +variable "vpn_connection_local_ipv4_network_cidr" { + type = string + description = "The IPv4 CIDR on the Customer Gateway (on-premises) side of the VPN connection" + default = "0.0.0.0/0" +} + +variable "vpn_connection_remote_ipv4_network_cidr" { + type = string + description = "The IPv4 CIDR on the AWS side of the VPN connection" + default = "0.0.0.0/0" +} + +variable "vpn_connection_log_retention_in_days" { + type = number + description = "Specifies the number of days you want to retain log events" + default = 30 + nullable = false +} + +variable "vpn_connection_tunnel1_dpd_timeout_action" { + type = string + description = "The action to take after DPD timeout occurs for the first VPN tunnel. Specify restart to restart the IKE initiation. Specify `clear` to end the IKE session. Valid values are `clear` | `none` | `restart`" + default = "clear" + nullable = false +} + +variable "vpn_connection_tunnel1_ike_versions" { + type = list(string) + description = "The IKE versions that are permitted for the first VPN tunnel. Valid values are ikev1 | ikev2" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel1_inside_cidr" { + type = string + description = "The CIDR block of the inside IP addresses for the first VPN tunnel" + default = null +} + +variable "vpn_connection_tunnel1_phase1_encryption_algorithms" { + type = list(string) + description = "List of one or more encryption algorithms that are permitted for the first VPN tunnel for phase 1 IKE negotiations. Valid values are AES128 | AES256 | AES128-GCM-16 | AES256-GCM-16" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel1_phase2_encryption_algorithms" { + type = list(string) + description = "List of one or more encryption algorithms that are permitted for the first VPN tunnel for phase 2 IKE negotiations. Valid values are AES128 | AES256 | AES128-GCM-16 | AES256-GCM-16" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel1_phase1_integrity_algorithms" { + type = list(string) + description = "One or more integrity algorithms that are permitted for the first VPN tunnel for phase 1 IKE negotiations. Valid values are SHA1 | SHA2-256 | SHA2-384 | SHA2-512" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel1_phase2_integrity_algorithms" { + type = list(string) + description = "One or more integrity algorithms that are permitted for the first VPN tunnel for phase 2 IKE negotiations. Valid values are SHA1 | SHA2-256 | SHA2-384 | SHA2-512" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel1_phase1_dh_group_numbers" { + type = list(string) + description = "List of one or more Diffie-Hellman group numbers that are permitted for the first VPN tunnel for phase 1 IKE negotiations. Valid values are 2 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel1_phase2_dh_group_numbers" { + type = list(string) + description = "List of one or more Diffie-Hellman group numbers that are permitted for the first VPN tunnel for phase 2 IKE negotiations. Valid values are 2 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel1_preshared_key" { + type = string + description = "The preshared key of the first VPN tunnel. The preshared key must be between 8 and 64 characters in length and cannot start with zero. Allowed characters are alphanumeric characters, periods(.) and underscores(_)" + default = null +} + +variable "vpn_connection_tunnel1_startup_action" { + type = string + description = "The action to take when the establishing the tunnel for the first VPN connection. By default, your customer gateway device must initiate the IKE negotiation and bring up the tunnel. Specify `start` for AWS to initiate the IKE negotiation. Valid values are `add` | `start`" + default = "add" + nullable = false +} + +variable "vpn_connection_tunnel1_cloudwatch_log_enabled" { + type = bool + description = "Enable or disable VPN tunnel logging feature for the tunnel" + default = false + nullable = false +} + +variable "vpn_connection_tunnel1_cloudwatch_log_output_format" { + type = string + description = "Set log format for the tunnel. Default format is json. Possible values are `json` and `text`" + default = "json" + nullable = false +} + +variable "vpn_connection_tunnel2_dpd_timeout_action" { + type = string + description = "The action to take after DPD timeout occurs for the second VPN tunnel. Specify restart to restart the IKE initiation. Specify clear to end the IKE session. Valid values are `clear` | `none` | `restart`" + default = "clear" + nullable = false +} + +variable "vpn_connection_tunnel2_ike_versions" { + type = list(string) + description = "The IKE versions that are permitted for the second VPN tunnel. Valid values are ikev1 | ikev2" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel2_inside_cidr" { + type = string + description = "The CIDR block of the inside IP addresses for the second VPN tunnel" + default = null +} + +variable "vpn_connection_tunnel2_phase1_encryption_algorithms" { + type = list(string) + description = "List of one or more encryption algorithms that are permitted for the second VPN tunnel for phase 1 IKE negotiations. Valid values are AES128 | AES256 | AES128-GCM-16 | AES256-GCM-16" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel2_phase2_encryption_algorithms" { + type = list(string) + description = "List of one or more encryption algorithms that are permitted for the second VPN tunnel for phase 2 IKE negotiations. Valid values are AES128 | AES256 | AES128-GCM-16 | AES256-GCM-16" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel2_phase1_integrity_algorithms" { + type = list(string) + description = "One or more integrity algorithms that are permitted for the second VPN tunnel for phase 1 IKE negotiations. Valid values are SHA1 | SHA2-256 | SHA2-384 | SHA2-512" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel2_phase2_integrity_algorithms" { + type = list(string) + description = "One or more integrity algorithms that are permitted for the second VPN tunnel for phase 2 IKE negotiations. Valid values are SHA1 | SHA2-256 | SHA2-384 | SHA2-512" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel2_phase1_dh_group_numbers" { + type = list(string) + description = "List of one or more Diffie-Hellman group numbers that are permitted for the second VPN tunnel for phase 1 IKE negotiations. Valid values are 2 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel2_phase2_dh_group_numbers" { + type = list(string) + description = "List of one or more Diffie-Hellman group numbers that are permitted for the second VPN tunnel for phase 2 IKE negotiations. Valid values are 2 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24" + default = [] + nullable = false +} + +variable "vpn_connection_tunnel2_preshared_key" { + type = string + description = "The preshared key of the second VPN tunnel. The preshared key must be between 8 and 64 characters in length and cannot start with zero. Allowed characters are alphanumeric characters, periods(.) and underscores(_)" + default = null +} + +variable "vpn_connection_tunnel2_startup_action" { + type = string + description = "The action to take when the establishing the tunnel for the second VPN connection. By default, your customer gateway device must initiate the IKE negotiation and bring up the tunnel. Specify `start` for AWS to initiate the IKE negotiation. Valid values are `add` | `start`" + default = "add" + nullable = false +} + +variable "vpn_connection_tunnel2_cloudwatch_log_enabled" { + type = bool + description = "Enable or disable VPN tunnel logging feature for the tunnel" + default = false + nullable = false +} + +variable "vpn_connection_tunnel2_cloudwatch_log_output_format" { + type = string + description = "Set log format for the tunnel. Default format is json. Possible values are `json` and `text`" + default = "json" + nullable = false +} + +variable "existing_transit_gateway_id" { + type = string + default = "" + description = "Existing Transit Gateway ID. If provided, the module will not create a Virtual Private Gateway but instead will use the transit_gateway. For setting up transit gateway we can use the cloudposse/transit-gateway/aws module and pass the output transit_gateway_id to this variable" +} + +variable "transit_gateway_enabled" { + type = bool + description = "Set to true to enable VPN connection to transit gateway and then pass in the existing_transit_gateway_id" + default = false + nullable = false +} + +variable "transit_gateway_route_table_id" { + type = string + description = "The ID of the route table for the transit gateway that you want to associate + propagate the VPN connection's TGW attachment" + default = null +} + +variable "transit_gateway_routes" { + type = map(object({ + blackhole = optional(bool, false) + destination_cidr_block = string + })) + description = "A map of transit gateway routes to create on the given TGW route table (via `transit_gateway_route_table_id`) for the created VPN Attachment. Use the key in the map to describe the route" + default = {} + nullable = false +} + +variable "preshared_key_enabled" { + type = bool + description = "Flag to enable adding the preshared keys to the VPN connection" + default = true + nullable = false +} + +variable "ssm_enabled" { + type = bool + description = "Flag to enable saving the `tunnel1_preshared_key` and `tunnel2_preshared_key` in the SSM Parameter Store" + default = false + nullable = false +} + +variable "ssm_path_prefix" { + type = string + description = "SSM Key path prefix for the associated SSM parameters" + default = "" + nullable = false +} diff --git a/modules/site-to-site-vpn/versions.tf b/modules/site-to-site-vpn/versions.tf new file mode 100644 index 000000000..2445498a0 --- /dev/null +++ b/modules/site-to-site-vpn/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + random = { + source = "hashicorp/random" + version = ">= 2.2" + } + } +} diff --git a/modules/spacelift/spaces/README.md b/modules/spacelift/spaces/README.md index 37f43cb73..44bbb35a2 100644 --- a/modules/spacelift/spaces/README.md +++ b/modules/spacelift/spaces/README.md @@ -117,7 +117,7 @@ No resources. | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [spaces](#input\_spaces) | A map of all Spaces to create in Spacelift |
map(object({
parent_space_id = string,
description = optional(string),
inherit_entities = optional(bool, false),
labels = optional(set(string), []),
policies = optional(map(object({
body = optional(string),
body_url = optional(string),
body_url_version = optional(string, "master"),
body_file_path = optional(string),
type = optional(string),
labels = optional(set(string), []),
})), {}),
}))
| n/a | yes | +| [spaces](#input\_spaces) | A map of all Spaces to create in Spacelift |
map(object({
parent_space_id = string,
description = optional(string),
inherit_entities = optional(bool, false),
labels = optional(set(string), []),
policies = optional(map(object({
body = optional(string),
body_url = optional(string),
body_url_version = optional(string, "master"),
body_file_path = optional(string),
type = optional(string),
labels = optional(set(string), []),
})), {}),
}))
| n/a | yes | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | From 53728bc6b8a8e3454e82162372f9751324c67939 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Thu, 5 Sep 2024 04:12:15 -0400 Subject: [PATCH 477/501] Correct Auth0 Parameter Intention (#1107) --- modules/auth0/app/README.md | 4 +- modules/auth0/app/main.tf | 37 ++++++++++++++++ modules/auth0/app/provider-auth0-client.tf | 42 ------------------- modules/auth0/app/variables.tf | 12 ++++++ modules/auth0/connection/README.md | 2 - .../auth0/connection/provider-auth0-client.tf | 42 ------------------- 6 files changed, 52 insertions(+), 87 deletions(-) diff --git a/modules/auth0/app/README.md b/modules/auth0/app/README.md index fa48107c0..223e4a4f9 100644 --- a/modules/auth0/app/README.md +++ b/modules/auth0/app/README.md @@ -78,6 +78,7 @@ components: | Name | Type | |------|------| | [auth0_client.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/client) | resource | +| [auth0_client_credentials.this](https://registry.terraform.io/providers/auth0/auth0/latest/docs/resources/client_credentials) | resource | | [aws_ssm_parameter.auth0_client_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.auth0_client_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.auth0_domain](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | @@ -95,9 +96,9 @@ components: | [auth0\_tenant\_environment\_name](#input\_auth0\_tenant\_environment\_name) | The name of the environment where the Auth0 tenant component is deployed. Defaults to the environment of the current stack. | `string` | `""` | no | | [auth0\_tenant\_stage\_name](#input\_auth0\_tenant\_stage\_name) | The name of the stage where the Auth0 tenant component is deployed. Defaults to the stage of the current stack. | `string` | `""` | no | | [auth0\_tenant\_tenant\_name](#input\_auth0\_tenant\_tenant\_name) | The name of the tenant where the Auth0 tenant component is deployed. Yes this is a bit redundant, since Auth0 also calls this resource a tenant. Defaults to the tenant of the current stack. | `string` | `""` | no | +| [authentication\_method](#input\_authentication\_method) | The authentication method for the client credentials | `string` | `"client_secret_post"` | no | | [callbacks](#input\_callbacks) | Allowed Callback URLs | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create\_auth0\_ssm\_parameters\_enabled](#input\_create\_auth0\_ssm\_parameters\_enabled) | Whether or not to create a duplicate of the AWS SSM parameter for the Auth0 domain, client ID, and client secret in this account. | `bool` | `false` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | @@ -114,6 +115,7 @@ components: | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [oidc\_conformant](#input\_oidc\_conformant) | OIDC Conformant | `bool` | `true` | no | +| [provider\_ssm\_base\_path](#input\_provider\_ssm\_base\_path) | The base path for the SSM parameters. If not defined, this is set to the module context ID. This is also required when `var.enabled` is set to `false` | `string` | `""` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [sso](#input\_sso) | Single Sign-On for the Auth0 app | `bool` | `true` | no | diff --git a/modules/auth0/app/main.tf b/modules/auth0/app/main.tf index 9a098c365..1e1ee4f68 100644 --- a/modules/auth0/app/main.tf +++ b/modules/auth0/app/main.tf @@ -1,5 +1,9 @@ locals { enabled = module.this.enabled + + ssm_path = coalesce(var.provider_ssm_base_path, module.this.id) + client_id_ssm_path = format("/%s/client_id", local.ssm_path) + client_secret_ssm_path = format("/%s/client_secret", local.ssm_path) } resource "auth0_client" "this" { @@ -23,3 +27,36 @@ resource "auth0_client" "this" { logo_uri = var.logo_uri } + +resource "auth0_client_credentials" "this" { + count = local.enabled ? 1 : 0 + + client_id = try(auth0_client.this[0].client_id, "") + authentication_method = var.authentication_method +} + +module "auth0_ssm_parameters" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.13.0" + + enabled = local.enabled + + parameter_write = [ + { + name = local.client_id_ssm_path + value = try(auth0_client.this[0].client_id, "") + type = "SecureString" + overwrite = "true" + description = "Auth0 client ID for the Auth0 ${module.this.id} application" + }, + { + name = local.client_secret_ssm_path + value = try(auth0_client_credentials.this[0].client_secret, "") + type = "SecureString" + overwrite = "true" + description = "Auth0 client secret for the Auth0 ${module.this.id} application" + } + ] + + context = module.this.context +} diff --git a/modules/auth0/app/provider-auth0-client.tf b/modules/auth0/app/provider-auth0-client.tf index 1b35cf9b5..f7fb49f27 100644 --- a/modules/auth0/app/provider-auth0-client.tf +++ b/modules/auth0/app/provider-auth0-client.tf @@ -105,45 +105,3 @@ provider "auth0" { client_secret = data.aws_ssm_parameter.auth0_client_secret.value debug = var.auth0_debug } - -# -# Finally if enabled, create a duplicate of the AWS SSM parameters for Auth0 in this account. -# -variable "create_auth0_ssm_parameters_enabled" { - description = "Whether or not to create a duplicate of the AWS SSM parameter for the Auth0 domain, client ID, and client secret in this account." - type = bool - default = false -} - -module "auth0_ssm_parameters" { - source = "cloudposse/ssm-parameter-store/aws" - version = "0.13.0" - - enabled = local.enabled && var.create_auth0_ssm_parameters_enabled - - parameter_write = [ - { - name = module.auth0_tenant[0].outputs.domain_ssm_path - value = data.aws_ssm_parameter.auth0_domain.value - type = "SecureString" - overwrite = "true" - description = "Auth0 domain value for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" - }, - { - name = module.auth0_tenant[0].outputs.client_id_ssm_path - value = data.aws_ssm_parameter.auth0_client_id.value - type = "SecureString" - overwrite = "true" - description = "Auth0 client ID for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" - }, - { - name = module.auth0_tenant[0].outputs.client_secret_ssm_path - value = data.aws_ssm_parameter.auth0_client_secret.value - type = "SecureString" - overwrite = "true" - description = "Auth0 client secret for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" - }, - ] - - context = module.this.context -} diff --git a/modules/auth0/app/variables.tf b/modules/auth0/app/variables.tf index bacbf38c0..8c7497fd2 100644 --- a/modules/auth0/app/variables.tf +++ b/modules/auth0/app/variables.tf @@ -62,3 +62,15 @@ variable "jwt_alg" { description = "JWT Algorithm" default = "RS256" } + +variable "provider_ssm_base_path" { + type = string + description = "The base path for the SSM parameters. If not defined, this is set to the module context ID. This is also required when `var.enabled` is set to `false`" + default = "" +} + +variable "authentication_method" { + type = string + description = "The authentication method for the client credentials" + default = "client_secret_post" +} diff --git a/modules/auth0/connection/README.md b/modules/auth0/connection/README.md index 89d4305a3..ddeeb5298 100644 --- a/modules/auth0/connection/README.md +++ b/modules/auth0/connection/README.md @@ -75,7 +75,6 @@ components: | Name | Source | Version | |------|--------|---------| | [auth0\_apps](#module\_auth0\_apps) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | -| [auth0\_ssm\_parameters](#module\_auth0\_ssm\_parameters) | cloudposse/ssm-parameter-store/aws | 0.13.0 | | [auth0\_tenant](#module\_auth0\_tenant) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | | [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | | [iam\_roles\_auth0\_provider](#module\_iam\_roles\_auth0\_provider) | ../../account-map/modules/iam-roles | n/a | @@ -107,7 +106,6 @@ components: | [brute\_force\_protection](#input\_brute\_force\_protection) | Indicates whether to enable brute force protection, which will limit the number of signups and failed logins from a suspicious IP address. | `bool` | `true` | no | | [connection\_name](#input\_connection\_name) | The name of the connection | `string` | `""` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create\_auth0\_ssm\_parameters\_enabled](#input\_create\_auth0\_ssm\_parameters\_enabled) | Whether or not to create a duplicate of the AWS SSM parameter for the Auth0 domain, client ID, and client secret in this account. | `bool` | `false` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [disable\_signup](#input\_disable\_signup) | Indicates whether to allow user sign-ups to your application. | `bool` | `false` | no | diff --git a/modules/auth0/connection/provider-auth0-client.tf b/modules/auth0/connection/provider-auth0-client.tf index 1b35cf9b5..f7fb49f27 100644 --- a/modules/auth0/connection/provider-auth0-client.tf +++ b/modules/auth0/connection/provider-auth0-client.tf @@ -105,45 +105,3 @@ provider "auth0" { client_secret = data.aws_ssm_parameter.auth0_client_secret.value debug = var.auth0_debug } - -# -# Finally if enabled, create a duplicate of the AWS SSM parameters for Auth0 in this account. -# -variable "create_auth0_ssm_parameters_enabled" { - description = "Whether or not to create a duplicate of the AWS SSM parameter for the Auth0 domain, client ID, and client secret in this account." - type = bool - default = false -} - -module "auth0_ssm_parameters" { - source = "cloudposse/ssm-parameter-store/aws" - version = "0.13.0" - - enabled = local.enabled && var.create_auth0_ssm_parameters_enabled - - parameter_write = [ - { - name = module.auth0_tenant[0].outputs.domain_ssm_path - value = data.aws_ssm_parameter.auth0_domain.value - type = "SecureString" - overwrite = "true" - description = "Auth0 domain value for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" - }, - { - name = module.auth0_tenant[0].outputs.client_id_ssm_path - value = data.aws_ssm_parameter.auth0_client_id.value - type = "SecureString" - overwrite = "true" - description = "Auth0 client ID for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" - }, - { - name = module.auth0_tenant[0].outputs.client_secret_ssm_path - value = data.aws_ssm_parameter.auth0_client_secret.value - type = "SecureString" - overwrite = "true" - description = "Auth0 client secret for the Auth0 ${local.auth0_tenant_tenant_name}-${local.auth0_tenant_environment_name}-${local.auth0_tenant_stage_name} tenant" - }, - ] - - context = module.this.context -} From 5582764bec941e75d964375ba1258a2afde8f11a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 9 Sep 2024 16:37:12 -0400 Subject: [PATCH 478/501] fix: Correct recommended `var.name` for `auth0/app` (#1108) --- modules/auth0/app/README.md | 9 +++++++-- modules/auth0/app/variables.tf | 2 +- modules/auth0/tenant/README.md | 6 ++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/modules/auth0/app/README.md b/modules/auth0/app/README.md index 223e4a4f9..87f22e1ed 100644 --- a/modules/auth0/app/README.md +++ b/modules/auth0/app/README.md @@ -17,6 +17,11 @@ client ID and client secret configured with the `auth0/tenant` component. Here's an example snippet for how to use this component. +> [!IMPORTANT] +> +> Be sure that the context ID does not overlap with the context ID of other Auth0 components, such as `auth0/tenant`. We +> use this ID to generate the SSM parameter names. + ```yaml # stacks/catalog/auth0/app.yaml components: @@ -24,7 +29,7 @@ components: auth0/app: vars: enabled: true - name: "auth0" + name: "auth0-app" # We can centralize plat-sandbox, plat-dev, and plat-staging all use a "nonprod" Auth0 tenant, which is deployed in plat-staging. auth0_tenant_stage_name: "plat-staging" @@ -115,9 +120,9 @@ components: | [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [oidc\_conformant](#input\_oidc\_conformant) | OIDC Conformant | `bool` | `true` | no | -| [provider\_ssm\_base\_path](#input\_provider\_ssm\_base\_path) | The base path for the SSM parameters. If not defined, this is set to the module context ID. This is also required when `var.enabled` is set to `false` | `string` | `""` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [ssm\_base\_path](#input\_ssm\_base\_path) | The base path for the SSM parameters. If not defined, this is set to the module context ID. This is also required when `var.enabled` is set to `false` | `string` | `""` | no | | [sso](#input\_sso) | Single Sign-On for the Auth0 app | `bool` | `true` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | diff --git a/modules/auth0/app/variables.tf b/modules/auth0/app/variables.tf index 8c7497fd2..62fb09e71 100644 --- a/modules/auth0/app/variables.tf +++ b/modules/auth0/app/variables.tf @@ -63,7 +63,7 @@ variable "jwt_alg" { default = "RS256" } -variable "provider_ssm_base_path" { +variable "ssm_base_path" { type = string description = "The base path for the SSM parameters. If not defined, this is set to the module context ID. This is also required when `var.enabled` is set to `false`" default = "" diff --git a/modules/auth0/tenant/README.md b/modules/auth0/tenant/README.md index afc9fffd3..171ce4e0c 100644 --- a/modules/auth0/tenant/README.md +++ b/modules/auth0/tenant/README.md @@ -16,6 +16,7 @@ components: auth0/tenant: vars: enabled: true + # Make sure this name does not conflict with other Auth0 components, such as `auth0/app` name: auth0 support_email: "tech@acme.com" support_url: "https://acme.com" @@ -61,6 +62,11 @@ auth0_client_secret_ssm_path = "/${module.this.id}/client_secret" For example, if we're deploying `auth0/tenant` into `plat-gbl-prod` and my default region is `us-west-2`, then I would add the following parameters to the `plat-prod` account in `us-west-2`: +> [!IMPORTANT] +> +> Be sure that this AWS SSM parameter path does not conflict with SSM parameters used by other Auth0 components, such as +> `auth0/app`. In both components, the SSM parameter paths are defined by the component deployment's context ID. + ``` /acme-plat-gbl-prod-auth0/domain /acme-plat-gbl-prod-auth0/client_id From d0a598445ff8ee28176835264f8499e7254ecbe6 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 9 Sep 2024 16:53:34 -0400 Subject: [PATCH 479/501] fix: Auth0 SSM Base Path (#1109) --- modules/auth0/app/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auth0/app/main.tf b/modules/auth0/app/main.tf index 1e1ee4f68..294a4db55 100644 --- a/modules/auth0/app/main.tf +++ b/modules/auth0/app/main.tf @@ -1,7 +1,7 @@ locals { enabled = module.this.enabled - ssm_path = coalesce(var.provider_ssm_base_path, module.this.id) + ssm_path = coalesce(var.ssm_base_path, module.this.id) client_id_ssm_path = format("/%s/client_id", local.ssm_path) client_secret_ssm_path = format("/%s/client_secret", local.ssm_path) } From ee6cdc808662efc74566e7d23f449b2dc2c2159d Mon Sep 17 00:00:00 2001 From: David Moran <23364162+wavemoran@users.noreply.github.com> Date: Wed, 11 Sep 2024 08:44:55 -0700 Subject: [PATCH 480/501] Add explicit parameter store path (#1110) --- modules/eks/external-secrets-operator/main.tf | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/eks/external-secrets-operator/main.tf b/modules/eks/external-secrets-operator/main.tf index 5dd92ce35..bd79a0400 100644 --- a/modules/eks/external-secrets-operator/main.tf +++ b/modules/eks/external-secrets-operator/main.tf @@ -48,9 +48,13 @@ module "external_secrets_operator" { actions = [ "ssm:GetParameter*" ] - resources = [for parameter_store_path in var.parameter_store_paths : ( - "arn:aws:ssm:${var.region}:${local.account}:parameter/${parameter_store_path}/*" - )] + resources = concat( + [for parameter_store_path in var.parameter_store_paths : ( + "arn:aws:ssm:${var.region}:${local.account}:parameter/${parameter_store_path}/*" + )], + [for parameter_store_path in var.parameter_store_paths : ( + "arn:aws:ssm:${var.region}:${local.account}:parameter/${parameter_store_path}" + )]) }, { sid = "DescribeParameters" From d791d5a5f1d56311ffebb83fb41554b30b16e4da Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 11 Sep 2024 13:37:45 -0400 Subject: [PATCH 481/501] Delete ECS Partial Task Definitions Guide (#1111) --- .../docs/ecs-partial-task-definitions.md | 209 ------------------ .../docs/ecs-partial-task-defintions.png | Bin 119726 -> 0 bytes 2 files changed, 209 deletions(-) delete mode 100644 modules/ecs-service/docs/ecs-partial-task-definitions.md delete mode 100644 modules/ecs-service/docs/ecs-partial-task-defintions.png diff --git a/modules/ecs-service/docs/ecs-partial-task-definitions.md b/modules/ecs-service/docs/ecs-partial-task-definitions.md deleted file mode 100644 index 18c7366a2..000000000 --- a/modules/ecs-service/docs/ecs-partial-task-definitions.md +++ /dev/null @@ -1,209 +0,0 @@ -# ECS Partial Task Definitions - -This document describes what partial task definitions are and how we can use them to set up ECS services using Terraform -and GitHub Actions. - -## The Problem - -Managing ECS Services is challenging. Ideally, we want our services to be managed by Terraform so everything is living -in code. However, we also want to update the task definition via GitOps as through the GitHub release lifecycle. This is -challenging because Terraform can create the task definition, but if updated by the application repository, the task -definition will be out of sync with the Terraform state. - -Managing it entirely through Terraform means we cannot easily update the newly built image by the application repository -unless we directly commit to the infrastructure repository, which is not ideal. - -Managing it entirely through the application repository means we cannot codify the infrastructure and have to hardcode -ARNs, secrets, and other infrastructure-specific configurations. - -## Introduction - -ECS Partial task definitions is the idea of breaking the task definition into smaller parts. This allows for easier -management of the task definition and makes it easier to update the task definition. - -We do this by setting up Terraform to manage a portion of the task definition, and the application repository to manage -another portion. - -The Terraform (infrastructure) portion is created first. It will create an ECS Service in ECS, and then upload the task -definition JSON to S3 as `task-template.json`.The application repository will have a `task-definition.json` git -controlled, during the development lifecycle, the application repository will download the task definition from S3, -merge the task definitions, then update the ECS Service with the new task definition. Finally, GitHub actions will -update the S3 bucket with the deployed task definition under `task-definition.json`. If Terraform is planned again, it -will use the new task definition as the base for the next deployment, thus not resetting the image or application -configuration. - -![how-does-partial-task-definition-work](./ecs-partial-task-defintions.png) - -### Pros - -The **benefit** to using this approach is that we can manage the task definition portion in Terraform with the -infrastructure, meaning secrets, volumes, and other ARNs can be managed in Terraform. If a filesystem ID updates we can -re-apply Terraform to update the task definition with the new filesystem ID. The application repository can manage the -container definitions, environment variables, and other application-specific configurations. This allows developers who -are closer to the application to quickly update the environment variables or other configuration. - -### Cons - -The drawback to this approach is that it is more complex than managing the task definition entirely in Terraform or the -application repository. It requires more setup and more moving parts. It can be confusing for a developer who is not -familiar with the setup to understand how the task definition is being managed and deployed. - -This also means that when something goes wrong, it becomes harder to troubleshoot as there are more moving parts. - -### Getting Setup - -#### Pre-requisites - -- Application Repository - [Cloud Posse Example ECS Application](https://github.com/cloudposse-examples/app-on-ecs) -- Infrastructure Repository -- ECS Cluster - [Cloud Posse Docs](https://docs.cloudposse.com/components/library/aws/ecs/) - - [Component](https://github.com/cloudposse/Terraform-aws-components/tree/main/modules/ecs). -- `ecs-service` - [Cloud Posse Docs](https://docs.cloudposse.com/components/library/aws/ecs-service/) - - [Component](https://github.com/cloudposse/Terraform-aws-components/tree/main/modules/ecs-service). - - **Must** use the Cloud Posse Component. - - [`v1.416.0`](https://github.com/cloudposse/Terraform-aws-components/releases/tag/1.416.0) or later. -- S3 Bucket - [Cloud Posse Docs](https://docs.cloudposse.com/components/library/aws/s3-bucket/) - - [Component](https://github.com/cloudposse/Terraform-aws-components/tree/main/modules/s3-bucket). - -#### Steps - -1. Set up the S3 Bucket that will store the task definition. - -
This bucket should be in the same account as the ECS Cluster. - -
-
- S3 Bucket Default Definition - - ```yaml - components: - terraform: - s3-bucket/defaults: - metadata: - type: abstract - vars: - enabled: true - account_map_tenant_name: core - # Suggested configuration for all buckets - user_enabled: false - acl: "private" - grants: null - force_destroy: false - versioning_enabled: false - allow_encrypted_uploads_only: true - block_public_acls: true - block_public_policy: true - ignore_public_acls: true - restrict_public_buckets: true - allow_ssl_requests_only: true - lifecycle_configuration_rules: - - id: default - enabled: true - abort_incomplete_multipart_upload_days: 90 - filter_and: - prefix: "" - tags: {} - # Move to Glacier after 2 years - transition: - - storage_class: GLACIER - days: 730 - # Never expire - expiration: {} - # Versioning isnt enabled, but these default values are still required - noncurrent_version_transition: - - storage_class: GLACIER - days: 90 - noncurrent_version_expiration: {} - ``` - -
- - ```yaml - import: - - catalog/s3-bucket/defaults - - components: - Terraform: - s3-bucket/ecs-tasks-mirror: #NOTE this is the component instance name. - metadata: - component: s3-bucket - inherits: - - s3-bucket/defaults - vars: - enabled: true - name: ecs-tasks-mirror - ``` - -2. Create an ECS Service in Terraform - -
Set up the ECS Service in Terraform using the - [`ecs-service` component](https://github.com/cloudposse/Terraform-aws-components/tree/main/modules/ecs-service). This - will create the ECS Service and upload the task definition to the S3 bucket. - -
To enable Partial Task Definitions, set the variable `s3_mirror_name` to be the component instance name of the - bucket to mirror to. For example `s3-bucket/ecs-tasks-mirror` - - ```yaml - components: - Terraform: - ecs-services/defaults: - metadata: - component: ecs-service - type: abstract - vars: - enabled: true - ecs_cluster_name: "ecs/cluster" - s3_mirror_name: s3-bucket/ecs-tasks-mirror - ``` - -3. Set up an Application repository with GitHub workflows. - - An example application repository can be found [here](https://github.com/cloudposse-examples/app-on-ecs). - -
Two things need to be pulled from this repository: - - - The `task-definition.json` file under `deploy/task-definition.json` - - The GitHub Workflows. - - An important note about the GitHub Workflows, in the example repository they all live under `.github/workflows`. This - is done so development of workflows can be fast, however we recommend moving the shared workflows to a separate - repository and calling them from the application repository. The application repository should only contain the - workflows `main-branch.yaml`, `release.yaml` and `feature-branch.yml`. - -
To enable Partial Task Definitions in the workflows, the call to - [`cloudposse/github-action-run-ecspresso` (link)](https://github.com/cloudposse-examples/app-on-ecs/blob/main/.github/workflows/workflow-cd-ecspresso.yml#L133-L147) - should have the input `mirror_to_s3_bucket` set to the S3 bucket name. the variable `use_partial_taskdefinition` - should be set to `'true'` - -
- Example GitHub Action Step - - ```yaml - - name: Deploy - uses: cloudposse/github-action-deploy-ecspresso@0.6.0 - continue-on-error: true - if: ${{ steps.db_migrate.outcome != 'failure' }} - id: deploy - with: - image: ${{ steps.image.outputs.out }} - image-tag: ${{ inputs.tag }} - region: ${{ steps.environment.outputs.region }} - operation: deploy - debug: false - cluster: ${{ steps.environment.outputs.cluster }} - application: ${{ steps.environment.outputs.name }} - taskdef-path: ${{ inputs.path }} - mirror_to_s3_bucket: ${{ steps.environment.outputs.s3-bucket }} - use_partial_taskdefinition: "true" - timeout: 10m - ``` - -
- -## Operation - -Changes through Terraform will not immediately be reflected in the ECS Service. This is because the task template has -been updated, but whatever was in the `task-definition.json` file in the S3 bucket will be used for deployment. - -To update the ECS Service after updating the Terraform for it, you must deploy through GitHub Actions. This will then -download the new template and create a new updated `task-defintion.json` to store in s3. diff --git a/modules/ecs-service/docs/ecs-partial-task-defintions.png b/modules/ecs-service/docs/ecs-partial-task-defintions.png deleted file mode 100644 index cbdfeef9519addf1df6776eb2592ffe7a36fd5c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119726 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
Ki4x6q_4fdO4 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⪙ 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 Date: Mon, 16 Sep 2024 14:43:39 -0700 Subject: [PATCH 482/501] feat: adds version upgrade variable input to component (#1113) --- modules/elasticache-redis/main.tf | 1 + modules/elasticache-redis/modules/redis_cluster/main.tf | 3 ++- .../elasticache-redis/modules/redis_cluster/variables.tf | 1 + modules/elasticache-redis/variables.tf | 6 ++++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/elasticache-redis/main.tf b/modules/elasticache-redis/main.tf index 620c06a70..0f2f91638 100644 --- a/modules/elasticache-redis/main.tf +++ b/modules/elasticache-redis/main.tf @@ -49,6 +49,7 @@ locals { transit_encryption_enabled = var.transit_encryption_enabled apply_immediately = var.apply_immediately automatic_failover_enabled = var.automatic_failover_enabled + auto_minor_version_upgrade = var.auto_minor_version_upgrade cloudwatch_metric_alarms_enabled = var.cloudwatch_metric_alarms_enabled auth_token_enabled = var.auth_token_enabled } diff --git a/modules/elasticache-redis/modules/redis_cluster/main.tf b/modules/elasticache-redis/modules/redis_cluster/main.tf index c7a9cf02d..37a3ee332 100644 --- a/modules/elasticache-redis/modules/redis_cluster/main.tf +++ b/modules/elasticache-redis/modules/redis_cluster/main.tf @@ -10,7 +10,7 @@ locals { module "redis" { source = "cloudposse/elasticache-redis/aws" - version = "1.2.2" + version = "1.4.1" name = var.cluster_name @@ -20,6 +20,7 @@ module "redis" { apply_immediately = var.cluster_attributes.apply_immediately at_rest_encryption_enabled = var.cluster_attributes.at_rest_encryption_enabled auth_token = local.auth_token + auto_minor_version_upgrade = var.cluster_attributes.auto_minor_version_upgrade automatic_failover_enabled = var.cluster_attributes.automatic_failover_enabled availability_zones = var.cluster_attributes.availability_zones multi_az_enabled = var.cluster_attributes.multi_az_enabled diff --git a/modules/elasticache-redis/modules/redis_cluster/variables.tf b/modules/elasticache-redis/modules/redis_cluster/variables.tf index 09bbf26e9..1c9af10cd 100644 --- a/modules/elasticache-redis/modules/redis_cluster/variables.tf +++ b/modules/elasticache-redis/modules/redis_cluster/variables.tf @@ -60,6 +60,7 @@ variable "cluster_attributes" { transit_encryption_enabled = bool apply_immediately = bool automatic_failover_enabled = bool + auto_minor_version_upgrade = bool auth_token_enabled = bool }) description = "Cluster attributes" diff --git a/modules/elasticache-redis/variables.tf b/modules/elasticache-redis/variables.tf index 3bf784a4a..b059c6c36 100644 --- a/modules/elasticache-redis/variables.tf +++ b/modules/elasticache-redis/variables.tf @@ -65,6 +65,12 @@ variable "automatic_failover_enabled" { description = "Enable automatic failover" } +variable "auto_minor_version_upgrade" { + type = bool + description = "Specifies whether minor version engine upgrades will be applied automatically to the underlying Cache Cluster instances during the maintenance window. Only supported if the engine version is 6 or higher." + default = false +} + variable "cloudwatch_metric_alarms_enabled" { type = bool description = "Boolean flag to enable/disable CloudWatch metrics alarms" From 2cac06b4e0e71003a31e1c597581b18685fd71bb Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Thu, 19 Sep 2024 17:38:07 -0400 Subject: [PATCH 483/501] Lamdba Component Update (#1115) --- modules/lambda/README.md | 244 +++++++++++------- modules/lambda/main.tf | 122 +++++---- modules/lambda/remote-state.tf | 14 + .../lambda/triggers_cloudwatch_event_rules.tf | 43 +++ modules/lambda/triggers_s3_notifications.tf | 58 +++++ modules/lambda/triggers_sqs_queue.tf | 81 ++++++ modules/lambda/variables.tf | 75 +++--- modules/lambda/versions.tf | 4 + 8 files changed, 449 insertions(+), 192 deletions(-) create mode 100644 modules/lambda/remote-state.tf create mode 100644 modules/lambda/triggers_cloudwatch_event_rules.tf create mode 100644 modules/lambda/triggers_s3_notifications.tf create mode 100644 modules/lambda/triggers_sqs_queue.tf diff --git a/modules/lambda/README.md b/modules/lambda/README.md index c8adb2505..60cce5310 100644 --- a/modules/lambda/README.md +++ b/modules/lambda/README.md @@ -1,11 +1,11 @@ --- tags: - - component/sso-saml-provider + - component/lambda - layer/software-delivery - provider/aws --- -# Component: `sso-saml-provider` +# Component: `lambda` This component is responsible for provisioning Lambda functions. @@ -83,123 +83,173 @@ components: # s3_key: hello-world-go.zip ``` - +### Notifications: + +#### SQS + +```yaml +sqs_notifications: + my-service-a: + sqs_component: + component: sqs-queue/my-service-a + my-service-b: + sqs_arn: arn:aws:sqs:us-west-2:111111111111:my-service-b +``` + +#### S3 + +```yaml +s3_notifications: + my-service-a: + bucket_component: + component: s3-bucket/my-service-a + events: ["s3:ObjectCreated:*"] + my-service-b: + bucket_name: my-service-b + events: ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"] +``` + +#### Cron (CloudWatch Event) + +```yaml +cloudwatch_event_rules: + schedule-a: + schedule_expression: "rate(5 minutes)" + schedule-b: + schedule_expression: "cron(0 20 * * ? *)" +``` + + ## Requirements -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.3.0 | -| [archive](#requirement\_archive) | >= 2.3.0 | -| [aws](#requirement\_aws) | >= 4.9.0 | +| Name | Version | +| ------------------------------------------------------------------------ | -------- | +| [terraform](#requirement_terraform) | >= 1.3.0 | +| [archive](#requirement_archive) | >= 2.3.0 | +| [aws](#requirement_aws) | >= 4.9.0 | +| [random](#requirement_random) | >= 3.0.0 | ## Providers -| Name | Version | -|------|---------| -| [archive](#provider\_archive) | >= 2.3.0 | -| [aws](#provider\_aws) | >= 4.9.0 | +| Name | Version | +| ------------------------------------------------------------ | -------- | +| [archive](#provider_archive) | >= 2.3.0 | +| [aws](#provider_aws) | >= 4.9.0 | +| [random](#provider_random) | >= 3.0.0 | ## Modules -| Name | Source | Version | -|------|--------|---------| -| [iam\_policy](#module\_iam\_policy) | cloudposse/iam-policy/aws | 1.0.1 | -| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | -| [label](#module\_label) | cloudposse/label/null | 0.25.0 | -| [lambda](#module\_lambda) | cloudposse/lambda-function/aws | 0.4.1 | -| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| Name | Source | Version | +| -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------- | +| [cloudwatch_event_rules_label](#module_cloudwatch_event_rules_label) | cloudposse/label/null | 0.25.0 | +| [iam_policy](#module_iam_policy) | cloudposse/iam-policy/aws | 1.0.1 | +| [iam_roles](#module_iam_roles) | ../account-map/modules/iam-roles | n/a | +| [lambda](#module_lambda) | cloudposse/lambda-function/aws | 0.6.1 | +| [s3_bucket](#module_s3_bucket) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [s3_bucket_notifications_component](#module_s3_bucket_notifications_component) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [sqs_iam_policy](#module_sqs_iam_policy) | cloudposse/iam-policy/aws | 1.0.1 | +| [sqs_queue](#module_sqs_queue) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [this](#module_this) | cloudposse/label/null | 0.25.0 | ## Resources -| Name | Type | -|------|------| -| [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [archive_file.lambdazip](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | -| [aws_ssm_parameter.cicd_ssm_param](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| Name | Type | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| [aws_cloudwatch_event_rule.event_rules](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.sns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.sqs_default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_event_source_mapping.event_source_mapping](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping) | resource | +| [aws_lambda_function_url.lambda_url](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function_url) | resource | +| [aws_lambda_permission.allow_cloudwatch_to_call_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [aws_lambda_permission.s3_notification](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [aws_s3_bucket_notification.s3_notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_notification) | resource | +| [random_pet.zip_recreator](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | +| [archive_file.lambdazip](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | +| [aws_ssm_parameter.cicd_ssm_param](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [architectures](#input\_architectures) | Instruction set architecture for your Lambda function. Valid values are ["x86\_64"] and ["arm64"].
Default is ["x86\_64"]. Removing this attribute, function's architecture stay the same. | `list(string)` | `null` | no | -| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [cicd\_s3\_key\_format](#input\_cicd\_s3\_key\_format) | The format of the S3 key to store the latest version/sha of the Lambda function. This is used with cicd\_ssm\_param\_name. Defaults to 'stage/{stage}/lambda/{function\_name}/%s.zip' | `string` | `null` | no | -| [cicd\_ssm\_param\_name](#input\_cicd\_ssm\_param\_name) | The name of the SSM parameter to store the latest version/sha of the Lambda function. This is used with cicd\_s3\_key\_format | `string` | `null` | no | -| [cloudwatch\_event\_rules](#input\_cloudwatch\_event\_rules) | Creates EventBridge (CloudWatch Events) rules for invoking the Lambda Function along with the required permissions. | `map(any)` | `{}` | no | -| [cloudwatch\_lambda\_insights\_enabled](#input\_cloudwatch\_lambda\_insights\_enabled) | Enable CloudWatch Lambda Insights for the Lambda Function. | `bool` | `false` | no | -| [cloudwatch\_log\_subscription\_filters](#input\_cloudwatch\_log\_subscription\_filters) | CloudWatch Logs subscription filter resources. Currently supports only Lambda functions as destinations. | `map(any)` | `{}` | no | -| [cloudwatch\_logs\_kms\_key\_arn](#input\_cloudwatch\_logs\_kms\_key\_arn) | The ARN of the KMS Key to use when encrypting log data. | `string` | `null` | no | -| [cloudwatch\_logs\_retention\_in\_days](#input\_cloudwatch\_logs\_retention\_in\_days) | Specifies the number of days you want to retain log events in the specified log group. Possible values are:
1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653, and 0. If you select 0, the events in the
log group are always retained and never expire. | `number` | `null` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [custom\_iam\_policy\_arns](#input\_custom\_iam\_policy\_arns) | ARNs of IAM policies to be attached to the Lambda role | `set(string)` | `[]` | no | -| [dead\_letter\_config\_target\_arn](#input\_dead\_letter\_config\_target\_arn) | ARN of an SNS topic or SQS queue to notify when an invocation fails. If this option is used, the function's IAM role
must be granted suitable access to write to the target object, which means allowing either the sns:Publish or
sqs:SendMessage action on this ARN, depending on which service is targeted." | `string` | `null` | no | -| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [description](#input\_description) | Description of what the Lambda Function does. | `string` | `null` | no | -| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [event\_source\_mappings](#input\_event\_source\_mappings) | Creates event source mappings to allow the Lambda function to get events from Kinesis, DynamoDB and SQS. The IAM role
of this Lambda function will be enhanced with necessary minimum permissions to get those events. | `any` | `{}` | no | -| [filename](#input\_filename) | The path to the function's deployment package within the local filesystem. If defined, The s3\_-prefixed options and image\_uri cannot be used. | `string` | `null` | no | -| [function\_name](#input\_function\_name) | Unique name for the Lambda Function. | `string` | `null` | no | -| [handler](#input\_handler) | The function entrypoint in your code. | `string` | `null` | no | -| [iam\_policy](#input\_iam\_policy) | IAM policy to attach to the Lambda role, specified as a Terraform object. This can be used with or instead of `var.policy_json`. |
object({
policy_id = optional(string, null)
version = optional(string, null)
statements = list(object({
sid = optional(string, null)
effect = optional(string, null)
actions = optional(list(string), null)
not_actions = optional(list(string), null)
resources = optional(list(string), null)
not_resources = optional(list(string), null)
conditions = optional(list(object({
test = string
variable = string
values = list(string)
})), [])
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
not_principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
}))
})
| `null` | no | -| [iam\_policy\_description](#input\_iam\_policy\_description) | Description of the IAM policy for the Lambda IAM role | `string` | `"Minimum SSM read permissions for Lambda IAM Role"` | no | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [ignore\_external\_function\_updates](#input\_ignore\_external\_function\_updates) | Ignore updates to the Lambda Function executed externally to the Terraform lifecycle. Set this to `true` if you're
using CodeDeploy, aws CLI or other external tools to update the Lambda Function code." | `bool` | `false` | no | -| [image\_config](#input\_image\_config) | The Lambda OCI [image configurations](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function#image_config)
block with three (optional) arguments:
- *entry\_point* - The ENTRYPOINT for the docker image (type `list(string)`).
- *command* - The CMD for the docker image (type `list(string)`).
- *working\_directory* - The working directory for the docker image (type `string`). | `any` | `{}` | no | -| [image\_uri](#input\_image\_uri) | The ECR image URI containing the function's deployment package. Conflicts with `filename`, `s3_bucket_name`, `s3_key`, and `s3_object_version`. | `string` | `null` | no | -| [kms\_key\_arn](#input\_kms\_key\_arn) | Amazon Resource Name (ARN) of the AWS Key Management Service (KMS) key that is used to encrypt environment variables.
If this configuration is not provided when environment variables are in use, AWS Lambda uses a default service key.
If this configuration is provided when environment variables are not in use, the AWS Lambda API does not save this
configuration and Terraform will show a perpetual difference of adding the key. To fix the perpetual difference,
remove this configuration. | `string` | `""` | no | -| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | -| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [lambda\_at\_edge\_enabled](#input\_lambda\_at\_edge\_enabled) | Enable Lambda@Edge for your Node.js or Python functions. The required trust relationship and publishing of function versions will be configured in this module. | `bool` | `false` | no | -| [lambda\_environment](#input\_lambda\_environment) | Environment (e.g. ENV variables) configuration for the Lambda function enable you to dynamically pass settings to your function code and libraries. |
object({
variables = map(string)
})
| `null` | no | -| [layers](#input\_layers) | List of Lambda Layer Version ARNs (maximum of 5) to attach to the Lambda Function. | `list(string)` | `[]` | no | -| [memory\_size](#input\_memory\_size) | Amount of memory in MB the Lambda Function can use at runtime. | `number` | `128` | no | -| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | -| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [package\_type](#input\_package\_type) | The Lambda deployment package type. Valid values are `Zip` and `Image`. | `string` | `"Zip"` | no | -| [permissions\_boundary](#input\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the role | `string` | `""` | no | -| [policy\_json](#input\_policy\_json) | IAM policy to attach to the Lambda role, specified as JSON. This can be used with or instead of `var.iam_policy`. | `string` | `null` | no | -| [publish](#input\_publish) | Whether to publish creation/change as new Lambda Function Version. | `bool` | `false` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input\_region) | AWS Region | `string` | n/a | yes | -| [reserved\_concurrent\_executions](#input\_reserved\_concurrent\_executions) | The amount of reserved concurrent executions for this lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `-1` | no | -| [runtime](#input\_runtime) | The runtime environment for the Lambda function you are uploading. | `string` | `null` | no | -| [s3\_bucket\_name](#input\_s3\_bucket\_name) | The name suffix of the S3 bucket containing the function's deployment package. Conflicts with filename and image\_uri.
This bucket must reside in the same AWS region where you are creating the Lambda function. | `string` | `null` | no | -| [s3\_full\_bucket\_name](#input\_s3\_full\_bucket\_name) | The full name of the S3 bucket containing the function's deployment package. Conflicts with filename and image\_uri.
This bucket must reside in the same AWS region where you are creating the Lambda function.

This is alternative to `var.s3_bucket_name` which formats the name for the current account. | `string` | `null` | no | -| [s3\_key](#input\_s3\_key) | The S3 key of an object containing the function's deployment package. Conflicts with filename and image\_uri. | `string` | `null` | no | -| [s3\_object\_version](#input\_s3\_object\_version) | The object version containing the function's deployment package. Conflicts with filename and image\_uri. | `string` | `null` | no | -| [sns\_subscriptions](#input\_sns\_subscriptions) | Creates subscriptions to SNS topics which trigger the Lambda Function. Required Lambda invocation permissions will be generated. | `map(any)` | `{}` | no | -| [source\_code\_hash](#input\_source\_code\_hash) | Used to trigger updates. Must be set to a base64-encoded SHA256 hash of the package file specified with either
filename or s3\_key. The usual way to set this is filebase64sha256('file.zip') where 'file.zip' is the local filename
of the lambda function source archive. | `string` | `""` | no | -| [ssm\_parameter\_names](#input\_ssm\_parameter\_names) | List of AWS Systems Manager Parameter Store parameter names. The IAM role of this Lambda function will be enhanced
with read permissions for those parameters. Parameters must start with a forward slash and can be encrypted with the
default KMS key. | `list(string)` | `null` | no | -| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [timeout](#input\_timeout) | The amount of time the Lambda Function has to run in seconds. | `number` | `3` | no | -| [tracing\_config\_mode](#input\_tracing\_config\_mode) | Tracing config mode of the Lambda function. Can be either PassThrough or Active. | `string` | `null` | no | -| [vpc\_config](#input\_vpc\_config) | Provide this to allow your function to access your VPC (if both 'subnet\_ids' and 'security\_group\_ids' are empty then
vpc\_config is considered to be empty or unset, see https://docs.aws.amazon.com/lambda/latest/dg/vpc.html for details). |
object({
security_group_ids = list(string)
subnet_ids = list(string)
})
| `null` | no | -| [zip](#input\_zip) | Zip Configuration for local file deployments |
object({
enabled = optional(bool, false)
output = optional(string, "output.zip")
input_dir = optional(string, null)
})
| `{}` | no | +| Name | Description | Type | Default | Required | +| --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | +| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [architectures](#input_architectures) | Instruction set architecture for your Lambda function. Valid values are ["x86\_64"] and ["arm64"].
Default is ["x86\_64"]. Removing this attribute, function's architecture stay the same. | `list(string)` | `null` | no | +| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [cicd_s3_key_format](#input_cicd_s3_key_format) | The format of the S3 key to store the latest version/sha of the Lambda function. This is used with cicd_ssm_param_name. Defaults to 'stage/{stage}/lambda/{function_name}/%s.zip' | `string` | `null` | no | +| [cicd_ssm_param_name](#input_cicd_ssm_param_name) | The name of the SSM parameter to store the latest version/sha of the Lambda function. This is used with cicd_s3_key_format | `string` | `null` | no | +| [cloudwatch_event_rules](#input_cloudwatch_event_rules) | Creates EventBridge (CloudWatch Events) rules for invoking the Lambda Function along with the required permissions. |
map(object({
description = optional(string)
event_bus_name = optional(string)
event_pattern = optional(string)
is_enabled = optional(bool)
name_prefix = optional(string)
role_arn = optional(string)
schedule_expression = optional(string)
}))
| `{}` | no | +| [cloudwatch_lambda_insights_enabled](#input_cloudwatch_lambda_insights_enabled) | Enable CloudWatch Lambda Insights for the Lambda Function. | `bool` | `false` | no | +| [cloudwatch_logs_kms_key_arn](#input_cloudwatch_logs_kms_key_arn) | The ARN of the KMS Key to use when encrypting log data. | `string` | `null` | no | +| [cloudwatch_logs_retention_in_days](#input_cloudwatch_logs_retention_in_days) | Specifies the number of days you want to retain log events in the specified log group. Possible values are:
1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653, and 0. If you select 0, the events in the
log group are always retained and never expire. | `number` | `null` | no | +| [context](#input_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional_tag_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [custom_iam_policy_arns](#input_custom_iam_policy_arns) | ARNs of IAM policies to be attached to the Lambda role | `set(string)` | `[]` | no | +| [dead_letter_config_target_arn](#input_dead_letter_config_target_arn) | ARN of an SNS topic or SQS queue to notify when an invocation fails. If this option is used, the function's IAM role
must be granted suitable access to write to the target object, which means allowing either the sns:Publish or
sqs:SendMessage action on this ARN, depending on which service is targeted." | `string` | `null` | no | +| [delimiter](#input_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [description](#input_description) | Description of what the Lambda Function does. | `string` | `null` | no | +| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [filename](#input_filename) | The path to the function's deployment package within the local filesystem. Works well with the `zip` variable. If defined, The s3\_-prefixed options and image_uri cannot be used. | `string` | `null` | no | +| [function_name](#input_function_name) | Unique name for the Lambda Function. | `string` | `null` | no | +| [function_url_enabled](#input_function_url_enabled) | Create a aws_lambda_function_url resource to expose the Lambda function | `bool` | `false` | no | +| [handler](#input_handler) | The function entrypoint in your code. | `string` | `null` | no | +| [iam_policy](#input_iam_policy) | IAM policy to attach to the Lambda role, specified as a Terraform object. This can be used with or instead of `var.policy_json`. |
object({
policy_id = optional(string, null)
version = optional(string, null)
statements = list(object({
sid = optional(string, null)
effect = optional(string, null)
actions = optional(list(string), null)
not_actions = optional(list(string), null)
resources = optional(list(string), null)
not_resources = optional(list(string), null)
conditions = optional(list(object({
test = string
variable = string
values = list(string)
})), [])
principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
not_principals = optional(list(object({
type = string
identifiers = list(string)
})), [])
}))
})
| `null` | no | +| [iam_policy_description](#input_iam_policy_description) | Description of the IAM policy for the Lambda IAM role | `string` | `"Minimum SSM read permissions for Lambda IAM Role"` | no | +| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [image_config](#input_image_config) | The Lambda OCI [image configurations](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function#image_config)
block with three (optional) arguments:
- _entry_point_ - The ENTRYPOINT for the docker image (type `list(string)`).
- _command_ - The CMD for the docker image (type `list(string)`).
- _working_directory_ - The working directory for the docker image (type `string`). | `any` | `{}` | no | +| [image_uri](#input_image_uri) | The ECR image URI containing the function's deployment package. Conflicts with `filename`, `s3_bucket_name`, `s3_key`, and `s3_object_version`. | `string` | `null` | no | +| [kms_key_arn](#input_kms_key_arn) | Amazon Resource Name (ARN) of the AWS Key Management Service (KMS) key that is used to encrypt environment variables.
If this configuration is not provided when environment variables are in use, AWS Lambda uses a default service key.
If this configuration is provided when environment variables are not in use, the AWS Lambda API does not save this
configuration and Terraform will show a perpetual difference of adding the key. To fix the perpetual difference,
remove this configuration. | `string` | `""` | no | +| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [lambda_at_edge_enabled](#input_lambda_at_edge_enabled) | Enable Lambda@Edge for your Node.js or Python functions. The required trust relationship and publishing of function versions will be configured in this module. | `bool` | `false` | no | +| [lambda_environment](#input_lambda_environment) | Environment (e.g. ENV variables) configuration for the Lambda function enable you to dynamically pass settings to your function code and libraries. |
object({
variables = map(string)
})
| `null` | no | +| [layers](#input_layers) | List of Lambda Layer Version ARNs (maximum of 5) to attach to the Lambda Function. | `list(string)` | `[]` | no | +| [memory_size](#input_memory_size) | Amount of memory in MB the Lambda Function can use at runtime. | `number` | `128` | no | +| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [package_type](#input_package_type) | The Lambda deployment package type. Valid values are `Zip` and `Image`. | `string` | `"Zip"` | no | +| [permissions_boundary](#input_permissions_boundary) | ARN of the policy that is used to set the permissions boundary for the role | `string` | `""` | no | +| [policy_json](#input_policy_json) | IAM policy to attach to the Lambda role, specified as JSON. This can be used with or instead of `var.iam_policy`. | `string` | `null` | no | +| [publish](#input_publish) | Whether to publish creation/change as new Lambda Function Version. | `bool` | `false` | no | +| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input_region) | AWS Region | `string` | n/a | yes | +| [reserved_concurrent_executions](#input_reserved_concurrent_executions) | The amount of reserved concurrent executions for this lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `-1` | no | +| [runtime](#input_runtime) | The runtime environment for the Lambda function you are uploading. | `string` | `null` | no | +| [s3_bucket_component](#input_s3_bucket_component) | The bucket component to use for the S3 bucket containing the function's deployment package. Conflicts with `s3_bucket_name`, `filename` and `image_uri`. |
object({
component = string
tenant = optional(string)
stage = optional(string)
environment = optional(string)
})
| `null` | no | +| [s3_bucket_name](#input_s3_bucket_name) | The name suffix of the S3 bucket containing the function's deployment package. Conflicts with filename and image_uri.
This bucket must reside in the same AWS region where you are creating the Lambda function. | `string` | `null` | no | +| [s3_key](#input_s3_key) | The S3 key of an object containing the function's deployment package. Conflicts with filename and image_uri. | `string` | `null` | no | +| [s3_notifications](#input_s3_notifications) | A map of S3 bucket notifications to trigger the Lambda function |
map(object({
bucket_name = optional(string)
bucket_component = optional(object({
component = string
environment = optional(string)
tenant = optional(string)
stage = optional(string)
}))
events = optional(list(string))
filter_prefix = optional(string)
filter_suffix = optional(string)
source_account = optional(string)
}))
| `{}` | no | +| [s3_object_version](#input_s3_object_version) | The object version containing the function's deployment package. Conflicts with filename and image_uri. | `string` | `null` | no | +| [source_code_hash](#input_source_code_hash) | Used to trigger updates. Must be set to a base64-encoded SHA256 hash of the package file specified with either
filename or s3_key. The usual way to set this is filebase64sha256('file.zip') where 'file.zip' is the local filename
of the lambda function source archive. | `string` | `""` | no | +| [sqs_notifications](#input_sqs_notifications) | A map of SQS queue notifications to trigger the Lambda function |
map(object({
sqs_arn = optional(string)
sqs_component = optional(object({
component = string
environment = optional(string)
tenant = optional(string)
stage = optional(string)
}))
batch_size = optional(number)
source_account = optional(string)
on_failure_arn = optional(string)
maximum_concurrency = optional(number)
}))
| `{}` | no | +| [ssm_parameter_names](#input_ssm_parameter_names) | List of AWS Systems Manager Parameter Store parameter names. The IAM role of this Lambda function will be enhanced
with read permissions for those parameters. Parameters must start with a forward slash and can be encrypted with the
default KMS key. | `list(string)` | `null` | no | +| [stage](#input_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input_timeout) | The amount of time the Lambda Function has to run in seconds. | `number` | `3` | no | +| [tracing_config_mode](#input_tracing_config_mode) | Tracing config mode of the Lambda function. Can be either PassThrough or Active. | `string` | `null` | no | +| [vpc_config](#input_vpc_config) | Provide this to allow your function to access your VPC (if both 'subnet_ids' and 'security_group_ids' are empty then
vpc_config is considered to be empty or unset, see https://docs.aws.amazon.com/lambda/latest/dg/vpc.html for details). |
object({
security_group_ids = list(string)
subnet_ids = list(string)
})
| `null` | no | +| [zip](#input_zip) | Zip Configuration for local file deployments |
object({
enabled = optional(bool, false)
output = optional(string, "output.zip")
input_dir = optional(string, null)
})
|
{
"enabled": false,
"output": "output.zip"
}
| no | ## Outputs -| Name | Description | -|------|-------------| -| [arn](#output\_arn) | ARN of the lambda function | -| [function\_name](#output\_function\_name) | Lambda function name | -| [invoke\_arn](#output\_invoke\_arn) | Invoke ARN of the lambda function | -| [qualified\_arn](#output\_qualified\_arn) | ARN identifying your Lambda Function Version (if versioning is enabled via publish = true) | -| [role\_arn](#output\_role\_arn) | Lambda IAM role ARN | -| [role\_name](#output\_role\_name) | Lambda IAM role name | +| Name | Description | +| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| [arn](#output_arn) | ARN of the lambda function | +| [function_name](#output_function_name) | Lambda function name | +| [invoke_arn](#output_invoke_arn) | Invoke ARN of the lambda function | +| [qualified_arn](#output_qualified_arn) | ARN identifying your Lambda Function Version (if versioning is enabled via publish = true) | +| [role_arn](#output_role_arn) | Lambda IAM role ARN | +| [role_name](#output_role_name) | Lambda IAM role name | + - ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/lambda) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/lambda/main.tf b/modules/lambda/main.tf index 323f82356..93ce28e94 100644 --- a/modules/lambda/main.tf +++ b/modules/lambda/main.tf @@ -1,14 +1,22 @@ locals { - enabled = module.this.enabled - iam_policy_enabled = local.enabled && (try(length(var.iam_policy), 0) > 0 || var.policy_json != null) - s3_bucket_computed_name = var.s3_bucket_name != null ? format("%s-%s-%s-%s-%s", module.this.namespace, module.this.tenant, module.this.environment, module.this.stage, var.s3_bucket_name) : null + enabled = module.this.enabled + var_iam_policy_enabled = local.enabled && (try(length(var.iam_policy), 0) > 0 || var.policy_json != null) + iam_policy_enabled = local.enabled && local.var_iam_policy_enabled - s3_full_bucket_name = coalesce(var.s3_full_bucket_name, local.s3_bucket_computed_name, "none") == "none" ? null : coalesce(var.s3_full_bucket_name, local.s3_bucket_computed_name) - function_name = coalesce(var.function_name, module.label.id) + s3_bucket_name = var.s3_bucket_name != null ? var.s3_bucket_name : one(module.s3_bucket[*].outputs.bucket_id) - cicd_s3_key_format = var.cicd_s3_key_format != null ? var.cicd_s3_key_format : "stage/${module.this.stage}/lambda/${local.function_name}/%s" - s3_key = var.s3_bucket_name == null ? null : (var.s3_key != null ? var.s3_key : format(local.cicd_s3_key_format, coalesce(one(data.aws_ssm_parameter.cicd_ssm_param[*].value), "example"))) + function_name = coalesce(var.function_name, module.this.id) + + var_policy_json = local.var_iam_policy_enabled ? [var.policy_json] : [] + + lambda_files = fileset("${path.module}/lambdas/${var.zip.input_dir == null ? "" : var.zip.input_dir}", "*") + file_content_map = var.zip.enabled ? [ + for f in local.lambda_files : filebase64sha256("${path.module}/lambdas/${coalesce(var.zip.input_dir, var.name)}/${f}") + ] : [] + output_zip_file = local.enabled && var.zip.enabled ? "${path.module}/lambdas/${random_pet.zip_recreator[0].id}.zip" : null + cicd_s3_key_format = var.cicd_s3_key_format != null ? var.cicd_s3_key_format : "stage/${module.this.stage}/lambda/${local.function_name}/%s" + s3_key = var.s3_key != null ? var.s3_key : format(local.cicd_s3_key_format, coalesce(one(data.aws_ssm_parameter.cicd_ssm_param[*].value), "example")) } data "aws_ssm_parameter" "cicd_ssm_param" { @@ -17,15 +25,6 @@ data "aws_ssm_parameter" "cicd_ssm_param" { name = var.cicd_ssm_param_name } -module "label" { - source = "cloudposse/label/null" - version = "0.25.0" - - attributes = [var.function_name] - - context = module.this.context -} - module "iam_policy" { count = local.iam_policy_enabled ? 1 : 0 source = "cloudposse/iam-policy/aws" @@ -33,9 +32,8 @@ module "iam_policy" { iam_policy_enabled = true iam_policy = var.iam_policy - iam_source_policy_documents = var.policy_json != null ? [var.policy_json] : [] - - context = module.this.context + iam_source_policy_documents = local.var_policy_json != null ? local.var_policy_json : [] + context = module.this.context } resource "aws_iam_role_policy_attachment" "default" { @@ -46,15 +44,27 @@ resource "aws_iam_role_policy_attachment" "default" { } data "archive_file" "lambdazip" { - count = var.zip.enabled ? 1 : 0 - type = "zip" - output_path = "${path.module}/lambdas/${var.zip.output}" + count = local.enabled && var.zip.enabled ? 1 : 0 + type = "zip" + + output_path = local.output_zip_file source_dir = "${path.module}/lambdas/${var.zip.input_dir}" + + depends_on = [random_pet.zip_recreator] +} + +resource "random_pet" "zip_recreator" { + count = local.enabled && var.zip.enabled ? 1 : 0 + + prefix = coalesce(module.this.name, "lambda") + keepers = { + file_content = join(",", local.file_content_map) + } } module "lambda" { source = "cloudposse/lambda-function/aws" - version = "0.4.1" + version = "0.6.1" function_name = local.function_name description = var.description @@ -63,37 +73,47 @@ module "lambda" { image_uri = var.image_uri image_config = var.image_config - filename = var.filename - s3_bucket = local.s3_full_bucket_name + filename = var.zip.enabled ? coalesce(data.archive_file.lambdazip[0].output_path, var.filename) : var.filename + s3_bucket = local.s3_bucket_name s3_key = local.s3_key s3_object_version = var.s3_object_version - architectures = var.architectures - cloudwatch_event_rules = var.cloudwatch_event_rules - cloudwatch_lambda_insights_enabled = var.cloudwatch_lambda_insights_enabled - cloudwatch_logs_retention_in_days = var.cloudwatch_logs_retention_in_days - cloudwatch_logs_kms_key_arn = var.cloudwatch_logs_kms_key_arn - cloudwatch_log_subscription_filters = var.cloudwatch_log_subscription_filters - ignore_external_function_updates = var.ignore_external_function_updates - event_source_mappings = var.event_source_mappings - kms_key_arn = var.kms_key_arn - lambda_at_edge_enabled = var.lambda_at_edge_enabled - layers = var.layers - memory_size = var.memory_size - package_type = var.package_type - permissions_boundary = var.permissions_boundary - publish = var.publish - reserved_concurrent_executions = var.reserved_concurrent_executions - runtime = var.runtime - sns_subscriptions = var.sns_subscriptions - source_code_hash = var.source_code_hash - ssm_parameter_names = var.ssm_parameter_names - timeout = var.timeout - tracing_config_mode = var.tracing_config_mode - vpc_config = var.vpc_config - custom_iam_policy_arns = var.custom_iam_policy_arns - dead_letter_config_target_arn = var.dead_letter_config_target_arn - iam_policy_description = var.iam_policy_description + architectures = var.architectures + cloudwatch_lambda_insights_enabled = var.cloudwatch_lambda_insights_enabled + cloudwatch_logs_retention_in_days = var.cloudwatch_logs_retention_in_days + cloudwatch_logs_kms_key_arn = var.cloudwatch_logs_kms_key_arn + kms_key_arn = var.kms_key_arn + lambda_at_edge_enabled = var.lambda_at_edge_enabled + layers = var.layers + memory_size = var.memory_size + package_type = var.package_type + permissions_boundary = var.permissions_boundary + publish = var.publish + reserved_concurrent_executions = var.reserved_concurrent_executions + runtime = var.runtime + source_code_hash = var.source_code_hash + ssm_parameter_names = var.ssm_parameter_names + timeout = var.timeout + tracing_config_mode = var.tracing_config_mode + vpc_config = var.vpc_config + custom_iam_policy_arns = var.custom_iam_policy_arns + dead_letter_config_target_arn = var.dead_letter_config_target_arn + iam_policy_description = var.iam_policy_description context = module.this.context } + +resource "aws_lambda_function_url" "lambda_url" { + count = var.function_url_enabled ? 1 : 0 + function_name = module.lambda.function_name + authorization_type = "AWS_IAM" + + cors { + allow_credentials = true + allow_origins = ["*"] + allow_methods = ["*"] + allow_headers = ["date", "keep-alive"] + expose_headers = ["keep-alive", "date"] + max_age = 86400 + } +} diff --git a/modules/lambda/remote-state.tf b/modules/lambda/remote-state.tf new file mode 100644 index 000000000..af8168f35 --- /dev/null +++ b/modules/lambda/remote-state.tf @@ -0,0 +1,14 @@ +module "s3_bucket" { + count = local.enabled && var.s3_bucket_component != null ? 1 : 0 + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = var.s3_bucket_component.component + + tenant = var.s3_bucket_component.tenant + environment = var.s3_bucket_component.environment + stage = var.s3_bucket_component.stage + + context = module.this.context +} diff --git a/modules/lambda/triggers_cloudwatch_event_rules.tf b/modules/lambda/triggers_cloudwatch_event_rules.tf new file mode 100644 index 000000000..e97aba2b8 --- /dev/null +++ b/modules/lambda/triggers_cloudwatch_event_rules.tf @@ -0,0 +1,43 @@ +module "cloudwatch_event_rules_label" { + for_each = var.cloudwatch_event_rules + + source = "cloudposse/label/null" + version = "0.25.0" + attributes = [each.key] + + context = module.this.context +} + +resource "aws_cloudwatch_event_rule" "event_rules" { + for_each = var.cloudwatch_event_rules + + name = module.cloudwatch_event_rules_label[each.key].id + + description = each.value.description + event_bus_name = each.value.event_bus_name + event_pattern = each.value.event_pattern + is_enabled = each.value.is_enabled + name_prefix = each.value.name_prefix + role_arn = each.value.role_arn + schedule_expression = each.value.schedule_expression + + tags = module.cloudwatch_event_rules_label[each.key].tags +} + +resource "aws_cloudwatch_event_target" "sns" { + for_each = var.cloudwatch_event_rules + + rule = aws_cloudwatch_event_rule.event_rules[each.key].name + target_id = "ScheduleExpression" + arn = module.lambda.arn +} + +resource "aws_lambda_permission" "allow_cloudwatch_to_call_lambda" { + for_each = var.cloudwatch_event_rules + + statement_id = format("%s-%s", "AllowExecutionFromCloudWatch", each.key) + action = "lambda:InvokeFunction" + function_name = module.lambda.function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.event_rules[each.key].arn +} diff --git a/modules/lambda/triggers_s3_notifications.tf b/modules/lambda/triggers_s3_notifications.tf new file mode 100644 index 000000000..ee8af935c --- /dev/null +++ b/modules/lambda/triggers_s3_notifications.tf @@ -0,0 +1,58 @@ +variable "s3_notifications" { + type = map(object({ + bucket_name = optional(string) + bucket_component = optional(object({ + component = string + environment = optional(string) + tenant = optional(string) + stage = optional(string) + })) + events = optional(list(string)) + filter_prefix = optional(string) + filter_suffix = optional(string) + source_account = optional(string) + })) + description = "A map of S3 bucket notifications to trigger the Lambda function" + default = {} +} + +module "s3_bucket_notifications_component" { + for_each = { for k, v in var.s3_notifications : k => v if v.bucket_component != null } + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = each.value.bucket_component.component + + tenant = each.value.bucket_component.tenant + environment = each.value.bucket_component.environment + stage = each.value.bucket_component.stage + + context = module.this.context +} + +resource "aws_lambda_permission" "s3_notification" { + for_each = var.s3_notifications + + statement_id = "AllowS3Invoke" + action = "lambda:InvokeFunction" + function_name = module.lambda.function_name + principal = "s3.amazonaws.com" + source_arn = format("arn:aws:s3:::%s", each.value.bucket_component == null ? each.value.bucket_name : module.s3_bucket_notifications_component[each.key].outputs.bucket_id) + source_account = each.value.source_account +} + +resource "aws_s3_bucket_notification" "s3_notifications" { + for_each = var.s3_notifications + + depends_on = [aws_lambda_permission.s3_notification] + + bucket = each.value.bucket_component == null ? each.value.bucket_name : module.s3_bucket_notifications_component[each.key].outputs.bucket_id + + lambda_function { + lambda_function_arn = module.lambda.arn + events = each.value.events == null ? ["s3:ObjectCreated:*"] : each.value.events + filter_prefix = each.value.filter_prefix + filter_suffix = each.value.filter_suffix + } +} diff --git a/modules/lambda/triggers_sqs_queue.tf b/modules/lambda/triggers_sqs_queue.tf new file mode 100644 index 000000000..747f87bdf --- /dev/null +++ b/modules/lambda/triggers_sqs_queue.tf @@ -0,0 +1,81 @@ +variable "sqs_notifications" { + type = map(object({ + sqs_arn = optional(string) + sqs_component = optional(object({ + component = string + environment = optional(string) + tenant = optional(string) + stage = optional(string) + })) + batch_size = optional(number) + source_account = optional(string) + on_failure_arn = optional(string) + maximum_concurrency = optional(number) + })) + description = "A map of SQS queue notifications to trigger the Lambda function" + default = {} +} + +module "sqs_queue" { + for_each = { for k, v in var.sqs_notifications : k => v if v.sqs_component != null } + + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.5.0" + + component = each.value.sqs_component.component + + tenant = each.value.sqs_component.tenant + environment = each.value.sqs_component.environment + stage = each.value.sqs_component.stage + + context = module.this.context +} + +module "sqs_iam_policy" { + for_each = var.sqs_notifications + + source = "cloudposse/iam-policy/aws" + version = "1.0.1" + + iam_policy_enabled = true + iam_policy = { + version = "2012-10-17" + statements = [ + { + effect = "Allow" + actions = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"] + resources = each.value.sqs_arn != null ? [each.value.sqs_arn.sqs_arn] : [module.sqs_queue[each.key].outputs.sqs_queue.queue_arn] + }, + ] + } + context = module.this.context +} + +resource "aws_iam_role_policy_attachment" "sqs_default" { + for_each = var.sqs_notifications + + role = module.lambda.role_name + policy_arn = module.sqs_iam_policy[each.key].policy_arn +} + +resource "aws_lambda_event_source_mapping" "event_source_mapping" { + for_each = var.sqs_notifications + + event_source_arn = each.value.sqs_arn != null ? [each.value.sqs_arn.sqs_arn] : module.sqs_queue[each.key].outputs.sqs_queue.queue_arn + function_name = module.lambda.function_name + batch_size = each.value.batch_size == null ? 1 : each.value.batch_size + + scaling_config { + maximum_concurrency = each.value.maximum_concurrency + } + dynamic "destination_config" { + for_each = { for k, v in each.value : k => v if k == "on_failure_arn" && v != null } + content { + on_failure { + destination_arn = destination_config.value + } + } + } + + depends_on = [aws_iam_role_policy_attachment.sqs_default] +} diff --git a/modules/lambda/variables.tf b/modules/lambda/variables.tf index f0736c8c2..8785a4037 100644 --- a/modules/lambda/variables.tf +++ b/modules/lambda/variables.tf @@ -19,7 +19,15 @@ variable "architectures" { } variable "cloudwatch_event_rules" { - type = map(any) + type = map(object({ + description = optional(string) + event_bus_name = optional(string) + event_pattern = optional(string) + is_enabled = optional(bool) + name_prefix = optional(string) + role_arn = optional(string) + schedule_expression = optional(string) + })) description = "Creates EventBridge (CloudWatch Events) rules for invoking the Lambda Function along with the required permissions." default = {} } @@ -46,12 +54,6 @@ variable "cloudwatch_logs_kms_key_arn" { default = null } -variable "cloudwatch_log_subscription_filters" { - type = map(any) - description = "CloudWatch Logs subscription filter resources. Currently supports only Lambda functions as destinations." - default = {} -} - variable "description" { type = string description = "Description of what the Lambda Function does." @@ -66,30 +68,12 @@ variable "lambda_environment" { default = null } -variable "event_source_mappings" { - type = any - description = < Date: Fri, 20 Sep 2024 20:48:24 +0300 Subject: [PATCH 484/501] Fix Update changelog workflow (#1116) --- .github/workflows/update-changelog.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index ffc433478..f11954fe9 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -94,11 +94,11 @@ jobs: function trimPath(relativePath) { return relativePath - .replace('components/terraform/', '') + .replace('modules/', '') .replace('/CHANGELOG.md', ''); } - const currentChangeLogFiles = findChangelogs('./current/components'); + const currentChangeLogFiles = findChangelogs('./current/modules'); const components = []; for (let i = 0; i < currentChangeLogFiles.length; i++) { From 421b5c0567d789ff2c6adf167ccf57c871c5e623 Mon Sep 17 00:00:00 2001 From: Brett Au <86862761+brett-au@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:20:56 -0400 Subject: [PATCH 485/501] feat: support delete protection for dynamodb (#1118) --- modules/dynamodb/README.md | 1 + modules/dynamodb/main.tf | 2 ++ modules/dynamodb/variables.tf | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/modules/dynamodb/README.md b/modules/dynamodb/README.md index ff68e2682..df1d3ca2e 100644 --- a/modules/dynamodb/README.md +++ b/modules/dynamodb/README.md @@ -77,6 +77,7 @@ No resources. | [autoscaler\_tags](#input\_autoscaler\_tags) | Additional resource tags for the autoscaler module | `map(string)` | `{}` | no | | [billing\_mode](#input\_billing\_mode) | DynamoDB Billing mode. Can be PROVISIONED or PAY\_PER\_REQUEST | `string` | `"PROVISIONED"` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [deletion\_protection\_enabled](#input\_deletion\_protection\_enabled) | Enable/disable DynamoDB table deletion protection | `bool` | `false` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dynamodb\_attributes](#input\_dynamodb\_attributes) | Additional DynamoDB attributes in the form of a list of mapped values |
list(object({
name = string
type = string
}))
| `[]` | no | diff --git a/modules/dynamodb/main.tf b/modules/dynamodb/main.tf index 979d66b2e..03982fb79 100644 --- a/modules/dynamodb/main.tf +++ b/modules/dynamodb/main.tf @@ -43,5 +43,7 @@ module "dynamodb_table" { enable_point_in_time_recovery = var.point_in_time_recovery_enabled + deletion_protection_enabled = var.deletion_protection_enabled + context = module.this.context } diff --git a/modules/dynamodb/variables.tf b/modules/dynamodb/variables.tf index a0e7b593e..a3ea0d22b 100644 --- a/modules/dynamodb/variables.tf +++ b/modules/dynamodb/variables.tf @@ -174,6 +174,12 @@ variable "replicas" { description = "List of regions to create a replica table in" } +variable "deletion_protection_enabled" { + type = bool + default = false + description = "Enable/disable DynamoDB table deletion protection" +} + variable "import_table" { type = object({ # Valid values are GZIP, ZSTD and NONE From 8a50e9a36e086bc199794f211cc7c700c593b945 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:40:14 +0300 Subject: [PATCH 486/501] Update Changelog for `1.497.0` (#1117) Co-authored-by: cloudposse-releaser[bot] <163353533+cloudposse-releaser[bot]@users.noreply.github.com> Co-authored-by: Igor Rodionov --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9c7cf94b..5fa33b103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # CHANGELOG +## 1.497.0 + + + +
+ Fix Update changelog workflow @goruha (#1116) + + ## what +* Fix modules path from `components/terraform` to `modules` + +## why +* It seems that `components/terraform` was testing value. In actual repo components are in `modules` directory + +## references +* DEV-2556 Investigate release issues with terraform-aws-components +
+ + + ## 1.298.0 (2023-08-28T20:56:25Z)
From 253d3bf6dcff0facca6838c831f7716718abd0dc Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 1 Oct 2024 07:48:51 -0400 Subject: [PATCH 487/501] feat: add detector features to guard duty component (#1112) --- modules/guardduty/README.md | 4 +++- modules/guardduty/main.tf | 18 +++++++++++++++++- modules/guardduty/variables.tf | 32 +++++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/modules/guardduty/README.md b/modules/guardduty/README.md index ce4e3163d..561a96a05 100644 --- a/modules/guardduty/README.md +++ b/modules/guardduty/README.md @@ -179,6 +179,7 @@ atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security | Name | Type | |------|------| +| [aws_guardduty_detector_feature.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector_feature) | resource | | [aws_guardduty_organization_admin_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_admin_account) | resource | | [aws_guardduty_organization_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_configuration) | resource | | [awsutils_guardduty_organization_settings.this](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/resources/guardduty_organization_settings) | resource | @@ -190,7 +191,7 @@ atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security |------|-------------|------|---------|:--------:| | [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `"core"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Admininstrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Administrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [auto\_enable\_organization\_members](#input\_auto\_enable\_organization\_members) | Indicates the auto-enablement configuration of GuardDuty for the member accounts in the organization. Valid values are `ALL`, `NEW`, `NONE`.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_organization_configuration#auto_enable_organization_members | `string` | `"NEW"` | no | | [cloudwatch\_enabled](#input\_cloudwatch\_enabled) | Flag to indicate whether CloudWatch logging should be enabled for GuardDuty | `bool` | `false` | no | @@ -201,6 +202,7 @@ atmos terraform apply guardduty/org-settings/uw1 -s core-uw1-security | [delegated\_administrator\_account\_name](#input\_delegated\_administrator\_account\_name) | The name of the account that is the AWS Organization Delegated Administrator account | `string` | `"core-security"` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [detector\_features](#input\_detector\_features) | A map of detector features for streaming foundational data sources to detect communication with known malicious domains and IP addresses and identify anomalous behavior.

For more information, see:
https://docs.aws.amazon.com/guardduty/latest/ug/guardduty-features-activation-model.html#guardduty-features

feature\_name:
The name of the detector feature. Possible values include: S3\_DATA\_EVENTS, EKS\_AUDIT\_LOGS, EBS\_MALWARE\_PROTECTION, RDS\_LOGIN\_EVENTS, EKS\_RUNTIME\_MONITORING, LAMBDA\_NETWORK\_LOGS, RUNTIME\_MONITORING. Specifying both EKS Runtime Monitoring (EKS\_RUNTIME\_MONITORING) and Runtime Monitoring (RUNTIME\_MONITORING) will cause an error. You can add only one of these two features because Runtime Monitoring already includes the threat detection for Amazon EKS resources. For more information, see: https://docs.aws.amazon.com/guardduty/latest/APIReference/API_DetectorFeatureConfiguration.html.
status:
The status of the detector feature. Valid values include: ENABLED or DISABLED.
additional\_configuration:
Optional information about the additional configuration for a feature in your GuardDuty account. For more information, see: https://docs.aws.amazon.com/guardduty/latest/APIReference/API_DetectorAdditionalConfiguration.html.
addon\_name:
The name of the add-on for which the configuration applies. Possible values include: EKS\_ADDON\_MANAGEMENT, ECS\_FARGATE\_AGENT\_MANAGEMENT, and EC2\_AGENT\_MANAGEMENT. For more information, see: https://docs.aws.amazon.com/guardduty/latest/APIReference/API_DetectorAdditionalConfiguration.html.
status:
The status of the add-on. Valid values include: ENABLED or DISABLED. |
map(object({
feature_name = string
status = string
additional_configuration = optional(object({
addon_name = string
status = string
}), null)
}))
| `{}` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [finding\_publishing\_frequency](#input\_finding\_publishing\_frequency) | The frequency of notifications sent for finding occurrences. If the detector is a GuardDuty member account, the value
is determined by the GuardDuty master account and cannot be modified, otherwise it defaults to SIX\_HOURS.

For standalone and GuardDuty master accounts, it must be configured in Terraform to enable drift detection.
Valid values for standalone and master accounts: FIFTEEN\_MINUTES, ONE\_HOUR, SIX\_HOURS."

For more information, see:
https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings_cloudwatch.html#guardduty_findings_cloudwatch_notification_frequency | `string` | `null` | no | diff --git a/modules/guardduty/main.tf b/modules/guardduty/main.tf index 9026384d0..633e5d59c 100644 --- a/modules/guardduty/main.tf +++ b/modules/guardduty/main.tf @@ -35,7 +35,7 @@ module "guardduty" { version = "0.5.0" finding_publishing_frequency = var.finding_publishing_frequency - create_sns_topic = var.create_sns_topic + create_sns_topic = local.create_sns_topic findings_notification_arn = var.findings_notification_arn subscribers = var.subscribers enable_cloudwatch = var.cloudwatch_enabled @@ -80,3 +80,19 @@ resource "aws_guardduty_organization_configuration" "this" { } } } + +resource "aws_guardduty_detector_feature" "this" { + for_each = { for k, v in var.detector_features : k => v if local.create_org_configuration } + + detector_id = module.guardduty_delegated_detector[0].outputs.guardduty_detector_id + name = each.value.feature_name + status = each.value.status + + dynamic "additional_configuration" { + for_each = each.value.additional_configuration != null ? [each.value.additional_configuration] : [] + content { + name = additional_configuration.value.addon_name + status = additional_configuration.value.status + } + } +} diff --git a/modules/guardduty/variables.tf b/modules/guardduty/variables.tf index 2e4c9eb4d..ba11f96dd 100644 --- a/modules/guardduty/variables.tf +++ b/modules/guardduty/variables.tf @@ -9,7 +9,7 @@ variable "admin_delegated" { default = false description = < Date: Tue, 1 Oct 2024 07:12:28 -0500 Subject: [PATCH 488/501] docs: fix typos using `codespell` (#1114) --- CHANGELOG.md | 14 +++++++------- README.md | 2 +- README.yaml | 2 +- deprecated/aws/backing-services/rds-replica.tf | 2 +- deprecated/aws/backing-services/rds.tf | 2 +- .../aws/grafana-backing-services/aurora-mysql.tf | 6 +++--- deprecated/aws/keycloak-backing-services/README.md | 2 +- deprecated/aws/kops/variables.tf | 2 +- deprecated/aws/sentry/aurora-postgres.tf | 4 ++-- deprecated/aws/teleport/main.tf | 2 +- .../aws/vpc-peering-intra-account/variables.tf | 8 ++++---- deprecated/aws/vpc/outputs.tf | 2 +- deprecated/aws/vpc/variables.tf | 2 +- deprecated/eks/eks-without-spotinst/main.tf | 2 +- deprecated/github-actions-runner/variables.tf | 2 +- .../securityhub/securityhub/common/README.md | 2 +- .../securityhub/securityhub/common/variables.tf | 2 +- deprecated/spacelift/docs/spacelift-overview.md | 2 +- .../tgw/cross-region-hub-connector/providers.tf | 2 +- modules/account-map/modules/iam-roles/providers.tf | 2 +- modules/account/README.md | 6 +++--- modules/aurora-mysql/README.md | 2 +- modules/aurora-postgres-resources/README.md | 2 +- modules/aurora-postgres-resources/variables.tf | 2 +- modules/aws-config/README.md | 2 +- modules/aws-inspector2/README.md | 4 ++-- modules/aws-inspector2/variables.tf | 2 +- modules/aws-saml/README.md | 2 +- modules/aws-sso/README.md | 4 ++-- modules/aws-sso/policy-DNSAdministratorAccess.tf | 2 +- modules/bastion/README.md | 2 +- modules/bastion/variables.tf | 2 +- .../modules/datadog_keys/README.md | 2 +- .../modules/datadog_keys/outputs.tf | 2 +- modules/datadog-logs-archive/README.md | 2 +- modules/datadog-monitor/catalog/monitors/efs.yaml | 2 +- modules/dns-delegated/README.md | 4 ++-- modules/dns-delegated/variables.tf | 2 +- modules/dns-primary/README.md | 2 +- modules/dns-primary/variables.tf | 2 +- modules/ec2-instance/README.md | 2 +- modules/ec2-instance/variables.tf | 2 +- modules/eks/argocd/CHANGELOG.md | 2 +- modules/eks/cluster/CHANGELOG.md | 4 ++-- modules/eks/cluster/variables.tf | 2 +- modules/eks/datadog-agent/CHANGELOG.md | 2 +- modules/eks/external-secrets-operator/README.md | 2 +- modules/eks/github-actions-runner/CHANGELOG.md | 2 +- modules/eks/idp-roles/README.md | 2 +- modules/github-runners/README.md | 2 +- modules/github-webhook/README.md | 2 +- modules/github-webhook/variables.tf | 2 +- modules/glue/job/README.md | 2 +- modules/glue/job/variables.tf | 2 +- modules/guardduty/README.md | 8 ++++---- modules/macie/README.md | 4 ++-- modules/macie/variables.tf | 2 +- modules/managed-prometheus/workspace/README.md | 2 +- modules/msk/security-group-variables.tf | 2 +- modules/mwaa/README.md | 4 ++-- modules/mwaa/variables.tf | 4 ++-- modules/security-hub/README.md | 4 ++-- modules/security-hub/main.tf | 4 ++-- modules/security-hub/variables.tf | 4 ++-- modules/sns-topic/variables.tf | 2 +- modules/spa-s3-cloudfront/CHANGELOG.md | 2 +- modules/spacelift/README.md | 8 ++++---- modules/tfstate-backend/iam.tf | 2 +- modules/tgw/README.md | 2 +- modules/tgw/hub/remote-state.tf | 4 ++-- modules/tgw/spoke/README.md | 2 +- modules/tgw/spoke/variables.tf | 2 +- modules/vpc-peering/README.md | 6 +++--- modules/vpc-peering/variables.tf | 6 +++--- modules/zscaler/README.md | 2 +- modules/zscaler/variables.tf | 2 +- 76 files changed, 111 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa33b103..4e8a867a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -531,7 +531,7 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 ### 🐛 Bug Fixes
- Karpenter bugfix, EKS add-ons to mangaed node group @Nuru (#816) + Karpenter bugfix, EKS add-ons to managed node group @Nuru (#816) ### what @@ -553,7 +553,7 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 ### what -- Upsteam the latest `ecs-service` component +- Upstream the latest `ecs-service` component ### why @@ -820,7 +820,7 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 ### why -- to help future implementors of CloudPosse's architectures +- to help future implementers of CloudPosse's architectures ### references @@ -841,7 +841,7 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0 - fix incorrect shape for one of the items in `aws_team_roles_rbac` - improve consistency -- remove variables that are not appliable for the component +- remove variables that are not applicable for the component ### references @@ -3840,7 +3840,7 @@ N/A ### what - bumped ecr -- remove unnecssary variable +- remove unnecessary variable ### why @@ -4627,7 +4627,7 @@ NOTE: I don't know if the default of `default` is valid or if it is `Default`. I ### what -- Bump Versin of EC2 Client VPN +- Bump Version of EC2 Client VPN ### why @@ -4758,7 +4758,7 @@ This is an alternative way of deprovisioning - proactive one. ``` There is another way to configure Karpenter to deprovision nodes called Consolidation. -This mode is preferred for workloads such as microservices and is imcompatible with setting +This mode is preferred for workloads such as microservices and is incompatible with setting up the ttlSecondsAfterEmpty . When set in consolidation mode Karpenter works to actively reduce cluster cost by identifying when nodes can be removed as their workloads will run on other nodes in the cluster and when nodes can be replaced with cheaper variants due diff --git a/README.md b/README.md index 79aedb85c..d86d9554e 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ update: dry_run: no ``` -For the full documentation on how to use the Component Updater GitHub Action, please see the [Atmos Intergations](https://atmos.tools/integrations/github-actions/component-updater) documentation. +For the full documentation on how to use the Component Updater GitHub Action, please see the [Atmos Integrations](https://atmos.tools/integrations/github-actions/component-updater) documentation. ## Using `pre-commit` Hooks diff --git a/README.yaml b/README.yaml index e96844f94..e264b3b75 100644 --- a/README.yaml +++ b/README.yaml @@ -147,7 +147,7 @@ usage: |- dry_run: no ``` - For the full documentation on how to use the Component Updater GitHub Action, please see the [Atmos Intergations](https://atmos.tools/integrations/github-actions/component-updater) documentation. + For the full documentation on how to use the Component Updater GitHub Action, please see the [Atmos Integrations](https://atmos.tools/integrations/github-actions/component-updater) documentation. ## Using `pre-commit` Hooks diff --git a/deprecated/aws/backing-services/rds-replica.tf b/deprecated/aws/backing-services/rds-replica.tf index d509b87a5..00dd10f1f 100644 --- a/deprecated/aws/backing-services/rds-replica.tf +++ b/deprecated/aws/backing-services/rds-replica.tf @@ -45,7 +45,7 @@ variable "rds_replica_snapshot" { variable "rds_replica_multi_az" { type = string default = "false" - description = "Run instaces in multiple az" + description = "Run instances in multiple az" } variable "rds_replica_storage_type" { diff --git a/deprecated/aws/backing-services/rds.tf b/deprecated/aws/backing-services/rds.tf index 5a892ab78..a65f13375 100644 --- a/deprecated/aws/backing-services/rds.tf +++ b/deprecated/aws/backing-services/rds.tf @@ -81,7 +81,7 @@ variable "rds_parameter_group_name" { variable "rds_multi_az" { type = string default = "false" - description = "Run instaces in multiple az" + description = "Run instances in multiple az" } variable "rds_storage_type" { diff --git a/deprecated/aws/grafana-backing-services/aurora-mysql.tf b/deprecated/aws/grafana-backing-services/aurora-mysql.tf index 049d676d2..dd18c3096 100644 --- a/deprecated/aws/grafana-backing-services/aurora-mysql.tf +++ b/deprecated/aws/grafana-backing-services/aurora-mysql.tf @@ -87,7 +87,7 @@ resource "random_string" "mysql_admin_password" { # "Read SSM parameter to get allowed CIDR blocks" data "aws_ssm_parameter" "allowed_cidr_blocks" { # The data source will throw an error if it cannot find the parameter, - # so do not reference it unless it is neeeded. + # so do not reference it unless it is needed. count = local.allowed_cidr_blocks_use_ssm ? 1 : 0 # name = substr(mysql_cluster_allowed_cidr_blocks, 0, 1) == "/" ? mysql_cluster_allowed_cidr_blocks : "/aws/service/global-infrastructure/version" @@ -97,7 +97,7 @@ data "aws_ssm_parameter" "allowed_cidr_blocks" { # "Read SSM parameter to get allowed VPC ID" data "aws_ssm_parameter" "vpc_id" { # The data source will throw an error if it cannot find the parameter, - # so do not reference it unless it is neeeded. + # so do not reference it unless it is needed. count = local.vpc_id_use_ssm ? 1 : 0 name = var.vpc_id } @@ -105,7 +105,7 @@ data "aws_ssm_parameter" "vpc_id" { # "Read SSM parameter to get allowed VPC subnet IDs" data "aws_ssm_parameter" "vpc_subnet_ids" { # The data source will throw an error if it cannot find the parameter, - # so do not reference it unless it is neeeded. + # so do not reference it unless it is needed. count = local.vpc_subnet_ids_use_ssm ? 1 : 0 name = var.vpc_subnet_ids } diff --git a/deprecated/aws/keycloak-backing-services/README.md b/deprecated/aws/keycloak-backing-services/README.md index 631b02f36..3a851b29f 100644 --- a/deprecated/aws/keycloak-backing-services/README.md +++ b/deprecated/aws/keycloak-backing-services/README.md @@ -43,7 +43,7 @@ an authorized local service. To keep the database encrypted, this module will have to be extended: 1 Create a KMS key for encrypting the database. Using the RDS default key is not advisable since the only practical advantage of the key comes from -limiting access to it, and the default key will likey have relatively +limiting access to it, and the default key will likely have relatively wide access. 1. Create an IAM role for Keycloak that has access to the key. Nodes running `kiam-server` will need to be able to assume this role. diff --git a/deprecated/aws/kops/variables.tf b/deprecated/aws/kops/variables.tf index 5651683a1..1281f713c 100644 --- a/deprecated/aws/kops/variables.tf +++ b/deprecated/aws/kops/variables.tf @@ -21,7 +21,7 @@ variable "name" { variable "region" { type = string default = "" - description = "AWS region for resources. Can be overriden by `resource_region` and `state_store_region`" + description = "AWS region for resources. Can be overridden by `resource_region` and `state_store_region`" } variable "state_store_region" { diff --git a/deprecated/aws/sentry/aurora-postgres.tf b/deprecated/aws/sentry/aurora-postgres.tf index 946b774d3..79685bea9 100644 --- a/deprecated/aws/sentry/aurora-postgres.tf +++ b/deprecated/aws/sentry/aurora-postgres.tf @@ -30,13 +30,13 @@ variable "postgres_db_name" { variable "aurora_postgres_engine_version" { type = string - description = "Database Engine Version for Aurora PostgeSQL" + description = "Database Engine Version for Aurora postgresql" default = "9.6.12" } variable "aurora_postgres_cluster_family" { type = string - description = "Database Engine Version for Aurora PostgeSQL" + description = "Database Engine Version for Aurora postgresql" default = "9.6.12" } diff --git a/deprecated/aws/teleport/main.tf b/deprecated/aws/teleport/main.tf index 557e1b979..214f5f788 100644 --- a/deprecated/aws/teleport/main.tf +++ b/deprecated/aws/teleport/main.tf @@ -91,7 +91,7 @@ resource "aws_iam_role" "teleport" { data "aws_iam_policy_document" "teleport" { // Teleport can use LetsEncrypt to get TLS certificates. For this // it needs additional permissions which are not included here. - // Teleport can use SSM to publish "join tokens" and retreive the enterprise // license, but that is not fully documented, so permissions to access SSM // are not included at this time. + // Teleport can use SSM to publish "join tokens" and retrieve the enterprise // license, but that is not fully documented, so permissions to access SSM // are not included at this time. // S3 permissions are needed to save and replay SSH sessions statement { diff --git a/deprecated/aws/vpc-peering-intra-account/variables.tf b/deprecated/aws/vpc-peering-intra-account/variables.tf index 94c6739aa..c80e31fe4 100644 --- a/deprecated/aws/vpc-peering-intra-account/variables.tf +++ b/deprecated/aws/vpc-peering-intra-account/variables.tf @@ -9,13 +9,13 @@ variable "aws_assume_role_arn" { variable "requestor_vpc_id" { type = string - description = "Requestor VPC ID" + description = "Requester VPC ID" default = "" } variable "requestor_vpc_tags" { type = map(string) - description = "Requestor VPC tags" + description = "Requester VPC tags" default = {} } @@ -38,12 +38,12 @@ variable "auto_accept" { variable "acceptor_allow_remote_vpc_dns_resolution" { default = "true" - description = "Allow acceptor VPC to resolve public DNS hostnames to private IP addresses when queried from instances in the requestor VPC" + description = "Allow acceptor VPC to resolve public DNS hostnames to private IP addresses when queried from instances in the requester VPC" } variable "requestor_allow_remote_vpc_dns_resolution" { default = "true" - description = "Allow requestor VPC to resolve public DNS hostnames to private IP addresses when queried from instances in the acceptor VPC" + description = "Allow requester VPC to resolve public DNS hostnames to private IP addresses when queried from instances in the acceptor VPC" } variable "namespace" { diff --git a/deprecated/aws/vpc/outputs.tf b/deprecated/aws/vpc/outputs.tf index 46be2a120..23fb4b993 100644 --- a/deprecated/aws/vpc/outputs.tf +++ b/deprecated/aws/vpc/outputs.tf @@ -23,7 +23,7 @@ output "cidr_block" { } output "availability_zones" { - description = "Comma-separated string list of avaialbility zones where subnets have been created" + description = "Comma-separated string list of availability zones where subnets have been created" value = aws_ssm_parameter.availability_zones.value } diff --git a/deprecated/aws/vpc/variables.tf b/deprecated/aws/vpc/variables.tf index ffc2423e1..7d3d7b213 100644 --- a/deprecated/aws/vpc/variables.tf +++ b/deprecated/aws/vpc/variables.tf @@ -30,7 +30,7 @@ variable "region" { variable "max_subnet_count" { default = 0 - description = "Sets the maximum amount of subnets to deploy. 0 will deploy a subnet for every provided availablility zone (in `availability_zones` variable) within the region" + description = "Sets the maximum amount of subnets to deploy. 0 will deploy a subnet for every provided availability zone (in `availability_zones` variable) within the region" } variable "availability_zones" { diff --git a/deprecated/eks/eks-without-spotinst/main.tf b/deprecated/eks/eks-without-spotinst/main.tf index 84b740754..369edc235 100644 --- a/deprecated/eks/eks-without-spotinst/main.tf +++ b/deprecated/eks/eks-without-spotinst/main.tf @@ -56,7 +56,7 @@ module "eks_cluster" { # exec_auth is more reliable than data_auth when the aws CLI is available # Details at https://github.com/cloudposse/terraform-aws-eks-cluster/releases/tag/0.42.0 kube_exec_auth_enabled = !var.kubeconfig_file_enabled - # If using `exec` method (recommended) for authentication, provide an explict + # If using `exec` method (recommended) for authentication, provide an explicit # IAM role ARN to exec as for authentication to EKS cluster. kube_exec_auth_role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) kube_exec_auth_role_arn_enabled = true diff --git a/deprecated/github-actions-runner/variables.tf b/deprecated/github-actions-runner/variables.tf index d547c9380..a3b02b113 100644 --- a/deprecated/github-actions-runner/variables.tf +++ b/deprecated/github-actions-runner/variables.tf @@ -192,7 +192,7 @@ variable "runner_configurations" { error_message = "Variable runner_configurations can contain only one target key of either `repo` or `org` not both." } - # runner_configuration may only conatain map keys "repo", "org", "runner_type", "autoscale_type" + # runner_configuration may only contain map keys "repo", "org", "runner_type", "autoscale_type" validation { condition = alltrue([for r in var.runner_configurations : alltrue([for k in keys(r) : contains(["repo", "org", "runner_type", "autoscale_type"], k)])]) error_message = "Unknown map key, must be one of repo, org, runner_type or autoscale_type." diff --git a/deprecated/securityhub/securityhub/common/README.md b/deprecated/securityhub/securityhub/common/README.md index 21e1e3761..7ca77c542 100644 --- a/deprecated/securityhub/securityhub/common/README.md +++ b/deprecated/securityhub/securityhub/common/README.md @@ -133,7 +133,7 @@ done |------|-------------|------|---------|:--------:| | [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `""` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the Security Hub Admininstrator account has been designated from the root account.

This component should be applied with this variable set to `false`, then the securityhub/root component should be applied
to designate the administrator account, then this component should be applied again with this variable set to `true`. | `bool` | `false` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the Security Hub Administrator account has been designated from the root account.

This component should be applied with this variable set to `false`, then the securityhub/root component should be applied
to designate the administrator account, then this component should be applied again with this variable set to `true`. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [central\_resource\_collector\_account](#input\_central\_resource\_collector\_account) | The name of the account that is the centralized aggregation account | `string` | n/a | yes | | [central\_resource\_collector\_region](#input\_central\_resource\_collector\_region) | The region that collects findings | `string` | n/a | yes | diff --git a/deprecated/securityhub/securityhub/common/variables.tf b/deprecated/securityhub/securityhub/common/variables.tf index bfde69fe0..476ee5c10 100644 --- a/deprecated/securityhub/securityhub/common/variables.tf +++ b/deprecated/securityhub/securityhub/common/variables.tf @@ -67,7 +67,7 @@ variable "admin_delegated" { type = bool default = false description = < [additional\_databases](#input\_additional\_databases) | Additional databases to be created with the cluster | `set(string)` | `[]` | no | | [additional\_grants](#input\_additional\_grants) | Create additional database user with specified grants.
If `var.ssm_password_source` is set, passwords will be retrieved from SSM parameter store,
otherwise, passwords will be generated and stored in SSM parameter store under the service's key. |
map(list(object({
grant : list(string)
db : string
})))
| `{}` | no | -| [additional\_schemas](#input\_additional\_schemas) | Create additonal schemas for a given database.
If no database is given, the schema will use the database used by the provider configuration |
map(object({
database : string
}))
| `{}` | no | +| [additional\_schemas](#input\_additional\_schemas) | Create additional schemas for a given database.
If no database is given, the schema will use the database used by the provider configuration |
map(object({
database : string
}))
| `{}` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [additional\_users](#input\_additional\_users) | Create additional database user for a service, specifying username, grants, and optional password.
If no password is specified, one will be generated. Username and password will be stored in
SSM parameter store under the service's key. |
map(object({
db_user : string
db_password : string
grants : list(object({
grant : list(string)
db : string
schema : string
object_type : string
}))
}))
| `{}` | no | | [admin\_password](#input\_admin\_password) | postgresql password for the admin user | `string` | `""` | no | diff --git a/modules/aurora-postgres-resources/variables.tf b/modules/aurora-postgres-resources/variables.tf index d723638b0..f46902939 100644 --- a/modules/aurora-postgres-resources/variables.tf +++ b/modules/aurora-postgres-resources/variables.tf @@ -94,7 +94,7 @@ variable "additional_schemas" { })) default = {} description = <<-EOT - Create additonal schemas for a given database. + Create additional schemas for a given database. If no database is given, the schema will use the database used by the provider configuration EOT } diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index 9878ca4ef..355ebad34 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -98,7 +98,7 @@ component by default. Note that this can be overridden by the `scope` variable i > > #### Using the organization default_scope > -> If default_scope == `organization`, AWS Config is global unless overriden in the `conformance_packs` items. You will +> If default_scope == `organization`, AWS Config is global unless overridden in the `conformance_packs` items. You will > need to update your org to allow the `config-multiaccountsetup.amazonaws.com` service access principal for this to > work. If you are using our `account` component, just add that principal to the `aws_service_access_principals` > variable. diff --git a/modules/aws-inspector2/README.md b/modules/aws-inspector2/README.md index 280f363ce..0b9b0a581 100644 --- a/modules/aws-inspector2/README.md +++ b/modules/aws-inspector2/README.md @@ -17,7 +17,7 @@ This component is responsible for configuring Inspector V2 within an AWS Organiz The deployment of this component requires multiple runs with different variable settings to properly configure the AWS Organization. First, you delegate Inspector V2 central management to the Administrator account (usually `security` -account). After the Adminstrator account is delegated, we configure the it to manage Inspector V2 across all the +account). After the Administrator account is delegated, we configure the it to manage Inspector V2 across all the Organization accounts and send all their findings to that account. In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization @@ -102,7 +102,7 @@ components: |------|-------------|------|---------|:--------:| | [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `"core"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Admininstrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Administrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [auto\_enable\_ec2](#input\_auto\_enable\_ec2) | Whether Amazon EC2 scans are automatically enabled for new members of the Amazon Inspector organization. | `bool` | `true` | no | | [auto\_enable\_ecr](#input\_auto\_enable\_ecr) | Whether Amazon ECR scans are automatically enabled for new members of the Amazon Inspector organization. | `bool` | `true` | no | diff --git a/modules/aws-inspector2/variables.tf b/modules/aws-inspector2/variables.tf index cde735da7..1880ef921 100644 --- a/modules/aws-inspector2/variables.tf +++ b/modules/aws-inspector2/variables.tf @@ -71,7 +71,7 @@ variable "admin_delegated" { default = false description = < [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS region | `string` | n/a | yes | -| [security\_group\_rules](#input\_security\_group\_rules) | A list of maps of Security Group rules.
The values of map is fully complated with `aws_security_group_rule` resource.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` |
[
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 0,
"protocol": -1,
"to_port": 0,
"type": "egress"
},
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 22,
"protocol": "tcp",
"to_port": 22,
"type": "ingress"
}
]
| no | +| [security\_group\_rules](#input\_security\_group\_rules) | A list of maps of Security Group rules.
The values of map is fully completed with `aws_security_group_rule` resource.
To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . | `list(any)` |
[
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 0,
"protocol": -1,
"to_port": 0,
"type": "egress"
},
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 22,
"protocol": "tcp",
"to_port": 22,
"type": "ingress"
}
]
| no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/bastion/variables.tf b/modules/bastion/variables.tf index 3ed81c211..f7cc2638a 100644 --- a/modules/bastion/variables.tf +++ b/modules/bastion/variables.tf @@ -44,7 +44,7 @@ variable "security_group_rules" { ] description = <<-EOT A list of maps of Security Group rules. - The values of map is fully complated with `aws_security_group_rule` resource. + The values of map is fully completed with `aws_security_group_rule` resource. To get more info see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule . EOT } diff --git a/modules/datadog-configuration/modules/datadog_keys/README.md b/modules/datadog-configuration/modules/datadog_keys/README.md index 4ffc16018..ea8c64aa5 100644 --- a/modules/datadog-configuration/modules/datadog_keys/README.md +++ b/modules/datadog-configuration/modules/datadog_keys/README.md @@ -95,6 +95,6 @@ provider "datadog" { | [datadog\_app\_key\_location](#output\_datadog\_app\_key\_location) | The Datadog APP key location in the secrets store | | [datadog\_secrets\_store\_type](#output\_datadog\_secrets\_store\_type) | The type of the secrets store to use for Datadog API and APP keys | | [datadog\_site](#output\_datadog\_site) | Datadog Site | -| [datadog\_tags](#output\_datadog\_tags) | The Context Tags in datadog tag format (list of strings formated as 'key:value') | +| [datadog\_tags](#output\_datadog\_tags) | The Context Tags in datadog tag format (list of strings formatted as 'key:value') | diff --git a/modules/datadog-configuration/modules/datadog_keys/outputs.tf b/modules/datadog-configuration/modules/datadog_keys/outputs.tf index d24f5f6e8..d5a7d62a6 100644 --- a/modules/datadog-configuration/modules/datadog_keys/outputs.tf +++ b/modules/datadog-configuration/modules/datadog_keys/outputs.tf @@ -40,5 +40,5 @@ output "datadog_api_key_location" { output "datadog_tags" { value = local.dd_tags - description = "The Context Tags in datadog tag format (list of strings formated as 'key:value')" + description = "The Context Tags in datadog tag format (list of strings formatted as 'key:value')" } diff --git a/modules/datadog-logs-archive/README.md b/modules/datadog-logs-archive/README.md index cf2a92e6b..6b0a3b482 100644 --- a/modules/datadog-logs-archive/README.md +++ b/modules/datadog-logs-archive/README.md @@ -18,7 +18,7 @@ A second bucket is created for cloudtrail, and a cloudtrail is configured to mon activity to the cloudtrail bucket. To forward these cloudtrail logs to datadog, the cloudtrail bucket's id must be added to the s3_buckets key for our datadog-lambda-forwarder component. -Both buckets support object lock, with overrideable defaults of COMPLIANCE mode with a duration of 7 days. +Both buckets support object lock, with overridable defaults of COMPLIANCE mode with a duration of 7 days. ## Prerequisites diff --git a/modules/datadog-monitor/catalog/monitors/efs.yaml b/modules/datadog-monitor/catalog/monitors/efs.yaml index b4136df6e..9453b79c4 100644 --- a/modules/datadog-monitor/catalog/monitors/efs.yaml +++ b/modules/datadog-monitor/catalog/monitors/efs.yaml @@ -103,7 +103,7 @@ efs-client-connection-anomaly: query: | avg(last_4h):anomalies(avg:aws.efs.client_connections{stage:${ stage }} by {aws_account,filesystemid,name,stage,tenant,environment,team}.as_count(), 'basic', 2, direction='both', alert_window='last_15m', interval=60, count_default_zero='true') >= 1 message: | - ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{name}}] EFS Client Connection Anomoly for filesystem {{filesystemid}}. + ({{tenant.name}}-{{environment.name}}-{{stage.name}}) [{{name}}] EFS Client Connection Anomaly for filesystem {{filesystemid}}. escalation_message: "" tags: managed-by: Terraform diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index dedfa2577..c39b1710d 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -39,7 +39,7 @@ components: # dns_soa_config configures the SOA record for the zone:: # - awsdns-hostmaster.amazon.com. ; AWS default value for administrator email address # - 1 ; serial number, not used by AWS - # - 7200 ; refresh time in seconds for secondary DNS servers to refreh SOA record + # - 7200 ; refresh time in seconds for secondary DNS servers to refresh SOA record # - 900 ; retry time in seconds for secondary DNS servers to retry failed SOA record update # - 1209600 ; expire time in seconds (1209600 is 2 weeks) for secondary DNS servers to remove SOA record if they cannot refresh it # - 60 ; nxdomain TTL, or time in seconds for secondary DNS servers to cache negative responses @@ -205,7 +205,7 @@ Takeaway | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [dns\_private\_zone\_enabled](#input\_dns\_private\_zone\_enabled) | Whether to set the zone to public or private | `bool` | `false` | no | -| [dns\_soa\_config](#input\_dns\_soa\_config) | Root domain name DNS SOA record:
- awsdns-hostmaster.amazon.com. ; AWS default value for administrator email address
- 1 ; serial number, not used by AWS
- 7200 ; refresh time in seconds for secondary DNS servers to refreh SOA record
- 900 ; retry time in seconds for secondary DNS servers to retry failed SOA record update
- 1209600 ; expire time in seconds (1209600 is 2 weeks) for secondary DNS servers to remove SOA record if they cannot refresh it
- 60 ; nxdomain TTL, or time in seconds for secondary DNS servers to cache negative responses
See [SOA Record Documentation](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/SOA-NSrecords.html) for more information. | `string` | `"awsdns-hostmaster.amazon.com. 1 7200 900 1209600 60"` | no | +| [dns\_soa\_config](#input\_dns\_soa\_config) | Root domain name DNS SOA record:
- awsdns-hostmaster.amazon.com. ; AWS default value for administrator email address
- 1 ; serial number, not used by AWS
- 7200 ; refresh time in seconds for secondary DNS servers to refresh SOA record
- 900 ; retry time in seconds for secondary DNS servers to retry failed SOA record update
- 1209600 ; expire time in seconds (1209600 is 2 weeks) for secondary DNS servers to remove SOA record if they cannot refresh it
- 60 ; nxdomain TTL, or time in seconds for secondary DNS servers to cache negative responses
See [SOA Record Documentation](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/SOA-NSrecords.html) for more information. | `string` | `"awsdns-hostmaster.amazon.com. 1 7200 900 1209600 60"` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | | [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | diff --git a/modules/dns-delegated/variables.tf b/modules/dns-delegated/variables.tf index 14d557918..209ececa7 100644 --- a/modules/dns-delegated/variables.tf +++ b/modules/dns-delegated/variables.tf @@ -81,7 +81,7 @@ variable "dns_soa_config" { Root domain name DNS SOA record: - awsdns-hostmaster.amazon.com. ; AWS default value for administrator email address - 1 ; serial number, not used by AWS - - 7200 ; refresh time in seconds for secondary DNS servers to refreh SOA record + - 7200 ; refresh time in seconds for secondary DNS servers to refresh SOA record - 900 ; retry time in seconds for secondary DNS servers to retry failed SOA record update - 1209600 ; expire time in seconds (1209600 is 2 weeks) for secondary DNS servers to remove SOA record if they cannot refresh it - 60 ; nxdomain TTL, or time in seconds for secondary DNS servers to cache negative responses diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index d8b64a66b..fe5e876cb 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -147,7 +147,7 @@ components: | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [dns\_soa\_config](#input\_dns\_soa\_config) | Root domain name DNS SOA record:
- awsdns-hostmaster.amazon.com. ; AWS default value for administrator email address
- 1 ; serial number, not used by AWS
- 7200 ; refresh time in seconds for secondary DNS servers to refreh SOA record
- 900 ; retry time in seconds for secondary DNS servers to retry failed SOA record update
- 1209600 ; expire time in seconds (1209600 is 2 weeks) for secondary DNS servers to remove SOA record if they cannot refresh it
- 60 ; nxdomain TTL, or time in seconds for secondary DNS servers to cache negative responses
See [SOA Record Documentation](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/SOA-NSrecords.html) for more information. | `string` | `"awsdns-hostmaster.amazon.com. 1 7200 900 1209600 60"` | no | +| [dns\_soa\_config](#input\_dns\_soa\_config) | Root domain name DNS SOA record:
- awsdns-hostmaster.amazon.com. ; AWS default value for administrator email address
- 1 ; serial number, not used by AWS
- 7200 ; refresh time in seconds for secondary DNS servers to refresh SOA record
- 900 ; retry time in seconds for secondary DNS servers to retry failed SOA record update
- 1209600 ; expire time in seconds (1209600 is 2 weeks) for secondary DNS servers to remove SOA record if they cannot refresh it
- 60 ; nxdomain TTL, or time in seconds for secondary DNS servers to cache negative responses
See [SOA Record Documentation](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/SOA-NSrecords.html) for more information. | `string` | `"awsdns-hostmaster.amazon.com. 1 7200 900 1209600 60"` | no | | [domain\_names](#input\_domain\_names) | Root domain name list, e.g. `["example.net"]` | `list(string)` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | diff --git a/modules/dns-primary/variables.tf b/modules/dns-primary/variables.tf index 162dc0cf2..6aa801287 100644 --- a/modules/dns-primary/variables.tf +++ b/modules/dns-primary/variables.tf @@ -42,7 +42,7 @@ variable "dns_soa_config" { Root domain name DNS SOA record: - awsdns-hostmaster.amazon.com. ; AWS default value for administrator email address - 1 ; serial number, not used by AWS - - 7200 ; refresh time in seconds for secondary DNS servers to refreh SOA record + - 7200 ; refresh time in seconds for secondary DNS servers to refresh SOA record - 900 ; retry time in seconds for secondary DNS servers to retry failed SOA record update - 1209600 ; expire time in seconds (1209600 is 2 weeks) for secondary DNS servers to remove SOA record if they cannot refresh it - 60 ; nxdomain TTL, or time in seconds for secondary DNS servers to cache negative responses diff --git a/modules/ec2-instance/README.md b/modules/ec2-instance/README.md index 26502b724..958618d7f 100644 --- a/modules/ec2-instance/README.md +++ b/modules/ec2-instance/README.md @@ -81,7 +81,7 @@ components: | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS region | `string` | n/a | yes | -| [security\_group\_rules](#input\_security\_group\_rules) | A list of maps of Security Group rules.
The values of map is fully complated with `aws_security_group_rule` resource.
To get more info see [security\_group\_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). | `list(any)` |
[
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 0,
"protocol": "-1",
"to_port": 65535,
"type": "egress"
}
]
| no | +| [security\_group\_rules](#input\_security\_group\_rules) | A list of maps of Security Group rules.
The values of map is fully completed with `aws_security_group_rule` resource.
To get more info see [security\_group\_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). | `list(any)` |
[
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 0,
"protocol": "-1",
"to_port": 65535,
"type": "egress"
}
]
| no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/ec2-instance/variables.tf b/modules/ec2-instance/variables.tf index 879e224e0..cd87f269c 100644 --- a/modules/ec2-instance/variables.tf +++ b/modules/ec2-instance/variables.tf @@ -52,7 +52,7 @@ variable "security_group_rules" { ] description = <<-EOT A list of maps of Security Group rules. - The values of map is fully complated with `aws_security_group_rule` resource. + The values of map is fully completed with `aws_security_group_rule` resource. To get more info see [security_group_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). EOT } diff --git a/modules/eks/argocd/CHANGELOG.md b/modules/eks/argocd/CHANGELOG.md index f88fcb32f..e1c5a2d2d 100644 --- a/modules/eks/argocd/CHANGELOG.md +++ b/modules/eks/argocd/CHANGELOG.md @@ -1,6 +1,6 @@ ## Components PR [#905](https://github.com/cloudposse/terraform-aws-components/pull/905) -The `notifictations.tf` file has been renamed to `notifications.tf`. Delete `notifictations.tf` after vendoring these +The `notifications.tf` file has been renamed to `notifications.tf`. Delete `notifications.tf` after vendoring these changes. ## Components PR [#851](https://github.com/cloudposse/terraform-aws-components/pull/851) diff --git a/modules/eks/cluster/CHANGELOG.md b/modules/eks/cluster/CHANGELOG.md index ae11e83df..fc7e302c0 100644 --- a/modules/eks/cluster/CHANGELOG.md +++ b/modules/eks/cluster/CHANGELOG.md @@ -37,7 +37,7 @@ Components PR [#1046](https://github.com/cloudposse/terraform-aws-components/pul Added support for passing extra arguments to `kubelet` and other startup modifications supported by EKS on Amazon Linux 2 via the -[`bootsrap.sh`](https://github.com/awslabs/amazon-eks-ami/blob/d87c6c49638216907cbd6630b6cadfd4825aed20/templates/al2/runtime/bootstrap.sh) +[`bootstrap.sh`](https://github.com/awslabs/amazon-eks-ami/blob/d87c6c49638216907cbd6630b6cadfd4825aed20/templates/al2/runtime/bootstrap.sh) script. This support should be considered an `alpha` version, as it may change when support for Amazon Linux 2023 is added, and @@ -424,7 +424,7 @@ Previously, this module added `identity` roles configured by the `aws_teams_rbac This never worked, and so now `aws_teams_rbac` is ignored. When upgrading, you may see these roles being removed from the `aws-auth`: this is expected and harmless. -### Better support for Manged Node Group Block Device Specifications +### Better support for Managed Node Group Block Device Specifications Previously, this module only supported specifying the disk size and encryption state for the root volume of Managed Node Groups. Now, the full set of block device specifications is supported, including the ability to specify the device name. diff --git a/modules/eks/cluster/variables.tf b/modules/eks/cluster/variables.tf index 88ad121bf..4d1e1d884 100644 --- a/modules/eks/cluster/variables.tf +++ b/modules/eks/cluster/variables.tf @@ -353,7 +353,7 @@ variable "node_group_defaults" { default = { desired_group_size = 1 # t3.medium is kept as the default for backward compatibility. - # Recommendation as of 2023-08-08 is c6a.large to provide reserve HA capacity regardless of Karpenter behavoir. + # Recommendation as of 2023-08-08 is c6a.large to provide reserve HA capacity regardless of Karpenter behavior. instance_types = ["t3.medium"] kubernetes_version = null # set to null to use cluster_kubernetes_version max_group_size = 100 diff --git a/modules/eks/datadog-agent/CHANGELOG.md b/modules/eks/datadog-agent/CHANGELOG.md index 7c45a6350..06748cc00 100644 --- a/modules/eks/datadog-agent/CHANGELOG.md +++ b/modules/eks/datadog-agent/CHANGELOG.md @@ -52,7 +52,7 @@ path is not used, e.g. `datadog.monitoring.svc.cluster.local.` ### Note for Bottlerocket users -If you are using Bottlerocket, you will want to uncomment the following from `vaules.yaml` or add it to your `values` +If you are using Bottlerocket, you will want to uncomment the following from `values.yaml` or add it to your `values` input: ```yaml diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md index a2c82edd4..413d201fa 100644 --- a/modules/eks/external-secrets-operator/README.md +++ b/modules/eks/external-secrets-operator/README.md @@ -49,7 +49,7 @@ assume-role acme-platform-gbl-sandbox-admin chamber write app MY_KEY my-value ``` -See `docs/recipies.md` for more information on managing secrets. +See `docs/recipes.md` for more information on managing secrets. ## Usage diff --git a/modules/eks/github-actions-runner/CHANGELOG.md b/modules/eks/github-actions-runner/CHANGELOG.md index 4a6f97722..d086975da 100644 --- a/modules/eks/github-actions-runner/CHANGELOG.md +++ b/modules/eks/github-actions-runner/CHANGELOG.md @@ -71,7 +71,7 @@ components: vars: # This first set of values you can just copy from here. # However, if you had customized the standard Helm configuration - # (such things as `cleanup_on_fail`, `atmoic`, or `timeout`), you + # (such things as `cleanup_on_fail`, `atomic`, or `timeout`), you # now need to do that per chart under the `charts` input. enabled: true name: "gha-runner-controller" diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md index 6eff24902..2f92cd320 100644 --- a/modules/eks/idp-roles/README.md +++ b/modules/eks/idp-roles/README.md @@ -8,7 +8,7 @@ tags: # Component: `eks/idp-roles` -This component installs the `idp-roles` for EKS clusters. These identity provider roles specify severl pre-determined +This component installs the `idp-roles` for EKS clusters. These identity provider roles specify several pre-determined permission levels for cluster users and come with bindings that make them easy to assign to Users and Groups. ## Usage diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 0cadce033..bae28ed0f 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -274,7 +274,7 @@ following tips: ## The GitHub Registration Token is valid, but the Runners are not registering with GitHub -If you first deployed the `github-action-token-rotator` component initally with an invalid configuration and then +If you first deployed the `github-action-token-rotator` component initially with an invalid configuration and then deployed the `github-runners` component, the instance runners will have failed to register with GitHub. After you correct `github-action-token-rotator` and have a valid GitHub Registration Token in SSM, _destroy and diff --git a/modules/github-webhook/README.md b/modules/github-webhook/README.md index 578ae47e9..de07f1b17 100644 --- a/modules/github-webhook/README.md +++ b/modules/github-webhook/README.md @@ -136,7 +136,7 @@ in that component's README. | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region. | `string` | n/a | yes | -| [remote\_state\_component\_name](#input\_remote\_state\_component\_name) | If fetching the Github Webhook value from remote-state, set this to the source compoennt name. For example, `eks/argocd`. | `string` | `""` | no | +| [remote\_state\_component\_name](#input\_remote\_state\_component\_name) | If fetching the Github Webhook value from remote-state, set this to the source component name. For example, `eks/argocd`. | `string` | `""` | no | | [remote\_state\_github\_webhook\_enabled](#input\_remote\_state\_github\_webhook\_enabled) | If `true`, pull the GitHub Webhook value from remote-state | `bool` | `true` | no | | [ssm\_github\_api\_key](#input\_ssm\_github\_api\_key) | SSM path to the GitHub API key | `string` | `"/argocd/github/api_key"` | no | | [ssm\_github\_webhook](#input\_ssm\_github\_webhook) | Format string of the SSM parameter path where the webhook will be pulled from. Only used if `var.webhook_github_secret` is not given. | `string` | `"/github/webhook"` | no | diff --git a/modules/github-webhook/variables.tf b/modules/github-webhook/variables.tf index 701706b36..280bf31d5 100644 --- a/modules/github-webhook/variables.tf +++ b/modules/github-webhook/variables.tf @@ -44,6 +44,6 @@ variable "remote_state_github_webhook_enabled" { variable "remote_state_component_name" { type = string - description = "If fetching the Github Webhook value from remote-state, set this to the source compoennt name. For example, `eks/argocd`." + description = "If fetching the Github Webhook value from remote-state, set this to the source component name. For example, `eks/argocd`." default = "" } diff --git a/modules/glue/job/README.md b/modules/glue/job/README.md index 5d0a2081c..d642f38a0 100644 --- a/modules/glue/job/README.md +++ b/modules/glue/job/README.md @@ -111,7 +111,7 @@ components: | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [timeout](#input\_timeout) | The job timeout in minutes. The default is 2880 minutes (48 hours) for `glueetl` and `pythonshell` jobs, and `null` (unlimted) for `gluestreaming` jobs | `number` | `2880` | no | +| [timeout](#input\_timeout) | The job timeout in minutes. The default is 2880 minutes (48 hours) for `glueetl` and `pythonshell` jobs, and `null` (unlimited) for `gluestreaming` jobs | `number` | `2880` | no | | [worker\_type](#input\_worker\_type) | The type of predefined worker that is allocated when a job runs. Accepts a value of `Standard`, `G.1X`, or `G.2X` | `string` | `null` | no | ## Outputs diff --git a/modules/glue/job/variables.tf b/modules/glue/job/variables.tf index 8a26521fb..af205769f 100644 --- a/modules/glue/job/variables.tf +++ b/modules/glue/job/variables.tf @@ -47,7 +47,7 @@ variable "security_configuration" { variable "timeout" { type = number - description = "The job timeout in minutes. The default is 2880 minutes (48 hours) for `glueetl` and `pythonshell` jobs, and `null` (unlimted) for `gluestreaming` jobs" + description = "The job timeout in minutes. The default is 2880 minutes (48 hours) for `glueetl` and `pythonshell` jobs, and `null` (unlimited) for `gluestreaming` jobs" default = 2880 } diff --git a/modules/guardduty/README.md b/modules/guardduty/README.md index 561a96a05..d0be2016f 100644 --- a/modules/guardduty/README.md +++ b/modules/guardduty/README.md @@ -59,10 +59,10 @@ region that existed before March 2019 and to any regions that have been opted-in In the examples below, we assume that the AWS Organization Management account is `root` and the AWS Organization Delegated Administrator account is `security`, both in the `core` tenant. -### Deploy to Delegated Admininstrator Account +### Deploy to Delegated Administrator Account First, the component is deployed to the -[Delegated Admininstrator](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) account in each +[Delegated Administrator](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html) account in each region in order to configure the central GuardDuty detector that each account will send its findings to. ```yaml @@ -89,9 +89,9 @@ atmos terraform apply guardduty/delegated-administrator/uw1 -s core-uw1-security ### Deploy to Organization Management (root) Account Next, the component is deployed to the AWS Organization Management, a/k/a `root`, Account in order to set the AWS -Organization Designated Admininstrator account. +Organization Designated Administrator account. -Note that you must use the `SuperAdmin` permissions as we are deploying to the AWS Organization Managment account. Since +Note that you must use the `SuperAdmin` permissions as we are deploying to the AWS Organization Management account. Since we are using the `SuperAdmin` user, it will already have access to the state bucket, so we set the `role_arn` of the backend config to null and set `var.privileged` to `true`. diff --git a/modules/macie/README.md b/modules/macie/README.md index 497e14d7e..1699cb2f2 100644 --- a/modules/macie/README.md +++ b/modules/macie/README.md @@ -68,7 +68,7 @@ atmos terraform apply macie/delegated-administrator -s core-ue1-security ### Deploy to Organization Management (root) Account Next, the component is deployed to the AWS Organization Management, a/k/a `root`, Account in order to set the AWS -Organization Designated Admininstrator account. +Organization Designated Administrator account. Note that you must `SuperAdmin` permissions as we are deploying to the AWS Organization Management account. Since we are using the `SuperAdmin` user, it will already have access to the state bucket, so we set the `role_arn` of the backend @@ -161,7 +161,7 @@ atmos terraform apply macie/org-settings/ue1 -s core-ue1-security |------|-------------|------|---------|:--------:| | [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `"core"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Admininstrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the GuardDuty
Administrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | | [delegated\_admininstrator\_component\_name](#input\_delegated\_admininstrator\_component\_name) | The name of the component that created the Macie account. | `string` | `"macie/delegated-administrator"` | no | diff --git a/modules/macie/variables.tf b/modules/macie/variables.tf index f962620cb..22414b9f7 100644 --- a/modules/macie/variables.tf +++ b/modules/macie/variables.tf @@ -9,7 +9,7 @@ variable "admin_delegated" { default = false description = < [allowed\_web\_access\_role\_names](#input\_allowed\_web\_access\_role\_names) | List of role names to allow airflow web access | `list(string)` | `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [create\_iam\_role](#input\_create\_iam\_role) | Enabling or disabling the creatation of a default IAM Role for AWS MWAA | `bool` | `true` | no | -| [create\_s3\_bucket](#input\_create\_s3\_bucket) | Enabling or disabling the creatation of an S3 bucket for AWS MWAA | `bool` | `true` | no | +| [create\_iam\_role](#input\_create\_iam\_role) | Enabling or disabling the creation of a default IAM Role for AWS MWAA | `bool` | `true` | no | +| [create\_s3\_bucket](#input\_create\_s3\_bucket) | Enabling or disabling the creation of an S3 bucket for AWS MWAA | `bool` | `true` | no | | [dag\_processing\_logs\_enabled](#input\_dag\_processing\_logs\_enabled) | Enabling or disabling the collection of logs for processing DAGs | `bool` | `false` | no | | [dag\_processing\_logs\_level](#input\_dag\_processing\_logs\_level) | DAG processing logging level. Valid values: CRITICAL, ERROR, WARNING, INFO, DEBUG | `string` | `"INFO"` | no | | [dag\_s3\_path](#input\_dag\_s3\_path) | Path to dags in s3 | `string` | `"dags"` | no | diff --git a/modules/mwaa/variables.tf b/modules/mwaa/variables.tf index 214a7c653..eceb0313a 100644 --- a/modules/mwaa/variables.tf +++ b/modules/mwaa/variables.tf @@ -5,13 +5,13 @@ variable "region" { variable "create_s3_bucket" { type = bool - description = "Enabling or disabling the creatation of an S3 bucket for AWS MWAA" + description = "Enabling or disabling the creation of an S3 bucket for AWS MWAA" default = true } variable "create_iam_role" { type = bool - description = "Enabling or disabling the creatation of a default IAM Role for AWS MWAA" + description = "Enabling or disabling the creation of a default IAM Role for AWS MWAA" default = true } diff --git a/modules/security-hub/README.md b/modules/security-hub/README.md index e962f3118..cfea17b41 100644 --- a/modules/security-hub/README.md +++ b/modules/security-hub/README.md @@ -198,7 +198,7 @@ atmos terraform apply security-hub/org-settings/uw1 -s core-uw1-security |------|-------------|------|---------|:--------:| | [account\_map\_tenant](#input\_account\_map\_tenant) | The tenant where the `account_map` component required by remote-state is deployed | `string` | `"core"` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the Security
Hub Admininstrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | +| [admin\_delegated](#input\_admin\_delegated) | A flag to indicate if the AWS Organization-wide settings should be created. This can only be done after the Security
Hub Administrator account has already been delegated from the AWS Org Management account (usually 'root'). See the
Deployment section of the README for more information. | `bool` | `false` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [auto\_enable\_organization\_members](#input\_auto\_enable\_organization\_members) | Flag to toggle auto-enablement of Security Hub for new member accounts in the organization.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_organization_configuration#auto_enable | `bool` | `true` | no | | [cloudwatch\_event\_rule\_pattern\_detail\_type](#input\_cloudwatch\_event\_rule\_pattern\_detail\_type) | The detail-type pattern used to match events that will be sent to SNS.

For more information, see:
https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html
https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html | `string` | `"ecurity Hub Findings - Imported"` | no | @@ -211,7 +211,7 @@ atmos terraform apply security-hub/org-settings/uw1 -s core-uw1-security | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [enabled\_standards](#input\_enabled\_standards) | A list of standards to enable in the account.

For example:
- standards/aws-foundational-security-best-practices/v/1.0.0
- ruleset/cis-aws-foundations-benchmark/v/1.2.0
- standards/pci-dss/v/3.2.1
- standards/cis-aws-foundations-benchmark/v/1.4.0 | `set(string)` | `[]` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [finding\_aggregation\_region](#input\_finding\_aggregation\_region) | If finding aggreation is enabled, the region that collects findings | `string` | `null` | no | +| [finding\_aggregation\_region](#input\_finding\_aggregation\_region) | If finding aggregation is enabled, the region that collects findings | `string` | `null` | no | | [finding\_aggregator\_enabled](#input\_finding\_aggregator\_enabled) | Flag to indicate whether a finding aggregator should be created

If you want to aggregate findings from one region, set this to `true`.

For more information, see:
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_finding_aggregator | `bool` | `false` | no | | [finding\_aggregator\_linking\_mode](#input\_finding\_aggregator\_linking\_mode) | Linking mode to use for the finding aggregator.

The possible values are:
- `ALL_REGIONS` - Aggregate from all regions
- `ALL_REGIONS_EXCEPT_SPECIFIED` - Aggregate from all regions except those specified in `var.finding_aggregator_regions`
- `SPECIFIED_REGIONS` - Aggregate from regions specified in `var.finding_aggregator_regions` | `string` | `"ALL_REGIONS"` | no | | [finding\_aggregator\_regions](#input\_finding\_aggregator\_regions) | A list of regions to aggregate findings from.

This is only used if `finding_aggregator_enabled` is `true`. | `any` | `null` | no | diff --git a/modules/security-hub/main.tf b/modules/security-hub/main.tf index 9ff80f673..d3ae10b08 100644 --- a/modules/security-hub/main.tf +++ b/modules/security-hub/main.tf @@ -24,9 +24,9 @@ data "aws_region" "this" { count = local.enabled ? 1 : 0 } -# If we are running in the AWS Org Management account, delegate Security Hub to the Delegated Admininstrator account +# If we are running in the AWS Org Management account, delegate Security Hub to the Delegated Administrator account # (usually the security account). We also need to turn on Security Hub in the Management account so that it can -# aggregate findings and be managed by the Delegated Admininstrator account. +# aggregate findings and be managed by the Delegated Administrator account. resource "aws_securityhub_organization_admin_account" "this" { count = local.create_org_delegation ? 1 : 0 diff --git a/modules/security-hub/variables.tf b/modules/security-hub/variables.tf index 6cdc54e79..d94639026 100644 --- a/modules/security-hub/variables.tf +++ b/modules/security-hub/variables.tf @@ -9,7 +9,7 @@ variable "admin_delegated" { default = false description = < 0 ? connection.account.environment : module.this.environment component = vpc_component_name }] @@ -11,7 +11,7 @@ locals { eks_connections = flatten([for connection in var.connections : [ for eks_component_name in connection.eks_component_names : { stage = connection.account.stage - tenant = connection.account.tenant # Defaults to empty string if tenant isnt defined + tenant = connection.account.tenant # Defaults to empty string if tenant isn't defined environment = length(connection.account.environment) > 0 ? connection.account.environment : module.this.environment component = eks_component_name }] diff --git a/modules/tgw/spoke/README.md b/modules/tgw/spoke/README.md index 816339c22..0134e689a 100644 --- a/modules/tgw/spoke/README.md +++ b/modules/tgw/spoke/README.md @@ -140,7 +140,7 @@ atmos terraform apply tgw/spoke -s -- | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [connections](#input\_connections) | A list of objects to define each TGW connections.

By default, each connection will look for only the default `vpc` component. |
list(object({
account = object({
stage = string
environment = optional(string, "")
tenant = optional(string, "")
})
vpc_component_names = optional(list(string), ["vpc"])
eks_component_names = optional(list(string), [])
}))
| `[]` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [cross\_region\_hub\_connector\_components](#input\_cross\_region\_hub\_connector\_components) | A map of cross-region hub connector components that provide this spoke with the appropriate Transit Gateway attachments IDs.
- The key should be the environment that the remote VPC is located in.
- The component is the name of the compoent in the remote region (e.g. `tgw/cross-region-hub-connector`)
- The environment is the region that the cross-region-hub-connector is deployed in.
e.g. the following would configure a component called `tgw/cross-region-hub-connector/use1` that is deployed in the
If use2 is the primary region, the following would be its configuration:
use1:
component: "tgw/cross-region-hub-connector"
environment: "use1" (the remote region)
and in the alternate region, the following would be its configuration:
use2:
component: "tgw/cross-region-hub-connector"
environment: "use1" (our own region) | `map(object({ component = string, environment = string }))` | `{}` | no | +| [cross\_region\_hub\_connector\_components](#input\_cross\_region\_hub\_connector\_components) | A map of cross-region hub connector components that provide this spoke with the appropriate Transit Gateway attachments IDs.
- The key should be the environment that the remote VPC is located in.
- The component is the name of the component in the remote region (e.g. `tgw/cross-region-hub-connector`)
- The environment is the region that the cross-region-hub-connector is deployed in.
e.g. the following would configure a component called `tgw/cross-region-hub-connector/use1` that is deployed in the
If use2 is the primary region, the following would be its configuration:
use1:
component: "tgw/cross-region-hub-connector"
environment: "use1" (the remote region)
and in the alternate region, the following would be its configuration:
use2:
component: "tgw/cross-region-hub-connector"
environment: "use1" (our own region) | `map(object({ component = string, environment = string }))` | `{}` | no | | [default\_route\_enabled](#input\_default\_route\_enabled) | Enable default routing via transit gateway, requires also nat gateway and instance to be disabled in vpc component. Default is disabled. | `bool` | `false` | no | | [default\_route\_outgoing\_account\_name](#input\_default\_route\_outgoing\_account\_name) | The account name which is used for outgoing traffic, when using the transit gateway as default route. | `string` | `null` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | diff --git a/modules/tgw/spoke/variables.tf b/modules/tgw/spoke/variables.tf index c87a0efaf..a4eec4e41 100644 --- a/modules/tgw/spoke/variables.tf +++ b/modules/tgw/spoke/variables.tf @@ -99,7 +99,7 @@ variable "cross_region_hub_connector_components" { description = <<-EOT A map of cross-region hub connector components that provide this spoke with the appropriate Transit Gateway attachments IDs. - The key should be the environment that the remote VPC is located in. - - The component is the name of the compoent in the remote region (e.g. `tgw/cross-region-hub-connector`) + - The component is the name of the component in the remote region (e.g. `tgw/cross-region-hub-connector`) - The environment is the region that the cross-region-hub-connector is deployed in. e.g. the following would configure a component called `tgw/cross-region-hub-connector/use1` that is deployed in the If use2 is the primary region, the following would be its configuration: diff --git a/modules/vpc-peering/README.md b/modules/vpc-peering/README.md index 53c9d6f18..fa3532890 100644 --- a/modules/vpc-peering/README.md +++ b/modules/vpc-peering/README.md @@ -237,9 +237,9 @@ atmos terraform apply vpc-peering -s ue1-prod | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS Region | `string` | n/a | yes | | [requester\_allow\_remote\_vpc\_dns\_resolution](#input\_requester\_allow\_remote\_vpc\_dns\_resolution) | Allow requester VPC to resolve public DNS hostnames to private IP addresses when queried from instances in the accepter VPC | `bool` | `true` | no | -| [requester\_role\_arn](#input\_requester\_role\_arn) | Requestor AWS assume role ARN, if not provided it will be assumed to be the current terraform role. | `string` | `null` | no | -| [requester\_vpc\_component\_name](#input\_requester\_vpc\_component\_name) | Requestor vpc component name | `string` | `"vpc"` | no | -| [requester\_vpc\_id](#input\_requester\_vpc\_id) | Requestor VPC ID, if not provided, it will be looked up by component using variable `requester_vpc_component_name` | `string` | `null` | no | +| [requester\_role\_arn](#input\_requester\_role\_arn) | Requester AWS assume role ARN, if not provided it will be assumed to be the current terraform role. | `string` | `null` | no | +| [requester\_vpc\_component\_name](#input\_requester\_vpc\_component\_name) | Requester vpc component name | `string` | `"vpc"` | no | +| [requester\_vpc\_id](#input\_requester\_vpc\_id) | Requester VPC ID, if not provided, it will be looked up by component using variable `requester_vpc_component_name` | `string` | `null` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | diff --git a/modules/vpc-peering/variables.tf b/modules/vpc-peering/variables.tf index 50fd53818..c7b43cd40 100644 --- a/modules/vpc-peering/variables.tf +++ b/modules/vpc-peering/variables.tf @@ -39,13 +39,13 @@ variable "accepter_stage_name" { variable "requester_vpc_id" { type = string - description = "Requestor VPC ID, if not provided, it will be looked up by component using variable `requester_vpc_component_name`" + description = "Requester VPC ID, if not provided, it will be looked up by component using variable `requester_vpc_component_name`" default = null } variable "requester_role_arn" { type = string - description = "Requestor AWS assume role ARN, if not provided it will be assumed to be the current terraform role." + description = "Requester AWS assume role ARN, if not provided it will be assumed to be the current terraform role." default = null } @@ -57,6 +57,6 @@ variable "requester_allow_remote_vpc_dns_resolution" { variable "requester_vpc_component_name" { type = string - description = "Requestor vpc component name" + description = "Requester vpc component name" default = "vpc" } diff --git a/modules/zscaler/README.md b/modules/zscaler/README.md index 4cd5bf6fd..787124b9e 100644 --- a/modules/zscaler/README.md +++ b/modules/zscaler/README.md @@ -105,7 +105,7 @@ import: | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [region](#input\_region) | AWS region | `string` | n/a | yes | | [secrets\_store\_type](#input\_secrets\_store\_type) | Secret store type for Zscaler provisioning keys. Valid values: `SSM`, `ASM` (but `ASM` not currently supported) | `string` | `"SSM"` | no | -| [security\_group\_rules](#input\_security\_group\_rules) | A list of maps of Security Group rules.
The values of map is fully complated with `aws_security_group_rule` resource.
To get more info see [security\_group\_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). | `list(any)` |
[
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 0,
"protocol": "-1",
"to_port": 65535,
"type": "egress"
}
]
| no | +| [security\_group\_rules](#input\_security\_group\_rules) | A list of maps of Security Group rules.
The values of map is fully completed with `aws_security_group_rule` resource.
To get more info see [security\_group\_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). | `list(any)` |
[
{
"cidr_blocks": [
"0.0.0.0/0"
],
"from_port": 0,
"protocol": "-1",
"to_port": 65535,
"type": "egress"
}
]
| no | | [stage](#input\_stage) | Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | | [tags](#input\_tags) | Additional tags (e.g. `map('BusinessUnit','XYZ')` | `map(string)` | `{}` | no | | [zscaler\_count](#input\_zscaler\_count) | The number of Zscaler instances. | `number` | `1` | no | diff --git a/modules/zscaler/variables.tf b/modules/zscaler/variables.tf index 8b7d5bff1..2dd827758 100644 --- a/modules/zscaler/variables.tf +++ b/modules/zscaler/variables.tf @@ -65,7 +65,7 @@ variable "security_group_rules" { ] description = <<-EOT A list of maps of Security Group rules. - The values of map is fully complated with `aws_security_group_rule` resource. + The values of map is fully completed with `aws_security_group_rule` resource. To get more info see [security_group_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule). EOT } From 207fba8617106e83a8af52d53849d63037153858 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:43:53 +0300 Subject: [PATCH 489/501] Update Changelog for `1.499.0` (#1123) Co-authored-by: cloudposse-releaser[bot] <163353533+cloudposse-releaser[bot]@users.noreply.github.com> Co-authored-by: Igor Rodionov --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e8a867a4..be99cd7ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # CHANGELOG +## 1.499.0 + + + +
+ feat: add detector features to guard duty component @dudymas (#1112) + + ## what + +- add detector features to guard duty + +## why + +- added functionality + +## references + +- [Detector Feature API](https://docs.aws.amazon.com/guardduty/latest/APIReference/API_DetectorFeatureConfiguration.html) + +
+ +
+ Update Changelog for `1.497.0` @github-actions (#1117) + + Update Changelog for [`1.497.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.497.0) +
+ + + ## 1.497.0 From e9f65688b95b9ea24823cfab899267adcd95d534 Mon Sep 17 00:00:00 2001 From: Matt Calhoun Date: Tue, 1 Oct 2024 10:36:09 -0400 Subject: [PATCH 490/501] add additional waf features (#791) Co-authored-by: Dan Miller Co-authored-by: Igor Rodionov --- modules/waf/README.md | 4 ++ modules/waf/alb.tf | 15 +++++ modules/waf/main.tf | 2 +- modules/waf/remote-state.tf | 0 modules/waf/variables.tf | 113 ++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 modules/waf/alb.tf mode change 100644 => 100755 modules/waf/remote-state.tf diff --git a/modules/waf/README.md b/modules/waf/README.md index 0e09e4014..71b523357 100644 --- a/modules/waf/README.md +++ b/modules/waf/README.md @@ -76,6 +76,8 @@ components: | Name | Type | |------|------| | [aws_ssm_parameter.acl_arn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_alb.alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/alb) | data source | +| [aws_lbs.alb_by_tags](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/lbs) | data source | ## Inputs @@ -83,6 +85,8 @@ components: |------|-------------|------|---------|:--------:| | [acl\_name](#input\_acl\_name) | Friendly name of the ACL. The ACL ARN will be stored in SSM under {ssm\_path\_prefix}/{acl\_name}/arn | `string` | n/a | yes | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [alb\_names](#input\_alb\_names) | list of ALB names to associate with the web ACL. | `list(string)` | `[]` | no | +| [alb\_tags](#input\_alb\_tags) | list of tags to match one or more ALBs to associate with the web ACL. | `list(map(string))` | `[]` | no | | [association\_resource\_arns](#input\_association\_resource\_arns) | A list of ARNs of the resources to associate with the web ACL.
This must be an ARN of an Application Load Balancer, Amazon API Gateway stage, or AWS AppSync.

Do not use this variable to associate a Cloudfront Distribution.
Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource.
For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html | `list(string)` | `[]` | no | | [association\_resource\_component\_selectors](#input\_association\_resource\_component\_selectors) | A list of Atmos component selectors to get from the remote state and associate their ARNs with the web ACL.
The components must be Application Load Balancers, Amazon API Gateway stages, or AWS AppSync.

component:
Atmos component name
component\_arn\_output:
The component output that defines the component ARN

Set `tenant`, `environment` and `stage` if the components are in different OUs, regions or accounts.

Do not use this variable to select a Cloudfront Distribution component.
Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource.
For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html |
list(object({
component = string
namespace = optional(string, null)
tenant = optional(string, null)
environment = optional(string, null)
stage = optional(string, null)
component_arn_output = string
}))
| `[]` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | diff --git a/modules/waf/alb.tf b/modules/waf/alb.tf new file mode 100644 index 000000000..a978df085 --- /dev/null +++ b/modules/waf/alb.tf @@ -0,0 +1,15 @@ +locals { + alb_arns = concat(local.alb_name_arns, local.alb_tag_arns) + alb_name_arns = [for alb_instance in data.aws_alb.alb : alb_instance.arn] + alb_tag_arns = flatten([for alb_instance in data.aws_lbs.alb_by_tags : alb_instance.arns]) +} + +data "aws_alb" "alb" { + for_each = toset(var.alb_names) + name = each.key +} + +data "aws_lbs" "alb_by_tags" { + for_each = { for i, v in var.alb_tags : i => v } + tags = each.value +} diff --git a/modules/waf/main.tf b/modules/waf/main.tf index 4f05ce854..04ef1ae2a 100644 --- a/modules/waf/main.tf +++ b/modules/waf/main.tf @@ -6,7 +6,7 @@ locals { if local.enabled ] - association_resource_arns = concat(var.association_resource_arns, local.association_resource_component_selectors_arns) + association_resource_arns = toset(concat(var.association_resource_arns, local.association_resource_component_selectors_arns, local.alb_arns)) log_destination_component_selectors = [ for i, v in var.log_destination_component_selectors : module.log_destination_components[i].outputs[v.component_output] diff --git a/modules/waf/remote-state.tf b/modules/waf/remote-state.tf old mode 100644 new mode 100755 diff --git a/modules/waf/variables.tf b/modules/waf/variables.tf index 11a46a60c..ac04944f9 100644 --- a/modules/waf/variables.tf +++ b/modules/waf/variables.tf @@ -107,6 +107,119 @@ variable "token_domains" { default = null } +# Logging configuration +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration.html +variable "log_destination_configs" { + type = list(string) + default = [] + description = "The Amazon Kinesis Data Firehose, CloudWatch Log log group, or S3 bucket Amazon Resource Names (ARNs) that you want to associate with the web ACL" +} + +variable "redacted_fields" { + type = map(object({ + method = optional(bool, false) + uri_path = optional(bool, false) + query_string = optional(bool, false) + single_header = optional(list(string), null) + })) + default = {} + description = <<-DOC + The parts of the request that you want to keep out of the logs. + You can only specify one of the following: `method`, `query_string`, `single_header`, or `uri_path` + + method: + Whether to enable redaction of the HTTP method. + The method indicates the type of operation that the request is asking the origin to perform. + uri_path: + Whether to enable redaction of the URI path. + This is the part of a web request that identifies a resource. + query_string: + Whether to enable redaction of the query string. + This is the part of a URL that appears after a `?` character, if any. + single_header: + The list of names of the query headers to redact. + DOC + nullable = false +} + +variable "logging_filter" { + type = object({ + default_behavior = string + filter = list(object({ + behavior = string + requirement = string + condition = list(object({ + action_condition = optional(object({ + action = string + }), null) + label_name_condition = optional(object({ + label_name = string + }), null) + })) + })) + }) + default = null + description = <<-DOC + A configuration block that specifies which web requests are kept in the logs and which are dropped. + You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation. + DOC +} + +# Association resources +variable "association_resource_arns" { + type = list(string) + default = [] + description = <<-DOC + A list of ARNs of the resources to associate with the web ACL. + This must be an ARN of an Application Load Balancer, Amazon API Gateway stage, or AWS AppSync. + + Do not use this variable to associate a Cloudfront Distribution. + Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource. + For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html + DOC + nullable = false +} + +variable "alb_names" { + description = "list of ALB names to associate with the web ACL." + type = list(string) + default = [] + nullable = false +} + +variable "alb_tags" { + description = "list of tags to match one or more ALBs to associate with the web ACL." + type = list(map(string)) + default = [] + nullable = false +} + +variable "association_resource_component_selectors" { + type = list(object({ + component = string + namespace = optional(string, null) + tenant = optional(string, null) + environment = optional(string, null) + stage = optional(string, null) + component_arn_output = string + })) + default = [] + description = <<-DOC + A list of Atmos component selectors to get from the remote state and associate their ARNs with the web ACL. + The components must be Application Load Balancers, Amazon API Gateway stages, or AWS AppSync. + + component: + Atmos component name + component_arn_output: + The component output that defines the component ARN + + Do not use this variable to select a Cloudfront Distribution component. + Instead, you should use the `web_acl_id` property on the `cloudfront_distribution` resource. + For more details, refer to https://docs.aws.amazon.com/waf/latest/APIReference/API_AssociateWebACL.html + DOC + nullable = false +} + # Rules variable "byte_match_statement_rules" { type = list(object({ From ab8df1d78b6312120a03c730baaa6726bcec442a Mon Sep 17 00:00:00 2001 From: Igor Rodionov Date: Tue, 1 Oct 2024 17:37:32 +0300 Subject: [PATCH 491/501] Fix release changelog space issue (#1122) --- .github/auto-release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/auto-release.yml b/.github/auto-release.yml index 17cd39c82..c9336dbc6 100644 --- a/.github/auto-release.yml +++ b/.github/auto-release.yml @@ -37,8 +37,7 @@ categories: change-template: |
$TITLE @$AUTHOR (#$NUMBER) - - $BODY + $BODY
template: | From 40d54f2b1ec6b61178ea67f8760a09ec612e1b7f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:01:02 +0300 Subject: [PATCH 492/501] Update Changelog for `1.500.0` (#1124) Co-authored-by: cloudposse-releaser[bot] <163353533+cloudposse-releaser[bot]@users.noreply.github.com> Co-authored-by: Igor Rodionov --- CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be99cd7ea..668ec3d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # CHANGELOG +## 1.500.0 + + + +## Affected Components +- [eks/argocd](https://docs.cloudposse.com/components/library/aws/eks/argocd#changelog) +- [eks/cluster](https://docs.cloudposse.com/components/library/aws/eks/cluster#changelog) +- [eks/datadog-agent](https://docs.cloudposse.com/components/library/aws/eks/datadog-agent#changelog) +- [eks/github-actions-runner](https://docs.cloudposse.com/components/library/aws/eks/github-actions-runner#changelog) +- [spa-s3-cloudfront](https://docs.cloudposse.com/components/library/aws/spa-s3-cloudfront#changelog) + + + + +
+ add additional waf features @mcalhoun (#791) + + ## what +* Add the ability to specify a list of ALBs to attach WAF to +* Add the ability to specify a list of tags to target ALBs to attach WAF to + +## why +* To provider greater flexibility in attaching WAF to ALBs +
+ +
+ Update Changelog for `1.499.0` @github-actions (#1123) + + Update Changelog for [`1.499.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.499.0) +
+ +
+ docs: fix typos using `codespell` @RoseSecurity (#1114) + + ## what and why + +> [!NOTE] +> Feel free to close this PR if the changes are not worth the review. I won't be offended + +- For context, I wanted to clean up some of the documentation in our repository, which identified several typos in our variables and READMEs. I decided to use `codespell` to automate this process and thought it might be useful for a quick cleanup here! + +### usage + +```sh +codespell -w +``` + +
+ + + ## 1.499.0 From 2de47e8079c0e3e0bfb26ade6ea2e26bca7a7e02 Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:09:19 -0400 Subject: [PATCH 493/501] docs: improve external-dns snippet in readme (#986) Co-authored-by: Igor Rodionov --- modules/eks/external-dns/README.md | 154 +++++++++++++++-------------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/modules/eks/external-dns/README.md b/modules/eks/external-dns/README.md index 77433eee3..0b8f02345 100644 --- a/modules/eks/external-dns/README.md +++ b/modules/eks/external-dns/README.md @@ -35,17 +35,21 @@ components: name: external-dns chart: external-dns chart_repository: https://charts.bitnami.com/bitnami - chart_version: "6.7.5" + chart_version: "6.33.0" create_namespace: true kubernetes_namespace: external-dns - - # Resources - limit_cpu: "200m" - limit_memory: "256Mi" - request_cpu: "100m" - request_memory: "128Mi" - + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + # Set this to a unique value to avoid conflicts with other external-dns instances managing the same zones. + # For example, when using blue-green deployment pattern to update EKS cluster. + txt_prefix: "" # You can use `chart_values` to set any other chart options. Treat `chart_values` as the root of the doc. + # See documentation for latest chart version and list of chart_values: https://artifacthub.io/packages/helm/bitnami/external-dns # # # For example # --- @@ -66,17 +70,17 @@ components: ## Requirements -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.0.0 | -| [aws](#requirement\_aws) | >= 4.9.0 | -| [helm](#requirement\_helm) | >= 2.0 | +| Name | Version | +|------------------------------------------------------------------------------|---------------------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [helm](#requirement\_helm) | >= 2.0 | | [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 | ## Providers -| Name | Version | -|------|---------| +| Name | Version | +|---------------------------------------------------|----------| | [aws](#provider\_aws) | >= 4.9.0 | ## Modules @@ -93,74 +97,72 @@ components: ## Resources -| Name | Type | -|------|------| +| Name | Type | +|-----------------------------------------------------------------------------------------------------------------------------|-------------| | [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | -| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | ## Inputs -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | -| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | -| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | -| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | n/a | yes | -| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | -| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | n/a | yes | -| [chart\_values](#input\_chart\_values) | Addition map values to yamlencode as `helm_release` values. | `any` | `{}` | no | -| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | -| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | -| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [crd\_enabled](#input\_crd\_enabled) | Install and use the integrated DNSEndpoint CRD. | `bool` | `false` | no | -| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `null` | no | -| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | -| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | -| [dns\_components](#input\_dns\_components) | A list of additional DNS components to search for ZoneIDs |
list(object({
component = string,
environment = optional(string)
}))
| `[]` | no | -| [dns\_gbl\_delegated\_environment\_name](#input\_dns\_gbl\_delegated\_environment\_name) | The name of the environment where global `dns_delegated` is provisioned | `string` | `"gbl"` | no | -| [dns\_gbl\_primary\_environment\_name](#input\_dns\_gbl\_primary\_environment\_name) | The name of the environment where global `dns_primary` is provisioned | `string` | `"gbl"` | no | -| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | -| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | -| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | -| [istio\_enabled](#input\_istio\_enabled) | Add istio gateways to monitored sources. | `bool` | `false` | no | -| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | -| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | -| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | -| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | -| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | -| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | -| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | -| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | -| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | -| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | -| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | -| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | n/a | yes | -| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | -| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | -| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | -| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | -| [metrics\_enabled](#input\_metrics\_enabled) | Whether or not to enable metrics in the helm chart. | `bool` | `false` | no | -| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | -| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | -| [policy](#input\_policy) | Modify how DNS records are synchronized between sources and providers (options: sync, upsert-only) | `string` | `"sync"` | no | -| [publish\_internal\_services](#input\_publish\_internal\_services) | Allow external-dns to publish DNS records for ClusterIP services | `bool` | `true` | no | -| [rbac\_enabled](#input\_rbac\_enabled) | Service Account for pods. | `bool` | `true` | no | -| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | -| [region](#input\_region) | AWS Region. | `string` | n/a | yes | -| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
|
{
"limits": {
"cpu": "200m",
"memory": "256Mi"
},
"requests": {
"cpu": "100m",
"memory": "128Mi"
}
}
| no | -| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | -| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | -| [txt\_prefix](#input\_txt\_prefix) | Prefix to create a TXT record with a name following the pattern prefix.``. | `string` | `"external-dns"` | no | -| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `null` | no | +| Name | Description | Type | Default | Required | +|----------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used. | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | n/a | yes | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | n/a | yes | +| [chart\_values](#input\_chart\_values) | Addition map values to yamlencode as `helm_release` values. | `any` | `{}` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed. | `string` | `null` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails. | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [crd\_enabled](#input\_crd\_enabled) | Install and use the integrated DNSEndpoint CRD. | `bool` | `false` | no | +| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [dns\_gbl\_delegated\_environment\_name](#input\_dns\_gbl\_delegated\_environment\_name) | The name of the environment where global `dns_delegated` is provisioned | `string` | `"gbl"` | no | +| [dns\_gbl\_primary\_environment\_name](#input\_dns\_gbl\_primary\_environment\_name) | The name of the environment where global `dns_primary` is provisioned | `string` | `"gbl"` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [istio\_enabled](#input\_istio\_enabled) | Add istio gateways to monitored sources. | `bool` | `false` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [metrics\_enabled](#input\_metrics\_enabled) | Whether or not to enable metrics in the helm chart. | `bool` | `false` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [policy](#input\_policy) | Modify how DNS records are synchronized between sources and providers (options: sync, upsert-only) | `string` | `"sync"` | no | +| [publish\_internal\_services](#input\_publish\_internal\_services) | Allow external-dns to publish DNS records for ClusterIP services | `bool` | `true` | no | +| [rbac\_enabled](#input\_rbac\_enabled) | Service Account for pods. | `bool` | `true` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region. | `string` | n/a | yes | +| [resources](#input\_resources) | The cpu and memory of the deployment's limits and requests. |
object({
limits = object({
cpu = string
memory = string
})
requests = object({
cpu = string
memory = string
})
})
|
{
"limits": {
"cpu": "200m",
"memory": "256Mi"
},
"requests": {
"cpu": "100m",
"memory": "128Mi"
}
}
| no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [timeout](#input\_timeout) | Time in seconds to wait for any individual kubernetes operation (like Jobs for hooks). Defaults to `300` seconds | `number` | `null` | no | +| [txt\_prefix](#input\_txt\_prefix) | Prefix to create a TXT record with a name following the pattern prefix.``. | `string` | `"external-dns"` | no | +| [wait](#input\_wait) | Will wait until all resources are in a ready state before marking the release as successful. It will wait for as long as `timeout`. Defaults to `true`. | `bool` | `null` | no | ## Outputs -| Name | Description | -|------|-------------| +| Name | Description | +|--------------------------------------------------------------|--------------------------------------| | [metadata](#output\_metadata) | Block status of the deployed release | From 757870874e04994f4f8f93d18df3652faf90eddb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:10:47 +0300 Subject: [PATCH 494/501] Update Changelog for `1.501.0` (#1125) Co-authored-by: cloudposse-releaser[bot] <163353533+cloudposse-releaser[bot]@users.noreply.github.com> Co-authored-by: Igor Rodionov --- CHANGELOG.md | 10428 +++++++++++++++++++++++++------------------------ 1 file changed, 5221 insertions(+), 5207 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 668ec3d95..1ce4ed9f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,39 +1,53 @@ -# CHANGELOG - -## 1.500.0 - - - -## Affected Components -- [eks/argocd](https://docs.cloudposse.com/components/library/aws/eks/argocd#changelog) -- [eks/cluster](https://docs.cloudposse.com/components/library/aws/eks/cluster#changelog) -- [eks/datadog-agent](https://docs.cloudposse.com/components/library/aws/eks/datadog-agent#changelog) -- [eks/github-actions-runner](https://docs.cloudposse.com/components/library/aws/eks/github-actions-runner#changelog) -- [spa-s3-cloudfront](https://docs.cloudposse.com/components/library/aws/spa-s3-cloudfront#changelog) - - - - -
- add additional waf features @mcalhoun (#791) - +# CHANGELOG + +## 1.501.0 + +
+ Fix release changelog space issue @goruha (#1122) +## what +* Fix release changelog space issue + +![CleanShot 2024-10-01 at 12 27 42@2x](https://github.com/user-attachments/assets/2d42740a-1d5d-4990-94ac-eb49bdfe4c32) + +## why +* Have nice changelog + +## references +* https://github.com/cloudposse/terraform-aws-components/pull/1117/files#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4edR10 + + +## 1.500.0 + + + +## Affected Components +- [eks/argocd](https://docs.cloudposse.com/components/library/aws/eks/argocd#changelog) +- [eks/cluster](https://docs.cloudposse.com/components/library/aws/eks/cluster#changelog) +- [eks/datadog-agent](https://docs.cloudposse.com/components/library/aws/eks/datadog-agent#changelog) +- [eks/github-actions-runner](https://docs.cloudposse.com/components/library/aws/eks/github-actions-runner#changelog) +- [spa-s3-cloudfront](https://docs.cloudposse.com/components/library/aws/spa-s3-cloudfront#changelog) + + +
+ add additional waf features @mcalhoun (#791) + ## what * Add the ability to specify a list of ALBs to attach WAF to * Add the ability to specify a list of tags to target ALBs to attach WAF to ## why -* To provider greater flexibility in attaching WAF to ALBs -
- -
- Update Changelog for `1.499.0` @github-actions (#1123) - - Update Changelog for [`1.499.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.499.0) -
- -
- docs: fix typos using `codespell` @RoseSecurity (#1114) - +* To provider greater flexibility in attaching WAF to ALBs +
+ +
+ Update Changelog for `1.499.0` @github-actions (#1123) + + Update Changelog for [`1.499.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.499.0) +
+ +
+ docs: fix typos using `codespell` @RoseSecurity (#1114) + ## what and why > [!NOTE] @@ -46,18 +60,18 @@ ```sh codespell -w ``` - -
- - - -## 1.499.0 - - - -
- feat: add detector features to guard duty component @dudymas (#1112) - + +
+ + + +## 1.499.0 + + + +
+ feat: add detector features to guard duty component @dudymas (#1112) + ## what - add detector features to guard duty @@ -69,24 +83,24 @@ codespell -w ## references - [Detector Feature API](https://docs.aws.amazon.com/guardduty/latest/APIReference/API_DetectorFeatureConfiguration.html) - -
- -
- Update Changelog for `1.497.0` @github-actions (#1117) - - Update Changelog for [`1.497.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.497.0) -
- - - -## 1.497.0 - - - -
- Fix Update changelog workflow @goruha (#1116) - + +
+ +
+ Update Changelog for `1.497.0` @github-actions (#1117) + + Update Changelog for [`1.497.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.497.0) +
+ + + +## 1.497.0 + + + +
+ Fix Update changelog workflow @goruha (#1116) + ## what * Fix modules path from `components/terraform` to `modules` @@ -94,5149 +108,5149 @@ codespell -w * It seems that `components/terraform` was testing value. In actual repo components are in `modules` directory ## references -* DEV-2556 Investigate release issues with terraform-aws-components -
- - - -## 1.298.0 (2023-08-28T20:56:25Z) - -
- Aurora Postgres Engine Options @milldr (#845) - -### what - -- Add scaling configuration variables for both Serverless and Serverless v2 to `aurora-postgres` -- Update `aurora-postgres` README - -### why - -- Support both serverless options -- Add an explanation for how to configure each, and where to find valid engine options - -### references - -- n/a - -
- -## 1.297.0 (2023-08-28T18:06:11Z) - -
- AWS provider V5 dependency updates @max-lobur (#729) - -### what - -- Update component dependencies for the AWS provider V5 - -Requested components: - -- cloudtrail-bucket -- config-bucket -- datadog-logs-archive -- eks/argocd -- eks/efs-controller -- eks/metric-server -- spacelift-worker-pool -- eks/external-secrets-operator - -### why - -- Maintenance - -
- -## 1.296.0 (2023-08-28T16:24:05Z) - -
- datadog agent update defaults @Benbentwo (#839) - -### what - -- prevent fargate agents -- use sockets instead of ports for APM -- enable other services - -### why - -- Default Datadog APM enabled over k8s - -### references - -
- -## 1.295.0 (2023-08-26T00:51:10Z) - -
- TGW FAQ and Spoke Alternate VPC Support @milldr (#840) - -### what - -- Added FAQ to the TGW upgrade guide for replacing attachments -- Added note about destroying TGW components -- Added option to not create TGW propagation and association when connecting an alternate VPC - -### why - -- When connecting an alternate VPC in the same region as the primary VPC, we do not want to create a duplicate TGW - propagation and association - -### references - -- n/a - -
- -## 1.294.0 (2023-08-26T00:07:42Z) - -
- Aurora Upstream: Serverless, Tags, Enabled: False @milldr (#841) - -### what - -- Set `module.context` to `module.cluster` across all resources -- Only set parameter for replica if cluster size is > 0 -- `enabled: false` support - -### why - -- Missing tags for SSM parameters for cluster attributes -- Serverless clusters set `cluster_size: 0`, which will break the SSM parameter for replica hostname (since it does not - exist) -- Support enabled false for `aurora-*-resources` components - -### references - -- n/a - -
- -## 1.293.2 (2023-08-24T15:50:53Z) - -### 🚀 Enhancements - -
- Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` @aknysh (#837) - -### what - -- Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` - -### why - -- Fix the issue described in https://github.com/cloudposse/terraform-aws-components/issues/771 - -### related - -- Closes https://github.com/cloudposse/terraform-aws-components/issues/771 - -
- -## 1.293.1 (2023-08-24T11:24:46Z) - -### 🐛 Bug Fixes - -
- [spacelift/worker-pool] Update providers.tf nesting @Nuru (#834) - -### what - -- Update relative path to `account-map` in `spacelift/worker-pool/providers.tf` - -### why - -- Fixes #828 - -
- -## 1.293.0 (2023-08-23T01:18:53Z) - -
- Add visibility to default VPC component name @milldr (#833) - -### what - -- Set the default component name for `vpc` in variables, not remote-state - -### why - -- Bring visibility to where the default is set - -### references - -- Follow up on comments on #832 - -
- -## 1.292.0 (2023-08-22T21:33:18Z) - -
- Aurora Optional `vpc` Component Names @milldr (#832) - -### what - -- Allow optional VPC component names in the aurora components - -### why - -- Support deploying the clusters for other VPC components than `"vpc"` - -### references - -- n/a - -
- -## 1.291.1 (2023-08-22T20:25:17Z) - -### 🐛 Bug Fixes - -
- [aws-sso] Fix root provider, restore `SetSourceIdentity` permission @Nuru (#830) - -### what - -For `aws-sso`: - -- Fix root provider, improperly restored in #740 -- Restore `SetSourceIdentity` permission inadvertently removed in #740 - -### why - -- When deploying to `identity`, `root` provider did not reference `root` account -- Likely unintentional removal due to merge error - -### references - -- #740 -- #738 - -
- -## 1.291.0 (2023-08-22T17:08:27Z) - -
- chore: remove defaults from components @dudymas (#831) - -### what - -- remove `defaults.auto.tfvars` from component modules - -### why - -- in favor of drying up configuration using atmos - -### Notes - -- Some defaults may not be captured yet. Regressions might occur. - -
- -## 1.290.0 (2023-08-21T18:57:43Z) - -
- Upgrade aws-config and conformance pack modules to 1.1.0 @johncblandii (#829) - -### what - -- Upgrade aws-config and conformance pack modules to 1.1.0 - -### why - -- They're outdated. - -### references - -- #771 - -
- -## 1.289.2 (2023-08-21T08:53:08Z) - -### 🐛 Bug Fixes - -
- [eks/alb-controller] Fix naming convention of overridable local variable @Nuru (#826) - -### what - -- [eks/alb-controller] Change name of local variable from `distributed_iam_policy_overridable` to - `overridable_distributed_iam_policy` - -### why - -- Cloud Posse style guide requires `overridable` as prefix, not suffix. - -
- -## 1.289.1 (2023-08-19T05:20:26Z) - -### 🐛 Bug Fixes - -
- [eks/alb-controller] Update ALB controller IAM policy @Nuru (#821) - -### what - -- [eks/alb-controller] Update ALB controller IAM policy - -### why - -- Previous policy had error preventing the creation of the ELB service-linked role - -
- -## 1.289.0 (2023-08-18T20:18:12Z) - -
- Spacelift Alternate git Providers @milldr (#825) - -### what - -- set alternate git provider blocks to filter under `settings.spacelift` - -### why - -- Debugging GitLab support specifically -- These settings should be defined under `settings.spacelift`, not as a top-level configuration - -### references - -- n/a - -
- -## 1.288.0 (2023-08-18T15:12:16Z) - -
- Placeholder for `upgrade-guide.md` @milldr (#823) - -### what - -- Added a placeholder file for `docs/upgrade-guide.md` with a basic explanation of what is to come - -### why - -- With #811 we moved the contents of this upgrade-guide file to the individual component. We plan to continue adding - upgrade guides for individual components, and in addition, create a higher-level upgrade guide here -- However, the build steps for refarch-scaffold expect `docs/upgrade-guide.md` to exist and are failing without it. We - need a placeholder until the `account-map`, etc changes are added to this file - -### references - -- Example of failing release: https://github.com/cloudposse/refarch-scaffold/actions/runs/5885022872 - -
- -## 1.287.2 (2023-08-18T14:42:49Z) - -### 🚀 Enhancements - -
- update boolean logic @mcalhoun (#822) - -### what - -- Update the GuardDuty component to enable GuardDuty on the root account - -### why - -The API call to designate organization members now fails with the following if GuardDuty was not already enabled in the -organization management (root) account : - -``` -Error: error designating guardduty administrator account members: [{ -│ AccountId: "111111111111, -│ Result: "Operation failed because your organization master must first enable GuardDuty to be added as a member" -│ }] -``` - -
- -## 1.287.1 (2023-08-17T16:41:24Z) - -### 🚀 Enhancements - -
- chore: Remove unused - @MaxymVlasov (#818) - -# why - -``` -TFLint in components/terraform/eks/cluster/: -2 issue(s) found: - -Warning: [Fixable] local.identity_account_name is declared but not used (terraform_unused_declarations) - - on main.tf line 9: - 9: identity_account_name = module.iam_roles.identity_account_account_name - -Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md - -Warning: [Fixable] variable "aws_teams_rbac" is declared but not used (terraform_unused_declarations) - - on variables.tf line 117: - 117: variable "aws_teams_rbac" { - -Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md -``` - -
- -## 1.287.0 (2023-08-17T15:52:57Z) - -
- Update `remote-states` modules to the latest version @aknysh (#820) - -### what - -- Update `remote-states` modules to the latest version - -### why - -- `remote-state` version `1.5.0` uses the latest version of `terraform-provider-utils` which uses the latest version of - Atmos with many new features and improvements - -
- -## 1.286.0 (2023-08-17T05:49:45Z) - -
- Update cloudposse/utils/aws to 1.3.0 @RoseSecurity (#815) - -### What: - -- Updated the following to utilize the newest version of `cloudposse/utils/aws`: - -``` -0.8.1 modules/spa-s3-cloudfront -1.1.0 modules/aws-config -1.1.0 modules/datadog-configuration/modules/datadog_keys -1.1.0 modules/dns-delegated -``` - -### Why: - -- `cloudposse/utils/aws` components were not updated to `1.3.0` - -### References: - -- [AWS Utils](https://github.com/cloudposse/terraform-aws-utils/releases/tag/1.3.0) - -
- -## 1.285.0 (2023-08-17T05:49:09Z) - -
- Update api-gateway-account-settings README.md @johncblandii (#819) - -### what - -- Updated the title - -### why - -- It was an extra helping of copy/pasta - -### references - -
- -## 1.284.0 (2023-08-17T02:10:47Z) - -
- Datadog upgrades @Nuru (#814) - -### what - -- Update Datadog components: - - `eks/datadog-agent` see `eks/datadog-agent/CHANGELOG.md` - - `datadog-configuration` better handling of `enabled = false` - - `datadog-integration` move "module count" back to "module" for better compatibility and maintainability, see - `datadog-integration/CHANGELOG.md` - - `datadog-lambda-forwared` fix issues around `enable = false` and incomplete destruction of resources (particularly - log groups) see `datadog-lambda-forwarder/CHANGELOG.md` - - Cleanup `datadog-monitor` see `datadog-monitor/CHANGELOG.md` for details. Possible breaking change in that several - inputs have been removed, but they were previously ignored anyway, so no infrastructure change should result from - you simply removing any inputs you had for the removed inputs. - - Update `datadog-sythetics` dependency `remote-state` version - - `datadog-synthetics-private-location` migrate control of namespace to `helm-release` module. Possible destruction - and recreation of component on upgrade. See CHANGELOG.md - -### why - -- More reliable deployments, especially when destroying or disabling them -- Bug fixes and new features - -
- -## 1.283.0 (2023-08-16T17:23:39Z) - -
- Update EC2-Autoscale-Group Modules to 0.35.1 @RoseSecurity (#809) - -### What: - -- Updated `modules/spacelift/worker-pool` from 0.34.2 to 0.35.1 and adapted new variable features -- Updated `modules/bastion` from 0.35.0 to 0.35.1 -- Updated `modules/github-runners` from 0.35.0 to 0.35.1 - -### Why: - -- Modules were utilizing previous `ec2-autoscale-group` versions - -### References: - -- [terraform-aws-ec2-autoscale-group](https://github.com/cloudposse/terraform-aws-ec2-autoscale-group/blob/main/variables.tf) -- [Terraform Registry](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#instance_refresh) - -
- -
- Update storage-class efs component documentation @max-lobur (#817) - -### what - -- Update storage-class efs component defaults - -### why - -- Follow component move outside of eks dir - -
- -## 1.282.1 (2023-08-15T21:48:02Z) - -### 🐛 Bug Fixes - -
- Karpenter bugfix, EKS add-ons to managed node group @Nuru (#816) - -### what - -- [eks/karpenter] use Instance Profile name from EKS output -- Clarify recommendation and fix defaults regarding deploying add-ons to managed node group - -### why - -- Bug fix: Karpenter did not work when legacy mode disabled -- Originally we expected to use Karpenter-only clusters and the documentation and defaults aligned with this. Now we - recommend all Add-Ons be deployed to a managed node group, but the defaults and documentation did not reflect this. - -
- -## 1.282.0 (2023-08-14T16:05:08Z) - -
- Upstream the latest ecs-service module @goruha (#810) - -### what - -- Upstream the latest `ecs-service` component - -### why - -- Support ecspresso deployments -- Support s3 task definition mirroring -- Support external ALB/NLN components - -
- -## 1.281.0 (2023-08-14T09:10:42Z) - -
- Refactor Changelog @milldr (#811) - -### what - -- moved changelog for individual components -- changed title - -### why - -- Title changelogs consistently by components version -- Separate changes by affected components - -### references - -- https://github.com/cloudposse/knowledge-base/discussions/132 - -
- -## 1.280.1 (2023-08-14T08:06:42Z) - -### 🚀 Enhancements - -
- Fix eks/cluster default values @Nuru (#813) - -### what - -- Fix eks/cluster `node_group_defaults` to default to legal (empty) values for `kubernetes_labels` and - `kubernetes_taints` -- Increase eks/cluster managed node group default disk size from 20 to 50 GB - -### why - -- Default values should be legal values or else they are not really defaults -- Nodes were running out of disk space just hosting daemon set pods at 20 GB - -
- -## 1.280.0 (2023-08-11T20:13:45Z) - -
- Updated ssm parameter versions @RoseSecurity (#812) - -### Why: - -- `cloudposse/ssm-parameter-store/aws` was out of date -- There are no new [changes](https://github.com/cloudposse/terraform-aws-ssm-parameter-store/releases/tag/0.11.0) - incorporated but just wanted to standardize new modules to updated version - -### What: - -- Updated the following to `v0.11.0`: - -``` -0.10.0 modules/argocd-repo -0.10.0 modules/aurora-mysql -0.10.0 modules/aurora-postgres -0.10.0 modules/datadog-configuration -0.10.0 modules/eks/platform -0.10.0 modules/opsgenie-team/modules/integration -0.10.0 modules/ses -0.9.1 modules/datadog-integration -``` - -
- -## 1.279.0 (2023-08-11T16:39:01Z) - -
- fix: restore argocd notification ssm lookups @dudymas (#764) - -### what - -- revert some changes to `argocd` component -- connect argocd notifications with ssm secrets -- remove `deployment_id` from `argocd-repo` component -- correct `app_hostname` since gha usually adds protocol - -### why - -- regressions with argocd notifications caused github actions to timeout -- `deployment_id` no longer needed for fascilitating communication between gha and ArgoCD -- application urls were incorrect and problematic during troubleshooting - -
- -## 1.278.0 (2023-08-09T21:54:09Z) - -
- Upstream `eks/keda` @milldr (#808) - -### what - -- Added the component `eks/keda` - -### why - -- We've deployed KEDA for a few customers now and the component should be upstreamed - -### references - -- n/a - -
- -## 1.277.0 (2023-08-09T20:39:21Z) - -
- Added Inputs for `elasticsearch` and `cognito` @milldr (#786) - -### what - -- Added `deletion_protection` for `cognito` -- Added options for dedicated master for `elasticsearch` - -### why - -- Allow the default options to be customized - -### references - -- Customer requested additions - -
- -## 1.276.1 (2023-08-09T20:30:36Z) - -
- Update upgrade-guide.md Version @milldr (#807) - -### what - -- Set the version to the correct updated release - -### why - -- Needs to match correct version - -### references - -#804 - -
- -### 🚀 Enhancements - -
- feat: allow email to be configured at account level @sgtoj (#799) - -### what - -- allow email to be configured at account level - -### why - -- to allow importing existing accounts with email address that does not met the organization standard naming format - -### references - -- n/a - -
- -## 1.276.0 (2023-08-09T16:38:40Z) - -
- Transit Gateway Cross-Region Support @milldr (#804) - -### what - -- Upgraded `tgw` components to support cross region connections -- Added back `tgw/cross-region-hub-connector` with overhaul to support updated `tgw/hub` component - -### why - -- Deploy `tgw/cross-region-hub-connector` to create peered TGW hubs -- Use `tgw/hub` both for in region and intra region connections - -### references - -- n/a - -
- -## 1.275.0 (2023-08-09T02:53:39Z) - -
- [eks/cluster] Proper handling of cold start and enabled=false @Nuru (#806) - -### what - -- Proper handling of cold start and `enabled=false` - -### why - -- Fixes #797 -- Supersedes and closes #798 -- Cloud Posse standard requires error-free operation and no resources created when `enabled` is `false`, but previously - this component had several errors - -
- -## 1.274.2 (2023-08-09T00:13:36Z) - -### 🚀 Enhancements - -
- Added Enabled Parameter to aws-saml/okta-user and datadog-synthetics-private-location @RoseSecurity (#805) - -### What: - -- Added `enabled` parameter for `modules/aws-saml/modules/okta-user/main.tf` and - `modules/datadog-private-location-ecs/main.tf` - -### Why: - -- No support for disabling the creation of the resources - -
- -## 1.274.1 (2023-08-09T00:11:55Z) - -### 🚀 Enhancements - -
- Updated Security Group Component to 2.2.0 @RoseSecurity (#803) - -### What: - -- Updated `bastion`, `redshift`, `rds`, `spacelift`, and `vpc` to utilize the newest version of - `cloudposse/security-group/aws` - -### Why: - -- `cloudposse/security-group/aws` components were not updated to `2.2.0` - -### References: - -- [AWS Security Group Component](https://github.com/cloudposse/terraform-aws-security-group/compare/2.0.0-rc1...2.2.0) - -
- -## 1.274.0 (2023-08-08T17:03:41Z) - -
- bug: update descriptions *_account_account_name variables @sgtoj (#801) - -### what - -- update descriptions `*_account_account_name` variables - - I replaced `stage` with `short` because that is the description used for the respective `outputs` entries - -### why - -- to help future implementers of CloudPosse's architectures - -### references - -- n/a - -
- -## 1.273.0 (2023-08-08T17:01:23Z) - -
- docs: fix issue with eks/cluster usage snippet @sgtoj (#796) - -### what - -- update usage snippet in readme for `eks/cluster` component - -### why - -- fix incorrect shape for one of the items in `aws_team_roles_rbac` -- improve consistency -- remove variables that are not applicable for the component - -### references - -- n/a - -
- -## 1.272.0 (2023-08-08T17:00:32Z) - -
- feat: filter out “SUSPENDED” accounts for account-map @sgtoj (#800) - -### what - -- filter out “SUSPENDED” accounts (aka accounts in waiting period for termination) for `account-map` component - -### why - -- suspended account cannot be used, so therefore it should not exist in the account-map -- allows for new _active_ accounts with same exact name of suspended account to exists and work with `account-map` - -### references - -- n/a - -
- -## 1.271.0 (2023-08-08T16:44:18Z) - -
- `eks/karpenter` Readme.md update @Benbentwo (#792) - -### what - -- Adding Karpenter troubleshooting to readme -- Adding https://endoflife.date/amazon-eks to `EKS/Cluster` - -### references - -- https://karpenter.sh/docs/troubleshooting/ -- https://endoflife.date/amazon-eks - -
- -## 1.270.0 (2023-08-07T21:54:49Z) - -
- [eks/cluster] Add support for BottleRocket and EFS add-on @Nuru (#795) - -### what - -- Add support for EKS EFS add-on -- Better support for Managed Node Group's Block Device Storage -- Deprecate and ignore `aws_teams_rbac` and remove `identity` roles from `aws-auth` -- Support `eks/cluster` provisioning EC2 Instance Profile for Karpenter nodes (disabled by default via legacy flags) -- More options for specifying Availability Zones -- Deprecate `eks/ebs-controller` and `eks/efs-controller` -- Deprecate `eks/eks-without-spotinst` - -### why - -- Support EKS add-ons, follow-up to #723 -- Support BottleRocket, `gp3` storage, and provisioned iops and throughput -- Feature never worked -- Avoid specific failure mode when deleting and recreating an EKS cluster -- Maintain feature parity with `vpc` component -- Replace with add-ons -- Was not being maintained or used - -
- -
- [eks/storage-class] Initial implementation @Nuru (#794) - -### what - -- Initial implementation of `eks/storage-class` - -### why - -- Until now, we provisioned StorageClasses as a part of deploying - [eks/ebs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/ebs-controller/main.tf#L20-L56) - and - [eks/efs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/efs-controller/main.tf#L48-L60). - However, with the switch from deploying "self-managed" controllers to EKS add-ons, we no longer deploy - `eks/ebs-controller` or `eks/efs-controller`. Therefore, we need a new component to manage StorageClasses - independently of controllers. - -### references - -- #723 - -
- -
- [eks/karpenter] Script to update Karpenter CRDs @Nuru (#793) - -### what - -- [eks/karpenter] Script to update Karpenter CRDs - -### why - -- Upgrading Karpenter to v0.28.0 requires updating CRDs, which is not handled by current Helm chart. This script updates - them by modifying the existing CRDs to be labeled as being managed by Helm, then installing the `karpenter-crd` Helm - chart. - -### references - -- Karpenter [CRD Upgrades](https://karpenter.sh/docs/upgrade-guide/#custom-resource-definition-crd-upgrades) - -
- -## 1.269.0 (2023-08-03T20:47:56Z) - -
- upstream `api-gateway` and `api-gateway-settings` @Benbentwo (#788) - -### what - -- Upstream api-gateway and it's corresponding settings component - -
- -## 1.268.0 (2023-08-01T05:04:37Z) - -
- Added new variable into `argocd-repo` component to configure ArgoCD's `ignore-differences` @zdmytriv (#785) - -### what - -- Added new variable into `argocd-repo` component to configure ArcoCD `ignore-differences` - -### why - -- There are cases when application and/or third-party operators might want to change k8s API objects. For example, - change the number of replicas in deployment. This will conflict with ArgoCD application because the ArgoCD controller - will spot drift and will try to make an application in sync with the codebase. - -### references - -- https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs - -
- -## 1.267.0 (2023-07-31T19:41:43Z) - -
- Spacelift `admin-stack` `var.description` @milldr (#787) - -### what - -- added missing description option - -### why - -- Variable is defined, but never passed to the modules - -### references - -n/a - -
- -## 1.266.0 (2023-07-29T18:00:25Z) - -
- Use s3_object_ownership variable @sjmiller609 (#779) - -### what - -- Pass s3_object_ownership variable into s3 module - -### why - -- I think it was accidentally not included -- Make possible to disable ACL from stack config - -### references - -- https://github.com/cloudposse/terraform-aws-s3-bucket/releases/tag/3.1.0 - -
- -## 1.265.0 (2023-07-28T21:35:14Z) - -
- `bastion` support for `availability_zones` and public IP and subnets @milldr (#783) - -### what - -- Add support for `availability_zones` -- Fix issue with public IP and subnets -- `tflint` requirements -- removed all unused locals, variables, formatting - -### why - -- All instance types are not available in all AZs in a region -- Bug fix - -### references - -- [Internal Slack reference](https://cloudposse.slack.com/archives/C048LCN8LKT/p1689085395494969) - -
- -## 1.264.0 (2023-07-28T18:57:28Z) - -
- Aurora Resource Submodule Requirements @milldr (#775) - -### what - -- Removed unnecessary requirement for aurora resources for the service name not to equal the user name for submodules of - both aurora resource components - -### why - -- This conditional doesn't add any value besides creating an unnecessary restriction. We should be able to create a user - name as the service name if we want - -### references - -- n/a - -
- -## 1.263.0 (2023-07-28T18:12:30Z) - -
- fix: restore notifications config in argocd @dudymas (#782) - -### what - -- Restore ssm configuration options for argocd notifications - -### why - -- notifications were not firing and tasks time out in some installations - -
- -## 1.262.0 (2023-07-27T17:05:37Z) - -
- Upstream `spa-s3-cloudfront` @milldr (#780) - -### what - -- Update module -- Add Cloudfront Invalidation permission to GitHub policy - -### why - -- Corrected bug in the module -- Allow GitHub Actions to run invalidations - -### references - -- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/288 - -
- -## 1.261.0 (2023-07-26T16:20:37Z) - -
- Upstream `spa-s3-cloudfront` @milldr (#778) - -### what - -- Upstream changes to `spa-s3-cloudfront` - -### why - -- Updated the included modules to support Terraform v5 -- Handle disabled WAF from remote-state - -### references - -- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/284 - -
- -## 1.260.1 (2023-07-25T05:10:20Z) - -### 🚀 Enhancements - -
- [vpc] bugfix, [aurora-postgres] & [cloudtrail-bucket] Tflint fixes @Nuru (#776) - -### what - -- [vpc]: disable vpc_endpoints when enabled = false -- [aurora-postgres]: ensure variables have explicit types -- [cloudtrail-bucket]: ensure variables have explicit types - -### why - -- bugfix -- tflint fix -- tflint fix - -
- -### 🐛 Bug Fixes - -
- [vpc] bugfix, [aurora-postgres] & [cloudtrail-bucket] Tflint fixes @Nuru (#776) - -### what - -- [vpc]: disable vpc_endpoints when enabled = false -- [aurora-postgres]: ensure variables have explicit types -- [cloudtrail-bucket]: ensure variables have explicit types - -### why - -- bugfix -- tflint fix -- tflint fix - -
- -## 1.260.0 (2023-07-23T23:08:53Z) - -
- Update `alb` component @aknysh (#773) - -### what - -- Update `alb` component - -### why - -- Fixes after provisioning and testing on AWS - -
- -## 1.259.0 (2023-07-20T04:32:13Z) - -
- `elasticsearch` DNS Component Lookup @milldr (#769) - -### what - -- add environment for `dns-delegated` component lookup - -### why - -- `elasticsearch` is deployed in a regional environment, but `dns-delegated` is deployed to `gbl` - -### references - -- n/a - -
- -## 1.258.0 (2023-07-20T04:17:31Z) - -
- Bump `lambda-elasticsearch-cleanup` module @milldr (#768) - -### what - -- bump version of `lambda-elasticsearch-cleanup` module - -### why - -- Support Terraform provider v5 - -### references - -- https://github.com/cloudposse/terraform-aws-lambda-elasticsearch-cleanup/pull/48 - -
- -## 1.257.0 (2023-07-20T03:04:51Z) - -
- Bump ECS cluster module @max-lobur (#752) - -### what - -- Update ECS cluster module - -### why - -- Maintenance - -
- -## 1.256.0 (2023-07-18T23:57:44Z) - -
- Bump `elasticache-redis` Module @milldr (#767) - -### what - -- Bump `elasticache-redis` module - -### why - -- Resolve issues with terraform provider v5 - -### references - -- https://github.com/cloudposse/terraform-aws-elasticache-redis/issues/199 - -
- -## 1.255.0 (2023-07-18T22:53:51Z) - -
- Aurora Postgres Enhanced Monitoring Input @milldr (#766) - -### what - -- Added `enhanced_monitoring_attributes` as option -- Set default `aurora-mysql` component name - -### why - -- Set this var with a custom value to avoid IAM role length restrictions (default unchanged) -- Set common value as default - -### references - -- n/a - -
- -## 1.254.0 (2023-07-18T21:00:30Z) - -
- feat: acm no longer requires zone @dudymas (#765) - -### what - -- `acm` only looks up zones if `process_domain_validation_options` is true - -### why - -- Allow external validation of acm certs - -
- -## 1.253.0 (2023-07-18T17:45:16Z) - -
- `alb` and `ssm-parameters` Upstream for Basic Use @milldr (#763) - -### what - -- `alb` component can get the ACM cert from either `dns-delegated` or `acm` -- Support deploying `ssm-parameters` without SOPS -- `waf` requires a value for `visibility_config` in the stack catalog - -### why - -- resolving bugs while deploying example components - -### references - -- https://cloudposse.atlassian.net/browse/JUMPSTART-1185 - -
- -## 1.252.0 (2023-07-18T16:14:23Z) - -
- fix: argocd flags, versions, and expressions @dudymas (#753) - -### what - -- adjust expressions in argocd -- update helmchart module -- tidy up variables - -### why - -- component wouldn't run - -
- -## 1.251.0 (2023-07-15T03:47:29Z) - -
- fix: ecs capacity provider typing @dudymas (#762) - -### what - -- Adjust typing of `capacity_providers_ec2` - -### why - -- Component doesn't work without these fixes - -
- -## 1.250.3 (2023-07-15T00:31:40Z) - -### 🚀 Enhancements - -
- Update `alb` and `eks/alb-controller` components @aknysh (#760) - -### what - -- Update `alb` and `eks/alb-controller` components - -### why - -- Remove unused variables and locals -- Apply variables that are defined in `variables.tf` but were not used - -
- -## 1.250.2 (2023-07-14T23:34:14Z) - -### 🚀 Enhancements - -
- [aws-teams] Remove obsolete restriction on assuming roles in identity account @Nuru (#761) - -### what - -- [aws-teams] Remove obsolete restriction on assuming roles in the `identity` account - -### why - -Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the -originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy -against assuming roles in the `identity` account. - -AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team -structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the -previous restriction is both not needed and actually hinders desired operation. - -
- -### 🐛 Bug Fixes - -
- [aws-teams] Remove obsolete restriction on assuming roles in identity account @Nuru (#761) - -### what - -- [aws-teams] Remove obsolete restriction on assuming roles in the `identity` account - -### why - -Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the -originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy -against assuming roles in the `identity` account. - -AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team -structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the -previous restriction is both not needed and actually hinders desired operation. - -
- -## 1.250.1 (2023-07-14T02:14:46Z) - -### 🚀 Enhancements - -
- [eks/karpenter-provisioner] minor improvements @Nuru (#759) - -### what - -- [eks/karpenter-provisioner]: - - Implement `metadata_options` - - Avoid Terraform errors by marking Provisoner `spec.requirements` a computed field - - Add explicit error message about Consolidation and TTL Seconds After Empty being mutually exclusive - - Add `instance-category` and `instance-generation` to example in README - - Make many inputs optional -- [eks/karpenter] Update README to indicate that version 0.19 or later of Karpenter is required to work with this code. - -### why - -- Bug Fix: Input was there, but was being ignored, leading to unexpected behavior -- If a requirement that had a default value was not supplied, Terraform would fail with an error about inconsistent - plans because Karpenter would fill in the default -- Show some default values and how to override them -- Reduce the burden of supplying empty fields - -
- -## 1.250.0 (2023-07-14T02:10:46Z) - -
- Add EKS addons and the required IRSA to the `eks` component @aknysh (#723) - -### what - -- Deprecate the `eks-iam` component -- Add EKS addons and the required IRSA for the addons to the `eks` component -- Add ability to specify configuration values and timeouts for addons -- Add ability to deploy addons to Fargate when necessary -- Add ability to omit specifying Availability Zones and infer them from private subnets -- Add recommended but optional and requiring opt-in: use a single Fargate Pod Execution Role for all Fargate Profiles - -### why - -- The `eks-iam` component is not in use (we now create the IAM roles for Kubernetes Service Accounts in the - https://github.com/cloudposse/terraform-aws-helm-release module), and has very old and outdated code - -- AWS recommends to provision the required EKS addons and not to rely on the managed addons (some of which are - automatically provisioned by EKS on a cluster) - -- Some EKS addons (e.g. `vpc-cni` and `aws-ebs-csi-driver`) require an IAM Role for Kubernetes Service Account (IRSA) - with specific permissions. Since these addons are critical for cluster functionality, we create the IRSA roles for the - addons in the `eks` component and provide the role ARNs to the addons - -- Some EKS addons can be configured. In particular, `coredns` requires configuration to enable it to be deployed to - Fargate. - -- Users relying on Karpenter to deploy all nodes and wanting to deploy `coredns` or `aws-ebs-csi-driver` addons need to - deploy them to Fargate or else the EKS deployment will fail. - -- Enable DRY specification of Availability Zones, and use of AZ IDs, by reading the VPCs AZs. - -- A cluster needs only one Fargate Pod Execution Role, and it was a mistake to provision one for every profile. However, - making the change would break existing clusters, so it is optional and requires opt-in. - -### references - -- https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html -- https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on -- https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html -- https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html -- https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role -- https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/#deploy-vpc-cni-managed-add-on -- https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html -- https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons -- https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role -- https://github.com/kubernetes-sigs/aws-ebs-csi-driver - -
- -## 1.249.0 (2023-07-14T01:23:37Z) - -
- Make alb-controller default Ingress actually the default Ingress @Nuru (#758) - -### what - -- Make the `alb-controller` default Ingress actually the default Ingress - -### why - -- When setting `default_ingress_enabled = true` it is a reasonable expectation that the deployed Ingress be marked as - the Default Ingress. The previous code suggests this was the intended behavior, but does not work with the current - Helm chart and may have never worked. - -
- -## 1.248.0 (2023-07-13T00:21:29Z) - -
- Upstream `gitops` Policy Update @milldr (#757) - -### what - -- allow actions on table resources - -### why - -- required to be able to query using a global secondary index - -### references - -- https://github.com/cloudposse/github-action-terraform-plan-storage/pull/16 - -
- -## 1.247.0 (2023-07-12T19:32:33Z) - -
- Update `waf` and `alb` components @aknysh (#755) - -### what - -- Update `waf` component -- Update `alb` component - -### why - -- For `waf` component, add missing features supported by the following resources: - - - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl - - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration - -- For `waf` component, remove deprecated features not supported by Terraform `aws` provider v5: - - - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade#resourceaws_wafv2_web_acl - - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade#resourceaws_wafv2_web_acl_logging_configuration - -- For `waf` component, allow specifying a list of Atmos components to read from the remote state and associate their - ARNs with the web ACL - -- For `alb` component, update the modules to the latest versions and allow specifying Atmos component names for the - remote state in the variables (for the cases where the Atmos component names are not standard) - -### references - -- https://github.com/cloudposse/terraform-aws-waf/pull/45 - -
- -## 1.246.0 (2023-07-12T18:57:58Z) - -
- `acm` Upstream @Benbentwo (#756) - -### what - -- Upstream ACM - -### why - -- New Variables - - `subject_alternative_names_prefixes` - - `domain_name_prefix` - -
- -## 1.245.0 (2023-07-11T19:36:11Z) - -
- Bump `spaces` module versions @milldr (#754) - -### what - -- bumped module version for `terraform-spacelift-cloud-infrastructure-automation` - -### why - -- New policy added to `spaces` - -### references - -- https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/1.1.0 - -
- -## 1.244.0 (2023-07-11T17:50:19Z) - -
- Upstream Spacelift and Documentation @milldr (#732) - -### what - -- Minor corrections to spacelift components -- Documentation - -### why - -- Deployed this at a customer and resolved the changed errors -- Adding documentation for updated Spacelift design - -### references - -- n/a - -
- -## 1.243.0 (2023-07-06T20:04:08Z) - -
- Upstream `gitops` @milldr (#735) - -### what - -- Upstream new component, `gitops` - -### why - -- This component is used to create a role for GitHub to assume. This role is used to assume the `gitops` team and is - required for enabling GitHub Action Terraform workflows - -### references - -- JUMPSTART-904 - -
- -## 1.242.1 (2023-07-05T19:46:08Z) - -### 🚀 Enhancements - -
- Use the new subnets data source @max-lobur (#737) - -### what - -- Use the new subnets data source - -### why - -- Planned migration according to https://github.com/hashicorp/terraform-provider-aws/pull/18803 - -
- -## 1.242.0 (2023-07-05T17:05:57Z) - -
- Restore backwards compatibility of account-map output @Nuru (#748) - -### what - -- Restore backwards compatibility of `account-map` output - -### why - -- PR #715 removed outputs from `account-map` that `iam-roles` relied on. Although it removed the references in - `iam-roles`, this imposed an ordering on the upgrade: the `iam-roles` code had to be deployed before the module could - be applied. That proved to be inconvenient. Furthermore, if a future `account-map` upgrade added outputs that - iam-roles`required, neither order of operations would go smoothly. With this update, the standard practice of applying`account-map` - before deploying code will work again. - -
- -## 1.241.0 (2023-07-05T16:52:58Z) - -
- Fixed broken links in READMEs @zdmytriv (#749) - -### what - -- Fixed broken links in READMEs - -### why - -- Fixed broken links in READMEs - -### references - -- https://github.com/cloudposse/terraform-aws-components/issues/747 - -
- -## 1.240.1 (2023-07-04T04:54:28Z) - -### Upgrade notes - -This fixes issues with `aws-sso` and `github-oidc-provider`. Versions from v1.227 through v1.240 should not be used. - -After installing this version of `aws-sso`, you may need to change the configuration in your stacks. See -[modules/aws-sso/changelog](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/aws-sso/CHANGELOG.md) -for more information. Note: this release is from PR #740 - -After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. See -the release notes for v1.238.1 for more information. - -### 🐛 Bug Fixes - -
- bugfix `aws-sso`, `github-oidc-provider` @Benbentwo (#740) - -### what - -- Bugfixes `filter` depreciation issue via module update to `1.1.1` -- Bugfixes missing `aws.root` provider -- Bugfixes `github-oidc-provider` v1.238.1 - -### why - -- Bugfixes - -### references - -- https://github.com/cloudposse/terraform-aws-sso/pull/44 -- closes #744 - -
- -## 1.240.0 (2023-07-03T18:14:14Z) - -
- Fix TFLint violations in account-map @MaxymVlasov (#745) - -### Why - -I'm too lazy to fix it each time when we get module updates via `atmos vendor` GHA - -### References - -- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_deprecated_index.md -- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_comment_syntax.md -- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md - -
- -## 1.239.0 (2023-06-29T23:34:53Z) - -
- Bump `cloudposse/ec2-autoscale-group/aws` to `0.35.0` @milldr (#734) - -### what - -- bumped ASG module version, `cloudposse/ec2-autoscale-group/aws` to `0.35.0` - -### why - -- Recent versions of this module resolve errors for these components - -### references - -- https://github.com/cloudposse/terraform-aws-ec2-autoscale-group - -
- -## 1.238.1 (2023-06-29T21:15:50Z) - -### Upgrade notes: - -There is a bug in this version of `github-oidc-provider`. Upgrade to version v1.240.1 or later instead. - -After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. - -- If you have dynamic Terraform roles enabled, then this should be configured like a normal component. The previous - component may have required you to set - - ```yaml - backend: - s3: - role_arn: null - ```` - - and **that configuration should be removed** everywhere. - -- If you only use SuperAdmin to deploy things to the `identity` account, then for the `identity` (and `root`, if - applicable) account **_only_**, set - - ```yaml - backend: - s3: - role_arn: null - vars: - superadmin: true - ```` - - **Deployments to other accounts should not have any of those settings**. - -### 🚀 Enhancements - -
- [github-oidc-provider] extra-compatible provider @Nuru (#742) - -### what && why - -- This updates `provider.tf` to provide compatibility with various legacy configurations as well as the current - reference architecture -- This update does NOT require updating `account-map` - -
- -## 1.238.0 (2023-06-29T19:39:15Z) - -
- IAM upgrades: SSO Permission Sets as Teams, SourceIdentity support, region independence @Nuru (#738) - -### what - -- Enable SSO Permission Sets to function as teams -- Allow SAML sign on via any regional endpoint, not only us-east-1 -- Allow use of AWS "Source Identity" for SAML and SSO users (not enabled for OIDC) - -### why - -- Reduce the friction between SSO permission sets and SAML roles by allowing people to use either interchangeably. - (Almost. SSO permission sets do not yet have the same permissions as SAML roles in the `identity` account itself.) -- Enable continued access in the event of a regional outage in us-east-1 as happened recently -- Enable auditing of who is using assumed roles - -### References - -- [Monitor and control actions taken with assumed roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_monitor.html) -- [How to integrate AWS STS SourceIdentity with your identity provider](https://aws.amazon.com/blogs/security/how-to-integrate-aws-sts-sourceidentity-with-your-identity-provider/) -- [AWS Sign-In endpoints](https://docs.aws.amazon.com/general/latest/gr/signin-service.html) -- [Available keys for SAML-based AWS STS federation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-saml) - -### Upgrade notes - -The regional endpoints and Source Identity support are non-controversial and cannot be disabled. They do, however, -require running `terraform apply` against `aws-saml`, `aws-teams`, and `aws-team-roles` in all accounts. - -#### AWS SSO updates - -To enable SSO Permission Sets to function as teams, you need to update `account-map` and `aws-sso`, then apply changes -to - -- `tfstate-backend` -- `aws-teams` -- `aws-team-roles` -- `aws-sso` - -This is all enabled by default. If you do not want it, you only need to update `account-map`, and add -`account-map/modules/roles-to-principles/variables_override.tf` in which you set -`overridable_team_permission_sets_enabled` to default to `false` - -Under the old `iam-primary-roles` component, corresponding permission sets were named `IdentityRoleAccess`. Under -the current `aws-teams` component, they are named `IdentityTeamAccess`. The current `account-map` defaults to the -latter convention. To use the earlier convention, add `account-map/modules/roles-to-principles/variables_override.tf` in -which you set `overridable_team_permission_set_name_pattern` to default to `"Identity%sRoleAccess"` - -There is a chance the resulting trust policies will be too big, especially for `tfstate-backend`. If you get an error -like - -``` -Cannot exceed quota for ACLSizePerRole: 2048 -``` - -You need to request a quota increase (Quota Code L-C07B4B0D), which will be automatically granted, usually in about 5 -minutes. The max quota is 4096, but we recommend increasing it to 3072 first, so you retain some breathing room for the -future. - -
- -## 1.237.0 (2023-06-27T22:27:49Z) - -
- Add Missing `github-oidc-provider` Thumbprint @milldr (#736) - -### what - -- include both thumbprints for GitHub OIDC - -### why - -- There are two possible intermediary certificates for the Actions SSL certificate and either can be returned by - Github's servers, requiring customers to trust both. This is a known behavior when the intermediary certificates are - cross-signed by the CA. - -### references - -- https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/ - -
- -## 1.236.0 (2023-06-26T18:14:29Z) - -
- Update `eks/echo-server` and `eks/alb-controller-ingress-group` components @aknysh (#733) - -### what - -- Update `eks/echo-server` and `eks/alb-controller-ingress-group` components -- Allow specifying - [alb.ingress.kubernetes.io/scheme](https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/#scheme) - (`internal` or `internet-facing`) - -### why - -- Allow the echo server to work with internal load balancers - -### references - -- https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/ - -
- -## 1.235.0 (2023-06-22T21:06:18Z) - -
- [account-map] Backwards compatibility for terraform profile users and eks/cluster @Nuru (#731) - -### what - -- [account-map/modules/iam-roles] Add `profiles_enabled` input to override global value -- [eks/cluster] Use `iam-roles` `profiles_enabled` input to force getting a role ARN even when profiles are in use -- [guardduty] Make providers compatible with static and dynamic TF roles - -### why - -- Previously, when the global `account-map` `profiles_enabled` flag was `true`, `iam_roles.terraform_role_arn` would be - null. However, `eks/cluster` requires `terraform_role_arn` regardless. -- Changes made in #728 work in environments that have not adopted dynamic Terraform roles but would fail in environments - that have (when using SuperAdmin) - -
- -## 1.234.0 (2023-06-21T22:44:55Z) - -
- [account-map] Feature flag to enable legacy Terraform role mapping @Nuru (#730) - -### what - -- [account-map] Add `legacy_terraform_uses_admin` feature flag to retain backwards compatibility - -### why - -- Historically, the `terraform` roles in `root` and `identity` were not used for Terraform plan/apply, but for other - things, and so the `terraform_roles` map output selected the `admin` roles for those accounts. This "wart" has been - remove in current `aws-team-roles` and `tfstate-backend` configurations, but for people who do not want to migrate to - the new conventions, this feature flag enables them to maintain the status quo with respect to role usage while taking - advantage of other updates to `account-map` and other components. - -### references - -This update is recommended for all customers wanting to use **_any_** component version 1.227 or later. - -- #715 -- - -
- -## 1.233.0 (2023-06-21T20:03:36Z) - -
- [lambda] feat: allows to use YAML instead of JSON for IAM policy @gberenice (#692) - -### what - -- BREAKING CHANGE: Actually use variable `function_name` to set the lambda function name. -- Make the variable `function_name` optional. When not set, the old null-lable-derived name will be use. -- Allow IAM policy to be specified in a custom terraform object as an alternative to JSON. - -### why - -- `function_name` was required to set, but it wasn't actually passed to `module "lambda"` inputs. -- Allow callers to stop providing `function_name` and preserve old behavior of using automatically generated name. -- When using [Atmos](https://atmos.tools/) to generate inputs from "stack" YAML files, having the ability to pass the - statements in as a custom object means specifying them via YAML, which makes the policy declaration in stack more - readable compared to embedding a JSON string in the YAML. - -
- -## 1.232.0 (2023-06-21T15:49:06Z) - -
- refactor securityhub component @mcalhoun (#728) - -### what - -- Refactor the Security Hub components into a single component - -### why - -- To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. - -
- -## 1.231.0 (2023-06-21T14:54:50Z) - -
- roll guard duty back to previous providers logic @mcalhoun (#727) - -### what - -- Roll the Guard Duty component back to using the previous logic for role assumption. - -### why - -- The newer method is causing the provider to try to assume the role twice. We get the error: - -``` -AWS Error: operation error STS: AssumeRole, https response error StatusCode: 403, RequestID: 00000000-0000-0000-0000-00000000, api error AccessDenied: User: arn:aws:sts::000000000000:assumed-role/acme-core-gbl-security-terraform/aws-go-sdk-1687312396297825294 is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::000000000000:role/acme-core-gbl-security-terraform -``` - -
- -## 1.230.0 (2023-06-21T01:49:52Z) - -
- refactor guardduty module @mcalhoun (#725) - -### what - -- Refactor the GuardDuty components into a single component - -### why - -- To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. - -
- -## 1.229.0 (2023-06-20T19:37:35Z) - -
- upstream `github-action-runners` dockerhub authentication @Benbentwo (#726) - -### what - -- Adds support for dockerhub authentication - -### why - -- Dockerhub limits are unrealistically low for actually using dockerhub as an image registry for automated builds - -
- -## 1.228.0 (2023-06-15T20:57:45Z) - -
- alb: use the https_ssl_policy @johncblandii (#722) - -### what - -- Apply the HTTPS policy - -### why - -- The policy was unused so it was defaulting to an old policy - -### references - -
- -## 1.227.0 (2023-06-12T23:41:45Z) - -Possibly breaking change: - -In this update, `account-map/modules/iam-roles` acquired a provider, making it no longer able to be used with `count`. -If you have code like - -```hcl -module "optional_role" { - count = local.optional_role_enabled ? 1 : 0 - - source = "../account-map/modules/iam-roles" - stage = var.optional_role_stage - context = module.this.context -} -``` - -You will need to rewrite it, removing the `count` parameter. It will be fine to always instantiate the module. If there -are problems with ensuring appropriate settings with the module is disabled, you can always replace them with the -component's inputs: - -```hcl -module "optional_role" { - source = "../account-map/modules/iam-roles" - stage = local.optional_role_enabled ? var.optional_role_stage : var.stage - context = module.this.context -} -``` - -The update to components 1.227.0 is huge, and you have options. - -- Enable, or not, dynamic Terraform IAM roles, which allow you to give some people (and Spacelift) the ability to run - Terraform plan in some accounts without allowing apply. Note that these users will still have read/write access to - Terraform state, but will not have IAM permissions to make changes in accounts. - [terraform_dynamic_role_enabled](https://github.com/cloudposse/terraform-aws-components/blob/1b338fe664e5debc5bbac30cfe42003f7458575a/modules/account-map/variables.tf#L96-L100) -- Update to new `aws-teams` team names. The new names are (except for support) distinct from team-roles, making it - easier to keep track. Also, the new managers team can run Terraform for identity and root in most (but not all) cases. -- Update to new `aws-team-roles`, including new permissions. The custom policies that have been removed are replaced in - the `aws-team-roles` configuration with AWS managed policy ARNs. This is required to add the `planner` role and - support the `terraform plan` restriction. -- Update the `providers.tf for` all components. Or some of them now, some later. Most components do not require updates, - but all of them have updates. The new `providers.tf`, when used with dynamic Terraform roles, allows users directly - logged into target accounts (rather than having roles in the `identity` account) to use Terraform in that account, and - also allows SuperAdmin to run Terraform in more cases (almost everywhere). - -**If you do not want any new features**, you only need to update `account-map` to v1.235 or later, to be compatible with -future components. Note that when updating `account-map` this way, you should update the code everywhere (all open PRs -and branches) before applying the Terraform changes, because the applied changes break the old code. - -If you want all the new features, we recommend updating all of the following to the current release in 1 PR: - -- account-map -- aws-teams -- aws-team-roles -- tfstate-backend - -
- Enable `terraform plan` access via dynamic Terraform roles @Nuru (#715) - -### Reviewers, please note: - -The PR changes a lot of files. In particular, the `providers.tf` and therefore the `README.md` for nearly every -component. Therefore it will likely be easier to review this PR one commit at a time. - -`import_role_arn` and `import_profile_name` have been removed as they are no longer needed. Current versions of -Terraform (probably beginning with v1.1.0, but maybe as late as 1.3.0, I have not found authoritative information) can -read data sources during plan and so no longer need a role to be explicitly specified while importing. Feel free to -perform your own tests to make yourself more comfortable that this is correct. - -### what - -- Updates to allow Terraform to dynamically assume a role based on the user, to allow some users to run `terraform plan` - but not `terraform apply` - - Deploy standard `providers.tf` to all components that need an `aws` provider - - Move extra provider configurations to separate file, so that `providers.tf` can remain consistent/identical among - components and thus be easily updated - - Create `provider-awsutils.mixin.tf` to provide consistent, maintainable implementation -- Make `aws-sso` vendor safe -- Deprecate `sso` module in favor of `aws-saml` - -### why - -- Allow users to try new code or updated configurations by running `terraform plan` without giving them permission to - make changes with Terraform -- Make it easier for people directly logged into target accounts to still run Terraform -- Follow-up to #697, which updated `aws-teams` and `aws-team-roles`, to make `aws-sso` consistent -- Reduce confusion by moving deprecated code to `deprecated/` - -
- -## 1.226.0 (2023-06-12T17:42:51Z) - -
- chore: Update and add more basic pre-commit hooks @MaxymVlasov (#714) - -### what - -Fix common issues in the repo - -### why - -It violates our basic checks, which adds a headache to using -https://github.com/cloudposse/github-action-atmos-component-updater as is - -![image](https://github.com/cloudposse/terraform-aws-components/assets/11096782/248febbe-b65f-4080-8078-376ef576b457) - -> **Note**: It is much simpler to review PR if -> [hide whitespace changes](https://github.com/cloudposse/terraform-aws-components/pull/714/files?w=1) - -
- -## 1.225.0 (2023-06-12T14:57:20Z) - -
- Removed list of components from main README.md @zdmytriv (#721) - -### what - -- Removed list of components from main README.md - -### why - -- That list is outdated - -### references - -
- -## 1.224.0 (2023-06-09T19:52:51Z) - -
- upstream argocd @Benbentwo (#634) - -### what - -- Upstream fixes that allow for Google OIDC - -
- -## 1.223.0 (2023-06-09T14:28:08Z) - -
- add new spacelift components @mcalhoun (#717) - -### what - -- Add the newly developed spacelift components -- Deprecate the previous components - -### why - -- We undertook a process of decomposing a monolithic module and broke it into smaller, composable pieces for a better - developer experience - -### references - -- Corresponding - [Upstream Module PR](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/143) - -
- -## 1.222.0 (2023-06-08T23:28:34Z) - -
- Karpenter Node Interruption Handler @milldr (#713) - -### what - -- Added Karpenter Interruption Handler to existing component - -### why - -- Interruption is supported by karpenter, but we need to deploy sqs queue and event bridge rules to enable - -### references - -- https://github.com/cloudposse/knowledge-base/discussions/127 - -
- -## 1.221.0 (2023-06-07T18:11:23Z) - -
- feat: New Component `aws-ssosync` @dudymas (#625) - -### what - -- adds a fork of [aws-ssosync](https://github.com/awslabs/ssosync) as a lambda on a 15m cronjob - -### Why - -Google is one of those identity providers that doesn't have good integration with AWS SSO. In order to sync groups and -users across we need to use some API calls, luckily AWS Built [aws-ssosync](https://github.com/awslabs/ssosync) to -handle that. - -Unfortunately, it required ASM so we use [Benbentwo/ssosync](https://github.com/Benbentwo/ssosync) as it removes that -requirement. - -
- -## 1.220.0 (2023-06-05T22:31:10Z) - -
- Disable helm experiments by default, block Kubernetes provider 2.21.0 @Nuru (#712) - -### what - -- Set `helm_manifest_experiment_enabled` to `false` by default -- Block Kubernetes provider 2.21.0 - -### why - -- The `helm_manifest_experiment_enabled` reliably breaks when a Helm chart installs CRDs. The initial reason for - enabling it was for better drift detection, but the provider seems to have fixed most if not all of the drift - detection issues since then. -- Kubernetes provider 2.21.0 had breaking changes which were reverted in 2.21.1. - -### references - -- https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084#issuecomment-1576711378 - -
- -## 1.219.0 (2023-06-05T20:23:17Z) - -
- Expand ECR GH OIDC Default Policy @milldr (#711) - -### what - -- updated default ECR GH OIDC policy - -### why - -- This policy should grant GH OIDC access both public and private ECR repos - -### references - -- https://cloudposse.slack.com/archives/CA4TC65HS/p1685993698149499?thread_ts=1685990234.560589&cid=CA4TC65HS - -
- -## 1.218.0 (2023-06-05T01:59:49Z) - -
- Move `profiles_enabled` logic out of `providers.tf` and into `iam-roles` @Nuru (#702) - -### what - -- For Terraform roles and profiles used in `providers.tf`, return `null` for unused option -- Rename variables to `overridable_*` and update documentation to recommend `variables_override.tf` for customization - -### why - -- Prepare for `providers.tf` updates to support dynamic Terraform roles -- ARB decision on customization compatible with vendoring - -
- -## 1.217.0 (2023-06-04T23:11:44Z) - -
- [eks/external-secrets-operator] Normalize variables, update dependencies @Nuru (#708) - -### what - -For `eks/external-secrets-operator`: - -- Normalize variables, update dependencies -- Exclude Kubernetes provider v2.21.0 - -### why - -- Bring in line with other Helm-based modules -- Take advantage of improvements in dependencies - -### references - -- [Breaking change in Kubernetes provider v2.21.0](https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084) - -
- -## 1.216.2 (2023-06-04T23:08:39Z) - -### 🚀 Enhancements - -
- Update modules for Terraform AWS provider v5 @Nuru (#707) - -### what - -- Update modules for Terraform AWS provider v5 - -### why - -- Provider version 5.0.0 was released with breaking changes. This fixes the breakage. - -### references - -- [v5 upgrade guide](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade) -- [v5.0.0 Release Notes](https://github.com/hashicorp/terraform-provider-aws/releases/tag/v5.0.0) - -
- -## 1.216.1 (2023-06-04T01:18:31Z) - -### 🚀 Enhancements - -
- Preserve custom roles when vendoring in updates @Nuru (#697) - -### what - -- Add `additional-policy-map.tf` as glue meant to be replaced by customers with map of their custom policies. - -### why - -- Currently, custom polices have to be manually added to the map in `main.tf`, but that gets overwritten with every - vendor update. Putting that map in a separate, optional file allows for the custom code to survive vendoring. - -
- -## 1.216.0 (2023-06-02T18:02:01Z) - -
- ssm-parameters: support tiers @johncblandii (#705) - -### what - -- Added support for ssm param tiers -- Updated the minimum version to `>= 1.3.0` to support `optional` parameters - -### why - -- `Standard` tier only supports 4096 characters. This allows Advanced and Intelligent Tiering support. - -### references - -
- -## 1.215.0 (2023-06-02T14:28:29Z) - -
- `.editorconfig` Typo @milldr (#704) - -### what - -fixed intent typo - -### why - -should be spelled "indent" - -### references - -https://cloudposse.slack.com/archives/C01EY65H1PA/p1685638634845009 - -
- -## 1.214.0 (2023-05-31T17:46:35Z) - -
- Transit Gateway `var.connections` Redesign @milldr (#685) - -### what - -- Updated how the connection variables for `tgw/hub` and `tgw/spoke` are defined -- Moved the old versions of `tgw` to `deprecated/tgw` - -### why - -- We want to be able to define multiple or alternately named `vpc` or `eks/cluster` components for both hub and spoke -- The cross-region components are not updated yet with this new design, since the current customers requesting these - updates do not need cross-region access at this time. But we want to still support the old design s.t. customers using - cross-region components can access the old components. We will need to update the cross-region components with follow - up effort - -### references - -- https://github.com/cloudposse/knowledge-base/discussions/112 - -
- -## 1.213.0 (2023-05-31T14:50:16Z) - -
- Introducing Security Hub @zdmytriv (#683) - -### what - -- Introducing Security Hub component - -### why - -Amazon Security Hub enables users to centrally manage and monitor the security and compliance of their AWS accounts and -resources. It aggregates, organizes, and prioritizes security findings from various AWS services, third-party tools, and -integrated partner solutions. - -Here are the key features and capabilities of Amazon Security Hub: - -- Centralized security management: Security Hub provides a centralized dashboard where users can view and manage - security findings from multiple AWS accounts and regions. This allows for a unified view of the security posture - across the entire AWS environment. - -- Automated security checks: Security Hub automatically performs continuous security checks on AWS resources, - configurations, and security best practices. It leverages industry standards and compliance frameworks, such as AWS - CIS Foundations Benchmark, to identify potential security issues. - -- Integrated partner solutions: Security Hub integrates with a wide range of AWS native services, as well as third-party - security products and solutions. This integration enables the ingestion and analysis of security findings from diverse - sources, offering a comprehensive security view. - -- Security standards and compliance: Security Hub provides compliance checks against industry standards and regulatory - frameworks, such as PCI DSS, HIPAA, and GDPR. It identifies non-compliant resources and provides guidance on - remediation actions to ensure adherence to security best practices. - -- Prioritized security findings: Security Hub analyzes and prioritizes security findings based on severity, enabling - users to focus on the most critical issues. It assigns severity levels and generates a consolidated view of security - alerts, allowing for efficient threat response and remediation. - -- Custom insights and event aggregation: Security Hub supports custom insights, allowing users to create their own rules - and filters to focus on specific security criteria or requirements. It also provides event aggregation and correlation - capabilities to identify related security findings and potential attack patterns. - -- Integration with other AWS services: Security Hub seamlessly integrates with other AWS services, such as AWS - CloudTrail, Amazon GuardDuty, AWS Config, and AWS IAM Access Analyzer. This integration allows for enhanced - visibility, automated remediation, and streamlined security operations. - -- Alert notifications and automation: Security Hub supports alert notifications through Amazon SNS, enabling users to - receive real-time notifications of security findings. It also facilitates automation and response through integration - with AWS Lambda, allowing for automated remediation actions. - -By utilizing Amazon Security Hub, organizations can improve their security posture, gain insights into security risks, -and effectively manage security compliance across their AWS accounts and resources. - -### references - -- https://aws.amazon.com/security-hub/ -- https://github.com/cloudposse/terraform-aws-security-hub/ - -
- -## 1.212.0 (2023-05-31T14:45:30Z) - -
- Introducing GuardDuty @zdmytriv (#682) - -### what - -- Introducing GuardDuty component - -### why - -AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by -continuously monitoring for malicious activities and unauthorized behaviors. GuardDuty analyzes various data sources -within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS logs, to detect potential security -threats. - -Key features and components of AWS GuardDuty include: - -- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence - to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event - logs and network traffic data to detect patterns, anomalies, and known attack techniques. - -- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global - community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, - domains, and other indicators of compromise. - -- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be - delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS - Lambda for immediate action or custom response workflows. - -- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and - monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security - policies and practices. - -- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS - Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of - security incidents and reduces the need for manual intervention. - -- Security findings and reports: GuardDuty provides detailed security findings and reports that include information - about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed - through the AWS Management Console or retrieved via APIs for further analysis and reporting. - -GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations -with an additional layer of security to proactively identify and respond to potential security risks. - -### references - -- https://aws.amazon.com/guardduty/ -- https://github.com/cloudposse/terraform-aws-guardduty - -
- -## 1.211.0 (2023-05-30T16:30:47Z) - -
- Upstream `aws-inspector` @milldr (#700) - -### what - -Upstream `aws-inspector` from past engagement - -### why - -- This component was never upstreamed and now were want to use it again -- AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate - the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically - assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security - vulnerabilities and deviations from security best practices. Here are some key features and functionalities of AWS - Inspector: - - - Security Assessments: AWS Inspector performs security assessments by analyzing the behavior of your resources and - identifying potential security vulnerabilities. It examines the network configuration, operating system settings, - and installed software to detect common security issues. - - - Vulnerability Detection: AWS Inspector uses a predefined set of rules to identify common vulnerabilities, - misconfigurations, and security exposures. It leverages industry-standard security best practices and continuously - updates its knowledge base to stay current with emerging threats. - - - Agent-Based Architecture: AWS Inspector utilizes an agent-based approach, where you install an Inspector agent on - your EC2 instances. The agent collects data about the system and its configuration, securely sends it to AWS - Inspector, and allows for more accurate and detailed assessments. - - - Security Findings: After performing an assessment, AWS Inspector generates detailed findings that highlight security - vulnerabilities, including their severity level, impact, and remediation steps. These findings can help you - prioritize and address security issues within your AWS environment. - - - Integration with AWS Services: AWS Inspector seamlessly integrates with other AWS services, such as AWS - CloudFormation, AWS Systems Manager, and AWS Security Hub. This allows you to automate security assessments, manage - findings, and centralize security information across your AWS infrastructure. - -### references - -DEV-942 - -
- -## 1.210.1 (2023-05-27T18:52:11Z) - -### 🚀 Enhancements - -
- Fix tags @aknysh (#701) - -### what - -- Fix tags - -### why - -- Typo - -
- -### 🐛 Bug Fixes - -
- Fix tags @aknysh (#701) - -### what - -- Fix tags - -### why - -- Typo - -
- -## 1.210.0 (2023-05-25T22:06:24Z) - -
- EKS FAQ for Addons @milldr (#699) - -### what - -Added docs for EKS Cluster Addons - -### why - -FAQ, requested for documentation - -### references - -DEV-846 - -
- -## 1.209.0 (2023-05-25T19:05:53Z) - -
- Update ALB controller IAM policy @Nuru (#696) - -### what - -- Update `eks/alb-controller` controller IAM policy - -### why - -- Email from AWS: - > On June 1, 2023, we will be adding an additional layer of security to ELB ‘Create*' API calls where API callers must - > have explicit access to add tags in their Identity and Access Management (IAM) policy. Currently, access to attach - > tags was implicitly granted with access to 'Create*' APIs. - -### references - -- [Updated IAM policy](https://github.com/kubernetes-sigs/aws-load-balancer-controller/pull/3068) - -
- -## 1.208.0 (2023-05-24T11:12:15Z) - -
- Managed rules for AWS Config @zdmytriv (#690) - -### what - -- Added option to specify Managed Rules for AWS Config in addition to Conformance Packs - -### why - -- Managed rules will allows to add and tune AWS predefined rules in addition to Conformance Packs - -### references - -- [About AWS Config Manager Rules](https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config_use-managed-rules.html) -- [List of AWS Config Managed Rules](https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html) - -
- -## 1.207.0 (2023-05-22T18:40:06Z) - -
- Corrections to `dms` components @milldr (#658) - -### what - -- Corrections to `dms` components - -### why - -- outputs were incorrect -- set pass and username with ssm - -### references - -- n/a - -
- -## 1.206.0 (2023-05-20T19:41:35Z) - -
- Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL @zdmytriv (#688) - -### what - -- Upgraded S3 Bucket module version - -### why - -- Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL - -### references - -- https://github.com/cloudposse/terraform-aws-s3-bucket/pull/178 - -
- -## 1.205.0 (2023-05-19T23:55:14Z) - -
- feat: add lambda monitors to datadog-monitor @dudymas (#686) - -### what - -- add lambda error monitor -- add datadog lambda log forwarder config monitor - -### why - -- Observability - -
- -## 1.204.1 (2023-05-19T19:54:05Z) - -### 🚀 Enhancements - -
- Update `module "datadog_configuration"` modules @aknysh (#684) - -### what - -- Update `module "datadog_configuration"` modules - -### why - -- The module does not accept the `region` variable -- The module must be always enabled to be able to read the Datadog API keys even if the component is disabled - -
- -## 1.204.0 (2023-05-18T20:31:49Z) - -
- `datadog-agent` bugfixes @Benbentwo (#681) - -### what - -- update datadog agent to latest -- remove variable in datadog configuration - -
- -## 1.203.0 (2023-05-18T19:44:08Z) - -
- Update `vpc` and `eks/cluster` components @aknysh (#677) - -### what - -- Update `vpc` and `eks/cluster` components - -### why - -- Use latest module versions - -- Take into account `var.availability_zones` for the EKS cluster itself. Only the `node-group` module was using - `var.availability_zones` to use the subnets from the provided AZs. The EKS cluster (control plane) was using all the - subnets provisioned in a VPC. This caused issues because EKS is not available in all AZs in a region, e.g. it's not - available in `us-east-1e` b/c of a limited capacity, and when using all AZs from `us-east-1`, the deployment fails - -- The latest version of the `vpc` component (which was updated in this PR as well) has the outputs to get a map of AZs - to the subnet IDs in each AZ - -``` - # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` - # `az_public_subnets_map` is a map of AZ names to list of public subnet IDs in the AZs - public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k)]) - - # Get only the private subnets that correspond to the AZs provided in `var.availability_zones` - # `az_private_subnets_map` is a map of AZ names to list of private subnet IDs in the AZs - private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k)]) -``` - -
- -## 1.202.0 (2023-05-18T16:15:12Z) - -
- feat: adds ability to list principals of Lambdas allowed to access ECR @gberenice (#680) - -### what - -- This change allows listing IDs of the accounts allowed to consume ECR. - -### why - -- This is supported by [terraform-aws-ecr](https://github.com/cloudposse/terraform-aws-ecr/tree/main), but not the - component. - -### references - -- N/A - -
- -## 1.201.0 (2023-05-18T15:08:54Z) - -
- Introducing AWS Config component @zdmytriv (#675) - -### what - -- Added AWS Config and related `config-bucket` components - -### why - -- Added AWS Config and related `config-bucket` components - -### references - -
- -## 1.200.1 (2023-05-18T14:52:10Z) - -### 🚀 Enhancements - -
- Fix `datadog` components @aknysh (#679) - -### what - -- Fix all `datadog` components - -### why - -- Variable `region` is not supported by the `datadog-configuration/modules/datadog_keys` submodule - -
- -## 1.200.0 (2023-05-17T09:19:40Z) - -- No changes - -## 1.199.0 (2023-05-16T15:01:56Z) - -
- `eks/alb-controller-ingress-group`: Corrected Tags to pull LB Data Resource @milldr (#676) - -### what - -- corrected tag reference for pull lb data resource - -### why - -- the tags that are used to pull the ALB that's created should be filtering using the same group_name that is given when - the LB is created - -### references - -- n/a - -
- -## 1.198.3 (2023-05-15T20:01:18Z) - -### 🐛 Bug Fixes - -
- Correct `cloudtrail` Account-Map Reference @milldr (#673) - -### what - -- Correctly pull Audit account from `account-map` for `cloudtrail` -- Remove `SessionName` from EKS RBAC user name wrongly added in #668 - -### why - -- account-map remote state was missing from the `cloudtrail` component -- Account names should be pulled from account-map, not using a variable -- Session Name automatically logged in `user.extra.sessionName.0` starting at Kubernetes 1.20, plus addition had a typo - and was only on Teams, not Team Roles - -### references - -- Resolves change requests https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193297727 and - https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193298107 -- Closes #672 -- [Internal Slack thread](https://cloudposse.slack.com/archives/CA4TC65HS/p1684122388801769) - -
- -## 1.198.2 (2023-05-15T19:47:39Z) - -### 🚀 Enhancements - -
- bump config yaml dependency on account component as it still depends on hashicorp template provider @lantier (#671) - -### what - -- Bump [cloudposse/config/yaml](https://github.com/cloudposse/terraform-yaml-config) module dependency from version - 1.0.1 to 1.0.2 - -### why - -- 1.0.1 still uses hashicorp/template provider, which has no M1 binary equivalent, 1.0.2 already uses the cloudposse - version which has the binary - -### references - -- (https://github.com/cloudposse/terraform-yaml-config/releases/tag/1.0.2) - -
- -## 1.198.1 (2023-05-15T18:55:09Z) - -### 🐛 Bug Fixes - -
- Fixed `route53-resolver-dns-firewall` for the case when logging is disabled @zdmytriv (#669) - -### what - -- Fixed `route53-resolver-dns-firewall` for the case when logging is disabled - -### why - -- Component still required bucket when logging disabled - -### references - -
- -## 1.198.0 (2023-05-15T17:37:47Z) - -
- Add `aws-shield` component @aknysh (#670) - -### what - -- Add `aws-shield` component - -### why - -- The component is responsible for enabling AWS Shield Advanced Protection for the following resources: - - - Application Load Balancers (ALBs) - - CloudFront Distributions - - Elastic IPs - - Route53 Hosted Zones - -This component also requires that the account where the component is being provisioned to has been -[subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/enable-ddos-prem.html). - -
- -## 1.197.2 (2023-05-15T15:25:39Z) - -### 🚀 Enhancements - -
- EKS terraform module variable type fix @PiotrPalkaSpotOn (#674) - -### what - -- use `bool` rather than `string` type for a variable that's designed to hold `true`/`false` value - -### why - -- using `string` makes the - [if .Values.pvc_enabled](https://github.com/SpotOnInc/cloudposse-actions-runner-controller-tf-module-bugfix/blob/f224c7a4ee8b2ab4baf6929710d6668bd8fc5e8c/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml#L1) - condition always true and creates persistent volumes even if they're not intended to use - -
- -## 1.197.1 (2023-05-11T20:39:03Z) - -### 🐛 Bug Fixes - -
- Remove (broken) root access to EKS clusters @Nuru (#668) - -### what - -- Remove (broken) root access to EKS clusters -- Include session name in audit trail of users accessing EKS - -### why - -- Test code granting access to all `root` users and roles was accidentally left in #645 and breaks when Tenants are part - of account names -- There is no reason to allow `root` users to access EKS clusters, so even when this code worked it was wrong -- Audit trail can keep track of who is performing actions - -### references - -- https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-iam-roles-when-multiple-users-need-identical-access-to-the-cluster - -
- -## 1.197.0 (2023-05-11T17:59:40Z) - -
- `rds` Component readme update @Benbentwo (#667) - -### what - -- Updating default example from mssql to postgres - -
- -## 1.196.0 (2023-05-11T17:56:41Z) - -
- Update `vpc-flow-logs` @milldr (#649) - -### what - -- Modernized `vpc-flow-logs` with latest conventions - -### why - -- Old version of the component was significantly out of date -- #498 - -### references - -- DEV-880 - -
- -## 1.195.0 (2023-05-11T07:27:29Z) - -
- Add `iam-policy` to `ecs-service` @milldr (#663) - -### what - -Add an option to attach the `iam-policy` resource to `ecs-service` - -### why - -This policy is already created, but is missing its attachment. We should attach this to the resource when enabled - -### references - -https://cloudposse.slack.com/archives/CA4TC65HS/p1683729972134479 - -
- -## 1.194.0 (2023-05-10T18:36:37Z) - -
- upstream `acm` and `datadog-integration` @Benbentwo (#666) - -### what - -- ACM allows disabling `*.my.domain` -- Datadog-Integration supports allow-list'ing regions - -
- -## 1.193.0 (2023-05-09T16:00:08Z) - -
- Add `route53-resolver-dns-firewall` and `network-firewall` components @aknysh (#651) - -### what - -- Add `route53-resolver-dns-firewall` component -- Add `network-firewall` component - -### why - -- The `route53-resolver-dns-firewall` component is responsible for provisioning - [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) - resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging - configuration - -- The `network-firewall` component is responsible for provisioning - [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, including Network Firewall, firewall policy, - rule groups, and logging configuration - -
- -## 1.192.0 (2023-05-09T15:40:43Z) - -
- [ecs-service] Added IAM policies for ecspresso deployments @goruha (#659) - -### what - -- [ecs-service] Added IAM policies for [Ecspresso](https://github.com/kayac/ecspresso) deployments - -
- -## 1.191.0 (2023-05-05T22:16:44Z) - -
- `elasticsearch` Corrections @milldr (#662) - -### what - -- Modernize Elasticsearch component - -### why - -- `elasticsearch` was not deployable as is. Added up-to-date config - -### references - -- n/a - -
- -## 1.190.0 (2023-05-05T18:46:26Z) - -
- fix: remove stray component.yaml in lambda @dudymas (#661) - -### what - -- Remove the `component.yaml` in the lambda component - -### why - -- Vendoring would potentially cause conflicts - -
- -## 1.189.0 (2023-05-05T18:22:04Z) - -
- fix: eks/efs-controller iam policy updates @dudymas (#660) - -### what - -- Update the iam policy for eks/efs-controller - -### why - -- Older permissions will not work with new versions of the controller - -### references - -- [official iam policy sample](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/iam-policy-example.json) - -
- -## 1.188.0 (2023-05-05T17:05:23Z) - -
- Move `eks/efs` to `efs` @milldr (#653) - -### what - -- Moved `eks/efs` to `efs` - -### why - -- `efs` shouldn't be a submodule of `eks`. You can deploy EFS without EKS - -### references - -- n/a - -
- -## 1.187.0 (2023-05-04T23:04:26Z) - -
- ARC enhancement, aws-config bugfix, DNS documentation @Nuru (#655) - -### what - -- Fix bug in `aws-config` -- Enhance documentation to explain relationship of `dns-primary` and `dns-delegated` components and `dns` account -- [`eks/actions-runner-controller`] Add support for annotations and improve support for ephemeral storage - -### why - -- Bugfix -- Customer query, supersedes and closes #652 -- Better support for longer lived jobs - -### references - -- https://github.com/actions/actions-runner-controller/issues/2562 - -
- -## 1.186.0 (2023-05-04T18:15:31Z) - -
- Update `RDS` @Benbentwo (#657) - -### what - -- Update RDS Modules -- Allow disabling Monitoring Role - -### why - -- Monitoring not always needed -- Context.tf Updates in modules - -
- -## 1.185.0 (2023-04-26T21:30:24Z) - -
- Add `amplify` component @aknysh (#650) - -### what - -- Add `amplify` component - -### why - -- Terraform component to provision AWS Amplify apps, backend environments, branches, domain associations, and webhooks - -### references - -- https://aws.amazon.com/amplify - -
- -## 1.184.0 (2023-04-25T14:29:29Z) - -
- Upstream: `eks/ebs-controller` @milldr (#640) - -### what - -- Added component for `eks/ebs-controller` - -### why - -- Upstreaming this component for general use - -### references - -- n/a - -
- -## 1.183.0 (2023-04-24T23:21:17Z) - -
- GitHub OIDC FAQ @milldr (#648) - -### what - -Added common question for GHA - -### why - -This is asked frequently - -### references - -https://cloudposse.slack.com/archives/C04N39YPVAS/p1682355553255269 - -
- -## 1.182.1 (2023-04-24T19:37:31Z) - -### 🚀 Enhancements - -
- [aws-config] Update usage info, add "help" and "teams" commands @Nuru (#647) - -### what - -Update `aws-config` command: - -- Add `teams` command and suggest "aws-config-teams" file name instead of "aws-config-saml" because we want to use - "aws-config-teams" for both SAML and SSO logins with Leapp handling the difference. -- Add `help` command -- Add more extensive help -- Do not rely on script generated by `account-map` for command `main()` function - -### why - -- Reflect latest design pattern -- Improved user experience - -
- -## 1.182.0 (2023-04-21T17:20:14Z) - -
- Athena CloudTrail Queries @milldr (#638) - -### what - -- added cloudtrail integration to athena -- conditionally allow audit account to decrypt kms key used for cloudtrail - -### why - -- allow queries against cloudtrail logs from a centralized account (audit) - -### references - -n/a - -
- -## 1.181.0 (2023-04-20T22:00:24Z) - -
- Format Identity Team Access Permission Set Name @milldr (#646) - -### what - -- format permission set roles with hyphens - -### why - -- pretty Permission Set naming. We want `devops-super` to format to `IdentityDevopsSuperTeamAccess` - -### references - -https://github.com/cloudposse/refarch-scaffold/pull/127 - -
- -## 1.180.0 (2023-04-20T21:12:28Z) - -
- Fix `s3-bucket` `var.bucket_name` @milldr (#637) - -### what - -changed default value for bucket name to empty string not null - -### why - -default bucket name should be empty string not null. Module checks against name length - -### references - -n/a - -
- -## 1.179.0 (2023-04-20T20:26:20Z) - -
- ecs-service: fix lint issues @kevcube (#636) - -
- -## 1.178.0 (2023-04-20T20:23:10Z) - -
- fix:aws-team-roles have stray locals @dudymas (#642) - -### what - -- remove locals from modules/aws-team-roles - -### why - -- breaks component when it tries to configure locals (the remote state for account_map isn't around) - -
- -## 1.177.0 (2023-04-20T05:13:53Z) - -
- Convert eks/cluster to aws-teams and aws-sso @Nuru (#645) - -### what - -- Convert `eks/cluster` to `aws-teams` -- Add `aws-sso` support to `eks/cluster` -- Undo automatic allowance of `identity` `aws-sso` permission sets into account roles added in #567 - -### why - -- Keep in sync with other modules -- #567 is a silent privilege escalation and not needed to accomplish desired goals - -
- -## 1.176.1 (2023-04-19T14:20:27Z) - -### 🚀 Enhancements - -
- fix: Use `vpc` without tenant @MaxymVlasov (#644) - -### why - -```bash -│ Error: Error in function call -│ -│ on remote-state.tf line 10, in module "vpc_flow_logs_bucket": -│ 10: tenant = coalesce(var.vpc_flow_logs_bucket_tenant_name, module.this.tenant) -│ ├──────────────── -│ │ while calling coalesce(vals...) -│ │ module.this.tenant is "" -│ │ var.vpc_flow_logs_bucket_tenant_name is null -│ -│ Call to function "coalesce" failed: no non-null, non-empty-string -│ arguments. -``` - -
- -## 1.176.0 (2023-04-18T18:46:38Z) - -
- feat: cloudtrail-bucket can have acl configured @dudymas (#643) - -### what - -- add `acl` var to `cloudtrail-bucket` component - -### why - -- Creating new cloudtrail buckets will fail if the acl isn't set to private - -### references - -- This is part of - [a security update from AWS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-faq.html) - -
- -## 1.175.0 (2023-04-11T12:11:46Z) - -
- [argocd-repo] Added ArgoCD git commit notifications @goruha (#633) - -### what - -- [argocd-repo] Added ArgoCD git commit notifications - -### why - -- ArgoCD sync deployment - -
- -## 1.174.0 (2023-04-11T08:53:06Z) - -
- [argocd] Added github commit status notifications @goruha (#631) - -### what - -- [argocd] Added github commit status notifications - -### why - -- ArgoCD sync deployment fix concurrent issue - -
- -## 1.173.0 (2023-04-06T19:21:23Z) - -
- Missing Version Pins for Bats @milldr (#629) - -### what - -added missing provider version pins - -### why - -missing provider versions, required for bats - -### references - -#626 #628, #627 - -
- -## 1.172.0 (2023-04-06T18:32:04Z) - -
- update datadog_lambda_forwarder ref for darwin_arm64 @kevcube (#626) - -### what - -- update datadog-lambda-forwarder module for darwin_arm64 - -### why - -- run on Darwin_arm64 hardware - -
- -## 1.171.0 (2023-04-06T18:11:40Z) - -
- Version Pinning Requirements @milldr (#628) - -### what - -- missing bats requirements resolved - -### why - -- PR #627 missed a few bats requirements in submodules - -### references - -- #627 -- #626 - -
- -## 1.170.0 (2023-04-06T17:38:24Z) - -
- Bats Version Pinning @milldr (#627) - -### what - -- upgraded pattern for version pinning - -### why - -- bats would fail for all of these components unless these versions are pinned as such - -### references - -- https://github.com/cloudposse/terraform-aws-components/pull/626 - -
- -## 1.169.0 (2023-04-05T20:28:39Z) - -
- [eks/actions-runner-controller]: support Runner Group, webhook queue size @Nuru (#621) - -### what - -- `eks/actions-runner-controller` - - Support - [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups) - - Enable configuration of the webhook queue size limit - - Change runner controller Docker image designation -- Add documentation on Runner Groups and Autoscaler configuration - -### why - -- Enable separate access control to self-hosted runners -- For users that launch a large number of jobs in a short period of time, allow bigger queues to avoid losing jobs -- Maintainers recommend new tag format. `ghcr.io` has better rate limits than `docker.io`. - -### references - -- https://github.com/actions/actions-runner-controller/issues/2056 - -
- -## 1.168.0 (2023-04-04T21:48:58Z) - -
- s3-bucket: use cloudposse template provider for arm64 @kevcube (#618) - -### what - -- use cloud posse's template provider - -### why - -- arm64 -- also this provider was not pinned in versions.tf so that had to be fixed somehow - -### references - -- closes #617 - -
- -## 1.167.0 (2023-04-04T18:14:45Z) - -
- chore: aws-sso modules updated to 1.0.0 @dudymas (#623) - -### what - -- upgrade aws-sso modules: permission_sets, sso_account_assignments, and sso_account_assignments_root - -### why - -- upstream updates - -
- -## 1.166.0 (2023-04-03T13:39:53Z) - -
- Add `datadog-synthetics` component @aknysh (#619) - -### what - -- Add `datadog-synthetics` component - -### why - -- This component is responsible for provisioning Datadog synthetic tests - -- Supports Datadog synthetics private locations - - - https://docs.datadoghq.com/getting_started/synthetics/private_location - - https://docs.datadoghq.com/synthetics/private_locations - -- Synthetic tests allow you to observe how your systems and applications are performing using simulated requests and - actions from the AWS managed locations around the globe and to monitor internal endpoints from private locations - -
- -## 1.165.0 (2023-03-31T22:11:26Z) - -
- Update `eks/cluster` README @milldr (#616) - -### what - -- Updated the README with EKS cluster - -### why - -The example stack is outdated. Add notes for Github OIDC and karpenter - -### references - -https://cloudposse.atlassian.net/browse/DEV-835 - -
- -## 1.164.1 (2023-03-30T20:03:15Z) - -### 🚀 Enhancements - -
- spacelift: Update README.md example login policy @johncblandii (#597) - -### what - -- Added support for allowing spaces read access to all members -- Added a reference for allowing spaces write access to the "Developers" group - -### why - -- Spacelift moved to Spaces Access Control - -### references - -- https://docs.spacelift.io/concepts/spaces/access-control - -
- -## 1.164.0 (2023-03-30T16:25:28Z) - -
- Update several component Readmes @Benbentwo (#611) - -### what - -- Update Readmes of many components from Refarch Docs - -
- -## 1.163.0 (2023-03-29T19:52:46Z) - -
- add providers to `mixins` folder @Benbentwo (#613) - -### what - -- Copies some common providers to the mixins folder - -### why - -- Have a central place where our common providers are held. - -
- -## 1.162.0 (2023-03-29T19:30:15Z) - -
- Added ArgoCD GitHub notification subscription @goruha (#615) - -### what - -- Added ArgoCD GitHub notification subscription - -### why - -- To use synchronous deployment pattern - -
- -## 1.161.1 (2023-03-29T17:20:27Z) - -### 🚀 Enhancements - -
- waf component, update dependency versions for aws provider and waf terraform module @arcaven (#612) - -### what - -- updates to waf module: - - aws provider from ~> 4.0 to => 4.0 - - module cloudposse/waf/aws from 0.0.4 to 0.2.0 - - different recommended catalog entry - -### why - -- @aknysh suggested some updates before we start using waf module - -
- -## 1.161.0 (2023-03-28T19:51:27Z) - -
- Quick fixes to EKS/ARC arm64 Support @Nuru (#610) - -### what - -- While supporting EKS/ARC `arm64`, continue to deploy `amd64` by default -- Make `tolerations.value` optional - -### why - -- Majority of echosystem support is currently `amd64` -- `tolerations.value` is option in Kubernetes spec - -### references - -- Corrects issue which escaped review in #609 - -
- -## 1.160.0 (2023-03-28T18:26:20Z) - -
- Upstream EKS/ARC amd64 Support @milldr (#609) - -### what - -Added arm64 support for eks/arc - -### why - -when supporting both amd64 and arm64, we need to select the correct architecture - -### references - -https://github.com/cloudposse/infra-live/pull/265 - -
- -## 1.159.0 (2023-03-27T16:19:29Z) - -
- Update account-map to output account information for aws-config script @Nuru (#608) - -### what - -- Update `account-map` to output account information for `aws-config` script -- Output AWS profile name for root of credential chain - -### why - -- Enable `aws-config` to output account IDs and to generate configuration for "AWS Extend Switch Roles" browser plugin -- Support multiple namespaces in a single infrastructure repo - -
- -
- Update CODEOWNERS to remove contributors @Nuru (#607) - -### what - -- Update CODEOWNERS to remove contributors - -### why - -- Require approval from engineering team (or in some cases admins) for all changes, to keep better quality control on - this repo - -
- -## 1.158.0 (2023-03-27T03:41:43Z) - -
- Upstream latest datadog-agent and datadog-configuration updates @nitrocode (#598) - -### what - -- Upstream latest datadog-agent and datadog-configuration updates - -### why - -- datadog irsa role -- removing unused input vars -- default to `public.ecr.aws` images -- ignore deprecated `default.auto.tfvars` -- move `datadog-agent` to `eks/` subfolder for consistency with other helm charts - -### references - -N/A - -
- -## 1.157.0 (2023-03-24T19:12:17Z) - -
- Remove `root_account_tenant_name` @milldr (#605) - -### what - -- bumped ecr -- remove unnecessary variable - -### why - -- ECR version update -- We shouldn't need to set `root_account_tenant_name` in providers -- Some Terraform docs are out-of-date - -### references - -- n/a - -
- -## 1.156.0 (2023-03-23T21:03:46Z) - -
- exposing variables from 2.0.0 of `VPC` module @Benbentwo (#604) - -### what - -- Adding vars for vpc module and sending them directly to module - -### references - -- https://github.com/cloudposse/terraform-aws-vpc/blob/master/variables.tf#L10-L44 - -
- -## 1.155.0 (2023-03-23T02:01:29Z) - -
- Add Privileged Option for GH OIDC @milldr (#603) - -### what - -- allow gh oidc role to use privileged as option for reading tf backend - -### why - -- If deploying GH OIDC with a component that needs to be applied with SuperAdmin (aws-teams) we need to set privileged - here - -### references - -- https://cloudposse.slack.com/archives/C04N39YPVAS/p1679409325357119 - -
- -## 1.154.0 (2023-03-22T17:40:35Z) - -
- update `opsgenie-team` to be delete-able via `enabled: false` @Benbentwo (#589) - -### what - -- Uses Datdaog Configuration as it's source of datadog variables -- Now supports `enabled: false` on a team to destroy it. - -
- -## 1.153.0 (2023-03-21T19:22:03Z) - -
- Upstream AWS Teams components @milldr (#600) - -### what - -- added eks view only policy - -### why - -- Provided updates from recent contracts - -### references - -- https://github.com/cloudposse/refarch-scaffold/pull/99 - -
- -## 1.152.0 (2023-03-21T15:42:51Z) - -
- upstream 'datadog-lambda-forwarder' @gberenice (#601) - -### what - -- Upgrade 'datadog-lambda-forwarder' component to v1.3.0 - -### why - -- Be able [to forward Cloudwatch Events](https://github.com/cloudposse/terraform-aws-datadog-lambda-forwarder/pull/48) - via components. - -### references - -- N/A - -
- -## 1.151.0 (2023-03-15T15:56:20Z) - -
- Upstream `eks/external-secrets-operator` @milldr (#595) - -### what - -- Adding new module for `eks/external-secrets-operator` - -### why - -- Other customers want to use this module now, and it needs to be upstreamed - -### references - -- n/a - -
- -## 1.150.0 (2023-03-14T20:20:41Z) - -
- chore(spacelift): update with dependency resource @dudymas (#594) - -### what - -- update spacelift component to 0.55.0 - -### why - -- support feature flag for spacelift_stack_dependency resource - -### references - -- [spacelift module 0.55.0](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/0.55.0) - -
- -## 1.149.0 (2023-03-13T15:25:25Z) - -
- Fix SSO SAML provider fixes @goruha (#592) - -### what - -- Fix SSO SAML provider fixes - -
- -## 1.148.0 (2023-03-10T18:07:36Z) - -
- ArgoCD SSO improvements @goruha (#590) - -### what - -- ArgoCD SSO improvements - -
- -## 1.147.0 (2023-03-10T17:52:18Z) - -
- Upstream: `eks/echo-server` @milldr (#591) - -### what - -- Adding the `ingress.alb.group_name` annotation to Echo Server - -### why - -- Required to set the ALB specifically, rather than using the default - -### references - -- n/a - -
- -## 1.146.0 (2023-03-08T23:13:13Z) - -
- Improve platform and external-dns for release engineering @goruha (#588) - -### what - -- `eks/external-dns` support `dns-primary` -- `eks/platform` support json query remote components outputs - -### why - -- `vanity domain` pattern support by `eks/external-dns` -- Improve flexibility of `eks/platform` - -
- -## 1.145.0 (2023-03-07T00:28:25Z) - -
- `eks/actions-runner-controller`: use coalesce @Benbentwo (#586) - -### what - -- use coalesce instead of try, as we need a value passed in here - -
- -## 1.144.0 (2023-03-05T20:24:09Z) - -
- Upgrade Remote State to `1.4.1` @milldr (#585) - -### what - -- Upgrade _all_ remote state modules (`cloudposse/stack-config/yaml//modules/remote-state`) to version `1.4.1` - -### why - -- In order to use go templating with Atmos, we need to use the latest cloudposse/utils version. This version is - specified by `1.4.1` - -### references - -- https://github.com/cloudposse/terraform-yaml-stack-config/releases/tag/1.4.1 - -
- -## 1.143.0 (2023-03-02T18:07:53Z) - -
- bugfix: rds anomalies monitor not sending team information @Benbentwo (#583) - -### what - -- Update monitor to have default CP tags - -
- -## 1.142.0 (2023-03-02T17:49:40Z) - -
- datadog-lambda-forwarder: if s3_buckets not set, module fails @kevcube (#581) - -This module attempts to do length() on the value for s3_buckets. - -We are not using s3_buckets, and it defaults to null, so length() fails. - -
- -## 1.141.0 (2023-03-01T19:10:07Z) - -
- `datadog-monitors`: Team Grouping @Benbentwo (#580) - -### what - -- grouping by team helps ensure the team tag is sent to Opsgenie - -### why - -- ensures most data is fed to a valid team tag instead of `@opsgenie-` - -
- -## 1.140.0 (2023-02-28T18:47:44Z) - -
- `spacelift` add missing `var.region` @johncblandii (#574) - -### what - -- Added the missing `var.region` - -### why - -- The AWS provider requires it and it was not available - -### references - -
- -## 1.139.0 (2023-02-28T18:46:35Z) - -
- datadog monitors improvements @Benbentwo (#579) - -### what - -- Datadog monitor improvements - - Prepends `()` e.g. `(tenant-environment-stage)` - - Fixes some messages that had improper syntax - dd uses `{{ var.name }}` - -### why - -- Datadog monitor improvements - -
- -## 1.138.0 (2023-02-28T18:45:48Z) - -
- update `account` readme.md @Benbentwo (#570) - -### what - -- Updated account readme - -
- -## 1.137.0 (2023-02-27T20:39:34Z) - -
- Update `eks/cluster` @Benbentwo (#578) - -### what - -- Update EKS Cluster Module to re-include addons - -
- -## 1.136.0 (2023-02-27T17:36:47Z) - -
- Set spacelift-worker-pool ami explicitly to x86_64 @arcaven (#577) - -### why - -- autoscaling group for spacelift-worker-pool will fail to launch when new arm64 images return first -- arm64 ami image is being returned first at the moment in us-east-1 - -### what - -- set spacelift-worker-pool ami statically to return only x86_64 results - -### references - -- Spacelift Worker Pool ASG may fail to scale due to ami/instance type mismatch #575 -- Note: this is an alternative to spacelift-worker-pool README update and AMI limits #573 which I read after, but I - think this filter approach will be more easily be refactored into setting this as an attribute in variables.tf in the - near future - -
- -## 1.135.0 (2023-02-27T13:56:48Z) - -
- github-runners add support for runner groups @johncblandii (#569) - -### what - -- Added optional support for separating runners by groups - -NOTE: I don't know if the default of `default` is valid or if it is `Default`. I'll confirm this soon. - -### why - -- Groups are supported by GitHub and allow for Actions to target specific runners by group vs by label - -### references - -- https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups - -
- -## 1.134.0 (2023-02-24T20:59:40Z) - -
- [account-map] Update remote config module version @goruha (#572) - -### what - -- Update remote config module version `1.4.1` - -### why - -- Solve terraform module version conflict - -
- -## 1.133.0 (2023-02-24T17:55:52Z) - -
- Fix ArgoCD minor issues @goruha (#571) - -### what - -- Fix slack notification annotations -- Fix CRD creation order - -### why - -- Fix ArgoCD bootstrap - -
- -## 1.132.0 (2023-02-23T04:33:29Z) - -
- Add spacelift-policy component @nitrocode (#556) - -### what - -- Add spacelift-policy component - -### why - -- De-couple policy creation from admin and child stacks -- Auto attach policies to remove additional terraform management of resources - -### references - -- Depends on PR https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/134 - -
- -## 1.131.0 (2023-02-23T01:13:58Z) - -
- SSO upgrades and Support for Assume Role from Identity Users @johncblandii (#567) - -### what - -- Upgraded `aws-sso` to use `0.7.1` modules -- Updated `account-map/modules/roles-to-principals` to support assume role from SSO users in the identity account -- Adjusted `aws-sso/policy-Identity-role-RoleAccess.tf` to use the identity account name vs the stage so it supports - names like `core-identity` instead of just `identity` - -### why - -- `aws-sso` users could not assume role to plan/apply terraform locally -- using `core-identity` as a name broke the `aws-sso` policy since account `identity` does not exist in - `full_account_map` - -### references - -
- -## 1.130.0 (2023-02-21T18:33:53Z) - -
- Add Redshift component @max-lobur (#563) - -### what - -- Add Redshift - -### why - -- Fulfilling the AWS catalog - -### references - -- https://github.com/cloudposse/terraform-aws-redshift-cluster - -
- -## 1.129.0 (2023-02-21T16:45:43Z) - -
- update dd agent docs @Benbentwo (#565) - -### what - -- Update Datadog Docs to be more clear on catalog entry - -
- -## 1.128.0 (2023-02-18T16:28:11Z) - -
- feat: updates spacelift to support policies outside of the comp folder @Gowiem (#522) - -### what - -- Adds back `policies_by_name_path` variable to spacelift component - -### why - -- Allows specifying spacelift policies outside of the component folder - -### references - -- N/A - -
- -## 1.127.0 (2023-02-16T17:53:31Z) - -
- [sso-saml-provider] Upstream SSO SAML provider component @goruha (#562) - -### what - -- [sso-saml-provider] Upstream SSO SAML provider component - -### why - -- Required for ArgoCD - -
- -## 1.126.0 (2023-02-14T23:01:00Z) - -
- upstream `opsgenie-team` @Benbentwo (#561) - -### what - -- Upstreams latest opsgenie-team component - -
- -## 1.125.0 (2023-02-14T21:45:32Z) - -
- [eks/argocd] Upstream ArgoCD @goruha (#560) - -### what - -- Upstream `eks/argocd` - -
- -## 1.124.0 (2023-02-14T17:34:29Z) - -
- `aws-backup` upstream @Benbentwo (#559) - -### what - -- Update `aws-backup` to latest - -
- -## 1.123.0 (2023-02-13T22:42:56Z) - -
- upstream lambda pt2 @Benbentwo (#558) - -### what - -- Add archive zip -- Change to python (no compile) - -
- -## 1.122.0 (2023-02-13T21:24:02Z) - -
- upstream `lambda` @Benbentwo (#557) - -### what - -- Upstream `lambda` component - -### why - -- Quickly deploy serverless code - -
- -## 1.121.0 (2023-02-13T16:59:16Z) - -
- Upstream `ACM` and `eks/Platform` for release_engineering @Benbentwo (#555) - -### what - -- ACM Component outputs it's acm url -- EKS/Platform will deploy many terraform outputs to SSM - -### why - -- These components are required for CP Release Engineering Setup - -
- -## 1.120.0 (2023-02-08T16:34:25Z) - -
- Upstream datadog logs archive @Benbentwo (#552) - -### what - -- Upstream DD Logs Archive - -
- -## 1.119.0 (2023-02-07T21:32:25Z) - -
- Upstream `dynamodb` @milldr (#512) - -### what - -- Updated the `dynamodb` component - -### why - -- maintaining up-to-date upstream component - -### references - -- N/A - -
- -## 1.118.0 (2023-02-07T20:15:17Z) - -
- fix dd-forwarder: datadog service config depends on lambda arn config @raybotha (#531) - -
- -## 1.117.0 (2023-02-07T19:44:32Z) - -
- Upstream `spa-s3-cloudfront` @milldr (#500) - -### what - -- Added missing component from upstream `spa-s3-cloudfront` - -### why - -- We use this component to provision Cloudfront and related resources - -### references - -- N/A - -
- -## 1.116.0 (2023-02-07T00:52:27Z) - -
- Upstream `aurora-mysql` @milldr (#517) - -### what - -- Upstreaming both `aurora-mysql` and `aurora-mysql-resources` - -### why - -- Added option for allowing ingress by account name, rather than requiring CIDR blocks copy and pasted -- Replaced the deprecated provider for MySQL -- Resolved issues with Terraform perma-drift for the resources component with granting "ALL" - -### references - -- Old provider, archived: https://github.com/hashicorp/terraform-provider-mysql -- New provider: https://github.com/petoju/terraform-provider-mysql - -
- -## 1.115.0 (2023-02-07T00:49:59Z) - -
- Upstream `aurora-postgres` @milldr (#518) - -### what - -- Upstreaming `aurora-postgres` and `aurora-postgres-resources` - -### why - -- TLC for these components -- Added options for adding ingress by account -- Cleaned up the submodule for the resources component -- Support creating schemas -- Support conditionally pulling passwords from SSM, similar to `aurora-mysql` - -
- -## 1.114.0 (2023-02-06T17:09:31Z) - -
- `datadog-private-locations` update helm provider @Benbentwo (#549) - -### what - -- Updates Helm Provider to the latest - -### why - -- New API Version - -
- -## 1.113.0 (2023-02-06T02:26:22Z) - -
- Remove extra var from stack example @johncblandii (#550) - -### what - -- Stack example has an old variable defined - -### why - -- `The root module does not declare a variable named "eks_tags_enabled" but a value was found in file "uw2-automation-vpc.terraform.tfvars.json".` - -### references - -
- -## 1.112.1 (2023-02-03T20:00:09Z) - -### 🚀 Enhancements - -
- Fixed non-html tags that fails rendering on docusaurus @zdmytriv (#546) - -### what - -- Fixed non-html tags - -### why - -- Rendering has been failing on docusaurus mdx/jsx engine - -
- -## 1.112.0 (2023-02-03T19:02:57Z) - -
- `datadog-agent` allow values var merged @Benbentwo (#548) - -### what - -- Allows values to be passed in and merged to values file - -### why - -- Need to be able to easily override values files - -
- -## 1.111.0 (2023-01-31T23:02:57Z) - -
- Update echo and alb-controller-ingress-group @Benbentwo (#547) - -### what - -- Allows target group to be targeted by echo server - -
- -## 1.110.0 (2023-01-26T00:25:13Z) - -
- Chore/acme/bootcamp core tenant @dudymas (#543) - -### what - -- upgrade the vpn module in the ec2-client-vpn component -- and protect outputs on ec2-client-vpn - -### why - -- saml docs were broken in refarch-scaffold. module was trying to alter the cert provider - -
- -## 1.109.0 (2023-01-24T20:01:56Z) - -
- Chore/acme/bootcamp spacelift @dudymas (#545) - -### what - -- adjust the type of context_filters in spacelift - -### why - -- was getting errors trying to apply spacelift component - -
- -## 1.108.0 (2023-01-20T22:36:54Z) - -
- EC2 Client VPN Version Bump @Benbentwo (#544) - -### what - -- Bump Version of EC2 Client VPN - -### why - -- Bugfixes issue with TLS provider - -### references - -- https://github.com/cloudposse/terraform-aws-ec2-client-vpn/pull/58 -- https://github.com/cloudposse/terraform-aws-ssm-tls-self-signed-cert/pull/20 - -
- -## 1.107.0 (2023-01-19T17:34:33Z) - -
- Update pod security context schema in cert-manager @max-lobur (#538) - -### what - -Pod security context `enabled` field has been deprecated. Now you just specify the options and that's it. Update the -options per recent schema. See references - -Tested on k8s 1.24 - -### why - -- Otherwise it does not pass Deployment validation on newer clusters. - -### references - -https://github.com/cert-manager/cert-manager/commit/c17b11fa01455eb1b83dce0c2c06be555e4d53eb - -
- -## 1.106.0 (2023-01-18T15:36:52Z) - -
- Fix github actions runner controller default variables @max-lobur (#542) - -### what - -Default value for string is null, not false - -### why - -- Otherwise this does not pass schema when you deploy it without storage requests - -
- -## 1.105.0 (2023-01-18T15:24:11Z) - -
- Update k8s metrics-server to latest @max-lobur (#537) - -### what - -Upgrade metrics-server Tested on k8s 1.24 via `kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes"` - -### why - -- The previous one was so old that bitnami has even removed the chart. - -
- -## 1.104.0 (2023-01-18T14:52:58Z) - -
- Pin kubernetes provider in metrics-server @max-lobur (#541) - -### what - -- Pin the k8s provider version -- Update versions - -### why - -- Fix CI - -### references - -- https://github.com/cloudposse/terraform-aws-components/pull/537 - -
- -## 1.103.0 (2023-01-17T21:09:56Z) - -
- fix(dns-primary/acm): include zone_name arg @dudymas (#540) - -### what - -- in dns-primary, revert version of acm module 0.17.0 -> 0.16.2 (17 is a preview) - -### why - -- primary zones must be specified now that names are trimmed before the dot (.) - -
- -## 1.102.0 (2023-01-17T16:09:59Z) - -
- Fix typo in karpenter-provisioner @max-lobur (#539) - -### what - -I formatted it last moment and did not notice that actually changed the object. Fixing that and reformatting all of it -so it's more obvious for future maintainers. - -### why - -- Fixing bug - -### references - -https://github.com/cloudposse/terraform-aws-components/pull/536 - -
- -## 1.101.0 (2023-01-17T07:47:30Z) - -
- Support setting consolidation in karpenter-provisioner @max-lobur (#536) - -### what - -This is an alternative way of deprovisioning - proactive one. - -``` -There is another way to configure Karpenter to deprovision nodes called Consolidation. -This mode is preferred for workloads such as microservices and is incompatible with setting -up the ttlSecondsAfterEmpty . When set in consolidation mode Karpenter works to actively -reduce cluster cost by identifying when nodes can be removed as their workloads will run -on other nodes in the cluster and when nodes can be replaced with cheaper variants due -to a change in the workloads -``` - -### why - -- To let users set a more aggressive deprovisioning strategy - -### references - -- https://ec2spotworkshops.com/karpenter/050_karpenter/consolidation.html - -
- -## 1.100.0 (2023-01-17T07:41:58Z) - -
- Sync karpenter chart values with the schema @max-lobur (#535) - -### what - -Based on -https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml#L129-L142 -all these should go under settings.aws - -### why - -Ensure compatibility with the new charts - -### references - -Based on https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml - -
- -## 1.99.0 (2023-01-13T14:59:16Z) - -
- fix(aws-sso): dont hardcode account name for root @dudymas (#534) - -### what - -- remove hardcoding for root account moniker -- change default tenant from `gov` to `core` (now convention) - -### why - -- tenant is not included in the account prefix. In this case, changed to be 'core' -- most accounts do not use `gov` as the root tenant - -
- -## 1.98.0 (2023-01-12T00:12:36Z) - -
- Bump spacelift to latest @nitrocode (#532) - -### what - -- Bump spacelift to latest - -### why - -- Latest - -### references - -N/A - -
- -## 1.97.0 (2023-01-11T01:16:33Z) - -
- Upstream EKS Action Runner Controller @milldr (#528) - -### what - -- Upstreaming the latest additions for the EKS actions runner controller component - -### why - -- We've added additional features for the ARC runners, primarily adding options for ephemeral storage and persistent - storage. Persistent storage can be used to add image caching with EFS -- Allow for setting a `webhook_startup_timeout` value different than `scale_down_delay_seconds`. Defaults to - `scale_down_delay_seconds` - -### references - -- N/A - -
- -## 1.96.0 (2023-01-05T21:19:22Z) - -
- Datadog Upstreams and Account Settings @Benbentwo (#533) - -### what - -- Datadog Upgrades (Bugfixes for Configuration on default datadog URL) -- Account Settings Fixes for emoji support and updated budgets - -### why - -- Upstreams - -
- -## 1.95.0 (2023-01-04T23:44:35Z) - -
- fix(aws-sso): add missing tf update perms @dudymas (#530) - -### what - -- Changes for supporting [Refarch Scaffold](github.com/cloudposse/refarch-scaffold) -- TerraformUpdateAccess permission set added - -### why - -- Allow SSO users to update dynamodb/s3 for terraform backend - -
- -## 1.94.0 (2022-12-21T18:38:15Z) - -
- upstream `spacelift` @Benbentwo (#526) - -### what - -- Updated Spacelift Component to latest -- Updated README with new example - -### why - -- Upstreams - -
- -## 1.93.0 (2022-12-21T18:37:37Z) - -
- upstream `ecs` & `ecs-service` @Benbentwo (#529) - -### what - -- upstream - - `ecs` - - `ecs-service` - -### why - -- `enabled` flag correctly destroys resources -- bugfixes and improvements -- datadog support for ecs services - -
- -## 1.92.0 (2022-12-21T18:36:35Z) - -
- Upstream Datadog @Benbentwo (#525) - -### what - -- Datadog updates -- New `datadog-configuration` component for setting up share functions and making codebase more dry - -
- -## 1.91.0 (2022-11-29T17:17:58Z) - -
- CPLIVE-320: Set VPC to use region-less AZs @nitrocode (#524) - -### what - -- Set VPC to use region-less AZs - -### why - -- Prevent having to set VPC AZs within global region defaults - -### references - -- CPLIVE-320 - -
- -## 1.90.2 (2022-11-20T05:41:14Z) - -### 🚀 Enhancements - -
- Use cloudposse/template for arm support @nitrocode (#510) - -### what - -- Use cloudposse/template for arm support - -### why - -- The new cloudposse/template provider has a darwin arm binary for M1 laptops - -### references - -- https://github.com/cloudposse/terraform-provider-template -- https://registry.terraform.io/providers/cloudposse/template/latest - -
- -## 1.90.1 (2022-10-31T13:27:37Z) - -### 🚀 Enhancements - -
- Allow vpc-peering to peer v2 to v2 @nitrocode (#521) - -### what - -- Allow vpc-peering to peer v2 to v2 - -### why - -- Alternative to transit gateway - -### references - -N/A - -
- -## 1.90.0 (2022-10-31T13:24:38Z) - -
- Upstream iam-role component @nitrocode (#520) - -### what - -- Upstream iam-role component - -### why - -- Create simple IAM roles - -### references - -- https://github.com/cloudposse/terraform-aws-iam-role - -
- -## 1.89.0 (2022-10-28T15:35:38Z) - -
- [eks/actions-runner-controller] Auth via GitHub App, prefer webhook auto-scaling @Nuru (#519) - -### what - -- Support and prefer authentication via GitHub app -- Support and prefer webhook-based autoscaling - -### why - -- GitHub app is much more restricted, plus has higher API rate limits -- Webhook-based autoscaling is proactive without being overly expensive - -
- -## 1.88.0 (2022-10-24T15:40:47Z) - -
- Upstream iam-service-linked-roles @nitrocode (#516) - -### what - -- Upstream iam-service-linked-roles (thanks to @aknysh for writing it) - -### why - -- Centralized component to create IAM service linked roles - -### references - -- N/A - -
- -## 1.87.0 (2022-10-22T19:12:36Z) - -
- Add account-quotas component @Nuru (#515) - -### what - -- Add `account-quotas` component to manage account service quota increase requests - -### why - -- Add service quotas to the infrastructure that can be represented in code - -### notes - -Cloud Posse has a [service quotas module](https://github.com/cloudposse/terraform-aws-service-quotas), but it has -issues, such as not allowing the service to be specified by name, and not having well documented inputs. It also takes a -list input, but Atmos does not merge lists, so a map input is more appropriate. Overall I like this component better, -and if others do, too, I will replace the existing module (only at version 0.1.0) with this code. - -
- -## 1.86.0 (2022-10-19T07:28:11Z) - -
- Update EKS basic components @Nuru (#509) - -### what && why - -Update EKS cluster and basic Kubernetes components for better behavior on initial deployment and on `terraform destroy`. - -- Update minimum Terraform version to 1.1.0 and use `one()` where applicable to manage resources that can be disabled - with `count = 0` and for bug fixes regarding destroy behavior -- Update `terraform-aws-eks-cluster` to v2.5.0 for better destroy behavior -- Update all components' (plus `account-map/modules/`)`remote-state` to v1.2.0 for better destroy behavior -- Update all components' `helm-release` to v0.7.0 and move namespace creation via Kubernetes provider into it to avoid - race conditions regarding creating IAM roles, Namespaces, and deployments, and to delete namespaces when destroyed -- Update `alb-controller` to deploy a default IngressClass for central, obvious configuration of shared default ingress - for services that do not have special needs. -- Add `alb-controller-ingress-class` for the rare case when we want to deploy a non-default IngressClass outside of the - component that will be using it -- Update `echo-server` to use the default IngressClass and not specify any configuration that affects other Ingresses, - and remove dependence on `alb-controller-ingress-group` (which should be deprecated in favor of - `alb-controller-ingress-class` and perhaps a specialized future `alb-controller-ingress`) -- Update `cert-manager` to remove `default.auto.tfvars` (which had a lot of settings) and add dependencies so that - initial deployment succeeds in one `terraform apply` and destroy works in one `terraform destroy` -- Update `external-dns` to remove `default.auto.tfvars` (which had a lot of settings) -- Update `karpenter` to v0.18.0, fix/update IAM policy (README still needs work, but leaving that for another day) -- Update `karpenter-provisioner` to require Terraform 1.3 and make elements of the Provisioner configuration optional. - Support block device mappings (previously broken). Avoid perpetual Terraform plan diff/drift caused by setting fields - to `null`. -- Update `reloader` -- Update `mixins/provider-helm` to better support `terraform destroy` and to default the Kubernetes client - authentication API version to `client.authentication.k8s.io/v1beta1` - -### references - -- https://github.com/cloudposse/terraform-aws-helm-release/pull/34 -- https://github.com/cloudposse/terraform-aws-eks-cluster/pull/169 -- https://github.com/cloudposse/terraform-yaml-stack-config/pull/56 -- https://github.com/hashicorp/terraform/issues/32023 - -
- -## 1.85.0 (2022-10-18T00:05:19Z) - -
- Upstream `github-runners` @milldr (#508) - -### what - -- Minor TLC updates for GitHub Runners ASG component - -### why - -- Maintaining up-to-date upstream - -
- -## 1.84.0 (2022-10-12T22:49:28Z) - -
- Fix feature allowing IAM users to assume team roles @Nuru (#507) - -### what - -- Replace `deny_all_iam_users` input with `iam_users_enabled` -- Fix implementation -- Provide more context for `bats` test failures - -### why - -- Cloud Posse style guide dictates that boolean feature flags have names ending with `_enabled` -- Previous implementation only removed 1 of 2 policy provisions that blocked IAM users from assuming a role, and - therefore IAM users were still not allowed to assume a role. Since the previous implementation did not work, a - breaking change (changing the variable name) does not need major warnings or a major version bump. -- Indication of what was being tested was too far removed from `bats` test failure message to be able to easily identify - what module had failed - -### notes - -Currently, any component provisioned by SuperAdmin needs to have a special provider configuration that requires -SuperAdmin to provision the component. This feature is part of what is needed to enable SuperAdmin (an IAM User) to work -with "normal" provider configurations. - -### references - -- Breaks change introduced in #495, but that didn't work anyway. - -
+* DEV-2556 Investigate release issues with terraform-aws-components +
+ + + +## 1.298.0 (2023-08-28T20:56:25Z) + +
+ Aurora Postgres Engine Options @milldr (#845) + +### what + +- Add scaling configuration variables for both Serverless and Serverless v2 to `aurora-postgres` +- Update `aurora-postgres` README + +### why + +- Support both serverless options +- Add an explanation for how to configure each, and where to find valid engine options + +### references + +- n/a + +
+ +## 1.297.0 (2023-08-28T18:06:11Z) + +
+ AWS provider V5 dependency updates @max-lobur (#729) + +### what + +- Update component dependencies for the AWS provider V5 + +Requested components: + +- cloudtrail-bucket +- config-bucket +- datadog-logs-archive +- eks/argocd +- eks/efs-controller +- eks/metric-server +- spacelift-worker-pool +- eks/external-secrets-operator + +### why + +- Maintenance + +
+ +## 1.296.0 (2023-08-28T16:24:05Z) + +
+ datadog agent update defaults @Benbentwo (#839) + +### what + +- prevent fargate agents +- use sockets instead of ports for APM +- enable other services + +### why + +- Default Datadog APM enabled over k8s + +### references + +
+ +## 1.295.0 (2023-08-26T00:51:10Z) + +
+ TGW FAQ and Spoke Alternate VPC Support @milldr (#840) + +### what + +- Added FAQ to the TGW upgrade guide for replacing attachments +- Added note about destroying TGW components +- Added option to not create TGW propagation and association when connecting an alternate VPC + +### why + +- When connecting an alternate VPC in the same region as the primary VPC, we do not want to create a duplicate TGW + propagation and association + +### references + +- n/a + +
+ +## 1.294.0 (2023-08-26T00:07:42Z) + +
+ Aurora Upstream: Serverless, Tags, Enabled: False @milldr (#841) + +### what + +- Set `module.context` to `module.cluster` across all resources +- Only set parameter for replica if cluster size is > 0 +- `enabled: false` support + +### why + +- Missing tags for SSM parameters for cluster attributes +- Serverless clusters set `cluster_size: 0`, which will break the SSM parameter for replica hostname (since it does not + exist) +- Support enabled false for `aurora-*-resources` components + +### references + +- n/a + +
+ +## 1.293.2 (2023-08-24T15:50:53Z) + +### 🚀 Enhancements + +
+ Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` @aknysh (#837) + +### what + +- Update `root_stack` output in `modules/spacelift/admin-stack/outputs.tf` + +### why + +- Fix the issue described in https://github.com/cloudposse/terraform-aws-components/issues/771 + +### related + +- Closes https://github.com/cloudposse/terraform-aws-components/issues/771 + +
+ +## 1.293.1 (2023-08-24T11:24:46Z) + +### 🐛 Bug Fixes + +
+ [spacelift/worker-pool] Update providers.tf nesting @Nuru (#834) + +### what + +- Update relative path to `account-map` in `spacelift/worker-pool/providers.tf` + +### why + +- Fixes #828 + +
+ +## 1.293.0 (2023-08-23T01:18:53Z) + +
+ Add visibility to default VPC component name @milldr (#833) + +### what + +- Set the default component name for `vpc` in variables, not remote-state + +### why + +- Bring visibility to where the default is set + +### references + +- Follow up on comments on #832 + +
+ +## 1.292.0 (2023-08-22T21:33:18Z) + +
+ Aurora Optional `vpc` Component Names @milldr (#832) + +### what + +- Allow optional VPC component names in the aurora components + +### why + +- Support deploying the clusters for other VPC components than `"vpc"` + +### references + +- n/a + +
+ +## 1.291.1 (2023-08-22T20:25:17Z) + +### 🐛 Bug Fixes + +
+ [aws-sso] Fix root provider, restore `SetSourceIdentity` permission @Nuru (#830) + +### what + +For `aws-sso`: + +- Fix root provider, improperly restored in #740 +- Restore `SetSourceIdentity` permission inadvertently removed in #740 + +### why + +- When deploying to `identity`, `root` provider did not reference `root` account +- Likely unintentional removal due to merge error + +### references + +- #740 +- #738 + +
+ +## 1.291.0 (2023-08-22T17:08:27Z) + +
+ chore: remove defaults from components @dudymas (#831) + +### what + +- remove `defaults.auto.tfvars` from component modules + +### why + +- in favor of drying up configuration using atmos + +### Notes + +- Some defaults may not be captured yet. Regressions might occur. + +
+ +## 1.290.0 (2023-08-21T18:57:43Z) + +
+ Upgrade aws-config and conformance pack modules to 1.1.0 @johncblandii (#829) + +### what + +- Upgrade aws-config and conformance pack modules to 1.1.0 + +### why + +- They're outdated. + +### references + +- #771 + +
+ +## 1.289.2 (2023-08-21T08:53:08Z) + +### 🐛 Bug Fixes + +
+ [eks/alb-controller] Fix naming convention of overridable local variable @Nuru (#826) + +### what + +- [eks/alb-controller] Change name of local variable from `distributed_iam_policy_overridable` to + `overridable_distributed_iam_policy` + +### why + +- Cloud Posse style guide requires `overridable` as prefix, not suffix. + +
+ +## 1.289.1 (2023-08-19T05:20:26Z) + +### 🐛 Bug Fixes + +
+ [eks/alb-controller] Update ALB controller IAM policy @Nuru (#821) + +### what + +- [eks/alb-controller] Update ALB controller IAM policy + +### why + +- Previous policy had error preventing the creation of the ELB service-linked role + +
+ +## 1.289.0 (2023-08-18T20:18:12Z) + +
+ Spacelift Alternate git Providers @milldr (#825) + +### what + +- set alternate git provider blocks to filter under `settings.spacelift` + +### why + +- Debugging GitLab support specifically +- These settings should be defined under `settings.spacelift`, not as a top-level configuration + +### references + +- n/a + +
+ +## 1.288.0 (2023-08-18T15:12:16Z) + +
+ Placeholder for `upgrade-guide.md` @milldr (#823) + +### what + +- Added a placeholder file for `docs/upgrade-guide.md` with a basic explanation of what is to come + +### why + +- With #811 we moved the contents of this upgrade-guide file to the individual component. We plan to continue adding + upgrade guides for individual components, and in addition, create a higher-level upgrade guide here +- However, the build steps for refarch-scaffold expect `docs/upgrade-guide.md` to exist and are failing without it. We + need a placeholder until the `account-map`, etc changes are added to this file + +### references + +- Example of failing release: https://github.com/cloudposse/refarch-scaffold/actions/runs/5885022872 + +
+ +## 1.287.2 (2023-08-18T14:42:49Z) + +### 🚀 Enhancements + +
+ update boolean logic @mcalhoun (#822) + +### what + +- Update the GuardDuty component to enable GuardDuty on the root account + +### why + +The API call to designate organization members now fails with the following if GuardDuty was not already enabled in the +organization management (root) account : + +``` +Error: error designating guardduty administrator account members: [{ +│ AccountId: "111111111111, +│ Result: "Operation failed because your organization master must first enable GuardDuty to be added as a member" +│ }] +``` + +
+ +## 1.287.1 (2023-08-17T16:41:24Z) + +### 🚀 Enhancements + +
+ chore: Remove unused + @MaxymVlasov (#818) + +# why + +``` +TFLint in components/terraform/eks/cluster/: +2 issue(s) found: + +Warning: [Fixable] local.identity_account_name is declared but not used (terraform_unused_declarations) + + on main.tf line 9: + 9: identity_account_name = module.iam_roles.identity_account_account_name + +Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md + +Warning: [Fixable] variable "aws_teams_rbac" is declared but not used (terraform_unused_declarations) + + on variables.tf line 117: + 117: variable "aws_teams_rbac" { + +Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md +``` + +
+ +## 1.287.0 (2023-08-17T15:52:57Z) + +
+ Update `remote-states` modules to the latest version @aknysh (#820) + +### what + +- Update `remote-states` modules to the latest version + +### why + +- `remote-state` version `1.5.0` uses the latest version of `terraform-provider-utils` which uses the latest version of + Atmos with many new features and improvements + +
+ +## 1.286.0 (2023-08-17T05:49:45Z) + +
+ Update cloudposse/utils/aws to 1.3.0 @RoseSecurity (#815) + +### What: + +- Updated the following to utilize the newest version of `cloudposse/utils/aws`: + +``` +0.8.1 modules/spa-s3-cloudfront +1.1.0 modules/aws-config +1.1.0 modules/datadog-configuration/modules/datadog_keys +1.1.0 modules/dns-delegated +``` + +### Why: + +- `cloudposse/utils/aws` components were not updated to `1.3.0` + +### References: + +- [AWS Utils](https://github.com/cloudposse/terraform-aws-utils/releases/tag/1.3.0) + +
+ +## 1.285.0 (2023-08-17T05:49:09Z) + +
+ Update api-gateway-account-settings README.md @johncblandii (#819) + +### what + +- Updated the title + +### why + +- It was an extra helping of copy/pasta + +### references + +
+ +## 1.284.0 (2023-08-17T02:10:47Z) + +
+ Datadog upgrades @Nuru (#814) + +### what + +- Update Datadog components: + - `eks/datadog-agent` see `eks/datadog-agent/CHANGELOG.md` + - `datadog-configuration` better handling of `enabled = false` + - `datadog-integration` move "module count" back to "module" for better compatibility and maintainability, see + `datadog-integration/CHANGELOG.md` + - `datadog-lambda-forwared` fix issues around `enable = false` and incomplete destruction of resources (particularly + log groups) see `datadog-lambda-forwarder/CHANGELOG.md` + - Cleanup `datadog-monitor` see `datadog-monitor/CHANGELOG.md` for details. Possible breaking change in that several + inputs have been removed, but they were previously ignored anyway, so no infrastructure change should result from + you simply removing any inputs you had for the removed inputs. + - Update `datadog-sythetics` dependency `remote-state` version + - `datadog-synthetics-private-location` migrate control of namespace to `helm-release` module. Possible destruction + and recreation of component on upgrade. See CHANGELOG.md + +### why + +- More reliable deployments, especially when destroying or disabling them +- Bug fixes and new features + +
+ +## 1.283.0 (2023-08-16T17:23:39Z) + +
+ Update EC2-Autoscale-Group Modules to 0.35.1 @RoseSecurity (#809) + +### What: + +- Updated `modules/spacelift/worker-pool` from 0.34.2 to 0.35.1 and adapted new variable features +- Updated `modules/bastion` from 0.35.0 to 0.35.1 +- Updated `modules/github-runners` from 0.35.0 to 0.35.1 + +### Why: + +- Modules were utilizing previous `ec2-autoscale-group` versions + +### References: + +- [terraform-aws-ec2-autoscale-group](https://github.com/cloudposse/terraform-aws-ec2-autoscale-group/blob/main/variables.tf) +- [Terraform Registry](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group#instance_refresh) + +
+ +
+ Update storage-class efs component documentation @max-lobur (#817) + +### what + +- Update storage-class efs component defaults + +### why + +- Follow component move outside of eks dir + +
+ +## 1.282.1 (2023-08-15T21:48:02Z) + +### 🐛 Bug Fixes + +
+ Karpenter bugfix, EKS add-ons to managed node group @Nuru (#816) + +### what + +- [eks/karpenter] use Instance Profile name from EKS output +- Clarify recommendation and fix defaults regarding deploying add-ons to managed node group + +### why + +- Bug fix: Karpenter did not work when legacy mode disabled +- Originally we expected to use Karpenter-only clusters and the documentation and defaults aligned with this. Now we + recommend all Add-Ons be deployed to a managed node group, but the defaults and documentation did not reflect this. + +
+ +## 1.282.0 (2023-08-14T16:05:08Z) + +
+ Upstream the latest ecs-service module @goruha (#810) + +### what + +- Upstream the latest `ecs-service` component + +### why + +- Support ecspresso deployments +- Support s3 task definition mirroring +- Support external ALB/NLN components + +
+ +## 1.281.0 (2023-08-14T09:10:42Z) + +
+ Refactor Changelog @milldr (#811) + +### what + +- moved changelog for individual components +- changed title + +### why + +- Title changelogs consistently by components version +- Separate changes by affected components + +### references + +- https://github.com/cloudposse/knowledge-base/discussions/132 + +
+ +## 1.280.1 (2023-08-14T08:06:42Z) + +### 🚀 Enhancements + +
+ Fix eks/cluster default values @Nuru (#813) + +### what + +- Fix eks/cluster `node_group_defaults` to default to legal (empty) values for `kubernetes_labels` and + `kubernetes_taints` +- Increase eks/cluster managed node group default disk size from 20 to 50 GB + +### why + +- Default values should be legal values or else they are not really defaults +- Nodes were running out of disk space just hosting daemon set pods at 20 GB + +
+ +## 1.280.0 (2023-08-11T20:13:45Z) + +
+ Updated ssm parameter versions @RoseSecurity (#812) + +### Why: + +- `cloudposse/ssm-parameter-store/aws` was out of date +- There are no new [changes](https://github.com/cloudposse/terraform-aws-ssm-parameter-store/releases/tag/0.11.0) + incorporated but just wanted to standardize new modules to updated version + +### What: + +- Updated the following to `v0.11.0`: + +``` +0.10.0 modules/argocd-repo +0.10.0 modules/aurora-mysql +0.10.0 modules/aurora-postgres +0.10.0 modules/datadog-configuration +0.10.0 modules/eks/platform +0.10.0 modules/opsgenie-team/modules/integration +0.10.0 modules/ses +0.9.1 modules/datadog-integration +``` + +
+ +## 1.279.0 (2023-08-11T16:39:01Z) + +
+ fix: restore argocd notification ssm lookups @dudymas (#764) + +### what + +- revert some changes to `argocd` component +- connect argocd notifications with ssm secrets +- remove `deployment_id` from `argocd-repo` component +- correct `app_hostname` since gha usually adds protocol + +### why + +- regressions with argocd notifications caused github actions to timeout +- `deployment_id` no longer needed for fascilitating communication between gha and ArgoCD +- application urls were incorrect and problematic during troubleshooting + +
+ +## 1.278.0 (2023-08-09T21:54:09Z) + +
+ Upstream `eks/keda` @milldr (#808) + +### what + +- Added the component `eks/keda` + +### why + +- We've deployed KEDA for a few customers now and the component should be upstreamed + +### references + +- n/a + +
+ +## 1.277.0 (2023-08-09T20:39:21Z) + +
+ Added Inputs for `elasticsearch` and `cognito` @milldr (#786) + +### what + +- Added `deletion_protection` for `cognito` +- Added options for dedicated master for `elasticsearch` + +### why + +- Allow the default options to be customized + +### references + +- Customer requested additions + +
+ +## 1.276.1 (2023-08-09T20:30:36Z) + +
+ Update upgrade-guide.md Version @milldr (#807) + +### what + +- Set the version to the correct updated release + +### why + +- Needs to match correct version + +### references + +#804 + +
+ +### 🚀 Enhancements + +
+ feat: allow email to be configured at account level @sgtoj (#799) + +### what + +- allow email to be configured at account level + +### why + +- to allow importing existing accounts with email address that does not met the organization standard naming format + +### references + +- n/a + +
+ +## 1.276.0 (2023-08-09T16:38:40Z) + +
+ Transit Gateway Cross-Region Support @milldr (#804) + +### what + +- Upgraded `tgw` components to support cross region connections +- Added back `tgw/cross-region-hub-connector` with overhaul to support updated `tgw/hub` component + +### why + +- Deploy `tgw/cross-region-hub-connector` to create peered TGW hubs +- Use `tgw/hub` both for in region and intra region connections + +### references + +- n/a + +
+ +## 1.275.0 (2023-08-09T02:53:39Z) + +
+ [eks/cluster] Proper handling of cold start and enabled=false @Nuru (#806) + +### what + +- Proper handling of cold start and `enabled=false` + +### why + +- Fixes #797 +- Supersedes and closes #798 +- Cloud Posse standard requires error-free operation and no resources created when `enabled` is `false`, but previously + this component had several errors + +
+ +## 1.274.2 (2023-08-09T00:13:36Z) + +### 🚀 Enhancements + +
+ Added Enabled Parameter to aws-saml/okta-user and datadog-synthetics-private-location @RoseSecurity (#805) + +### What: + +- Added `enabled` parameter for `modules/aws-saml/modules/okta-user/main.tf` and + `modules/datadog-private-location-ecs/main.tf` + +### Why: + +- No support for disabling the creation of the resources + +
+ +## 1.274.1 (2023-08-09T00:11:55Z) + +### 🚀 Enhancements + +
+ Updated Security Group Component to 2.2.0 @RoseSecurity (#803) + +### What: + +- Updated `bastion`, `redshift`, `rds`, `spacelift`, and `vpc` to utilize the newest version of + `cloudposse/security-group/aws` + +### Why: + +- `cloudposse/security-group/aws` components were not updated to `2.2.0` + +### References: + +- [AWS Security Group Component](https://github.com/cloudposse/terraform-aws-security-group/compare/2.0.0-rc1...2.2.0) + +
+ +## 1.274.0 (2023-08-08T17:03:41Z) + +
+ bug: update descriptions *_account_account_name variables @sgtoj (#801) + +### what + +- update descriptions `*_account_account_name` variables + - I replaced `stage` with `short` because that is the description used for the respective `outputs` entries + +### why + +- to help future implementers of CloudPosse's architectures + +### references + +- n/a + +
+ +## 1.273.0 (2023-08-08T17:01:23Z) + +
+ docs: fix issue with eks/cluster usage snippet @sgtoj (#796) + +### what + +- update usage snippet in readme for `eks/cluster` component + +### why + +- fix incorrect shape for one of the items in `aws_team_roles_rbac` +- improve consistency +- remove variables that are not applicable for the component + +### references + +- n/a + +
+ +## 1.272.0 (2023-08-08T17:00:32Z) + +
+ feat: filter out “SUSPENDED” accounts for account-map @sgtoj (#800) + +### what + +- filter out “SUSPENDED” accounts (aka accounts in waiting period for termination) for `account-map` component + +### why + +- suspended account cannot be used, so therefore it should not exist in the account-map +- allows for new _active_ accounts with same exact name of suspended account to exists and work with `account-map` + +### references + +- n/a + +
+ +## 1.271.0 (2023-08-08T16:44:18Z) + +
+ `eks/karpenter` Readme.md update @Benbentwo (#792) + +### what + +- Adding Karpenter troubleshooting to readme +- Adding https://endoflife.date/amazon-eks to `EKS/Cluster` + +### references + +- https://karpenter.sh/docs/troubleshooting/ +- https://endoflife.date/amazon-eks + +
+ +## 1.270.0 (2023-08-07T21:54:49Z) + +
+ [eks/cluster] Add support for BottleRocket and EFS add-on @Nuru (#795) + +### what + +- Add support for EKS EFS add-on +- Better support for Managed Node Group's Block Device Storage +- Deprecate and ignore `aws_teams_rbac` and remove `identity` roles from `aws-auth` +- Support `eks/cluster` provisioning EC2 Instance Profile for Karpenter nodes (disabled by default via legacy flags) +- More options for specifying Availability Zones +- Deprecate `eks/ebs-controller` and `eks/efs-controller` +- Deprecate `eks/eks-without-spotinst` + +### why + +- Support EKS add-ons, follow-up to #723 +- Support BottleRocket, `gp3` storage, and provisioned iops and throughput +- Feature never worked +- Avoid specific failure mode when deleting and recreating an EKS cluster +- Maintain feature parity with `vpc` component +- Replace with add-ons +- Was not being maintained or used + +
+ +
+ [eks/storage-class] Initial implementation @Nuru (#794) + +### what + +- Initial implementation of `eks/storage-class` + +### why + +- Until now, we provisioned StorageClasses as a part of deploying + [eks/ebs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/ebs-controller/main.tf#L20-L56) + and + [eks/efs-controller](https://github.com/cloudposse/terraform-aws-components/blob/ba309ab4ffa96169b2b8dadce0643d13c1bd3ae9/modules/eks/efs-controller/main.tf#L48-L60). + However, with the switch from deploying "self-managed" controllers to EKS add-ons, we no longer deploy + `eks/ebs-controller` or `eks/efs-controller`. Therefore, we need a new component to manage StorageClasses + independently of controllers. + +### references + +- #723 + +
+ +
+ [eks/karpenter] Script to update Karpenter CRDs @Nuru (#793) + +### what + +- [eks/karpenter] Script to update Karpenter CRDs + +### why + +- Upgrading Karpenter to v0.28.0 requires updating CRDs, which is not handled by current Helm chart. This script updates + them by modifying the existing CRDs to be labeled as being managed by Helm, then installing the `karpenter-crd` Helm + chart. + +### references + +- Karpenter [CRD Upgrades](https://karpenter.sh/docs/upgrade-guide/#custom-resource-definition-crd-upgrades) + +
+ +## 1.269.0 (2023-08-03T20:47:56Z) + +
+ upstream `api-gateway` and `api-gateway-settings` @Benbentwo (#788) + +### what + +- Upstream api-gateway and it's corresponding settings component + +
+ +## 1.268.0 (2023-08-01T05:04:37Z) + +
+ Added new variable into `argocd-repo` component to configure ArgoCD's `ignore-differences` @zdmytriv (#785) + +### what + +- Added new variable into `argocd-repo` component to configure ArcoCD `ignore-differences` + +### why + +- There are cases when application and/or third-party operators might want to change k8s API objects. For example, + change the number of replicas in deployment. This will conflict with ArgoCD application because the ArgoCD controller + will spot drift and will try to make an application in sync with the codebase. + +### references + +- https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#respect-ignore-difference-configs + +
+ +## 1.267.0 (2023-07-31T19:41:43Z) + +
+ Spacelift `admin-stack` `var.description` @milldr (#787) + +### what + +- added missing description option + +### why + +- Variable is defined, but never passed to the modules + +### references + +n/a + +
+ +## 1.266.0 (2023-07-29T18:00:25Z) + +
+ Use s3_object_ownership variable @sjmiller609 (#779) + +### what + +- Pass s3_object_ownership variable into s3 module + +### why + +- I think it was accidentally not included +- Make possible to disable ACL from stack config + +### references + +- https://github.com/cloudposse/terraform-aws-s3-bucket/releases/tag/3.1.0 + +
+ +## 1.265.0 (2023-07-28T21:35:14Z) + +
+ `bastion` support for `availability_zones` and public IP and subnets @milldr (#783) + +### what + +- Add support for `availability_zones` +- Fix issue with public IP and subnets +- `tflint` requirements -- removed all unused locals, variables, formatting + +### why + +- All instance types are not available in all AZs in a region +- Bug fix + +### references + +- [Internal Slack reference](https://cloudposse.slack.com/archives/C048LCN8LKT/p1689085395494969) + +
+ +## 1.264.0 (2023-07-28T18:57:28Z) + +
+ Aurora Resource Submodule Requirements @milldr (#775) + +### what + +- Removed unnecessary requirement for aurora resources for the service name not to equal the user name for submodules of + both aurora resource components + +### why + +- This conditional doesn't add any value besides creating an unnecessary restriction. We should be able to create a user + name as the service name if we want + +### references + +- n/a + +
+ +## 1.263.0 (2023-07-28T18:12:30Z) + +
+ fix: restore notifications config in argocd @dudymas (#782) + +### what + +- Restore ssm configuration options for argocd notifications + +### why + +- notifications were not firing and tasks time out in some installations + +
+ +## 1.262.0 (2023-07-27T17:05:37Z) + +
+ Upstream `spa-s3-cloudfront` @milldr (#780) + +### what + +- Update module +- Add Cloudfront Invalidation permission to GitHub policy + +### why + +- Corrected bug in the module +- Allow GitHub Actions to run invalidations + +### references + +- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/288 + +
+ +## 1.261.0 (2023-07-26T16:20:37Z) + +
+ Upstream `spa-s3-cloudfront` @milldr (#778) + +### what + +- Upstream changes to `spa-s3-cloudfront` + +### why + +- Updated the included modules to support Terraform v5 +- Handle disabled WAF from remote-state + +### references + +- https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn/pull/284 + +
+ +## 1.260.1 (2023-07-25T05:10:20Z) + +### 🚀 Enhancements + +
+ [vpc] bugfix, [aurora-postgres] & [cloudtrail-bucket] Tflint fixes @Nuru (#776) + +### what + +- [vpc]: disable vpc_endpoints when enabled = false +- [aurora-postgres]: ensure variables have explicit types +- [cloudtrail-bucket]: ensure variables have explicit types + +### why + +- bugfix +- tflint fix +- tflint fix + +
+ +### 🐛 Bug Fixes + +
+ [vpc] bugfix, [aurora-postgres] & [cloudtrail-bucket] Tflint fixes @Nuru (#776) + +### what + +- [vpc]: disable vpc_endpoints when enabled = false +- [aurora-postgres]: ensure variables have explicit types +- [cloudtrail-bucket]: ensure variables have explicit types + +### why + +- bugfix +- tflint fix +- tflint fix + +
+ +## 1.260.0 (2023-07-23T23:08:53Z) + +
+ Update `alb` component @aknysh (#773) + +### what + +- Update `alb` component + +### why + +- Fixes after provisioning and testing on AWS + +
+ +## 1.259.0 (2023-07-20T04:32:13Z) + +
+ `elasticsearch` DNS Component Lookup @milldr (#769) + +### what + +- add environment for `dns-delegated` component lookup + +### why + +- `elasticsearch` is deployed in a regional environment, but `dns-delegated` is deployed to `gbl` + +### references + +- n/a + +
+ +## 1.258.0 (2023-07-20T04:17:31Z) + +
+ Bump `lambda-elasticsearch-cleanup` module @milldr (#768) + +### what + +- bump version of `lambda-elasticsearch-cleanup` module + +### why + +- Support Terraform provider v5 + +### references + +- https://github.com/cloudposse/terraform-aws-lambda-elasticsearch-cleanup/pull/48 + +
+ +## 1.257.0 (2023-07-20T03:04:51Z) + +
+ Bump ECS cluster module @max-lobur (#752) + +### what + +- Update ECS cluster module + +### why + +- Maintenance + +
+ +## 1.256.0 (2023-07-18T23:57:44Z) + +
+ Bump `elasticache-redis` Module @milldr (#767) + +### what + +- Bump `elasticache-redis` module + +### why + +- Resolve issues with terraform provider v5 + +### references + +- https://github.com/cloudposse/terraform-aws-elasticache-redis/issues/199 + +
+ +## 1.255.0 (2023-07-18T22:53:51Z) + +
+ Aurora Postgres Enhanced Monitoring Input @milldr (#766) + +### what + +- Added `enhanced_monitoring_attributes` as option +- Set default `aurora-mysql` component name + +### why + +- Set this var with a custom value to avoid IAM role length restrictions (default unchanged) +- Set common value as default + +### references + +- n/a + +
+ +## 1.254.0 (2023-07-18T21:00:30Z) + +
+ feat: acm no longer requires zone @dudymas (#765) + +### what + +- `acm` only looks up zones if `process_domain_validation_options` is true + +### why + +- Allow external validation of acm certs + +
+ +## 1.253.0 (2023-07-18T17:45:16Z) + +
+ `alb` and `ssm-parameters` Upstream for Basic Use @milldr (#763) + +### what + +- `alb` component can get the ACM cert from either `dns-delegated` or `acm` +- Support deploying `ssm-parameters` without SOPS +- `waf` requires a value for `visibility_config` in the stack catalog + +### why + +- resolving bugs while deploying example components + +### references + +- https://cloudposse.atlassian.net/browse/JUMPSTART-1185 + +
+ +## 1.252.0 (2023-07-18T16:14:23Z) + +
+ fix: argocd flags, versions, and expressions @dudymas (#753) + +### what + +- adjust expressions in argocd +- update helmchart module +- tidy up variables + +### why + +- component wouldn't run + +
+ +## 1.251.0 (2023-07-15T03:47:29Z) + +
+ fix: ecs capacity provider typing @dudymas (#762) + +### what + +- Adjust typing of `capacity_providers_ec2` + +### why + +- Component doesn't work without these fixes + +
+ +## 1.250.3 (2023-07-15T00:31:40Z) + +### 🚀 Enhancements + +
+ Update `alb` and `eks/alb-controller` components @aknysh (#760) + +### what + +- Update `alb` and `eks/alb-controller` components + +### why + +- Remove unused variables and locals +- Apply variables that are defined in `variables.tf` but were not used + +
+ +## 1.250.2 (2023-07-14T23:34:14Z) + +### 🚀 Enhancements + +
+ [aws-teams] Remove obsolete restriction on assuming roles in identity account @Nuru (#761) + +### what + +- [aws-teams] Remove obsolete restriction on assuming roles in the `identity` account + +### why + +Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the +originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy +against assuming roles in the `identity` account. + +AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team +structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the +previous restriction is both not needed and actually hinders desired operation. + +
+ +### 🐛 Bug Fixes + +
+ [aws-teams] Remove obsolete restriction on assuming roles in identity account @Nuru (#761) + +### what + +- [aws-teams] Remove obsolete restriction on assuming roles in the `identity` account + +### why + +Some time ago, there was an implied permission for any IAM role to assume any other IAM role in the same account if the +originating role had sufficient permissions to perform `sts:AssumeRole`. For this reason, we had an explicit policy +against assuming roles in the `identity` account. + +AWS has removed that implied permission and now requires all roles to have explicit trust policies. Our current Team +structure requires Teams (e.g. `spacelift`) to be able to assume roles in `identity` (e.g. `planner`). Therefore, the +previous restriction is both not needed and actually hinders desired operation. + +
+ +## 1.250.1 (2023-07-14T02:14:46Z) + +### 🚀 Enhancements + +
+ [eks/karpenter-provisioner] minor improvements @Nuru (#759) + +### what + +- [eks/karpenter-provisioner]: + - Implement `metadata_options` + - Avoid Terraform errors by marking Provisoner `spec.requirements` a computed field + - Add explicit error message about Consolidation and TTL Seconds After Empty being mutually exclusive + - Add `instance-category` and `instance-generation` to example in README + - Make many inputs optional +- [eks/karpenter] Update README to indicate that version 0.19 or later of Karpenter is required to work with this code. + +### why + +- Bug Fix: Input was there, but was being ignored, leading to unexpected behavior +- If a requirement that had a default value was not supplied, Terraform would fail with an error about inconsistent + plans because Karpenter would fill in the default +- Show some default values and how to override them +- Reduce the burden of supplying empty fields + +
+ +## 1.250.0 (2023-07-14T02:10:46Z) + +
+ Add EKS addons and the required IRSA to the `eks` component @aknysh (#723) + +### what + +- Deprecate the `eks-iam` component +- Add EKS addons and the required IRSA for the addons to the `eks` component +- Add ability to specify configuration values and timeouts for addons +- Add ability to deploy addons to Fargate when necessary +- Add ability to omit specifying Availability Zones and infer them from private subnets +- Add recommended but optional and requiring opt-in: use a single Fargate Pod Execution Role for all Fargate Profiles + +### why + +- The `eks-iam` component is not in use (we now create the IAM roles for Kubernetes Service Accounts in the + https://github.com/cloudposse/terraform-aws-helm-release module), and has very old and outdated code + +- AWS recommends to provision the required EKS addons and not to rely on the managed addons (some of which are + automatically provisioned by EKS on a cluster) + +- Some EKS addons (e.g. `vpc-cni` and `aws-ebs-csi-driver`) require an IAM Role for Kubernetes Service Account (IRSA) + with specific permissions. Since these addons are critical for cluster functionality, we create the IRSA roles for the + addons in the `eks` component and provide the role ARNs to the addons + +- Some EKS addons can be configured. In particular, `coredns` requires configuration to enable it to be deployed to + Fargate. + +- Users relying on Karpenter to deploy all nodes and wanting to deploy `coredns` or `aws-ebs-csi-driver` addons need to + deploy them to Fargate or else the EKS deployment will fail. + +- Enable DRY specification of Availability Zones, and use of AZ IDs, by reading the VPCs AZs. + +- A cluster needs only one Fargate Pod Execution Role, and it was a mistake to provision one for every profile. However, + making the change would break existing clusters, so it is optional and requires opt-in. + +### references + +- https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html +- https://docs.aws.amazon.com/eks/latest/userguide/managing-add-ons.html#creating-an-add-on +- https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html +- https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html +- https://docs.aws.amazon.com/eks/latest/userguide/cni-iam-role.html#cni-iam-role-create-role +- https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/#deploy-vpc-cni-managed-add-on +- https://docs.aws.amazon.com/eks/latest/userguide/csi-iam-role.html +- https://aws.amazon.com/blogs/containers/amazon-ebs-csi-driver-is-now-generally-available-in-amazon-eks-add-ons +- https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html#csi-iam-role +- https://github.com/kubernetes-sigs/aws-ebs-csi-driver + +
+ +## 1.249.0 (2023-07-14T01:23:37Z) + +
+ Make alb-controller default Ingress actually the default Ingress @Nuru (#758) + +### what + +- Make the `alb-controller` default Ingress actually the default Ingress + +### why + +- When setting `default_ingress_enabled = true` it is a reasonable expectation that the deployed Ingress be marked as + the Default Ingress. The previous code suggests this was the intended behavior, but does not work with the current + Helm chart and may have never worked. + +
+ +## 1.248.0 (2023-07-13T00:21:29Z) + +
+ Upstream `gitops` Policy Update @milldr (#757) + +### what + +- allow actions on table resources + +### why + +- required to be able to query using a global secondary index + +### references + +- https://github.com/cloudposse/github-action-terraform-plan-storage/pull/16 + +
+ +## 1.247.0 (2023-07-12T19:32:33Z) + +
+ Update `waf` and `alb` components @aknysh (#755) + +### what + +- Update `waf` component +- Update `alb` component + +### why + +- For `waf` component, add missing features supported by the following resources: + + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration + +- For `waf` component, remove deprecated features not supported by Terraform `aws` provider v5: + + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade#resourceaws_wafv2_web_acl + - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade#resourceaws_wafv2_web_acl_logging_configuration + +- For `waf` component, allow specifying a list of Atmos components to read from the remote state and associate their + ARNs with the web ACL + +- For `alb` component, update the modules to the latest versions and allow specifying Atmos component names for the + remote state in the variables (for the cases where the Atmos component names are not standard) + +### references + +- https://github.com/cloudposse/terraform-aws-waf/pull/45 + +
+ +## 1.246.0 (2023-07-12T18:57:58Z) + +
+ `acm` Upstream @Benbentwo (#756) + +### what + +- Upstream ACM + +### why + +- New Variables + - `subject_alternative_names_prefixes` + - `domain_name_prefix` + +
+ +## 1.245.0 (2023-07-11T19:36:11Z) + +
+ Bump `spaces` module versions @milldr (#754) + +### what + +- bumped module version for `terraform-spacelift-cloud-infrastructure-automation` + +### why + +- New policy added to `spaces` + +### references + +- https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/1.1.0 + +
+ +## 1.244.0 (2023-07-11T17:50:19Z) + +
+ Upstream Spacelift and Documentation @milldr (#732) + +### what + +- Minor corrections to spacelift components +- Documentation + +### why + +- Deployed this at a customer and resolved the changed errors +- Adding documentation for updated Spacelift design + +### references + +- n/a + +
+ +## 1.243.0 (2023-07-06T20:04:08Z) + +
+ Upstream `gitops` @milldr (#735) + +### what + +- Upstream new component, `gitops` + +### why + +- This component is used to create a role for GitHub to assume. This role is used to assume the `gitops` team and is + required for enabling GitHub Action Terraform workflows + +### references + +- JUMPSTART-904 + +
+ +## 1.242.1 (2023-07-05T19:46:08Z) + +### 🚀 Enhancements + +
+ Use the new subnets data source @max-lobur (#737) + +### what + +- Use the new subnets data source + +### why + +- Planned migration according to https://github.com/hashicorp/terraform-provider-aws/pull/18803 + +
+ +## 1.242.0 (2023-07-05T17:05:57Z) + +
+ Restore backwards compatibility of account-map output @Nuru (#748) + +### what + +- Restore backwards compatibility of `account-map` output + +### why + +- PR #715 removed outputs from `account-map` that `iam-roles` relied on. Although it removed the references in + `iam-roles`, this imposed an ordering on the upgrade: the `iam-roles` code had to be deployed before the module could + be applied. That proved to be inconvenient. Furthermore, if a future `account-map` upgrade added outputs that + iam-roles`required, neither order of operations would go smoothly. With this update, the standard practice of applying`account-map` + before deploying code will work again. + +
+ +## 1.241.0 (2023-07-05T16:52:58Z) + +
+ Fixed broken links in READMEs @zdmytriv (#749) + +### what + +- Fixed broken links in READMEs + +### why + +- Fixed broken links in READMEs + +### references + +- https://github.com/cloudposse/terraform-aws-components/issues/747 + +
+ +## 1.240.1 (2023-07-04T04:54:28Z) + +### Upgrade notes + +This fixes issues with `aws-sso` and `github-oidc-provider`. Versions from v1.227 through v1.240 should not be used. + +After installing this version of `aws-sso`, you may need to change the configuration in your stacks. See +[modules/aws-sso/changelog](https://github.com/cloudposse/terraform-aws-components/blob/main/modules/aws-sso/CHANGELOG.md) +for more information. Note: this release is from PR #740 + +After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. See +the release notes for v1.238.1 for more information. + +### 🐛 Bug Fixes + +
+ bugfix `aws-sso`, `github-oidc-provider` @Benbentwo (#740) + +### what + +- Bugfixes `filter` depreciation issue via module update to `1.1.1` +- Bugfixes missing `aws.root` provider +- Bugfixes `github-oidc-provider` v1.238.1 + +### why + +- Bugfixes + +### references + +- https://github.com/cloudposse/terraform-aws-sso/pull/44 +- closes #744 + +
+ +## 1.240.0 (2023-07-03T18:14:14Z) + +
+ Fix TFLint violations in account-map @MaxymVlasov (#745) + +### Why + +I'm too lazy to fix it each time when we get module updates via `atmos vendor` GHA + +### References + +- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_deprecated_index.md +- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_comment_syntax.md +- https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.4.0/docs/rules/terraform_unused_declarations.md + +
+ +## 1.239.0 (2023-06-29T23:34:53Z) + +
+ Bump `cloudposse/ec2-autoscale-group/aws` to `0.35.0` @milldr (#734) + +### what + +- bumped ASG module version, `cloudposse/ec2-autoscale-group/aws` to `0.35.0` + +### why + +- Recent versions of this module resolve errors for these components + +### references + +- https://github.com/cloudposse/terraform-aws-ec2-autoscale-group + +
+ +## 1.238.1 (2023-06-29T21:15:50Z) + +### Upgrade notes: + +There is a bug in this version of `github-oidc-provider`. Upgrade to version v1.240.1 or later instead. + +After installing this version of `github-oidc-provider`, you may need to change the configuration in your stacks. + +- If you have dynamic Terraform roles enabled, then this should be configured like a normal component. The previous + component may have required you to set + + ```yaml + backend: + s3: + role_arn: null + ```` + + and **that configuration should be removed** everywhere. + +- If you only use SuperAdmin to deploy things to the `identity` account, then for the `identity` (and `root`, if + applicable) account **_only_**, set + + ```yaml + backend: + s3: + role_arn: null + vars: + superadmin: true + ```` + + **Deployments to other accounts should not have any of those settings**. + +### 🚀 Enhancements + +
+ [github-oidc-provider] extra-compatible provider @Nuru (#742) + +### what && why + +- This updates `provider.tf` to provide compatibility with various legacy configurations as well as the current + reference architecture +- This update does NOT require updating `account-map` + +
+ +## 1.238.0 (2023-06-29T19:39:15Z) + +
+ IAM upgrades: SSO Permission Sets as Teams, SourceIdentity support, region independence @Nuru (#738) + +### what + +- Enable SSO Permission Sets to function as teams +- Allow SAML sign on via any regional endpoint, not only us-east-1 +- Allow use of AWS "Source Identity" for SAML and SSO users (not enabled for OIDC) + +### why + +- Reduce the friction between SSO permission sets and SAML roles by allowing people to use either interchangeably. + (Almost. SSO permission sets do not yet have the same permissions as SAML roles in the `identity` account itself.) +- Enable continued access in the event of a regional outage in us-east-1 as happened recently +- Enable auditing of who is using assumed roles + +### References + +- [Monitor and control actions taken with assumed roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_monitor.html) +- [How to integrate AWS STS SourceIdentity with your identity provider](https://aws.amazon.com/blogs/security/how-to-integrate-aws-sts-sourceidentity-with-your-identity-provider/) +- [AWS Sign-In endpoints](https://docs.aws.amazon.com/general/latest/gr/signin-service.html) +- [Available keys for SAML-based AWS STS federation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-saml) + +### Upgrade notes + +The regional endpoints and Source Identity support are non-controversial and cannot be disabled. They do, however, +require running `terraform apply` against `aws-saml`, `aws-teams`, and `aws-team-roles` in all accounts. + +#### AWS SSO updates + +To enable SSO Permission Sets to function as teams, you need to update `account-map` and `aws-sso`, then apply changes +to + +- `tfstate-backend` +- `aws-teams` +- `aws-team-roles` +- `aws-sso` + +This is all enabled by default. If you do not want it, you only need to update `account-map`, and add +`account-map/modules/roles-to-principles/variables_override.tf` in which you set +`overridable_team_permission_sets_enabled` to default to `false` + +Under the old `iam-primary-roles` component, corresponding permission sets were named `IdentityRoleAccess`. Under +the current `aws-teams` component, they are named `IdentityTeamAccess`. The current `account-map` defaults to the +latter convention. To use the earlier convention, add `account-map/modules/roles-to-principles/variables_override.tf` in +which you set `overridable_team_permission_set_name_pattern` to default to `"Identity%sRoleAccess"` + +There is a chance the resulting trust policies will be too big, especially for `tfstate-backend`. If you get an error +like + +``` +Cannot exceed quota for ACLSizePerRole: 2048 +``` + +You need to request a quota increase (Quota Code L-C07B4B0D), which will be automatically granted, usually in about 5 +minutes. The max quota is 4096, but we recommend increasing it to 3072 first, so you retain some breathing room for the +future. + +
+ +## 1.237.0 (2023-06-27T22:27:49Z) + +
+ Add Missing `github-oidc-provider` Thumbprint @milldr (#736) + +### what + +- include both thumbprints for GitHub OIDC + +### why + +- There are two possible intermediary certificates for the Actions SSL certificate and either can be returned by + Github's servers, requiring customers to trust both. This is a known behavior when the intermediary certificates are + cross-signed by the CA. + +### references + +- https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/ + +
+ +## 1.236.0 (2023-06-26T18:14:29Z) + +
+ Update `eks/echo-server` and `eks/alb-controller-ingress-group` components @aknysh (#733) + +### what + +- Update `eks/echo-server` and `eks/alb-controller-ingress-group` components +- Allow specifying + [alb.ingress.kubernetes.io/scheme](https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/#scheme) + (`internal` or `internet-facing`) + +### why + +- Allow the echo server to work with internal load balancers + +### references + +- https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/ + +
+ +## 1.235.0 (2023-06-22T21:06:18Z) + +
+ [account-map] Backwards compatibility for terraform profile users and eks/cluster @Nuru (#731) + +### what + +- [account-map/modules/iam-roles] Add `profiles_enabled` input to override global value +- [eks/cluster] Use `iam-roles` `profiles_enabled` input to force getting a role ARN even when profiles are in use +- [guardduty] Make providers compatible with static and dynamic TF roles + +### why + +- Previously, when the global `account-map` `profiles_enabled` flag was `true`, `iam_roles.terraform_role_arn` would be + null. However, `eks/cluster` requires `terraform_role_arn` regardless. +- Changes made in #728 work in environments that have not adopted dynamic Terraform roles but would fail in environments + that have (when using SuperAdmin) + +
+ +## 1.234.0 (2023-06-21T22:44:55Z) + +
+ [account-map] Feature flag to enable legacy Terraform role mapping @Nuru (#730) + +### what + +- [account-map] Add `legacy_terraform_uses_admin` feature flag to retain backwards compatibility + +### why + +- Historically, the `terraform` roles in `root` and `identity` were not used for Terraform plan/apply, but for other + things, and so the `terraform_roles` map output selected the `admin` roles for those accounts. This "wart" has been + remove in current `aws-team-roles` and `tfstate-backend` configurations, but for people who do not want to migrate to + the new conventions, this feature flag enables them to maintain the status quo with respect to role usage while taking + advantage of other updates to `account-map` and other components. + +### references + +This update is recommended for all customers wanting to use **_any_** component version 1.227 or later. + +- #715 +- + +
+ +## 1.233.0 (2023-06-21T20:03:36Z) + +
+ [lambda] feat: allows to use YAML instead of JSON for IAM policy @gberenice (#692) + +### what + +- BREAKING CHANGE: Actually use variable `function_name` to set the lambda function name. +- Make the variable `function_name` optional. When not set, the old null-lable-derived name will be use. +- Allow IAM policy to be specified in a custom terraform object as an alternative to JSON. + +### why + +- `function_name` was required to set, but it wasn't actually passed to `module "lambda"` inputs. +- Allow callers to stop providing `function_name` and preserve old behavior of using automatically generated name. +- When using [Atmos](https://atmos.tools/) to generate inputs from "stack" YAML files, having the ability to pass the + statements in as a custom object means specifying them via YAML, which makes the policy declaration in stack more + readable compared to embedding a JSON string in the YAML. + +
+ +## 1.232.0 (2023-06-21T15:49:06Z) + +
+ refactor securityhub component @mcalhoun (#728) + +### what + +- Refactor the Security Hub components into a single component + +### why + +- To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. + +
+ +## 1.231.0 (2023-06-21T14:54:50Z) + +
+ roll guard duty back to previous providers logic @mcalhoun (#727) + +### what + +- Roll the Guard Duty component back to using the previous logic for role assumption. + +### why + +- The newer method is causing the provider to try to assume the role twice. We get the error: + +``` +AWS Error: operation error STS: AssumeRole, https response error StatusCode: 403, RequestID: 00000000-0000-0000-0000-00000000, api error AccessDenied: User: arn:aws:sts::000000000000:assumed-role/acme-core-gbl-security-terraform/aws-go-sdk-1687312396297825294 is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::000000000000:role/acme-core-gbl-security-terraform +``` + +
+ +## 1.230.0 (2023-06-21T01:49:52Z) + +
+ refactor guardduty module @mcalhoun (#725) + +### what + +- Refactor the GuardDuty components into a single component + +### why + +- To improve the overall dev experience and to prevent needing to do multiple deploys with variable changes in-between. + +
+ +## 1.229.0 (2023-06-20T19:37:35Z) + +
+ upstream `github-action-runners` dockerhub authentication @Benbentwo (#726) + +### what + +- Adds support for dockerhub authentication + +### why + +- Dockerhub limits are unrealistically low for actually using dockerhub as an image registry for automated builds + +
+ +## 1.228.0 (2023-06-15T20:57:45Z) + +
+ alb: use the https_ssl_policy @johncblandii (#722) + +### what + +- Apply the HTTPS policy + +### why + +- The policy was unused so it was defaulting to an old policy + +### references + +
+ +## 1.227.0 (2023-06-12T23:41:45Z) + +Possibly breaking change: + +In this update, `account-map/modules/iam-roles` acquired a provider, making it no longer able to be used with `count`. +If you have code like + +```hcl +module "optional_role" { + count = local.optional_role_enabled ? 1 : 0 + + source = "../account-map/modules/iam-roles" + stage = var.optional_role_stage + context = module.this.context +} +``` + +You will need to rewrite it, removing the `count` parameter. It will be fine to always instantiate the module. If there +are problems with ensuring appropriate settings with the module is disabled, you can always replace them with the +component's inputs: + +```hcl +module "optional_role" { + source = "../account-map/modules/iam-roles" + stage = local.optional_role_enabled ? var.optional_role_stage : var.stage + context = module.this.context +} +``` + +The update to components 1.227.0 is huge, and you have options. + +- Enable, or not, dynamic Terraform IAM roles, which allow you to give some people (and Spacelift) the ability to run + Terraform plan in some accounts without allowing apply. Note that these users will still have read/write access to + Terraform state, but will not have IAM permissions to make changes in accounts. + [terraform_dynamic_role_enabled](https://github.com/cloudposse/terraform-aws-components/blob/1b338fe664e5debc5bbac30cfe42003f7458575a/modules/account-map/variables.tf#L96-L100) +- Update to new `aws-teams` team names. The new names are (except for support) distinct from team-roles, making it + easier to keep track. Also, the new managers team can run Terraform for identity and root in most (but not all) cases. +- Update to new `aws-team-roles`, including new permissions. The custom policies that have been removed are replaced in + the `aws-team-roles` configuration with AWS managed policy ARNs. This is required to add the `planner` role and + support the `terraform plan` restriction. +- Update the `providers.tf for` all components. Or some of them now, some later. Most components do not require updates, + but all of them have updates. The new `providers.tf`, when used with dynamic Terraform roles, allows users directly + logged into target accounts (rather than having roles in the `identity` account) to use Terraform in that account, and + also allows SuperAdmin to run Terraform in more cases (almost everywhere). + +**If you do not want any new features**, you only need to update `account-map` to v1.235 or later, to be compatible with +future components. Note that when updating `account-map` this way, you should update the code everywhere (all open PRs +and branches) before applying the Terraform changes, because the applied changes break the old code. + +If you want all the new features, we recommend updating all of the following to the current release in 1 PR: + +- account-map +- aws-teams +- aws-team-roles +- tfstate-backend + +
+ Enable `terraform plan` access via dynamic Terraform roles @Nuru (#715) + +### Reviewers, please note: + +The PR changes a lot of files. In particular, the `providers.tf` and therefore the `README.md` for nearly every +component. Therefore it will likely be easier to review this PR one commit at a time. + +`import_role_arn` and `import_profile_name` have been removed as they are no longer needed. Current versions of +Terraform (probably beginning with v1.1.0, but maybe as late as 1.3.0, I have not found authoritative information) can +read data sources during plan and so no longer need a role to be explicitly specified while importing. Feel free to +perform your own tests to make yourself more comfortable that this is correct. + +### what + +- Updates to allow Terraform to dynamically assume a role based on the user, to allow some users to run `terraform plan` + but not `terraform apply` + - Deploy standard `providers.tf` to all components that need an `aws` provider + - Move extra provider configurations to separate file, so that `providers.tf` can remain consistent/identical among + components and thus be easily updated + - Create `provider-awsutils.mixin.tf` to provide consistent, maintainable implementation +- Make `aws-sso` vendor safe +- Deprecate `sso` module in favor of `aws-saml` + +### why + +- Allow users to try new code or updated configurations by running `terraform plan` without giving them permission to + make changes with Terraform +- Make it easier for people directly logged into target accounts to still run Terraform +- Follow-up to #697, which updated `aws-teams` and `aws-team-roles`, to make `aws-sso` consistent +- Reduce confusion by moving deprecated code to `deprecated/` + +
+ +## 1.226.0 (2023-06-12T17:42:51Z) + +
+ chore: Update and add more basic pre-commit hooks @MaxymVlasov (#714) + +### what + +Fix common issues in the repo + +### why + +It violates our basic checks, which adds a headache to using +https://github.com/cloudposse/github-action-atmos-component-updater as is + +![image](https://github.com/cloudposse/terraform-aws-components/assets/11096782/248febbe-b65f-4080-8078-376ef576b457) + +> **Note**: It is much simpler to review PR if +> [hide whitespace changes](https://github.com/cloudposse/terraform-aws-components/pull/714/files?w=1) + +
+ +## 1.225.0 (2023-06-12T14:57:20Z) + +
+ Removed list of components from main README.md @zdmytriv (#721) + +### what + +- Removed list of components from main README.md + +### why + +- That list is outdated + +### references + +
+ +## 1.224.0 (2023-06-09T19:52:51Z) + +
+ upstream argocd @Benbentwo (#634) + +### what + +- Upstream fixes that allow for Google OIDC + +
+ +## 1.223.0 (2023-06-09T14:28:08Z) + +
+ add new spacelift components @mcalhoun (#717) + +### what + +- Add the newly developed spacelift components +- Deprecate the previous components + +### why + +- We undertook a process of decomposing a monolithic module and broke it into smaller, composable pieces for a better + developer experience + +### references + +- Corresponding + [Upstream Module PR](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/143) + +
+ +## 1.222.0 (2023-06-08T23:28:34Z) + +
+ Karpenter Node Interruption Handler @milldr (#713) + +### what + +- Added Karpenter Interruption Handler to existing component + +### why + +- Interruption is supported by karpenter, but we need to deploy sqs queue and event bridge rules to enable + +### references + +- https://github.com/cloudposse/knowledge-base/discussions/127 + +
+ +## 1.221.0 (2023-06-07T18:11:23Z) + +
+ feat: New Component `aws-ssosync` @dudymas (#625) + +### what + +- adds a fork of [aws-ssosync](https://github.com/awslabs/ssosync) as a lambda on a 15m cronjob + +### Why + +Google is one of those identity providers that doesn't have good integration with AWS SSO. In order to sync groups and +users across we need to use some API calls, luckily AWS Built [aws-ssosync](https://github.com/awslabs/ssosync) to +handle that. + +Unfortunately, it required ASM so we use [Benbentwo/ssosync](https://github.com/Benbentwo/ssosync) as it removes that +requirement. + +
+ +## 1.220.0 (2023-06-05T22:31:10Z) + +
+ Disable helm experiments by default, block Kubernetes provider 2.21.0 @Nuru (#712) + +### what + +- Set `helm_manifest_experiment_enabled` to `false` by default +- Block Kubernetes provider 2.21.0 + +### why + +- The `helm_manifest_experiment_enabled` reliably breaks when a Helm chart installs CRDs. The initial reason for + enabling it was for better drift detection, but the provider seems to have fixed most if not all of the drift + detection issues since then. +- Kubernetes provider 2.21.0 had breaking changes which were reverted in 2.21.1. + +### references + +- https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084#issuecomment-1576711378 + +
+ +## 1.219.0 (2023-06-05T20:23:17Z) + +
+ Expand ECR GH OIDC Default Policy @milldr (#711) + +### what + +- updated default ECR GH OIDC policy + +### why + +- This policy should grant GH OIDC access both public and private ECR repos + +### references + +- https://cloudposse.slack.com/archives/CA4TC65HS/p1685993698149499?thread_ts=1685990234.560589&cid=CA4TC65HS + +
+ +## 1.218.0 (2023-06-05T01:59:49Z) + +
+ Move `profiles_enabled` logic out of `providers.tf` and into `iam-roles` @Nuru (#702) + +### what + +- For Terraform roles and profiles used in `providers.tf`, return `null` for unused option +- Rename variables to `overridable_*` and update documentation to recommend `variables_override.tf` for customization + +### why + +- Prepare for `providers.tf` updates to support dynamic Terraform roles +- ARB decision on customization compatible with vendoring + +
+ +## 1.217.0 (2023-06-04T23:11:44Z) + +
+ [eks/external-secrets-operator] Normalize variables, update dependencies @Nuru (#708) + +### what + +For `eks/external-secrets-operator`: + +- Normalize variables, update dependencies +- Exclude Kubernetes provider v2.21.0 + +### why + +- Bring in line with other Helm-based modules +- Take advantage of improvements in dependencies + +### references + +- [Breaking change in Kubernetes provider v2.21.0](https://github.com/hashicorp/terraform-provider-kubernetes/pull/2084) + +
+ +## 1.216.2 (2023-06-04T23:08:39Z) + +### 🚀 Enhancements + +
+ Update modules for Terraform AWS provider v5 @Nuru (#707) + +### what + +- Update modules for Terraform AWS provider v5 + +### why + +- Provider version 5.0.0 was released with breaking changes. This fixes the breakage. + +### references + +- [v5 upgrade guide](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/version-5-upgrade) +- [v5.0.0 Release Notes](https://github.com/hashicorp/terraform-provider-aws/releases/tag/v5.0.0) + +
+ +## 1.216.1 (2023-06-04T01:18:31Z) + +### 🚀 Enhancements + +
+ Preserve custom roles when vendoring in updates @Nuru (#697) + +### what + +- Add `additional-policy-map.tf` as glue meant to be replaced by customers with map of their custom policies. + +### why + +- Currently, custom polices have to be manually added to the map in `main.tf`, but that gets overwritten with every + vendor update. Putting that map in a separate, optional file allows for the custom code to survive vendoring. + +
+ +## 1.216.0 (2023-06-02T18:02:01Z) + +
+ ssm-parameters: support tiers @johncblandii (#705) + +### what + +- Added support for ssm param tiers +- Updated the minimum version to `>= 1.3.0` to support `optional` parameters + +### why + +- `Standard` tier only supports 4096 characters. This allows Advanced and Intelligent Tiering support. + +### references + +
+ +## 1.215.0 (2023-06-02T14:28:29Z) + +
+ `.editorconfig` Typo @milldr (#704) + +### what + +fixed intent typo + +### why + +should be spelled "indent" + +### references + +https://cloudposse.slack.com/archives/C01EY65H1PA/p1685638634845009 + +
+ +## 1.214.0 (2023-05-31T17:46:35Z) + +
+ Transit Gateway `var.connections` Redesign @milldr (#685) + +### what + +- Updated how the connection variables for `tgw/hub` and `tgw/spoke` are defined +- Moved the old versions of `tgw` to `deprecated/tgw` + +### why + +- We want to be able to define multiple or alternately named `vpc` or `eks/cluster` components for both hub and spoke +- The cross-region components are not updated yet with this new design, since the current customers requesting these + updates do not need cross-region access at this time. But we want to still support the old design s.t. customers using + cross-region components can access the old components. We will need to update the cross-region components with follow + up effort + +### references + +- https://github.com/cloudposse/knowledge-base/discussions/112 + +
+ +## 1.213.0 (2023-05-31T14:50:16Z) + +
+ Introducing Security Hub @zdmytriv (#683) + +### what + +- Introducing Security Hub component + +### why + +Amazon Security Hub enables users to centrally manage and monitor the security and compliance of their AWS accounts and +resources. It aggregates, organizes, and prioritizes security findings from various AWS services, third-party tools, and +integrated partner solutions. + +Here are the key features and capabilities of Amazon Security Hub: + +- Centralized security management: Security Hub provides a centralized dashboard where users can view and manage + security findings from multiple AWS accounts and regions. This allows for a unified view of the security posture + across the entire AWS environment. + +- Automated security checks: Security Hub automatically performs continuous security checks on AWS resources, + configurations, and security best practices. It leverages industry standards and compliance frameworks, such as AWS + CIS Foundations Benchmark, to identify potential security issues. + +- Integrated partner solutions: Security Hub integrates with a wide range of AWS native services, as well as third-party + security products and solutions. This integration enables the ingestion and analysis of security findings from diverse + sources, offering a comprehensive security view. + +- Security standards and compliance: Security Hub provides compliance checks against industry standards and regulatory + frameworks, such as PCI DSS, HIPAA, and GDPR. It identifies non-compliant resources and provides guidance on + remediation actions to ensure adherence to security best practices. + +- Prioritized security findings: Security Hub analyzes and prioritizes security findings based on severity, enabling + users to focus on the most critical issues. It assigns severity levels and generates a consolidated view of security + alerts, allowing for efficient threat response and remediation. + +- Custom insights and event aggregation: Security Hub supports custom insights, allowing users to create their own rules + and filters to focus on specific security criteria or requirements. It also provides event aggregation and correlation + capabilities to identify related security findings and potential attack patterns. + +- Integration with other AWS services: Security Hub seamlessly integrates with other AWS services, such as AWS + CloudTrail, Amazon GuardDuty, AWS Config, and AWS IAM Access Analyzer. This integration allows for enhanced + visibility, automated remediation, and streamlined security operations. + +- Alert notifications and automation: Security Hub supports alert notifications through Amazon SNS, enabling users to + receive real-time notifications of security findings. It also facilitates automation and response through integration + with AWS Lambda, allowing for automated remediation actions. + +By utilizing Amazon Security Hub, organizations can improve their security posture, gain insights into security risks, +and effectively manage security compliance across their AWS accounts and resources. + +### references + +- https://aws.amazon.com/security-hub/ +- https://github.com/cloudposse/terraform-aws-security-hub/ + +
+ +## 1.212.0 (2023-05-31T14:45:30Z) + +
+ Introducing GuardDuty @zdmytriv (#682) + +### what + +- Introducing GuardDuty component + +### why + +AWS GuardDuty is a managed threat detection service. It is designed to help protect AWS accounts and workloads by +continuously monitoring for malicious activities and unauthorized behaviors. GuardDuty analyzes various data sources +within your AWS environment, such as AWS CloudTrail logs, VPC Flow Logs, and DNS logs, to detect potential security +threats. + +Key features and components of AWS GuardDuty include: + +- Threat detection: GuardDuty employs machine learning algorithms, anomaly detection, and integrated threat intelligence + to identify suspicious activities, unauthorized access attempts, and potential security threats. It analyzes event + logs and network traffic data to detect patterns, anomalies, and known attack techniques. + +- Threat intelligence: GuardDuty leverages threat intelligence feeds from AWS, trusted partners, and the global + community to enhance its detection capabilities. It uses this intelligence to identify known malicious IP addresses, + domains, and other indicators of compromise. + +- Real-time alerts: When GuardDuty identifies a potential security issue, it generates real-time alerts that can be + delivered through AWS CloudWatch Events. These alerts can be integrated with other AWS services like Amazon SNS or AWS + Lambda for immediate action or custom response workflows. + +- Multi-account support: GuardDuty can be enabled across multiple AWS accounts, allowing centralized management and + monitoring of security across an entire organization's AWS infrastructure. This helps to maintain consistent security + policies and practices. + +- Automated remediation: GuardDuty integrates with other AWS services, such as AWS Macie, AWS Security Hub, and AWS + Systems Manager, to facilitate automated threat response and remediation actions. This helps to minimize the impact of + security incidents and reduces the need for manual intervention. + +- Security findings and reports: GuardDuty provides detailed security findings and reports that include information + about detected threats, affected AWS resources, and recommended remediation actions. These findings can be accessed + through the AWS Management Console or retrieved via APIs for further analysis and reporting. + +GuardDuty offers a scalable and flexible approach to threat detection within AWS environments, providing organizations +with an additional layer of security to proactively identify and respond to potential security risks. + +### references + +- https://aws.amazon.com/guardduty/ +- https://github.com/cloudposse/terraform-aws-guardduty + +
+ +## 1.211.0 (2023-05-30T16:30:47Z) + +
+ Upstream `aws-inspector` @milldr (#700) + +### what + +Upstream `aws-inspector` from past engagement + +### why + +- This component was never upstreamed and now were want to use it again +- AWS Inspector is a security assessment service offered by Amazon Web Services (AWS). It helps you analyze and evaluate + the security and compliance of your applications and infrastructure deployed on AWS. AWS Inspector automatically + assesses the resources within your AWS environment, such as Amazon EC2 instances, for potential security + vulnerabilities and deviations from security best practices. Here are some key features and functionalities of AWS + Inspector: + + - Security Assessments: AWS Inspector performs security assessments by analyzing the behavior of your resources and + identifying potential security vulnerabilities. It examines the network configuration, operating system settings, + and installed software to detect common security issues. + + - Vulnerability Detection: AWS Inspector uses a predefined set of rules to identify common vulnerabilities, + misconfigurations, and security exposures. It leverages industry-standard security best practices and continuously + updates its knowledge base to stay current with emerging threats. + + - Agent-Based Architecture: AWS Inspector utilizes an agent-based approach, where you install an Inspector agent on + your EC2 instances. The agent collects data about the system and its configuration, securely sends it to AWS + Inspector, and allows for more accurate and detailed assessments. + + - Security Findings: After performing an assessment, AWS Inspector generates detailed findings that highlight security + vulnerabilities, including their severity level, impact, and remediation steps. These findings can help you + prioritize and address security issues within your AWS environment. + + - Integration with AWS Services: AWS Inspector seamlessly integrates with other AWS services, such as AWS + CloudFormation, AWS Systems Manager, and AWS Security Hub. This allows you to automate security assessments, manage + findings, and centralize security information across your AWS infrastructure. + +### references + +DEV-942 + +
+ +## 1.210.1 (2023-05-27T18:52:11Z) + +### 🚀 Enhancements + +
+ Fix tags @aknysh (#701) + +### what + +- Fix tags + +### why + +- Typo + +
+ +### 🐛 Bug Fixes + +
+ Fix tags @aknysh (#701) + +### what + +- Fix tags + +### why + +- Typo + +
+ +## 1.210.0 (2023-05-25T22:06:24Z) + +
+ EKS FAQ for Addons @milldr (#699) + +### what + +Added docs for EKS Cluster Addons + +### why + +FAQ, requested for documentation + +### references + +DEV-846 + +
+ +## 1.209.0 (2023-05-25T19:05:53Z) + +
+ Update ALB controller IAM policy @Nuru (#696) + +### what + +- Update `eks/alb-controller` controller IAM policy + +### why + +- Email from AWS: + > On June 1, 2023, we will be adding an additional layer of security to ELB ‘Create*' API calls where API callers must + > have explicit access to add tags in their Identity and Access Management (IAM) policy. Currently, access to attach + > tags was implicitly granted with access to 'Create*' APIs. + +### references + +- [Updated IAM policy](https://github.com/kubernetes-sigs/aws-load-balancer-controller/pull/3068) + +
+ +## 1.208.0 (2023-05-24T11:12:15Z) + +
+ Managed rules for AWS Config @zdmytriv (#690) + +### what + +- Added option to specify Managed Rules for AWS Config in addition to Conformance Packs + +### why + +- Managed rules will allows to add and tune AWS predefined rules in addition to Conformance Packs + +### references + +- [About AWS Config Manager Rules](https://docs.aws.amazon.com/config/latest/developerguide/evaluate-config_use-managed-rules.html) +- [List of AWS Config Managed Rules](https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html) + +
+ +## 1.207.0 (2023-05-22T18:40:06Z) + +
+ Corrections to `dms` components @milldr (#658) + +### what + +- Corrections to `dms` components + +### why + +- outputs were incorrect +- set pass and username with ssm + +### references + +- n/a + +
+ +## 1.206.0 (2023-05-20T19:41:35Z) + +
+ Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL @zdmytriv (#688) + +### what + +- Upgraded S3 Bucket module version + +### why + +- Upgrade S3 Bucket module to support recent changes made by AWS team regarding ACL + +### references + +- https://github.com/cloudposse/terraform-aws-s3-bucket/pull/178 + +
+ +## 1.205.0 (2023-05-19T23:55:14Z) + +
+ feat: add lambda monitors to datadog-monitor @dudymas (#686) + +### what + +- add lambda error monitor +- add datadog lambda log forwarder config monitor + +### why + +- Observability + +
+ +## 1.204.1 (2023-05-19T19:54:05Z) + +### 🚀 Enhancements + +
+ Update `module "datadog_configuration"` modules @aknysh (#684) + +### what + +- Update `module "datadog_configuration"` modules + +### why + +- The module does not accept the `region` variable +- The module must be always enabled to be able to read the Datadog API keys even if the component is disabled + +
+ +## 1.204.0 (2023-05-18T20:31:49Z) + +
+ `datadog-agent` bugfixes @Benbentwo (#681) + +### what + +- update datadog agent to latest +- remove variable in datadog configuration + +
+ +## 1.203.0 (2023-05-18T19:44:08Z) + +
+ Update `vpc` and `eks/cluster` components @aknysh (#677) + +### what + +- Update `vpc` and `eks/cluster` components + +### why + +- Use latest module versions + +- Take into account `var.availability_zones` for the EKS cluster itself. Only the `node-group` module was using + `var.availability_zones` to use the subnets from the provided AZs. The EKS cluster (control plane) was using all the + subnets provisioned in a VPC. This caused issues because EKS is not available in all AZs in a region, e.g. it's not + available in `us-east-1e` b/c of a limited capacity, and when using all AZs from `us-east-1`, the deployment fails + +- The latest version of the `vpc` component (which was updated in this PR as well) has the outputs to get a map of AZs + to the subnet IDs in each AZ + +``` + # Get only the public subnets that correspond to the AZs provided in `var.availability_zones` + # `az_public_subnets_map` is a map of AZ names to list of public subnet IDs in the AZs + public_subnet_ids = flatten([for k, v in local.vpc_outputs.az_public_subnets_map : v if contains(var.availability_zones, k)]) + + # Get only the private subnets that correspond to the AZs provided in `var.availability_zones` + # `az_private_subnets_map` is a map of AZ names to list of private subnet IDs in the AZs + private_subnet_ids = flatten([for k, v in local.vpc_outputs.az_private_subnets_map : v if contains(var.availability_zones, k)]) +``` + +
+ +## 1.202.0 (2023-05-18T16:15:12Z) + +
+ feat: adds ability to list principals of Lambdas allowed to access ECR @gberenice (#680) + +### what + +- This change allows listing IDs of the accounts allowed to consume ECR. + +### why + +- This is supported by [terraform-aws-ecr](https://github.com/cloudposse/terraform-aws-ecr/tree/main), but not the + component. + +### references + +- N/A + +
+ +## 1.201.0 (2023-05-18T15:08:54Z) + +
+ Introducing AWS Config component @zdmytriv (#675) + +### what + +- Added AWS Config and related `config-bucket` components + +### why + +- Added AWS Config and related `config-bucket` components + +### references + +
+ +## 1.200.1 (2023-05-18T14:52:10Z) + +### 🚀 Enhancements + +
+ Fix `datadog` components @aknysh (#679) + +### what + +- Fix all `datadog` components + +### why + +- Variable `region` is not supported by the `datadog-configuration/modules/datadog_keys` submodule + +
+ +## 1.200.0 (2023-05-17T09:19:40Z) + +- No changes + +## 1.199.0 (2023-05-16T15:01:56Z) + +
+ `eks/alb-controller-ingress-group`: Corrected Tags to pull LB Data Resource @milldr (#676) + +### what + +- corrected tag reference for pull lb data resource + +### why + +- the tags that are used to pull the ALB that's created should be filtering using the same group_name that is given when + the LB is created + +### references + +- n/a + +
+ +## 1.198.3 (2023-05-15T20:01:18Z) + +### 🐛 Bug Fixes + +
+ Correct `cloudtrail` Account-Map Reference @milldr (#673) + +### what + +- Correctly pull Audit account from `account-map` for `cloudtrail` +- Remove `SessionName` from EKS RBAC user name wrongly added in #668 + +### why + +- account-map remote state was missing from the `cloudtrail` component +- Account names should be pulled from account-map, not using a variable +- Session Name automatically logged in `user.extra.sessionName.0` starting at Kubernetes 1.20, plus addition had a typo + and was only on Teams, not Team Roles + +### references + +- Resolves change requests https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193297727 and + https://github.com/cloudposse/terraform-aws-components/pull/638#discussion_r1193298107 +- Closes #672 +- [Internal Slack thread](https://cloudposse.slack.com/archives/CA4TC65HS/p1684122388801769) + +
+ +## 1.198.2 (2023-05-15T19:47:39Z) + +### 🚀 Enhancements + +
+ bump config yaml dependency on account component as it still depends on hashicorp template provider @lantier (#671) + +### what + +- Bump [cloudposse/config/yaml](https://github.com/cloudposse/terraform-yaml-config) module dependency from version + 1.0.1 to 1.0.2 + +### why + +- 1.0.1 still uses hashicorp/template provider, which has no M1 binary equivalent, 1.0.2 already uses the cloudposse + version which has the binary + +### references + +- (https://github.com/cloudposse/terraform-yaml-config/releases/tag/1.0.2) + +
+ +## 1.198.1 (2023-05-15T18:55:09Z) + +### 🐛 Bug Fixes + +
+ Fixed `route53-resolver-dns-firewall` for the case when logging is disabled @zdmytriv (#669) + +### what + +- Fixed `route53-resolver-dns-firewall` for the case when logging is disabled + +### why + +- Component still required bucket when logging disabled + +### references + +
+ +## 1.198.0 (2023-05-15T17:37:47Z) + +
+ Add `aws-shield` component @aknysh (#670) + +### what + +- Add `aws-shield` component + +### why + +- The component is responsible for enabling AWS Shield Advanced Protection for the following resources: + + - Application Load Balancers (ALBs) + - CloudFront Distributions + - Elastic IPs + - Route53 Hosted Zones + +This component also requires that the account where the component is being provisioned to has been +[subscribed to AWS Shield Advanced](https://docs.aws.amazon.com/waf/latest/developerguide/enable-ddos-prem.html). + +
+ +## 1.197.2 (2023-05-15T15:25:39Z) + +### 🚀 Enhancements + +
+ EKS terraform module variable type fix @PiotrPalkaSpotOn (#674) + +### what + +- use `bool` rather than `string` type for a variable that's designed to hold `true`/`false` value + +### why + +- using `string` makes the + [if .Values.pvc_enabled](https://github.com/SpotOnInc/cloudposse-actions-runner-controller-tf-module-bugfix/blob/f224c7a4ee8b2ab4baf6929710d6668bd8fc5e8c/modules/eks/actions-runner-controller/charts/actions-runner/templates/runnerdeployment.yaml#L1) + condition always true and creates persistent volumes even if they're not intended to use + +
+ +## 1.197.1 (2023-05-11T20:39:03Z) + +### 🐛 Bug Fixes + +
+ Remove (broken) root access to EKS clusters @Nuru (#668) + +### what + +- Remove (broken) root access to EKS clusters +- Include session name in audit trail of users accessing EKS + +### why + +- Test code granting access to all `root` users and roles was accidentally left in #645 and breaks when Tenants are part + of account names +- There is no reason to allow `root` users to access EKS clusters, so even when this code worked it was wrong +- Audit trail can keep track of who is performing actions + +### references + +- https://aws.github.io/aws-eks-best-practices/security/docs/iam/#use-iam-roles-when-multiple-users-need-identical-access-to-the-cluster + +
+ +## 1.197.0 (2023-05-11T17:59:40Z) + +
+ `rds` Component readme update @Benbentwo (#667) + +### what + +- Updating default example from mssql to postgres + +
+ +## 1.196.0 (2023-05-11T17:56:41Z) + +
+ Update `vpc-flow-logs` @milldr (#649) + +### what + +- Modernized `vpc-flow-logs` with latest conventions + +### why + +- Old version of the component was significantly out of date +- #498 + +### references + +- DEV-880 + +
+ +## 1.195.0 (2023-05-11T07:27:29Z) + +
+ Add `iam-policy` to `ecs-service` @milldr (#663) + +### what + +Add an option to attach the `iam-policy` resource to `ecs-service` + +### why + +This policy is already created, but is missing its attachment. We should attach this to the resource when enabled + +### references + +https://cloudposse.slack.com/archives/CA4TC65HS/p1683729972134479 + +
+ +## 1.194.0 (2023-05-10T18:36:37Z) + +
+ upstream `acm` and `datadog-integration` @Benbentwo (#666) + +### what + +- ACM allows disabling `*.my.domain` +- Datadog-Integration supports allow-list'ing regions + +
+ +## 1.193.0 (2023-05-09T16:00:08Z) + +
+ Add `route53-resolver-dns-firewall` and `network-firewall` components @aknysh (#651) + +### what + +- Add `route53-resolver-dns-firewall` component +- Add `network-firewall` component + +### why + +- The `route53-resolver-dns-firewall` component is responsible for provisioning + [Route 53 Resolver DNS Firewall](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html) + resources, including Route 53 Resolver DNS Firewall, domain lists, firewall rule groups, firewall rules, and logging + configuration + +- The `network-firewall` component is responsible for provisioning + [AWS Network Firewall](https://aws.amazon.com/network-firewal) resources, including Network Firewall, firewall policy, + rule groups, and logging configuration + +
+ +## 1.192.0 (2023-05-09T15:40:43Z) + +
+ [ecs-service] Added IAM policies for ecspresso deployments @goruha (#659) + +### what + +- [ecs-service] Added IAM policies for [Ecspresso](https://github.com/kayac/ecspresso) deployments + +
+ +## 1.191.0 (2023-05-05T22:16:44Z) + +
+ `elasticsearch` Corrections @milldr (#662) + +### what + +- Modernize Elasticsearch component + +### why + +- `elasticsearch` was not deployable as is. Added up-to-date config + +### references + +- n/a + +
+ +## 1.190.0 (2023-05-05T18:46:26Z) + +
+ fix: remove stray component.yaml in lambda @dudymas (#661) + +### what + +- Remove the `component.yaml` in the lambda component + +### why + +- Vendoring would potentially cause conflicts + +
+ +## 1.189.0 (2023-05-05T18:22:04Z) + +
+ fix: eks/efs-controller iam policy updates @dudymas (#660) + +### what + +- Update the iam policy for eks/efs-controller + +### why + +- Older permissions will not work with new versions of the controller + +### references + +- [official iam policy sample](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/docs/iam-policy-example.json) + +
+ +## 1.188.0 (2023-05-05T17:05:23Z) + +
+ Move `eks/efs` to `efs` @milldr (#653) + +### what + +- Moved `eks/efs` to `efs` + +### why + +- `efs` shouldn't be a submodule of `eks`. You can deploy EFS without EKS + +### references + +- n/a + +
+ +## 1.187.0 (2023-05-04T23:04:26Z) + +
+ ARC enhancement, aws-config bugfix, DNS documentation @Nuru (#655) + +### what + +- Fix bug in `aws-config` +- Enhance documentation to explain relationship of `dns-primary` and `dns-delegated` components and `dns` account +- [`eks/actions-runner-controller`] Add support for annotations and improve support for ephemeral storage + +### why + +- Bugfix +- Customer query, supersedes and closes #652 +- Better support for longer lived jobs + +### references + +- https://github.com/actions/actions-runner-controller/issues/2562 + +
+ +## 1.186.0 (2023-05-04T18:15:31Z) + +
+ Update `RDS` @Benbentwo (#657) + +### what + +- Update RDS Modules +- Allow disabling Monitoring Role + +### why + +- Monitoring not always needed +- Context.tf Updates in modules + +
+ +## 1.185.0 (2023-04-26T21:30:24Z) + +
+ Add `amplify` component @aknysh (#650) + +### what + +- Add `amplify` component + +### why + +- Terraform component to provision AWS Amplify apps, backend environments, branches, domain associations, and webhooks + +### references + +- https://aws.amazon.com/amplify + +
+ +## 1.184.0 (2023-04-25T14:29:29Z) + +
+ Upstream: `eks/ebs-controller` @milldr (#640) + +### what + +- Added component for `eks/ebs-controller` + +### why + +- Upstreaming this component for general use + +### references + +- n/a + +
+ +## 1.183.0 (2023-04-24T23:21:17Z) + +
+ GitHub OIDC FAQ @milldr (#648) + +### what + +Added common question for GHA + +### why + +This is asked frequently + +### references + +https://cloudposse.slack.com/archives/C04N39YPVAS/p1682355553255269 + +
+ +## 1.182.1 (2023-04-24T19:37:31Z) + +### 🚀 Enhancements + +
+ [aws-config] Update usage info, add "help" and "teams" commands @Nuru (#647) + +### what + +Update `aws-config` command: + +- Add `teams` command and suggest "aws-config-teams" file name instead of "aws-config-saml" because we want to use + "aws-config-teams" for both SAML and SSO logins with Leapp handling the difference. +- Add `help` command +- Add more extensive help +- Do not rely on script generated by `account-map` for command `main()` function + +### why + +- Reflect latest design pattern +- Improved user experience + +
+ +## 1.182.0 (2023-04-21T17:20:14Z) + +
+ Athena CloudTrail Queries @milldr (#638) + +### what + +- added cloudtrail integration to athena +- conditionally allow audit account to decrypt kms key used for cloudtrail + +### why + +- allow queries against cloudtrail logs from a centralized account (audit) + +### references + +n/a + +
+ +## 1.181.0 (2023-04-20T22:00:24Z) + +
+ Format Identity Team Access Permission Set Name @milldr (#646) + +### what + +- format permission set roles with hyphens + +### why + +- pretty Permission Set naming. We want `devops-super` to format to `IdentityDevopsSuperTeamAccess` + +### references + +https://github.com/cloudposse/refarch-scaffold/pull/127 + +
+ +## 1.180.0 (2023-04-20T21:12:28Z) + +
+ Fix `s3-bucket` `var.bucket_name` @milldr (#637) + +### what + +changed default value for bucket name to empty string not null + +### why + +default bucket name should be empty string not null. Module checks against name length + +### references + +n/a + +
+ +## 1.179.0 (2023-04-20T20:26:20Z) + +
+ ecs-service: fix lint issues @kevcube (#636) + +
+ +## 1.178.0 (2023-04-20T20:23:10Z) + +
+ fix:aws-team-roles have stray locals @dudymas (#642) + +### what + +- remove locals from modules/aws-team-roles + +### why + +- breaks component when it tries to configure locals (the remote state for account_map isn't around) + +
+ +## 1.177.0 (2023-04-20T05:13:53Z) + +
+ Convert eks/cluster to aws-teams and aws-sso @Nuru (#645) + +### what + +- Convert `eks/cluster` to `aws-teams` +- Add `aws-sso` support to `eks/cluster` +- Undo automatic allowance of `identity` `aws-sso` permission sets into account roles added in #567 + +### why + +- Keep in sync with other modules +- #567 is a silent privilege escalation and not needed to accomplish desired goals + +
+ +## 1.176.1 (2023-04-19T14:20:27Z) + +### 🚀 Enhancements + +
+ fix: Use `vpc` without tenant @MaxymVlasov (#644) + +### why + +```bash +│ Error: Error in function call +│ +│ on remote-state.tf line 10, in module "vpc_flow_logs_bucket": +│ 10: tenant = coalesce(var.vpc_flow_logs_bucket_tenant_name, module.this.tenant) +│ ├──────────────── +│ │ while calling coalesce(vals...) +│ │ module.this.tenant is "" +│ │ var.vpc_flow_logs_bucket_tenant_name is null +│ +│ Call to function "coalesce" failed: no non-null, non-empty-string +│ arguments. +``` + +
+ +## 1.176.0 (2023-04-18T18:46:38Z) + +
+ feat: cloudtrail-bucket can have acl configured @dudymas (#643) + +### what + +- add `acl` var to `cloudtrail-bucket` component + +### why + +- Creating new cloudtrail buckets will fail if the acl isn't set to private + +### references + +- This is part of + [a security update from AWS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-faq.html) + +
+ +## 1.175.0 (2023-04-11T12:11:46Z) + +
+ [argocd-repo] Added ArgoCD git commit notifications @goruha (#633) + +### what + +- [argocd-repo] Added ArgoCD git commit notifications + +### why + +- ArgoCD sync deployment + +
+ +## 1.174.0 (2023-04-11T08:53:06Z) + +
+ [argocd] Added github commit status notifications @goruha (#631) + +### what + +- [argocd] Added github commit status notifications + +### why + +- ArgoCD sync deployment fix concurrent issue + +
+ +## 1.173.0 (2023-04-06T19:21:23Z) + +
+ Missing Version Pins for Bats @milldr (#629) + +### what + +added missing provider version pins + +### why + +missing provider versions, required for bats + +### references + +#626 #628, #627 + +
+ +## 1.172.0 (2023-04-06T18:32:04Z) + +
+ update datadog_lambda_forwarder ref for darwin_arm64 @kevcube (#626) + +### what + +- update datadog-lambda-forwarder module for darwin_arm64 + +### why + +- run on Darwin_arm64 hardware + +
+ +## 1.171.0 (2023-04-06T18:11:40Z) + +
+ Version Pinning Requirements @milldr (#628) + +### what + +- missing bats requirements resolved + +### why + +- PR #627 missed a few bats requirements in submodules + +### references + +- #627 +- #626 + +
+ +## 1.170.0 (2023-04-06T17:38:24Z) + +
+ Bats Version Pinning @milldr (#627) + +### what + +- upgraded pattern for version pinning + +### why + +- bats would fail for all of these components unless these versions are pinned as such + +### references + +- https://github.com/cloudposse/terraform-aws-components/pull/626 + +
+ +## 1.169.0 (2023-04-05T20:28:39Z) + +
+ [eks/actions-runner-controller]: support Runner Group, webhook queue size @Nuru (#621) + +### what + +- `eks/actions-runner-controller` + - Support + [Runner Groups](https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups) + - Enable configuration of the webhook queue size limit + - Change runner controller Docker image designation +- Add documentation on Runner Groups and Autoscaler configuration + +### why + +- Enable separate access control to self-hosted runners +- For users that launch a large number of jobs in a short period of time, allow bigger queues to avoid losing jobs +- Maintainers recommend new tag format. `ghcr.io` has better rate limits than `docker.io`. + +### references + +- https://github.com/actions/actions-runner-controller/issues/2056 + +
+ +## 1.168.0 (2023-04-04T21:48:58Z) + +
+ s3-bucket: use cloudposse template provider for arm64 @kevcube (#618) + +### what + +- use cloud posse's template provider + +### why + +- arm64 +- also this provider was not pinned in versions.tf so that had to be fixed somehow + +### references + +- closes #617 + +
+ +## 1.167.0 (2023-04-04T18:14:45Z) + +
+ chore: aws-sso modules updated to 1.0.0 @dudymas (#623) + +### what + +- upgrade aws-sso modules: permission_sets, sso_account_assignments, and sso_account_assignments_root + +### why + +- upstream updates + +
+ +## 1.166.0 (2023-04-03T13:39:53Z) + +
+ Add `datadog-synthetics` component @aknysh (#619) + +### what + +- Add `datadog-synthetics` component + +### why + +- This component is responsible for provisioning Datadog synthetic tests + +- Supports Datadog synthetics private locations + + - https://docs.datadoghq.com/getting_started/synthetics/private_location + - https://docs.datadoghq.com/synthetics/private_locations + +- Synthetic tests allow you to observe how your systems and applications are performing using simulated requests and + actions from the AWS managed locations around the globe and to monitor internal endpoints from private locations + +
+ +## 1.165.0 (2023-03-31T22:11:26Z) + +
+ Update `eks/cluster` README @milldr (#616) + +### what + +- Updated the README with EKS cluster + +### why + +The example stack is outdated. Add notes for Github OIDC and karpenter + +### references + +https://cloudposse.atlassian.net/browse/DEV-835 + +
+ +## 1.164.1 (2023-03-30T20:03:15Z) + +### 🚀 Enhancements + +
+ spacelift: Update README.md example login policy @johncblandii (#597) + +### what + +- Added support for allowing spaces read access to all members +- Added a reference for allowing spaces write access to the "Developers" group + +### why + +- Spacelift moved to Spaces Access Control + +### references + +- https://docs.spacelift.io/concepts/spaces/access-control + +
+ +## 1.164.0 (2023-03-30T16:25:28Z) + +
+ Update several component Readmes @Benbentwo (#611) + +### what + +- Update Readmes of many components from Refarch Docs + +
+ +## 1.163.0 (2023-03-29T19:52:46Z) + +
+ add providers to `mixins` folder @Benbentwo (#613) + +### what + +- Copies some common providers to the mixins folder + +### why + +- Have a central place where our common providers are held. + +
+ +## 1.162.0 (2023-03-29T19:30:15Z) + +
+ Added ArgoCD GitHub notification subscription @goruha (#615) + +### what + +- Added ArgoCD GitHub notification subscription + +### why + +- To use synchronous deployment pattern + +
+ +## 1.161.1 (2023-03-29T17:20:27Z) + +### 🚀 Enhancements + +
+ waf component, update dependency versions for aws provider and waf terraform module @arcaven (#612) + +### what + +- updates to waf module: + - aws provider from ~> 4.0 to => 4.0 + - module cloudposse/waf/aws from 0.0.4 to 0.2.0 + - different recommended catalog entry + +### why + +- @aknysh suggested some updates before we start using waf module + +
+ +## 1.161.0 (2023-03-28T19:51:27Z) + +
+ Quick fixes to EKS/ARC arm64 Support @Nuru (#610) + +### what + +- While supporting EKS/ARC `arm64`, continue to deploy `amd64` by default +- Make `tolerations.value` optional + +### why + +- Majority of echosystem support is currently `amd64` +- `tolerations.value` is option in Kubernetes spec + +### references + +- Corrects issue which escaped review in #609 + +
+ +## 1.160.0 (2023-03-28T18:26:20Z) + +
+ Upstream EKS/ARC amd64 Support @milldr (#609) + +### what + +Added arm64 support for eks/arc + +### why + +when supporting both amd64 and arm64, we need to select the correct architecture + +### references + +https://github.com/cloudposse/infra-live/pull/265 + +
+ +## 1.159.0 (2023-03-27T16:19:29Z) + +
+ Update account-map to output account information for aws-config script @Nuru (#608) + +### what + +- Update `account-map` to output account information for `aws-config` script +- Output AWS profile name for root of credential chain + +### why + +- Enable `aws-config` to output account IDs and to generate configuration for "AWS Extend Switch Roles" browser plugin +- Support multiple namespaces in a single infrastructure repo + +
+ +
+ Update CODEOWNERS to remove contributors @Nuru (#607) + +### what + +- Update CODEOWNERS to remove contributors + +### why + +- Require approval from engineering team (or in some cases admins) for all changes, to keep better quality control on + this repo + +
+ +## 1.158.0 (2023-03-27T03:41:43Z) + +
+ Upstream latest datadog-agent and datadog-configuration updates @nitrocode (#598) + +### what + +- Upstream latest datadog-agent and datadog-configuration updates + +### why + +- datadog irsa role +- removing unused input vars +- default to `public.ecr.aws` images +- ignore deprecated `default.auto.tfvars` +- move `datadog-agent` to `eks/` subfolder for consistency with other helm charts + +### references + +N/A + +
+ +## 1.157.0 (2023-03-24T19:12:17Z) + +
+ Remove `root_account_tenant_name` @milldr (#605) + +### what + +- bumped ecr +- remove unnecessary variable + +### why + +- ECR version update +- We shouldn't need to set `root_account_tenant_name` in providers +- Some Terraform docs are out-of-date + +### references + +- n/a + +
+ +## 1.156.0 (2023-03-23T21:03:46Z) + +
+ exposing variables from 2.0.0 of `VPC` module @Benbentwo (#604) + +### what + +- Adding vars for vpc module and sending them directly to module + +### references + +- https://github.com/cloudposse/terraform-aws-vpc/blob/master/variables.tf#L10-L44 + +
+ +## 1.155.0 (2023-03-23T02:01:29Z) + +
+ Add Privileged Option for GH OIDC @milldr (#603) + +### what + +- allow gh oidc role to use privileged as option for reading tf backend + +### why + +- If deploying GH OIDC with a component that needs to be applied with SuperAdmin (aws-teams) we need to set privileged + here + +### references + +- https://cloudposse.slack.com/archives/C04N39YPVAS/p1679409325357119 + +
+ +## 1.154.0 (2023-03-22T17:40:35Z) + +
+ update `opsgenie-team` to be delete-able via `enabled: false` @Benbentwo (#589) + +### what + +- Uses Datdaog Configuration as it's source of datadog variables +- Now supports `enabled: false` on a team to destroy it. + +
+ +## 1.153.0 (2023-03-21T19:22:03Z) + +
+ Upstream AWS Teams components @milldr (#600) + +### what + +- added eks view only policy + +### why + +- Provided updates from recent contracts + +### references + +- https://github.com/cloudposse/refarch-scaffold/pull/99 + +
+ +## 1.152.0 (2023-03-21T15:42:51Z) + +
+ upstream 'datadog-lambda-forwarder' @gberenice (#601) + +### what + +- Upgrade 'datadog-lambda-forwarder' component to v1.3.0 + +### why + +- Be able [to forward Cloudwatch Events](https://github.com/cloudposse/terraform-aws-datadog-lambda-forwarder/pull/48) + via components. + +### references + +- N/A + +
+ +## 1.151.0 (2023-03-15T15:56:20Z) + +
+ Upstream `eks/external-secrets-operator` @milldr (#595) + +### what + +- Adding new module for `eks/external-secrets-operator` + +### why + +- Other customers want to use this module now, and it needs to be upstreamed + +### references + +- n/a + +
+ +## 1.150.0 (2023-03-14T20:20:41Z) + +
+ chore(spacelift): update with dependency resource @dudymas (#594) + +### what + +- update spacelift component to 0.55.0 + +### why + +- support feature flag for spacelift_stack_dependency resource + +### references + +- [spacelift module 0.55.0](https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/releases/tag/0.55.0) + +
+ +## 1.149.0 (2023-03-13T15:25:25Z) + +
+ Fix SSO SAML provider fixes @goruha (#592) + +### what + +- Fix SSO SAML provider fixes + +
+ +## 1.148.0 (2023-03-10T18:07:36Z) + +
+ ArgoCD SSO improvements @goruha (#590) + +### what + +- ArgoCD SSO improvements + +
+ +## 1.147.0 (2023-03-10T17:52:18Z) + +
+ Upstream: `eks/echo-server` @milldr (#591) + +### what + +- Adding the `ingress.alb.group_name` annotation to Echo Server + +### why + +- Required to set the ALB specifically, rather than using the default + +### references + +- n/a + +
+ +## 1.146.0 (2023-03-08T23:13:13Z) + +
+ Improve platform and external-dns for release engineering @goruha (#588) + +### what + +- `eks/external-dns` support `dns-primary` +- `eks/platform` support json query remote components outputs + +### why + +- `vanity domain` pattern support by `eks/external-dns` +- Improve flexibility of `eks/platform` + +
+ +## 1.145.0 (2023-03-07T00:28:25Z) + +
+ `eks/actions-runner-controller`: use coalesce @Benbentwo (#586) + +### what + +- use coalesce instead of try, as we need a value passed in here + +
+ +## 1.144.0 (2023-03-05T20:24:09Z) + +
+ Upgrade Remote State to `1.4.1` @milldr (#585) + +### what + +- Upgrade _all_ remote state modules (`cloudposse/stack-config/yaml//modules/remote-state`) to version `1.4.1` + +### why + +- In order to use go templating with Atmos, we need to use the latest cloudposse/utils version. This version is + specified by `1.4.1` + +### references + +- https://github.com/cloudposse/terraform-yaml-stack-config/releases/tag/1.4.1 + +
+ +## 1.143.0 (2023-03-02T18:07:53Z) + +
+ bugfix: rds anomalies monitor not sending team information @Benbentwo (#583) + +### what + +- Update monitor to have default CP tags + +
+ +## 1.142.0 (2023-03-02T17:49:40Z) + +
+ datadog-lambda-forwarder: if s3_buckets not set, module fails @kevcube (#581) + +This module attempts to do length() on the value for s3_buckets. + +We are not using s3_buckets, and it defaults to null, so length() fails. + +
+ +## 1.141.0 (2023-03-01T19:10:07Z) + +
+ `datadog-monitors`: Team Grouping @Benbentwo (#580) + +### what + +- grouping by team helps ensure the team tag is sent to Opsgenie + +### why + +- ensures most data is fed to a valid team tag instead of `@opsgenie-` + +
+ +## 1.140.0 (2023-02-28T18:47:44Z) + +
+ `spacelift` add missing `var.region` @johncblandii (#574) + +### what + +- Added the missing `var.region` + +### why + +- The AWS provider requires it and it was not available + +### references + +
+ +## 1.139.0 (2023-02-28T18:46:35Z) + +
+ datadog monitors improvements @Benbentwo (#579) + +### what + +- Datadog monitor improvements + - Prepends `()` e.g. `(tenant-environment-stage)` + - Fixes some messages that had improper syntax - dd uses `{{ var.name }}` + +### why + +- Datadog monitor improvements + +
+ +## 1.138.0 (2023-02-28T18:45:48Z) + +
+ update `account` readme.md @Benbentwo (#570) + +### what + +- Updated account readme + +
+ +## 1.137.0 (2023-02-27T20:39:34Z) + +
+ Update `eks/cluster` @Benbentwo (#578) + +### what + +- Update EKS Cluster Module to re-include addons + +
+ +## 1.136.0 (2023-02-27T17:36:47Z) + +
+ Set spacelift-worker-pool ami explicitly to x86_64 @arcaven (#577) + +### why + +- autoscaling group for spacelift-worker-pool will fail to launch when new arm64 images return first +- arm64 ami image is being returned first at the moment in us-east-1 + +### what + +- set spacelift-worker-pool ami statically to return only x86_64 results + +### references + +- Spacelift Worker Pool ASG may fail to scale due to ami/instance type mismatch #575 +- Note: this is an alternative to spacelift-worker-pool README update and AMI limits #573 which I read after, but I + think this filter approach will be more easily be refactored into setting this as an attribute in variables.tf in the + near future + +
+ +## 1.135.0 (2023-02-27T13:56:48Z) + +
+ github-runners add support for runner groups @johncblandii (#569) + +### what + +- Added optional support for separating runners by groups + +NOTE: I don't know if the default of `default` is valid or if it is `Default`. I'll confirm this soon. + +### why + +- Groups are supported by GitHub and allow for Actions to target specific runners by group vs by label + +### references + +- https://docs.github.com/en/actions/hosting-your-own-runners/managing-access-to-self-hosted-runners-using-groups + +
+ +## 1.134.0 (2023-02-24T20:59:40Z) + +
+ [account-map] Update remote config module version @goruha (#572) + +### what + +- Update remote config module version `1.4.1` + +### why + +- Solve terraform module version conflict + +
+ +## 1.133.0 (2023-02-24T17:55:52Z) + +
+ Fix ArgoCD minor issues @goruha (#571) + +### what + +- Fix slack notification annotations +- Fix CRD creation order + +### why + +- Fix ArgoCD bootstrap + +
+ +## 1.132.0 (2023-02-23T04:33:29Z) + +
+ Add spacelift-policy component @nitrocode (#556) + +### what + +- Add spacelift-policy component + +### why + +- De-couple policy creation from admin and child stacks +- Auto attach policies to remove additional terraform management of resources + +### references + +- Depends on PR https://github.com/cloudposse/terraform-spacelift-cloud-infrastructure-automation/pull/134 + +
+ +## 1.131.0 (2023-02-23T01:13:58Z) + +
+ SSO upgrades and Support for Assume Role from Identity Users @johncblandii (#567) + +### what + +- Upgraded `aws-sso` to use `0.7.1` modules +- Updated `account-map/modules/roles-to-principals` to support assume role from SSO users in the identity account +- Adjusted `aws-sso/policy-Identity-role-RoleAccess.tf` to use the identity account name vs the stage so it supports + names like `core-identity` instead of just `identity` + +### why + +- `aws-sso` users could not assume role to plan/apply terraform locally +- using `core-identity` as a name broke the `aws-sso` policy since account `identity` does not exist in + `full_account_map` + +### references + +
+ +## 1.130.0 (2023-02-21T18:33:53Z) + +
+ Add Redshift component @max-lobur (#563) + +### what + +- Add Redshift + +### why + +- Fulfilling the AWS catalog + +### references + +- https://github.com/cloudposse/terraform-aws-redshift-cluster + +
+ +## 1.129.0 (2023-02-21T16:45:43Z) + +
+ update dd agent docs @Benbentwo (#565) + +### what + +- Update Datadog Docs to be more clear on catalog entry + +
+ +## 1.128.0 (2023-02-18T16:28:11Z) + +
+ feat: updates spacelift to support policies outside of the comp folder @Gowiem (#522) + +### what + +- Adds back `policies_by_name_path` variable to spacelift component + +### why + +- Allows specifying spacelift policies outside of the component folder + +### references + +- N/A + +
+ +## 1.127.0 (2023-02-16T17:53:31Z) + +
+ [sso-saml-provider] Upstream SSO SAML provider component @goruha (#562) + +### what + +- [sso-saml-provider] Upstream SSO SAML provider component + +### why + +- Required for ArgoCD + +
+ +## 1.126.0 (2023-02-14T23:01:00Z) + +
+ upstream `opsgenie-team` @Benbentwo (#561) + +### what + +- Upstreams latest opsgenie-team component + +
+ +## 1.125.0 (2023-02-14T21:45:32Z) + +
+ [eks/argocd] Upstream ArgoCD @goruha (#560) + +### what + +- Upstream `eks/argocd` + +
+ +## 1.124.0 (2023-02-14T17:34:29Z) + +
+ `aws-backup` upstream @Benbentwo (#559) + +### what + +- Update `aws-backup` to latest + +
+ +## 1.123.0 (2023-02-13T22:42:56Z) + +
+ upstream lambda pt2 @Benbentwo (#558) + +### what + +- Add archive zip +- Change to python (no compile) + +
+ +## 1.122.0 (2023-02-13T21:24:02Z) + +
+ upstream `lambda` @Benbentwo (#557) + +### what + +- Upstream `lambda` component + +### why + +- Quickly deploy serverless code + +
+ +## 1.121.0 (2023-02-13T16:59:16Z) + +
+ Upstream `ACM` and `eks/Platform` for release_engineering @Benbentwo (#555) + +### what + +- ACM Component outputs it's acm url +- EKS/Platform will deploy many terraform outputs to SSM + +### why + +- These components are required for CP Release Engineering Setup + +
+ +## 1.120.0 (2023-02-08T16:34:25Z) + +
+ Upstream datadog logs archive @Benbentwo (#552) + +### what + +- Upstream DD Logs Archive + +
+ +## 1.119.0 (2023-02-07T21:32:25Z) + +
+ Upstream `dynamodb` @milldr (#512) + +### what + +- Updated the `dynamodb` component + +### why + +- maintaining up-to-date upstream component + +### references + +- N/A + +
+ +## 1.118.0 (2023-02-07T20:15:17Z) + +
+ fix dd-forwarder: datadog service config depends on lambda arn config @raybotha (#531) + +
+ +## 1.117.0 (2023-02-07T19:44:32Z) + +
+ Upstream `spa-s3-cloudfront` @milldr (#500) + +### what + +- Added missing component from upstream `spa-s3-cloudfront` + +### why + +- We use this component to provision Cloudfront and related resources + +### references + +- N/A + +
+ +## 1.116.0 (2023-02-07T00:52:27Z) + +
+ Upstream `aurora-mysql` @milldr (#517) + +### what + +- Upstreaming both `aurora-mysql` and `aurora-mysql-resources` + +### why + +- Added option for allowing ingress by account name, rather than requiring CIDR blocks copy and pasted +- Replaced the deprecated provider for MySQL +- Resolved issues with Terraform perma-drift for the resources component with granting "ALL" + +### references + +- Old provider, archived: https://github.com/hashicorp/terraform-provider-mysql +- New provider: https://github.com/petoju/terraform-provider-mysql + +
+ +## 1.115.0 (2023-02-07T00:49:59Z) + +
+ Upstream `aurora-postgres` @milldr (#518) + +### what + +- Upstreaming `aurora-postgres` and `aurora-postgres-resources` + +### why + +- TLC for these components +- Added options for adding ingress by account +- Cleaned up the submodule for the resources component +- Support creating schemas +- Support conditionally pulling passwords from SSM, similar to `aurora-mysql` + +
+ +## 1.114.0 (2023-02-06T17:09:31Z) + +
+ `datadog-private-locations` update helm provider @Benbentwo (#549) + +### what + +- Updates Helm Provider to the latest + +### why + +- New API Version + +
+ +## 1.113.0 (2023-02-06T02:26:22Z) + +
+ Remove extra var from stack example @johncblandii (#550) + +### what + +- Stack example has an old variable defined + +### why + +- `The root module does not declare a variable named "eks_tags_enabled" but a value was found in file "uw2-automation-vpc.terraform.tfvars.json".` + +### references + +
+ +## 1.112.1 (2023-02-03T20:00:09Z) + +### 🚀 Enhancements + +
+ Fixed non-html tags that fails rendering on docusaurus @zdmytriv (#546) + +### what + +- Fixed non-html tags + +### why + +- Rendering has been failing on docusaurus mdx/jsx engine + +
+ +## 1.112.0 (2023-02-03T19:02:57Z) + +
+ `datadog-agent` allow values var merged @Benbentwo (#548) + +### what + +- Allows values to be passed in and merged to values file + +### why + +- Need to be able to easily override values files + +
+ +## 1.111.0 (2023-01-31T23:02:57Z) + +
+ Update echo and alb-controller-ingress-group @Benbentwo (#547) + +### what + +- Allows target group to be targeted by echo server + +
+ +## 1.110.0 (2023-01-26T00:25:13Z) + +
+ Chore/acme/bootcamp core tenant @dudymas (#543) + +### what + +- upgrade the vpn module in the ec2-client-vpn component +- and protect outputs on ec2-client-vpn + +### why + +- saml docs were broken in refarch-scaffold. module was trying to alter the cert provider + +
+ +## 1.109.0 (2023-01-24T20:01:56Z) + +
+ Chore/acme/bootcamp spacelift @dudymas (#545) + +### what + +- adjust the type of context_filters in spacelift + +### why + +- was getting errors trying to apply spacelift component + +
+ +## 1.108.0 (2023-01-20T22:36:54Z) + +
+ EC2 Client VPN Version Bump @Benbentwo (#544) + +### what + +- Bump Version of EC2 Client VPN + +### why + +- Bugfixes issue with TLS provider + +### references + +- https://github.com/cloudposse/terraform-aws-ec2-client-vpn/pull/58 +- https://github.com/cloudposse/terraform-aws-ssm-tls-self-signed-cert/pull/20 + +
+ +## 1.107.0 (2023-01-19T17:34:33Z) + +
+ Update pod security context schema in cert-manager @max-lobur (#538) + +### what + +Pod security context `enabled` field has been deprecated. Now you just specify the options and that's it. Update the +options per recent schema. See references + +Tested on k8s 1.24 + +### why + +- Otherwise it does not pass Deployment validation on newer clusters. + +### references + +https://github.com/cert-manager/cert-manager/commit/c17b11fa01455eb1b83dce0c2c06be555e4d53eb + +
+ +## 1.106.0 (2023-01-18T15:36:52Z) + +
+ Fix github actions runner controller default variables @max-lobur (#542) + +### what + +Default value for string is null, not false + +### why + +- Otherwise this does not pass schema when you deploy it without storage requests + +
+ +## 1.105.0 (2023-01-18T15:24:11Z) + +
+ Update k8s metrics-server to latest @max-lobur (#537) + +### what + +Upgrade metrics-server Tested on k8s 1.24 via `kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes"` + +### why + +- The previous one was so old that bitnami has even removed the chart. + +
+ +## 1.104.0 (2023-01-18T14:52:58Z) + +
+ Pin kubernetes provider in metrics-server @max-lobur (#541) + +### what + +- Pin the k8s provider version +- Update versions + +### why + +- Fix CI + +### references + +- https://github.com/cloudposse/terraform-aws-components/pull/537 + +
+ +## 1.103.0 (2023-01-17T21:09:56Z) + +
+ fix(dns-primary/acm): include zone_name arg @dudymas (#540) + +### what + +- in dns-primary, revert version of acm module 0.17.0 -> 0.16.2 (17 is a preview) + +### why + +- primary zones must be specified now that names are trimmed before the dot (.) + +
+ +## 1.102.0 (2023-01-17T16:09:59Z) + +
+ Fix typo in karpenter-provisioner @max-lobur (#539) + +### what + +I formatted it last moment and did not notice that actually changed the object. Fixing that and reformatting all of it +so it's more obvious for future maintainers. + +### why + +- Fixing bug + +### references + +https://github.com/cloudposse/terraform-aws-components/pull/536 + +
+ +## 1.101.0 (2023-01-17T07:47:30Z) + +
+ Support setting consolidation in karpenter-provisioner @max-lobur (#536) + +### what + +This is an alternative way of deprovisioning - proactive one. + +``` +There is another way to configure Karpenter to deprovision nodes called Consolidation. +This mode is preferred for workloads such as microservices and is incompatible with setting +up the ttlSecondsAfterEmpty . When set in consolidation mode Karpenter works to actively +reduce cluster cost by identifying when nodes can be removed as their workloads will run +on other nodes in the cluster and when nodes can be replaced with cheaper variants due +to a change in the workloads +``` + +### why + +- To let users set a more aggressive deprovisioning strategy + +### references + +- https://ec2spotworkshops.com/karpenter/050_karpenter/consolidation.html + +
+ +## 1.100.0 (2023-01-17T07:41:58Z) + +
+ Sync karpenter chart values with the schema @max-lobur (#535) + +### what + +Based on +https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml#L129-L142 +all these should go under settings.aws + +### why + +Ensure compatibility with the new charts + +### references + +Based on https://github.com/aws/karpenter/blob/92b3d4a0b029cae6a9d6536517ba42d70c3ebf8c/charts/karpenter/values.yaml + +
+ +## 1.99.0 (2023-01-13T14:59:16Z) + +
+ fix(aws-sso): dont hardcode account name for root @dudymas (#534) + +### what + +- remove hardcoding for root account moniker +- change default tenant from `gov` to `core` (now convention) + +### why + +- tenant is not included in the account prefix. In this case, changed to be 'core' +- most accounts do not use `gov` as the root tenant + +
+ +## 1.98.0 (2023-01-12T00:12:36Z) + +
+ Bump spacelift to latest @nitrocode (#532) + +### what + +- Bump spacelift to latest + +### why + +- Latest + +### references + +N/A + +
+ +## 1.97.0 (2023-01-11T01:16:33Z) + +
+ Upstream EKS Action Runner Controller @milldr (#528) + +### what + +- Upstreaming the latest additions for the EKS actions runner controller component + +### why + +- We've added additional features for the ARC runners, primarily adding options for ephemeral storage and persistent + storage. Persistent storage can be used to add image caching with EFS +- Allow for setting a `webhook_startup_timeout` value different than `scale_down_delay_seconds`. Defaults to + `scale_down_delay_seconds` + +### references + +- N/A + +
+ +## 1.96.0 (2023-01-05T21:19:22Z) + +
+ Datadog Upstreams and Account Settings @Benbentwo (#533) + +### what + +- Datadog Upgrades (Bugfixes for Configuration on default datadog URL) +- Account Settings Fixes for emoji support and updated budgets + +### why + +- Upstreams + +
+ +## 1.95.0 (2023-01-04T23:44:35Z) + +
+ fix(aws-sso): add missing tf update perms @dudymas (#530) + +### what + +- Changes for supporting [Refarch Scaffold](github.com/cloudposse/refarch-scaffold) +- TerraformUpdateAccess permission set added + +### why + +- Allow SSO users to update dynamodb/s3 for terraform backend + +
+ +## 1.94.0 (2022-12-21T18:38:15Z) + +
+ upstream `spacelift` @Benbentwo (#526) + +### what + +- Updated Spacelift Component to latest +- Updated README with new example + +### why + +- Upstreams + +
+ +## 1.93.0 (2022-12-21T18:37:37Z) + +
+ upstream `ecs` & `ecs-service` @Benbentwo (#529) + +### what + +- upstream + - `ecs` + - `ecs-service` + +### why + +- `enabled` flag correctly destroys resources +- bugfixes and improvements +- datadog support for ecs services + +
+ +## 1.92.0 (2022-12-21T18:36:35Z) + +
+ Upstream Datadog @Benbentwo (#525) + +### what + +- Datadog updates +- New `datadog-configuration` component for setting up share functions and making codebase more dry + +
+ +## 1.91.0 (2022-11-29T17:17:58Z) + +
+ CPLIVE-320: Set VPC to use region-less AZs @nitrocode (#524) + +### what + +- Set VPC to use region-less AZs + +### why + +- Prevent having to set VPC AZs within global region defaults + +### references + +- CPLIVE-320 + +
+ +## 1.90.2 (2022-11-20T05:41:14Z) + +### 🚀 Enhancements + +
+ Use cloudposse/template for arm support @nitrocode (#510) + +### what + +- Use cloudposse/template for arm support + +### why + +- The new cloudposse/template provider has a darwin arm binary for M1 laptops + +### references + +- https://github.com/cloudposse/terraform-provider-template +- https://registry.terraform.io/providers/cloudposse/template/latest + +
+ +## 1.90.1 (2022-10-31T13:27:37Z) + +### 🚀 Enhancements + +
+ Allow vpc-peering to peer v2 to v2 @nitrocode (#521) + +### what + +- Allow vpc-peering to peer v2 to v2 + +### why + +- Alternative to transit gateway + +### references + +N/A + +
+ +## 1.90.0 (2022-10-31T13:24:38Z) + +
+ Upstream iam-role component @nitrocode (#520) + +### what + +- Upstream iam-role component + +### why + +- Create simple IAM roles + +### references + +- https://github.com/cloudposse/terraform-aws-iam-role + +
+ +## 1.89.0 (2022-10-28T15:35:38Z) + +
+ [eks/actions-runner-controller] Auth via GitHub App, prefer webhook auto-scaling @Nuru (#519) + +### what + +- Support and prefer authentication via GitHub app +- Support and prefer webhook-based autoscaling + +### why + +- GitHub app is much more restricted, plus has higher API rate limits +- Webhook-based autoscaling is proactive without being overly expensive + +
+ +## 1.88.0 (2022-10-24T15:40:47Z) + +
+ Upstream iam-service-linked-roles @nitrocode (#516) + +### what + +- Upstream iam-service-linked-roles (thanks to @aknysh for writing it) + +### why + +- Centralized component to create IAM service linked roles + +### references + +- N/A + +
+ +## 1.87.0 (2022-10-22T19:12:36Z) + +
+ Add account-quotas component @Nuru (#515) + +### what + +- Add `account-quotas` component to manage account service quota increase requests + +### why + +- Add service quotas to the infrastructure that can be represented in code + +### notes + +Cloud Posse has a [service quotas module](https://github.com/cloudposse/terraform-aws-service-quotas), but it has +issues, such as not allowing the service to be specified by name, and not having well documented inputs. It also takes a +list input, but Atmos does not merge lists, so a map input is more appropriate. Overall I like this component better, +and if others do, too, I will replace the existing module (only at version 0.1.0) with this code. + +
+ +## 1.86.0 (2022-10-19T07:28:11Z) + +
+ Update EKS basic components @Nuru (#509) + +### what && why + +Update EKS cluster and basic Kubernetes components for better behavior on initial deployment and on `terraform destroy`. + +- Update minimum Terraform version to 1.1.0 and use `one()` where applicable to manage resources that can be disabled + with `count = 0` and for bug fixes regarding destroy behavior +- Update `terraform-aws-eks-cluster` to v2.5.0 for better destroy behavior +- Update all components' (plus `account-map/modules/`)`remote-state` to v1.2.0 for better destroy behavior +- Update all components' `helm-release` to v0.7.0 and move namespace creation via Kubernetes provider into it to avoid + race conditions regarding creating IAM roles, Namespaces, and deployments, and to delete namespaces when destroyed +- Update `alb-controller` to deploy a default IngressClass for central, obvious configuration of shared default ingress + for services that do not have special needs. +- Add `alb-controller-ingress-class` for the rare case when we want to deploy a non-default IngressClass outside of the + component that will be using it +- Update `echo-server` to use the default IngressClass and not specify any configuration that affects other Ingresses, + and remove dependence on `alb-controller-ingress-group` (which should be deprecated in favor of + `alb-controller-ingress-class` and perhaps a specialized future `alb-controller-ingress`) +- Update `cert-manager` to remove `default.auto.tfvars` (which had a lot of settings) and add dependencies so that + initial deployment succeeds in one `terraform apply` and destroy works in one `terraform destroy` +- Update `external-dns` to remove `default.auto.tfvars` (which had a lot of settings) +- Update `karpenter` to v0.18.0, fix/update IAM policy (README still needs work, but leaving that for another day) +- Update `karpenter-provisioner` to require Terraform 1.3 and make elements of the Provisioner configuration optional. + Support block device mappings (previously broken). Avoid perpetual Terraform plan diff/drift caused by setting fields + to `null`. +- Update `reloader` +- Update `mixins/provider-helm` to better support `terraform destroy` and to default the Kubernetes client + authentication API version to `client.authentication.k8s.io/v1beta1` + +### references + +- https://github.com/cloudposse/terraform-aws-helm-release/pull/34 +- https://github.com/cloudposse/terraform-aws-eks-cluster/pull/169 +- https://github.com/cloudposse/terraform-yaml-stack-config/pull/56 +- https://github.com/hashicorp/terraform/issues/32023 + +
+ +## 1.85.0 (2022-10-18T00:05:19Z) + +
+ Upstream `github-runners` @milldr (#508) + +### what + +- Minor TLC updates for GitHub Runners ASG component + +### why + +- Maintaining up-to-date upstream + +
+ +## 1.84.0 (2022-10-12T22:49:28Z) + +
+ Fix feature allowing IAM users to assume team roles @Nuru (#507) + +### what + +- Replace `deny_all_iam_users` input with `iam_users_enabled` +- Fix implementation +- Provide more context for `bats` test failures + +### why + +- Cloud Posse style guide dictates that boolean feature flags have names ending with `_enabled` +- Previous implementation only removed 1 of 2 policy provisions that blocked IAM users from assuming a role, and + therefore IAM users were still not allowed to assume a role. Since the previous implementation did not work, a + breaking change (changing the variable name) does not need major warnings or a major version bump. +- Indication of what was being tested was too far removed from `bats` test failure message to be able to easily identify + what module had failed + +### notes + +Currently, any component provisioned by SuperAdmin needs to have a special provider configuration that requires +SuperAdmin to provision the component. This feature is part of what is needed to enable SuperAdmin (an IAM User) to work +with "normal" provider configurations. + +### references + +- Breaks change introduced in #495, but that didn't work anyway. + +
From 3e47d0f5b2abf4fc37fdfbf39657d7077ef68581 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 1 Oct 2024 19:49:42 +0200 Subject: [PATCH 495/501] upstream `tailscale` (#835) Co-authored-by: Jeremy White Co-authored-by: Igor Rodionov --- modules/eks/tailscale/README.md | 124 +++++++++ modules/eks/tailscale/context.tf | 279 +++++++++++++++++++ modules/eks/tailscale/main.tf | 265 ++++++++++++++++++ modules/eks/tailscale/outputs.tf | 4 + modules/eks/tailscale/provider-kubernetes.tf | 132 +++++++++ modules/eks/tailscale/providers.tf | 29 ++ modules/eks/tailscale/remote-state.tf | 20 ++ modules/eks/tailscale/variables.tf | 63 +++++ modules/eks/tailscale/versions.tf | 14 + 9 files changed, 930 insertions(+) create mode 100644 modules/eks/tailscale/README.md create mode 100644 modules/eks/tailscale/context.tf create mode 100644 modules/eks/tailscale/main.tf create mode 100644 modules/eks/tailscale/outputs.tf create mode 100644 modules/eks/tailscale/provider-kubernetes.tf create mode 100644 modules/eks/tailscale/providers.tf create mode 100644 modules/eks/tailscale/remote-state.tf create mode 100644 modules/eks/tailscale/variables.tf create mode 100644 modules/eks/tailscale/versions.tf diff --git a/modules/eks/tailscale/README.md b/modules/eks/tailscale/README.md new file mode 100644 index 000000000..74866021b --- /dev/null +++ b/modules/eks/tailscale/README.md @@ -0,0 +1,124 @@ +# Component: eks/tailscale + +## Usage + +**Stack Level**: Regional + +Use this in the catalog or use these variables to overwrite the catalog values. + +```yaml +components: + terraform: + eks/tailscale: + vars: + enabled: true + name: tailscale + create_namespace: true + kubernetes_namespace: "tailscale" + image_repo: tailscale/k8s-operator + image_tag: unstable +``` + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.7.1 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.7.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [store\_read](#module\_store\_read) | cloudposse/ssm-parameter-store/aws | 0.10.0 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_cluster_role.tailscale_operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/cluster_role) | resource | +| [kubernetes_cluster_role_binding.tailscale_operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/cluster_role_binding) | resource | +| [kubernetes_deployment.operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | +| [kubernetes_namespace.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_role.operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role) | resource | +| [kubernetes_role.proxies](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role) | resource | +| [kubernetes_role_binding.operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding) | resource | +| [kubernetes_role_binding.proxies](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding) | resource | +| [kubernetes_secret.operator_oauth](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | +| [kubernetes_service_account.operator](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account) | resource | +| [kubernetes_service_account.proxies](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account) | resource | +| [aws_eks_cluster.kubernetes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster) | data source | +| [aws_eks_cluster_auth.eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source | +| [aws_subnet.vpc_subnets](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart\_values](#input\_chart\_values) | Addition map values to yamlencode as `helm_release` values. | `any` | `{}` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false`. | `bool` | `false` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [deployment\_name](#input\_deployment\_name) | Name of the tailscale deployment, defaults to `tailscale` if this is null | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [eks\_component\_name](#input\_eks\_component\_name) | The name of the eks component | `string` | `"eks/cluster"` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [env](#input\_env) | Map of ENV vars in the format `key=value`. These ENV vars will be set in the `utils` provider before executing the data source | `map(string)` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `true` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [image\_repo](#input\_image\_repo) | Image repository for the deployment | `string` | `"ghcr.io/tailscale/tailscale"` | no | +| [image\_tag](#input\_image\_tag) | Image Tag for the deployment. | `string` | `"latest"` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kube\_secret](#input\_kube\_secret) | Kube Secret Name for tailscale | `string` | `"tailscale"` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes kube config file | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | The namespace to install the release into. | `string` | n/a | yes | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [routes](#input\_routes) | List of CIDR Ranges or IPs to allow Tailscale to connect to | `list(string)` | `[]` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [deployment](#output\_deployment) | Tail scale operator deployment K8S resource | + + + +## References + +- https://github.com/Ealenn/tailscale diff --git a/modules/eks/tailscale/context.tf b/modules/eks/tailscale/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/tailscale/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/tailscale/main.tf b/modules/eks/tailscale/main.tf new file mode 100644 index 000000000..f14108d47 --- /dev/null +++ b/modules/eks/tailscale/main.tf @@ -0,0 +1,265 @@ +locals { + enabled = module.this.enabled + create_namespace = local.enabled + + routes = join(",", concat(var.routes, [for k, v in data.aws_subnet.vpc_subnets : v.cidr_block])) +} + +module "store_read" { + source = "cloudposse/ssm-parameter-store/aws" + version = "0.10.0" + + parameter_read = [ + "/tailscale/client_id", + "/tailscale/client_secret", + ] +} + +resource "kubernetes_secret" "operator_oauth" { + metadata { + name = "operator-oauth" + namespace = var.kubernetes_namespace + } + data = { + client_id = module.store_read.map["/tailscale/client_id"] + client_secret = module.store_read.map["/tailscale/client_secret"] + } +} + +resource "kubernetes_namespace" "default" { + count = local.create_namespace ? 1 : 0 + + metadata { + name = var.kubernetes_namespace + + labels = module.this.tags + } +} + + +resource "kubernetes_service_account" "proxies" { + metadata { + name = "proxies" + namespace = var.kubernetes_namespace + } +} + +resource "kubernetes_role" "proxies" { + metadata { + name = "proxies" + namespace = var.kubernetes_namespace + } + + rule { + verbs = ["*"] + api_groups = [""] + resources = ["secrets"] + } +} + +resource "kubernetes_role_binding" "proxies" { + metadata { + name = "proxies" + namespace = var.kubernetes_namespace + } + + subject { + kind = "ServiceAccount" + name = "proxies" + namespace = var.kubernetes_namespace + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = "proxies" + } +} + +resource "kubernetes_service_account" "operator" { + metadata { + name = "operator" + namespace = var.kubernetes_namespace + } +} + +resource "kubernetes_cluster_role" "tailscale_operator" { + metadata { + name = "tailscale-operator" + } + + rule { + verbs = ["*"] + api_groups = [""] + resources = ["services", "services/status"] + } +} + +resource "kubernetes_cluster_role_binding" "tailscale_operator" { + metadata { + name = "tailscale-operator" + } + + subject { + kind = "ServiceAccount" + name = "operator" + namespace = var.kubernetes_namespace + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = "tailscale-operator" + } +} + +resource "kubernetes_role" "operator" { + metadata { + name = "operator" + namespace = var.kubernetes_namespace + } + + rule { + verbs = ["*"] + api_groups = [""] + resources = ["secrets"] + } + + rule { + verbs = ["*"] + api_groups = ["apps"] + resources = ["statefulsets"] + } +} + +resource "kubernetes_role_binding" "operator" { + metadata { + name = "operator" + namespace = var.kubernetes_namespace + } + + subject { + kind = "ServiceAccount" + name = "operator" + namespace = var.kubernetes_namespace + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = "operator" + } +} + +resource "kubernetes_deployment" "operator" { + metadata { + name = coalesce(var.deployment_name, "tailscale-operator") + namespace = var.kubernetes_namespace + labels = { + app = "tailscale" + } + } + + spec { + replicas = 1 + + selector { + match_labels = { + app = "operator" + } + } + + template { + metadata { + labels = { + app = "operator" + } + } + + spec { + volume { + name = "oauth" + + secret { + secret_name = "operator-oauth" + } + } + + container { + image = format("%s:%s", var.image_repo, var.image_tag) + name = "tailscale" + + env { + name = "OPERATOR_HOSTNAME" + value = format("%s-%s-%s-%s", "tailscale-operator", var.tenant, var.environment, var.stage) + } + + env { + name = "OPERATOR_SECRET" + value = "operator" + } + + env { + name = "OPERATOR_LOGGING" + value = "info" + } + + env { + name = "OPERATOR_NAMESPACE" + + value_from { + field_ref { + field_path = "metadata.namespace" + } + } + } + + env { + name = "CLIENT_ID_FILE" + value = "/oauth/client_id" + } + + env { + name = "CLIENT_SECRET_FILE" + value = "/oauth/client_secret" + } + + env { + name = "PROXY_IMAGE" + value = "tailscale/tailscale:unstable" + } + + env { + name = "PROXY_TAGS" + value = "tag:k8s" + } + + env { + name = "AUTH_PROXY" + value = "false" + } + + resources { + requests = { + cpu = "500m" + + memory = "100Mi" + } + } + + volume_mount { + name = "oauth" + read_only = true + mount_path = "/oauth" + } + } + + service_account_name = "operator" + } + } + + strategy { + type = "Recreate" + } + } +} diff --git a/modules/eks/tailscale/outputs.tf b/modules/eks/tailscale/outputs.tf new file mode 100644 index 000000000..811fe1ff4 --- /dev/null +++ b/modules/eks/tailscale/outputs.tf @@ -0,0 +1,4 @@ +output "deployment" { + value = kubernetes_deployment.operator + description = "Tail scale operator deployment K8S resource" +} diff --git a/modules/eks/tailscale/provider-kubernetes.tf b/modules/eks/tailscale/provider-kubernetes.tf new file mode 100644 index 000000000..00cfd1542 --- /dev/null +++ b/modules/eks/tailscale/provider-kubernetes.tf @@ -0,0 +1,132 @@ +################## +# +# This file is a drop-in to provide a helm provider. +# +# All the following variables are just about configuring the Kubernetes provider +# to be able to modify EKS cluster. The reason there are so many options is +# because at various times, each one of them has had problems, so we give you a choice. +# +# The reason there are so many "enabled" inputs rather than automatically +# detecting whether or not they are enabled based on the value of the input +# is that any logic based on input values requires the values to be known during +# the "plan" phase of Terraform, and often they are not, which causes problems. +# +variable "kubeconfig_file_enabled" { + type = bool + default = false + description = "If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster" +} + +variable "kubeconfig_file" { + type = string + default = "" + description = "The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true`" +} + +variable "kubeconfig_context" { + type = string + default = "" + description = "Context to choose from the Kubernetes kube config file" +} + +variable "kube_data_auth_enabled" { + type = bool + default = false + description = <<-EOT + If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. + EOT +} + +variable "kube_exec_auth_enabled" { + type = bool + default = true + description = <<-EOT + If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster. + Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. + EOT +} + +variable "kube_exec_auth_role_arn" { + type = string + default = "" + description = "The role ARN for `aws eks get-token` to use" +} + +variable "kube_exec_auth_role_arn_enabled" { + type = bool + default = true + description = "If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token`" +} + +variable "kube_exec_auth_aws_profile" { + type = string + default = "" + description = "The AWS config profile for `aws eks get-token` to use" +} + +variable "kube_exec_auth_aws_profile_enabled" { + type = bool + default = false + description = "If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token`" +} + +variable "kubeconfig_exec_auth_api_version" { + type = string + default = "client.authentication.k8s.io/v1beta1" + description = "The Kubernetes API version of the credentials returned by the `exec` auth plugin" +} + +variable "helm_manifest_experiment_enabled" { + type = bool + default = true + description = "Enable storing of the rendered manifest for helm_release so the full diff of what is changing can been seen in the plan" +} + +locals { + kubeconfig_file_enabled = var.kubeconfig_file_enabled + kube_exec_auth_enabled = local.kubeconfig_file_enabled ? false : var.kube_exec_auth_enabled + kube_data_auth_enabled = local.kube_exec_auth_enabled ? false : var.kube_data_auth_enabled + + # Eventually we might try to get this from an environment variable + kubeconfig_exec_auth_api_version = var.kubeconfig_exec_auth_api_version + + exec_profile = local.kube_exec_auth_enabled && var.kube_exec_auth_aws_profile_enabled ? [ + "--profile", var.kube_exec_auth_aws_profile + ] : [] + + kube_exec_auth_role_arn = coalesce(var.kube_exec_auth_role_arn, var.import_role_arn, module.iam_roles.terraform_role_arn) + exec_role = local.kube_exec_auth_enabled && var.kube_exec_auth_role_arn_enabled ? [ + "--role-arn", local.kube_exec_auth_role_arn + ] : [] + + certificate_authority_data = module.eks.outputs.eks_cluster_certificate_authority_data + eks_cluster_id = module.eks.outputs.eks_cluster_id + eks_cluster_endpoint = module.eks.outputs.eks_cluster_endpoint +} + +data "aws_eks_cluster_auth" "eks" { + count = local.kube_data_auth_enabled ? 1 : 0 + name = local.eks_cluster_id +} + +provider "kubernetes" { + host = local.eks_cluster_endpoint + cluster_ca_certificate = base64decode(local.certificate_authority_data) + token = local.kube_data_auth_enabled ? data.aws_eks_cluster_auth.eks[0].token : null + # The Kubernetes provider will use information from KUBECONFIG if it exists, but if the default cluster + # in KUBECONFIG is some other cluster, this will cause problems, so we override it always. + config_path = local.kubeconfig_file_enabled ? var.kubeconfig_file : "" + config_context = var.kubeconfig_context + + dynamic "exec" { + for_each = local.kube_exec_auth_enabled ? ["exec"] : [] + content { + api_version = local.kubeconfig_exec_auth_api_version + command = "aws" + args = concat(local.exec_profile, [ + "eks", "get-token", "--cluster-name", local.eks_cluster_id + ], local.exec_role) + } + } +} diff --git a/modules/eks/tailscale/providers.tf b/modules/eks/tailscale/providers.tf new file mode 100644 index 000000000..c2419aabb --- /dev/null +++ b/modules/eks/tailscale/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/eks/tailscale/remote-state.tf b/modules/eks/tailscale/remote-state.tf new file mode 100644 index 000000000..42155e076 --- /dev/null +++ b/modules/eks/tailscale/remote-state.tf @@ -0,0 +1,20 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = var.eks_component_name + + context = module.this.context +} + +data "aws_eks_cluster" "kubernetes" { + count = local.enabled ? 1 : 0 + + name = module.eks.outputs.eks_cluster_id +} + +data "aws_subnet" "vpc_subnets" { + for_each = local.enabled ? data.aws_eks_cluster.kubernetes[0].vpc_config[0].subnet_ids : [] + + id = each.value +} diff --git a/modules/eks/tailscale/variables.tf b/modules/eks/tailscale/variables.tf new file mode 100644 index 000000000..90be9cc2a --- /dev/null +++ b/modules/eks/tailscale/variables.tf @@ -0,0 +1,63 @@ +variable "region" { + type = string + description = "AWS Region" +} + +variable "eks_component_name" { + type = string + description = "The name of the eks component" + default = "eks/cluster" +} + +variable "chart_values" { + type = any + description = "Addition map values to yamlencode as `helm_release` values." + default = {} +} + +variable "deployment_name" { + type = string + description = "Name of the tailscale deployment, defaults to `tailscale` if this is null" + default = null +} + +variable "image_repo" { + type = string + description = "Image repository for the deployment" + default = "ghcr.io/tailscale/tailscale" +} + +variable "image_tag" { + type = string + description = "Image Tag for the deployment." + default = "latest" +} + +variable "create_namespace" { + type = bool + description = "Create the namespace if it does not yet exist. Defaults to `false`." + default = false +} + +variable "kubernetes_namespace" { + type = string + description = "The namespace to install the release into." +} + +variable "kube_secret" { + type = string + description = "Kube Secret Name for tailscale" + default = "tailscale" +} + +variable "routes" { + type = list(string) + description = "List of CIDR Ranges or IPs to allow Tailscale to connect to" + default = [] +} + +variable "env" { + type = map(string) + description = "Map of ENV vars in the format `key=value`. These ENV vars will be set in the `utils` provider before executing the data source" + default = null +} diff --git a/modules/eks/tailscale/versions.tf b/modules/eks/tailscale/versions.tf new file mode 100644 index 000000000..8b70f9f52 --- /dev/null +++ b/modules/eks/tailscale/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.7.1" + } + } +} From 34ee0a94354d7543c6904928c3abdbce9aae8284 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 1 Oct 2024 20:56:22 +0200 Subject: [PATCH 496/501] Upstream `redshift-serverless` (#838) Co-authored-by: Igor Rodionov --- modules/redshift-serverless/README.md | 135 ++++++++++ modules/redshift-serverless/context.tf | 279 ++++++++++++++++++++ modules/redshift-serverless/main.tf | 109 ++++++++ modules/redshift-serverless/outputs.tf | 70 +++++ modules/redshift-serverless/providers.tf | 29 ++ modules/redshift-serverless/remote-state.tf | 8 + modules/redshift-serverless/ssm.tf | 30 +++ modules/redshift-serverless/variables.tf | 136 ++++++++++ modules/redshift-serverless/versions.tf | 14 + 9 files changed, 810 insertions(+) create mode 100644 modules/redshift-serverless/README.md create mode 100644 modules/redshift-serverless/context.tf create mode 100644 modules/redshift-serverless/main.tf create mode 100644 modules/redshift-serverless/outputs.tf create mode 100644 modules/redshift-serverless/providers.tf create mode 100644 modules/redshift-serverless/remote-state.tf create mode 100644 modules/redshift-serverless/ssm.tf create mode 100644 modules/redshift-serverless/variables.tf create mode 100644 modules/redshift-serverless/versions.tf diff --git a/modules/redshift-serverless/README.md b/modules/redshift-serverless/README.md new file mode 100644 index 000000000..8e0ef6092 --- /dev/null +++ b/modules/redshift-serverless/README.md @@ -0,0 +1,135 @@ +# Component: `redshift` + +This component is responsible for provisioning Redshift clusters. + +## Usage + +**Stack Level**: Regional + +Here are some example snippets for how to use this component: + +```yaml +components: + terraform: + redshift: + settings: + spacelift: + workspace_enabled: true + vars: + enabled: true + name: redshift + admin_user: admin + database_name: dev + +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0.0 | +| [aws](#requirement\_aws) | ~> 4.0 | +| [random](#requirement\_random) | ~> 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | ~> 4.0 | +| [random](#provider\_random) | ~> 3.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [iam\_roles](#module\_iam\_roles) | ../account-map/modules/iam-roles | n/a | +| [redshift\_sg](#module\_redshift\_sg) | cloudposse/security-group/aws | 2.0.0-rc1 | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | +| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 1.4.1 | + +## Resources + +| Name | Type | +|------|------| +| [aws_redshiftserverless_endpoint_access.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/redshiftserverless_endpoint_access) | resource | +| [aws_redshiftserverless_namespace.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/redshiftserverless_namespace) | resource | +| [aws_redshiftserverless_workgroup.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/redshiftserverless_workgroup) | resource | +| [aws_ssm_parameter.admin_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.admin_user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [aws_ssm_parameter.endpoint](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | +| [random_password.admin_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | +| [random_pet.admin_user](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [admin\_password](#input\_admin\_password) | Password for the master DB user. Required unless a snapshot\_identifier is provided | `string` | `null` | no | +| [admin\_user](#input\_admin\_user) | Username for the master DB user. Required unless a snapshot\_identifier is provided | `string` | `null` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [base\_capacity](#input\_base\_capacity) | The base data warehouse capacity of the workgroup in Redshift Processing Units (RPUs). | `number` | `128` | no | +| [config\_parameter](#input\_config\_parameter) | A list of Redshift config parameters to apply to the workgroup. |
list(object({
parameter_key = string
parameter_value = any
}))
| `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [custom\_sg\_allow\_all\_egress](#input\_custom\_sg\_allow\_all\_egress) | Whether to allow all egress traffic or not | `bool` | `true` | no | +| [custom\_sg\_enabled](#input\_custom\_sg\_enabled) | Whether to use custom security group or not | `bool` | `false` | no | +| [custom\_sg\_rules](#input\_custom\_sg\_rules) | n/a |
list(object({
key = string
type = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
| `[]` | no | +| [database\_name](#input\_database\_name) | The name of the first database to be created when the cluster is created | `string` | `null` | no | +| [default\_iam\_role\_arn](#input\_default\_iam\_role\_arn) | The Amazon Resource Name (ARN) of the IAM role to set as a default in the namespace | `string` | `null` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [endpoint\_name](#input\_endpoint\_name) | Endpoint name for the redshift endpoint, if null, is set to $stage-$name | `string` | `null` | no | +| [enhanced\_vpc\_routing](#input\_enhanced\_vpc\_routing) | The value that specifies whether to turn on enhanced virtual private cloud (VPC) routing, which forces Amazon Redshift Serverless to route traffic through your VPC instead of over the internet. | `bool` | `true` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [iam\_roles](#input\_iam\_roles) | A list of IAM roles to associate with the namespace. | `list(string)` | `[]` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [import\_profile\_name](#input\_import\_profile\_name) | AWS Profile name to use when importing a resource | `string` | `null` | no | +| [import\_role\_arn](#input\_import\_role\_arn) | IAM Role ARN to use when importing a resource | `string` | `null` | no | +| [kms\_alias\_name\_ssm](#input\_kms\_alias\_name\_ssm) | KMS alias name for SSM | `string` | `"alias/aws/ssm"` | no | +| [kms\_key\_id](#input\_kms\_key\_id) | The ARN of the Amazon Web Services Key Management Service key used to encrypt your data. | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [log\_exports](#input\_log\_exports) | The types of logs the namespace can export. Available export types are `userlog`, `connectionlog`, and `useractivitylog`. | `set(string)` | `[]` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS region | `string` | n/a | yes | +| [security\_group\_ids](#input\_security\_group\_ids) | An array of security group IDs to associate with the endpoint. | `list(string)` | `null` | no | +| [ssm\_path\_prefix](#input\_ssm\_path\_prefix) | SSM path prefix (without leading or trailing slash) | `string` | `"redshift"` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [use\_private\_subnets](#input\_use\_private\_subnets) | Whether to use private or public subnets for the Redshift cluster | `bool` | `true` | no | +| [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | An array of security group IDs to associate with the workgroup. | `list(string)` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [endpoint\_address](#output\_endpoint\_address) | The DNS address of the VPC endpoint. | +| [endpoint\_arn](#output\_endpoint\_arn) | Amazon Resource Name (ARN) of the Redshift Serverless Endpoint Access. | +| [endpoint\_id](#output\_endpoint\_id) | The Redshift Endpoint Access Name. | +| [endpoint\_name](#output\_endpoint\_name) | Endpoint Name. | +| [endpoint\_port](#output\_endpoint\_port) | The port that Amazon Redshift Serverless listens on. | +| [endpoint\_subnet\_ids](#output\_endpoint\_subnet\_ids) | Subnets used in redshift serverless endpoint. | +| [endpoint\_vpc\_endpoint](#output\_endpoint\_vpc\_endpoint) | The VPC endpoint or the Redshift Serverless workgroup. See VPC Endpoint below. | +| [namespace\_arn](#output\_namespace\_arn) | Amazon Resource Name (ARN) of the Redshift Serverless Namespace. | +| [namespace\_id](#output\_namespace\_id) | The Redshift Namespace Name. | +| [namespace\_namespace\_id](#output\_namespace\_namespace\_id) | The Redshift Namespace ID. | +| [workgroup\_arn](#output\_workgroup\_arn) | Amazon Resource Name (ARN) of the Redshift Serverless Workgroup. | +| [workgroup\_endpoint](#output\_workgroup\_endpoint) | The Redshift Serverless Endpoint. | +| [workgroup\_id](#output\_workgroup\_id) | The Redshift Workgroup Name. | +| [workgroup\_workgroup\_id](#output\_workgroup\_workgroup\_id) | The Redshift Workgroup ID. | + + + +## References + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/redshift) - Cloud Posse's upstream component + + +[](https://cpco.io/component) diff --git a/modules/redshift-serverless/context.tf b/modules/redshift-serverless/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/redshift-serverless/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/redshift-serverless/main.tf b/modules/redshift-serverless/main.tf new file mode 100644 index 000000000..168b40eaf --- /dev/null +++ b/modules/redshift-serverless/main.tf @@ -0,0 +1,109 @@ + +locals { + enabled = module.this.enabled + subnet_ids = var.use_private_subnets ? module.vpc.outputs.private_subnet_ids : module.vpc.outputs.public_subnet_ids + admin_user = var.admin_user != null && var.admin_user != "" ? var.admin_user : join("", random_pet.admin_user.*.id) + admin_password = var.admin_password != null && var.admin_password != "" ? var.admin_password : join("", random_password.admin_password.*.result) +} + +resource "random_pet" "admin_user" { + count = local.enabled && (var.admin_user == null || var.admin_user == "") ? 1 : 0 + + length = 2 + separator = "_" + + keepers = { + db_name = var.database_name + } +} + +resource "random_password" "admin_password" { + count = local.enabled && (var.admin_password == null || var.admin_password == "") ? 1 : 0 + + length = 33 + # Leave special characters out to avoid quoting and other issues. + # Special characters have no additional security compared to increasing length. + special = false + override_special = "!#$%^&*()<>-_" + + keepers = { + db_name = var.database_name + } +} + +module "redshift_sg" { + count = local.enabled && var.custom_sg_enabled ? 1 : 0 + + source = "cloudposse/security-group/aws" + version = "2.0.0-rc1" + + create_before_destroy = true + preserve_security_group_id = true + + attributes = ["redshift"] + + # Allow unlimited egress + allow_all_egress = var.custom_sg_allow_all_egress + + rules = var.custom_sg_rules + + vpc_id = module.vpc.outputs.vpc_id + + context = module.this.context +} + + +resource "aws_redshiftserverless_workgroup" "default" { + count = local.enabled ? 1 : 0 + + namespace_name = aws_redshiftserverless_namespace.default[0].namespace_name + + depends_on = [ + aws_redshiftserverless_namespace.default[0] + ] + + workgroup_name = module.this.id + + base_capacity = var.base_capacity + enhanced_vpc_routing = var.enhanced_vpc_routing + publicly_accessible = var.publicly_accessible + security_group_ids = coalesce(var.security_group_ids, module.redshift_sg[*].id, []) + subnet_ids = local.subnet_ids + + dynamic "config_parameter" { + for_each = var.config_parameter + content { + parameter_key = config_parameter.key + parameter_value = config_parameter.value + } + } + tags = module.this.tags + +} + +resource "aws_redshiftserverless_namespace" "default" { + count = local.enabled ? 1 : 0 + + namespace_name = module.this.id + + admin_user_password = local.admin_password + admin_username = local.admin_user + db_name = var.database_name + default_iam_role_arn = var.default_iam_role_arn + iam_roles = var.iam_roles + kms_key_id = var.kms_key_id + log_exports = var.log_exports + + tags = var.tags +} + + +resource "aws_redshiftserverless_endpoint_access" "default" { + count = local.enabled ? 1 : 0 + + workgroup_name = aws_redshiftserverless_workgroup.default[0].workgroup_name + + endpoint_name = var.endpoint_name == null ? format("%s-%s", module.this.stage, module.this.name) : var.endpoint_name + subnet_ids = local.subnet_ids + vpc_security_group_ids = var.vpc_security_group_ids != null ? var.vpc_security_group_ids : [module.vpc.outputs.vpc_default_security_group_id] +} diff --git a/modules/redshift-serverless/outputs.tf b/modules/redshift-serverless/outputs.tf new file mode 100644 index 000000000..15d07032d --- /dev/null +++ b/modules/redshift-serverless/outputs.tf @@ -0,0 +1,70 @@ +output "endpoint_arn" { + description = "Amazon Resource Name (ARN) of the Redshift Serverless Endpoint Access." + value = join("", aws_redshiftserverless_endpoint_access.default[*].arn) +} + +output "endpoint_id" { + description = "The Redshift Endpoint Access Name." + value = join("", aws_redshiftserverless_endpoint_access.default[*].id) +} + +output "endpoint_address" { + description = "The DNS address of the VPC endpoint." + value = join("", aws_redshiftserverless_endpoint_access.default[*].address) +} + +output "endpoint_port" { + description = "The port that Amazon Redshift Serverless listens on." + value = join("", aws_redshiftserverless_endpoint_access.default[*].port) +} + +output "endpoint_vpc_endpoint" { + description = "The VPC endpoint or the Redshift Serverless workgroup. See VPC Endpoint below." + value = aws_redshiftserverless_endpoint_access.default[0].vpc_endpoint + # value = join("", aws_redshiftserverless_endpoint_access.default[*].vpc_endpoint) +} + +output "endpoint_name" { + description = "Endpoint Name." + value = join("", aws_redshiftserverless_endpoint_access.default[*].endpoint_name) +} + +output "endpoint_subnet_ids" { + description = "Subnets used in redshift serverless endpoint." + value = aws_redshiftserverless_endpoint_access.default[0].subnet_ids +} + +output "namespace_arn" { + description = "Amazon Resource Name (ARN) of the Redshift Serverless Namespace." + value = join("", aws_redshiftserverless_namespace.default[*].arn) +} + +output "namespace_id" { + description = "The Redshift Namespace Name." + value = join("", aws_redshiftserverless_namespace.default[*].id) +} + +output "namespace_namespace_id" { + description = "The Redshift Namespace ID." + value = join("", aws_redshiftserverless_namespace.default[*].namespace_id) +} + +output "workgroup_arn" { + description = "Amazon Resource Name (ARN) of the Redshift Serverless Workgroup." + value = join("", aws_redshiftserverless_workgroup.default[*].arn) +} + +output "workgroup_id" { + description = "The Redshift Workgroup Name." + value = join("", aws_redshiftserverless_workgroup.default[*].id) +} + +output "workgroup_workgroup_id" { + description = "The Redshift Workgroup ID." + value = join("", aws_redshiftserverless_workgroup.default[*].workgroup_id) +} + +output "workgroup_endpoint" { + description = "The Redshift Serverless Endpoint." + value = aws_redshiftserverless_workgroup.default[0].endpoint +} diff --git a/modules/redshift-serverless/providers.tf b/modules/redshift-serverless/providers.tf new file mode 100644 index 000000000..08ee01b2a --- /dev/null +++ b/modules/redshift-serverless/providers.tf @@ -0,0 +1,29 @@ +provider "aws" { + region = var.region + + profile = module.iam_roles.profiles_enabled ? coalesce(var.import_profile_name, module.iam_roles.terraform_profile_name) : null + + dynamic "assume_role" { + for_each = module.iam_roles.profiles_enabled ? [] : ["role"] + content { + role_arn = coalesce(var.import_role_arn, module.iam_roles.terraform_role_arn) + } + } +} + +module "iam_roles" { + source = "../account-map/modules/iam-roles" + context = module.this.context +} + +variable "import_profile_name" { + type = string + default = null + description = "AWS Profile name to use when importing a resource" +} + +variable "import_role_arn" { + type = string + default = null + description = "IAM Role ARN to use when importing a resource" +} diff --git a/modules/redshift-serverless/remote-state.tf b/modules/redshift-serverless/remote-state.tf new file mode 100644 index 000000000..3e0ccd51e --- /dev/null +++ b/modules/redshift-serverless/remote-state.tf @@ -0,0 +1,8 @@ +module "vpc" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.4.1" + + component = "vpc" + + context = module.this.context +} diff --git a/modules/redshift-serverless/ssm.tf b/modules/redshift-serverless/ssm.tf new file mode 100644 index 000000000..5a4051d70 --- /dev/null +++ b/modules/redshift-serverless/ssm.tf @@ -0,0 +1,30 @@ +resource "aws_ssm_parameter" "admin_user" { + count = local.enabled ? 1 : 0 + + name = format("/%s/%s", var.ssm_path_prefix, "admin_user") + value = local.admin_user + description = "Redshift cluster admin username" + type = "String" + overwrite = true +} + +resource "aws_ssm_parameter" "admin_password" { + count = local.enabled ? 1 : 0 + + name = format("/%s/%s", var.ssm_path_prefix, "admin_password") + value = local.admin_password + description = "Redshift cluster admin password" + type = "SecureString" + key_id = var.kms_alias_name_ssm + overwrite = true +} + +resource "aws_ssm_parameter" "endpoint" { + count = local.enabled ? 1 : 0 + + name = format("/%s/%s", var.ssm_path_prefix, "endpoint") + value = aws_redshiftserverless_workgroup.default[0].endpoint[0].address + description = "Redshift endpoint address" + type = "String" + overwrite = true +} diff --git a/modules/redshift-serverless/variables.tf b/modules/redshift-serverless/variables.tf new file mode 100644 index 000000000..e33765419 --- /dev/null +++ b/modules/redshift-serverless/variables.tf @@ -0,0 +1,136 @@ +variable "region" { + type = string + description = "AWS region" +} + +variable "admin_user" { + type = string + default = null + description = "Username for the master DB user. Required unless a snapshot_identifier is provided" +} + +variable "admin_password" { + type = string + default = null + description = "Password for the master DB user. Required unless a snapshot_identifier is provided" +} + +variable "database_name" { + type = string + default = null + description = "The name of the first database to be created when the cluster is created" +} + +variable "default_iam_role_arn" { + type = string + default = null + description = "The Amazon Resource Name (ARN) of the IAM role to set as a default in the namespace" +} + +variable "iam_roles" { + type = list(string) + default = [] + description = "A list of IAM roles to associate with the namespace." +} + +variable "kms_key_id" { + type = string + default = null + description = "The ARN of the Amazon Web Services Key Management Service key used to encrypt your data." +} + +variable "log_exports" { + type = set(string) + default = [] + description = "The types of logs the namespace can export. Available export types are `userlog`, `connectionlog`, and `useractivitylog`." +} + +variable "use_private_subnets" { + type = bool + default = true + description = "Whether to use private or public subnets for the Redshift cluster" +} + +variable "publicly_accessible" { + type = bool + default = false + description = "If true, the cluster can be accessed from a public network" +} + +// AWS KMS alias used for encryption/decryption of SSM secure strings +variable "kms_alias_name_ssm" { + type = string + default = "alias/aws/ssm" + description = "KMS alias name for SSM" +} + +variable "ssm_path_prefix" { + type = string + default = "redshift" + description = "SSM path prefix (without leading or trailing slash)" +} + +variable "security_group_ids" { + type = list(string) + default = null + description = "An array of security group IDs to associate with the endpoint." +} + +variable "vpc_security_group_ids" { + type = list(string) + default = null + description = "An array of security group IDs to associate with the workgroup." +} + +variable "base_capacity" { + type = number + default = 128 + description = "The base data warehouse capacity of the workgroup in Redshift Processing Units (RPUs)." +} + +variable "config_parameter" { + type = list(object({ + parameter_key = string + parameter_value = any + })) + default = [] + description = "A list of Redshift config parameters to apply to the workgroup." +} + +variable "enhanced_vpc_routing" { + type = bool + default = true + description = "The value that specifies whether to turn on enhanced virtual private cloud (VPC) routing, which forces Amazon Redshift Serverless to route traffic through your VPC instead of over the internet." +} + +variable "endpoint_name" { + type = string + default = null + description = "Endpoint name for the redshift endpoint, if null, is set to $stage-$name" +} + +variable "custom_sg_enabled" { + type = bool + default = false + description = "Whether to use custom security group or not" +} +variable "custom_sg_allow_all_egress" { + type = bool + default = true + description = "Whether to allow all egress traffic or not" +} + +variable "custom_sg_rules" { + type = list(object({ + key = string + type = string + from_port = number + to_port = number + protocol = string + cidr_blocks = list(string) + description = string + })) + default = [] + description = "Custom security group rules" + +} diff --git a/modules/redshift-serverless/versions.tf b/modules/redshift-serverless/versions.tf new file mode 100644 index 000000000..5b9bb0612 --- /dev/null +++ b/modules/redshift-serverless/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } + } +} From 6118bf4d6165110423f11f5ce328848939971f1b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:07:27 +0300 Subject: [PATCH 497/501] Update Changelog for `1.502.0` (#1126) Co-authored-by: cloudposse-releaser[bot] <163353533+cloudposse-releaser[bot]@users.noreply.github.com> Co-authored-by: Igor Rodionov --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce4ed9f7..d2f866a33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,52 @@ # CHANGELOG + +## 1.502.0 + + + +
+ upstream `tailscale` @Benbentwo (#835) +## what +* Initial Tailscale deployment + +## why +* tailscale operators + +## references +* https://github.com/tailscale/tailscale/tree/main/docs/k8s + +
+ +
+ Update Changelog for `1.501.0` @github-actions (#1125) +Update Changelog for [`1.501.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.501.0) +
+ +
+ docs: improve external-dns snippet in readme @sgtoj (#986) +## what + +- update the `eks/external-dns` component example in readme + - set latest chart version + - set the resource configure properly + - add `txt_prefix` var to snippet + +## why + +- help the future engineers deploying or updating external-dns + +## references + +- n/a + +
+ +
+ Update Changelog for `1.500.0` @github-actions (#1124) +Update Changelog for [`1.500.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.500.0) +
+ + ## 1.501.0 From fc5299c50bc07567bee0d812acfd3ad06f87a9ad Mon Sep 17 00:00:00 2001 From: RoseSecurity <72598486+RoseSecurity@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:13:34 -0500 Subject: [PATCH 498/501] feat: allow vulnerability scanning of Argo repository and implement ignore changes for non-change drift (#1120) Co-authored-by: Igor Rodionov --- modules/argocd-repo/main.tf | 9 ++++++++- modules/argocd-repo/variables.tf | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/modules/argocd-repo/main.tf b/modules/argocd-repo/main.tf index 12f2facd3..43a7109d3 100644 --- a/modules/argocd-repo/main.tf +++ b/modules/argocd-repo/main.tf @@ -49,7 +49,8 @@ resource "github_repository" "default" { description = var.description auto_init = true # will create a 'main' branch - visibility = "private" + visibility = "private" + vulnerability_alerts = var.vulnerability_alerts_enabled } resource "github_branch_default" "default" { @@ -90,6 +91,12 @@ resource "github_branch_protection" "default" { join("", data.github_user.automation_user[*].node_id), ] : [] } + + lifecycle { + ignore_changes = [ + restrict_pushes[0].push_allowances + ] + } } data "github_team" "default" { diff --git a/modules/argocd-repo/variables.tf b/modules/argocd-repo/variables.tf index 65b336576..0f4716517 100644 --- a/modules/argocd-repo/variables.tf +++ b/modules/argocd-repo/variables.tf @@ -151,6 +151,12 @@ variable "push_restrictions_enabled" { default = true } +variable "vulnerability_alerts_enabled" { + type = bool + description = "Enable security alerts for vulnerable dependencies" + default = false +} + variable "slack_notifications_channel" { type = string default = "" From 4ce379e72856cd1532ef38f4e76d2f4118a5a12c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:47:41 +0300 Subject: [PATCH 499/501] Update Changelog for `1.504.0` (#1128) Co-authored-by: cloudposse-releaser[bot] <163353533+cloudposse-releaser[bot]@users.noreply.github.com> --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f866a33..be4b4f288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # CHANGELOG +## 1.504.0 + + + +
+ feat: allow vulnerability scanning of Argo repository and implement ignore changes for non-change drift @RoseSecurity (#1120) +## what + +- Attempted to refactor code to ensure changes don't occur on each run (did not resolve) +- Opened an issue with [GitHub](https://github.com/integrations/terraform-provider-github/issues/2243) but is still in the triaging state +- This is a quick fix for addressing the following non-change + +```console +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # github_branch_protection.default[0] will be updated in-place + ~ resource "github_branch_protection" "default" { + id = "XXXXXXX" + # (10 unchanged attributes hidden) + + ~ restrict_pushes { + ~ push_allowances = [ + + "XXXXXXX", + ] +``` + +## why + +- [X] Adds lifecycle meta-argument for ignoring changes to `push_allowances` +- [X] Enable vulnerability alerting for vulnerable dependencies by default to address `tfsec` findings + +## Testing + +- [X] Validated with `atmos validate stacks` +- [X] Performed successful `atmos terraform deploy` on component + +
+ +
+ Update Changelog for `1.502.0` @github-actions (#1126) +Update Changelog for [`1.502.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.502.0) +
+ + + ## 1.502.0 From 544025037b310783775ba4aa03d90e18cb7e7b48 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 1 Oct 2024 15:47:55 -0400 Subject: [PATCH 500/501] fix: account-quota drift reduced (#1102) Co-authored-by: Igor Rodionov Co-authored-by: Dan Miller --- modules/account-quotas/README.md | 17 +++++++++++++++++ modules/account-quotas/main.tf | 28 +++++++++++++++++++++++++++- modules/account-quotas/outputs.tf | 2 +- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/modules/account-quotas/README.md b/modules/account-quotas/README.md index 92db56f36..f17dd7aec 100644 --- a/modules/account-quotas/README.md +++ b/modules/account-quotas/README.md @@ -38,9 +38,21 @@ aws --region us-east-1 service-quotas list-service-quotas --service-code ec2 If you make a request to raise a quota, the output will show the requested value as `value` while the request is pending. +### Special usage Notes + Even though the Terraform will submit the support request, you may need to follow up with AWS support to get the request approved, via the AWS console or email. +#### Resources are destroyed on change + +Because the AWS API often returns default values rather than configured or applicable values for a given quota, we have +to ignore the value returned by the API or else face perpetual drift. To allow us to change the value in the future, +even though we are ignoring it, we encode the value in the resource key, so that a change of value will result in a new +resource being created and the old one being destroyed. Destroying the old resource has no actual effect (it does not +even close an open request), so it is safe to do. + +### Example + Here's an example snippet for how to use this component. ```yaml @@ -128,5 +140,10 @@ components: - AWS CLI [command to list service codes](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/service-quotas/list-services.html): `aws service-quotas list-services` +- AWS CLI + [command to list service quotas](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/service-quotas/list-service-quotas.html) + `aws service-quotas list-service-quotas`. Note where it says "For some quotas, only the default values are available." +- [Medium article](https://medium.com/@jsonk/the-limit-does-not-exist-hidden-visibility-of-aws-service-limits-4b786f846bc0) + explaining how many AWS service limits are not available. [](https://cpco.io/component) diff --git a/modules/account-quotas/main.tf b/modules/account-quotas/main.tf index 0305efe81..1e40992c8 100644 --- a/modules/account-quotas/main.tf +++ b/modules/account-quotas/main.tf @@ -21,6 +21,26 @@ locals { quota_code = quota.quota_code != null ? quota.quota_code : data.aws_servicequotas_service_quota.by_name[k].quota_code value = quota.value } } + + # Because the API often returns default values rather than configured or applicable values, + # we have to ignore the value returned by the API or else face perpetual drift. + # To allow us to change the value in the future, even though we are ignoring it, + # we encode the value in the resource key, so that a change of value will + # result in a new resource being created and the old one being destroyed. + # Destroying the old resource has no actual effect, it does not even close + # an open request, so it is safe to do. + + quota_requests = { for k, quota in local.quotas_coded_map : + format("%v/%v/%v", quota.service_code, quota.quota_code, quota.value) => merge( + quota, { input_map_key = k } + ) + } + + quota_results = { for k, v in local.quota_requests : v.input_map_key => merge( + { for k, v in aws_servicequotas_service_quota.this[k] : k => v if k != "value" }, + { "value reported (may be inaccurate)" = aws_servicequotas_service_quota.this[k].value }, + { "value requested" = v.value } + ) } } data "aws_servicequotas_service" "by_name" { @@ -37,9 +57,15 @@ data "aws_servicequotas_service_quota" "by_name" { } resource "aws_servicequotas_service_quota" "this" { - for_each = local.quotas_coded_map + for_each = local.quota_requests quota_code = each.value.quota_code service_code = each.value.service_code value = each.value.value + + lifecycle { + # Literally about 50% of the time, the actual value set is not available, + # so the default value is reported instead, resulting in permanent drift. + ignore_changes = [value] + } } diff --git a/modules/account-quotas/outputs.tf b/modules/account-quotas/outputs.tf index 6258c97f3..48cd0feda 100644 --- a/modules/account-quotas/outputs.tf +++ b/modules/account-quotas/outputs.tf @@ -1,4 +1,4 @@ output "quotas" { - value = aws_servicequotas_service_quota.this + value = local.quota_results description = "Full report on all service quotas managed by this component." } From 0645fac4b777ecec80c16bf18522dcf159782562 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Oct 2024 11:01:43 +0300 Subject: [PATCH 501/501] Update Changelog for `1.505.0` (#1129) Co-authored-by: cloudposse-releaser[bot] <163353533+cloudposse-releaser[bot]@users.noreply.github.com> --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be4b4f288..636aff9f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # CHANGELOG +## 1.505.0 + + + +
+ fix: account-quota drift reduced @dudymas (#1102) +## what + +- encode values into a `for_each` on service quota resources + +## why + +- terraform sometimes gets bad state back from the AWS API, so fetched results +ought to be ignored. Instead, input values should be respected as truth. + +## references + +- AWS CLI + [command to list service quotas](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/service-quotas/list-service-quotas.html) `aws service-quotas list-service-quotas`. + Note where it says "For some quotas, only the default values are available." +- [Medium article](https://medium.com/@jsonk/the-limit-does-not-exist-hidden-visibility-of-aws-service-limits-4b786f846bc0) + explaining how many AWS service limits are not available. + + +
+ +
+ Update Changelog for `1.504.0` @github-actions (#1128) +Update Changelog for [`1.504.0`](https://github.com/cloudposse/terraform-aws-components/releases/tag/1.504.0) +
+ + + ## 1.504.0

_ypw|mLR5PWu+=78Tphk?>cpZ7W|6GZGm3zq8 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%0D}X?j*q=3ZJqRO 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 -# terraform-aws-components [![Latest Release](https://img.shields.io/github/release/cloudposse/terraform-aws-components.svg)](https://github.com/cloudposse/terraform-aws-components/releases/latest) [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) +[![Project Banner](.github/banner.png?raw=true)](https://cpco.io/homepage) + [![Latest Release](https://img.shields.io/github/release/cloudposse/terraform-aws-components.svg)](https://github.com/cloudposse/terraform-aws-components/releases/latest) [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) -[![README Header][readme_header_img]][readme_header_link] - -[![Cloud Posse][logo]](https://cpco.io/homepage) From fbb02dcc89c3f89beedc5dcd5dba34590bfea089 Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Mon, 22 Jan 2024 11:15:39 -0500 Subject: [PATCH 348/501] Add AWS Glue components (#958) Co-authored-by: cloudpossebot --- LICENSE | 2 +- modules/account-map/README.md | 2 +- modules/account-settings/README.md | 2 +- modules/account/README.md | 2 +- modules/acm/README.md | 2 +- modules/alb/README.md | 2 +- .../api-gateway-account-settings/README.md | 2 +- modules/api-gateway-rest-api/README.md | 2 +- modules/argocd-repo/README.md | 2 +- modules/athena/README.md | 2 +- modules/aurora-mysql-resources/README.md | 2 +- modules/aurora-mysql/README.md | 2 +- modules/aurora-postgres-resources/README.md | 2 +- modules/aurora-postgres/README.md | 2 +- modules/aws-backup/README.md | 2 +- modules/aws-config/README.md | 2 +- modules/aws-inspector/README.md | 2 +- modules/aws-saml/README.md | 2 +- modules/aws-shield/README.md | 2 +- modules/bastion/README.md | 2 +- modules/cloudtrail-bucket/README.md | 4 +- modules/cloudtrail/README.md | 4 +- modules/cloudwatch-logs/README.md | 2 +- modules/cognito/README.md | 2 +- modules/config-bucket/README.md | 2 +- modules/datadog-configuration/README.md | 2 +- modules/datadog-integration/README.md | 2 +- modules/datadog-lambda-forwarder/README.md | 2 +- modules/datadog-monitor/README.md | 2 +- .../datadog-private-location-ecs/README.md | 2 +- modules/dms/endpoint/README.md | 2 +- modules/dms/iam/README.md | 2 +- modules/dms/replication-instance/README.md | 2 +- modules/dms/replication-task/README.md | 2 +- modules/dns-delegated/README.md | 4 +- modules/dns-primary/README.md | 4 +- modules/documentdb/README.md | 2 +- modules/dynamodb/README.md | 2 +- modules/ecr/README.md | 4 +- modules/ecs-service/README.md | 2 +- modules/ecs/README.md | 2 +- modules/efs/README.md | 2 +- .../eks/actions-runner-controller/README.md | 2 +- .../alb-controller-ingress-group/README.md | 2 +- modules/eks/datadog-agent/README.md | 2 +- modules/eks/echo-server/README.md | 2 +- modules/eks/github-actions-runner/README.md | 2 +- modules/eks/keda/README.md | 2 +- modules/eks/redis-operator/README.md | 2 +- modules/eks/redis/README.md | 2 +- modules/elasticache-redis/README.md | 2 +- modules/elasticsearch/README.md | 2 +- modules/github-action-token-rotator/README.md | 2 +- modules/github-oidc-provider/README.md | 2 +- modules/github-oidc-role/README.md | 2 +- modules/github-runners/README.md | 2 +- modules/gitops/README.md | 2 +- .../README.md | 2 +- modules/global-accelerator/README.md | 2 +- modules/glue/catalog-database/README.md | 103 +++++++ modules/glue/catalog-database/context.tf | 279 ++++++++++++++++++ modules/glue/catalog-database/main.tf | 30 ++ modules/glue/catalog-database/outputs.tf | 14 + modules/glue/catalog-database/providers.tf | 19 ++ modules/glue/catalog-database/remote-state.tf | 8 + modules/glue/catalog-database/variables.tf | 74 +++++ modules/glue/catalog-database/versions.tf | 14 + modules/glue/catalog-table/README.md | 113 +++++++ modules/glue/catalog-table/context.tf | 279 ++++++++++++++++++ modules/glue/catalog-table/main.tf | 42 +++ modules/glue/catalog-table/outputs.tf | 14 + modules/glue/catalog-table/providers.tf | 19 ++ modules/glue/catalog-table/remote-state.tf | 17 ++ modules/glue/catalog-table/variables.tf | 186 ++++++++++++ modules/glue/catalog-table/versions.tf | 14 + modules/glue/connection/README.md | 122 ++++++++ modules/glue/connection/context.tf | 279 ++++++++++++++++++ modules/glue/connection/main.tf | 48 +++ modules/glue/connection/outputs.tf | 29 ++ modules/glue/connection/providers.tf | 19 ++ modules/glue/connection/remote-state.tf | 15 + modules/glue/connection/sg.tf | 42 +++ modules/glue/connection/ssm.tf | 17 ++ modules/glue/connection/variables.tf | 129 ++++++++ modules/glue/connection/versions.tf | 14 + modules/glue/crawler/README.md | 115 ++++++++ modules/glue/crawler/context.tf | 279 ++++++++++++++++++ modules/glue/crawler/main.tf | 38 +++ modules/glue/crawler/outputs.tf | 14 + modules/glue/crawler/providers.tf | 19 ++ modules/glue/crawler/remote-state.tf | 34 +++ modules/glue/crawler/variables.tf | 165 +++++++++++ modules/glue/crawler/versions.tf | 14 + modules/glue/iam/README.md | 89 ++++++ modules/glue/iam/context.tf | 279 ++++++++++++++++++ modules/glue/iam/main.tf | 15 + modules/glue/iam/outputs.tf | 14 + modules/glue/iam/providers.tf | 19 ++ modules/glue/iam/variables.tf | 22 ++ modules/glue/iam/versions.tf | 14 + modules/glue/job/README.md | 122 ++++++++ modules/glue/job/context.tf | 279 ++++++++++++++++++ modules/glue/job/iam.tf | 78 +++++ modules/glue/job/main.tf | 36 +++ modules/glue/job/outputs.tf | 14 + modules/glue/job/providers.tf | 19 ++ modules/glue/job/remote-state.tf | 26 ++ modules/glue/job/variables.tf | 142 +++++++++ modules/glue/job/versions.tf | 14 + modules/glue/registry/README.md | 86 ++++++ modules/glue/registry/context.tf | 279 ++++++++++++++++++ modules/glue/registry/main.tf | 9 + modules/glue/registry/outputs.tf | 14 + modules/glue/registry/providers.tf | 19 ++ modules/glue/registry/variables.tf | 16 + modules/glue/registry/versions.tf | 14 + modules/glue/schema/README.md | 97 ++++++ modules/glue/schema/context.tf | 279 ++++++++++++++++++ modules/glue/schema/main.tf | 13 + modules/glue/schema/outputs.tf | 34 +++ modules/glue/schema/providers.tf | 19 ++ modules/glue/schema/remote-state.tf | 8 + modules/glue/schema/variables.tf | 49 +++ modules/glue/schema/versions.tf | 14 + modules/glue/trigger/README.md | 105 +++++++ modules/glue/trigger/context.tf | 279 ++++++++++++++++++ modules/glue/trigger/main.tf | 27 ++ modules/glue/trigger/outputs.tf | 14 + modules/glue/trigger/providers.tf | 19 ++ modules/glue/trigger/remote-state.tf | 31 ++ modules/glue/trigger/variables.tf | 107 +++++++ modules/glue/trigger/versions.tf | 14 + modules/glue/workflow/README.md | 88 ++++++ modules/glue/workflow/context.tf | 279 ++++++++++++++++++ modules/glue/workflow/main.tf | 11 + modules/glue/workflow/outputs.tf | 14 + modules/glue/workflow/providers.tf | 19 ++ modules/glue/workflow/variables.tf | 28 ++ modules/glue/workflow/versions.tf | 14 + modules/iam-role/README.md | 2 +- modules/iam-service-linked-roles/README.md | 2 +- modules/ipam/README.md | 2 +- modules/kinesis-stream/README.md | 2 +- modules/kms/README.md | 2 +- modules/lakeformation/README.md | 2 +- modules/lambda/README.md | 2 +- modules/mq-broker/README.md | 2 +- modules/mwaa/README.md | 2 +- modules/network-firewall/README.md | 2 +- modules/opsgenie-team/README.md | 2 +- modules/philips-labs-github-runners/README.md | 2 +- modules/rds/README.md | 2 +- modules/redshift/README.md | 2 +- .../route53-resolver-dns-firewall/README.md | 2 +- modules/s3-bucket/README.md | 2 +- modules/ses/README.md | 2 +- modules/snowflake-database/README.md | 2 +- modules/sns-topic/README.md | 2 +- modules/spa-s3-cloudfront/README.md | 2 +- modules/spacelift/worker-pool/README.md | 2 +- modules/sqs-queue/README.md | 2 +- modules/ssm-parameters/README.md | 2 +- modules/tfstate-backend/README.md | 2 +- .../tgw/cross-region-hub-connector/README.md | 2 +- modules/tgw/hub/README.md | 2 +- modules/tgw/spoke/README.md | 2 +- modules/vpc-flow-logs-bucket/README.md | 2 +- modules/vpc-peering/README.md | 2 +- modules/vpc/README.md | 2 +- modules/waf/README.md | 2 +- modules/zscaler/README.md | 2 +- 171 files changed, 5894 insertions(+), 96 deletions(-) create mode 100644 modules/glue/catalog-database/README.md create mode 100644 modules/glue/catalog-database/context.tf create mode 100644 modules/glue/catalog-database/main.tf create mode 100644 modules/glue/catalog-database/outputs.tf create mode 100644 modules/glue/catalog-database/providers.tf create mode 100644 modules/glue/catalog-database/remote-state.tf create mode 100644 modules/glue/catalog-database/variables.tf create mode 100644 modules/glue/catalog-database/versions.tf create mode 100644 modules/glue/catalog-table/README.md create mode 100644 modules/glue/catalog-table/context.tf create mode 100644 modules/glue/catalog-table/main.tf create mode 100644 modules/glue/catalog-table/outputs.tf create mode 100644 modules/glue/catalog-table/providers.tf create mode 100644 modules/glue/catalog-table/remote-state.tf create mode 100644 modules/glue/catalog-table/variables.tf create mode 100644 modules/glue/catalog-table/versions.tf create mode 100644 modules/glue/connection/README.md create mode 100644 modules/glue/connection/context.tf create mode 100644 modules/glue/connection/main.tf create mode 100644 modules/glue/connection/outputs.tf create mode 100644 modules/glue/connection/providers.tf create mode 100644 modules/glue/connection/remote-state.tf create mode 100644 modules/glue/connection/sg.tf create mode 100644 modules/glue/connection/ssm.tf create mode 100644 modules/glue/connection/variables.tf create mode 100644 modules/glue/connection/versions.tf create mode 100644 modules/glue/crawler/README.md create mode 100644 modules/glue/crawler/context.tf create mode 100644 modules/glue/crawler/main.tf create mode 100644 modules/glue/crawler/outputs.tf create mode 100644 modules/glue/crawler/providers.tf create mode 100644 modules/glue/crawler/remote-state.tf create mode 100644 modules/glue/crawler/variables.tf create mode 100644 modules/glue/crawler/versions.tf create mode 100644 modules/glue/iam/README.md create mode 100644 modules/glue/iam/context.tf create mode 100644 modules/glue/iam/main.tf create mode 100644 modules/glue/iam/outputs.tf create mode 100644 modules/glue/iam/providers.tf create mode 100644 modules/glue/iam/variables.tf create mode 100644 modules/glue/iam/versions.tf create mode 100644 modules/glue/job/README.md create mode 100644 modules/glue/job/context.tf create mode 100644 modules/glue/job/iam.tf create mode 100644 modules/glue/job/main.tf create mode 100644 modules/glue/job/outputs.tf create mode 100644 modules/glue/job/providers.tf create mode 100644 modules/glue/job/remote-state.tf create mode 100644 modules/glue/job/variables.tf create mode 100644 modules/glue/job/versions.tf create mode 100644 modules/glue/registry/README.md create mode 100644 modules/glue/registry/context.tf create mode 100644 modules/glue/registry/main.tf create mode 100644 modules/glue/registry/outputs.tf create mode 100644 modules/glue/registry/providers.tf create mode 100644 modules/glue/registry/variables.tf create mode 100644 modules/glue/registry/versions.tf create mode 100644 modules/glue/schema/README.md create mode 100644 modules/glue/schema/context.tf create mode 100644 modules/glue/schema/main.tf create mode 100644 modules/glue/schema/outputs.tf create mode 100644 modules/glue/schema/providers.tf create mode 100644 modules/glue/schema/remote-state.tf create mode 100644 modules/glue/schema/variables.tf create mode 100644 modules/glue/schema/versions.tf create mode 100644 modules/glue/trigger/README.md create mode 100644 modules/glue/trigger/context.tf create mode 100644 modules/glue/trigger/main.tf create mode 100644 modules/glue/trigger/outputs.tf create mode 100644 modules/glue/trigger/providers.tf create mode 100644 modules/glue/trigger/remote-state.tf create mode 100644 modules/glue/trigger/variables.tf create mode 100644 modules/glue/trigger/versions.tf create mode 100644 modules/glue/workflow/README.md create mode 100644 modules/glue/workflow/context.tf create mode 100644 modules/glue/workflow/main.tf create mode 100644 modules/glue/workflow/outputs.tf create mode 100644 modules/glue/workflow/providers.tf create mode 100644 modules/glue/workflow/variables.tf create mode 100644 modules/glue/workflow/versions.tf diff --git a/LICENSE b/LICENSE index 7afefb95c..861ef1854 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2018-2023 Cloud Posse, LLC + Copyright 2018-2024 Cloud Posse, 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/modules/account-map/README.md b/modules/account-map/README.md index a3f5da09a..7e207c9c7 100644 --- a/modules/account-map/README.md +++ b/modules/account-map/README.md @@ -152,6 +152,6 @@ components: ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/account-map) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-map) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/account-settings/README.md b/modules/account-settings/README.md index 132ca39a5..ff9dd3f15 100644 --- a/modules/account-settings/README.md +++ b/modules/account-settings/README.md @@ -140,6 +140,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/account-settings) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-settings) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/account/README.md b/modules/account/README.md index 8ed60bf4b..c3c2d50cb 100644 --- a/modules/account/README.md +++ b/modules/account/README.md @@ -424,6 +424,6 @@ atmos terraform apply account --stack gbl-root ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/account) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/acm/README.md b/modules/acm/README.md index 7d087ba30..38f60328d 100644 --- a/modules/acm/README.md +++ b/modules/acm/README.md @@ -132,6 +132,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/acm) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/acm) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/alb/README.md b/modules/alb/README.md index daf9950de..351997e17 100644 --- a/modules/alb/README.md +++ b/modules/alb/README.md @@ -128,7 +128,7 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/alb) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/alb) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/api-gateway-account-settings/README.md b/modules/api-gateway-account-settings/README.md index c716057ec..04571f694 100644 --- a/modules/api-gateway-account-settings/README.md +++ b/modules/api-gateway-account-settings/README.md @@ -79,7 +79,7 @@ No resources. ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/api-gateway-settings) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/api-gateway-settings) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/api-gateway-rest-api/README.md b/modules/api-gateway-rest-api/README.md index 2683a498c..d18e1efd4 100644 --- a/modules/api-gateway-rest-api/README.md +++ b/modules/api-gateway-rest-api/README.md @@ -120,7 +120,7 @@ components: ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/argocd-repo/README.md b/modules/argocd-repo/README.md index 260692fb9..d5ffb546e 100644 --- a/modules/argocd-repo/README.md +++ b/modules/argocd-repo/README.md @@ -179,7 +179,7 @@ $ terraform import -var "import_profile_name=eg-mgmt-gbl-corp-admin" -var-file=" ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/argocd-repo) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/argocd-repo) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/athena/README.md b/modules/athena/README.md index 8b2e27ad6..ce1e8cc69 100644 --- a/modules/athena/README.md +++ b/modules/athena/README.md @@ -197,7 +197,7 @@ component ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/athena) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/athena) - Cloud Posse's upstream component * [Querying AWS CloudTrail logs with AWS Athena](https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html) [](https://cpco.io/component) diff --git a/modules/aurora-mysql-resources/README.md b/modules/aurora-mysql-resources/README.md index 3f5a8318b..e958d130c 100644 --- a/modules/aurora-mysql-resources/README.md +++ b/modules/aurora-mysql-resources/README.md @@ -127,7 +127,7 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aurora-mysql-resources) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-mysql-resources) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aurora-mysql/README.md b/modules/aurora-mysql/README.md index 7bf18fd10..2662da0de 100644 --- a/modules/aurora-mysql/README.md +++ b/modules/aurora-mysql/README.md @@ -269,7 +269,7 @@ Reploying the component should show no changes. For example, `atmos terraform ap ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aurora-mysql) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-mysql) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aurora-postgres-resources/README.md b/modules/aurora-postgres-resources/README.md index 9744ebce6..ff041f3ba 100644 --- a/modules/aurora-postgres-resources/README.md +++ b/modules/aurora-postgres-resources/README.md @@ -124,7 +124,7 @@ For databases and schemas, there are not a lot of other privileges to grant, and ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aurora-postgres-resources) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-postgres-resources) - Cloud Posse's upstream component * PostgreSQL references (select the correct version of PostgreSQL at the top of the page): * [GRANT command](https://www.postgresql.org/docs/14/sql-grant.html) diff --git a/modules/aurora-postgres/README.md b/modules/aurora-postgres/README.md index dcf371345..1d7c22f6e 100644 --- a/modules/aurora-postgres/README.md +++ b/modules/aurora-postgres/README.md @@ -358,7 +358,7 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aurora-postgres) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aurora-postgres) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-backup/README.md b/modules/aws-backup/README.md index 5e21b670c..b8e0b292c 100644 --- a/modules/aws-backup/README.md +++ b/modules/aws-backup/README.md @@ -263,7 +263,7 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aws-backup) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-backup) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-config/README.md b/modules/aws-config/README.md index 920153566..c63e3b6d7 100644 --- a/modules/aws-config/README.md +++ b/modules/aws-config/README.md @@ -201,7 +201,7 @@ atmos terraform plan aws-config-{each region} --stack {each region}-{each stage} ## References * [AWS Config Documentation](https://docs.aws.amazon.com/config/index.html) -* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aws-config) +* [Cloud Posse's upstream component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-config) * [Conformance Packs documentation](https://docs.aws.amazon.com/config/latest/developerguide/conformance-packs.html) * [AWS Managed Sample Conformance Packs](https://github.com/awslabs/aws-config-rules/tree/master/aws-config-conformance-packs) diff --git a/modules/aws-inspector/README.md b/modules/aws-inspector/README.md index 835a15f14..5f75652c4 100644 --- a/modules/aws-inspector/README.md +++ b/modules/aws-inspector/README.md @@ -99,5 +99,5 @@ By customizing the configuration with the appropriate rules, you can tailor the | [inspector](#output\_inspector) | The AWS Inspector module outputs | ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/TODO) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/TODO) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-saml/README.md b/modules/aws-saml/README.md index 339538c83..67e14c855 100644 --- a/modules/aws-saml/README.md +++ b/modules/aws-saml/README.md @@ -88,7 +88,7 @@ components: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/sso) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/sso) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/aws-shield/README.md b/modules/aws-shield/README.md index b6345477e..78fd384a0 100644 --- a/modules/aws-shield/README.md +++ b/modules/aws-shield/README.md @@ -162,6 +162,6 @@ This leads to more simplified inter-component dependencies and minimizes the nee ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/aws-shield) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/aws-shield) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/bastion/README.md b/modules/bastion/README.md index b0c991c36..2c99962bb 100644 --- a/modules/bastion/README.md +++ b/modules/bastion/README.md @@ -134,6 +134,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/bastion) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/bastion) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/cloudtrail-bucket/README.md b/modules/cloudtrail-bucket/README.md index 723114132..633101aed 100644 --- a/modules/cloudtrail-bucket/README.md +++ b/modules/cloudtrail-bucket/README.md @@ -1,6 +1,6 @@ # Component: `cloudtrail-bucket` -This component is responsible for provisioning a bucket for storing cloudtrail logs for auditing purposes. It's expected to be used alongside [the `cloudtrail` component](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/cloudtrail). +This component is responsible for provisioning a bucket for storing cloudtrail logs for auditing purposes. It's expected to be used alongside [the `cloudtrail` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail). ## Usage @@ -90,6 +90,6 @@ No resources. ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/cloudtrail-bucket) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail-bucket) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/cloudtrail/README.md b/modules/cloudtrail/README.md index e4b702562..47175bc07 100644 --- a/modules/cloudtrail/README.md +++ b/modules/cloudtrail/README.md @@ -1,7 +1,7 @@ # Component: `cloudtrail` This component is responsible for provisioning cloudtrail auditing in an individual account. It's expected to be used alongside -[the `cloudtrail-bucket` component](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/cloudtrail-bucket) +[the `cloudtrail-bucket` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail-bucket) as it utilizes that bucket via remote state. This component can either be deployed selectively to various accounts with `is_organization_trail=false`, or alternatively @@ -116,6 +116,6 @@ components: ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/cloudtrail) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudtrail) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/cloudwatch-logs/README.md b/modules/cloudwatch-logs/README.md index 99ebd591b..128a10cba 100644 --- a/modules/cloudwatch-logs/README.md +++ b/modules/cloudwatch-logs/README.md @@ -92,6 +92,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/cloudwatch-logs) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cloudwatch-logs) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/cognito/README.md b/modules/cognito/README.md index 25f440db1..b87c3ae04 100644 --- a/modules/cognito/README.md +++ b/modules/cognito/README.md @@ -207,6 +207,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/cognito) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/cognito) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/config-bucket/README.md b/modules/config-bucket/README.md index b4b140bb6..9d9151732 100644 --- a/modules/config-bucket/README.md +++ b/modules/config-bucket/README.md @@ -99,6 +99,6 @@ No resources. ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/config-bucket) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/config-bucket) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-configuration/README.md b/modules/datadog-configuration/README.md index f97f16511..de47b95ee 100644 --- a/modules/datadog-configuration/README.md +++ b/modules/datadog-configuration/README.md @@ -141,7 +141,7 @@ provider "datadog" { ## References * Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-configuration) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-configuration) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-integration/README.md b/modules/datadog-integration/README.md index ca0db46a3..123027d84 100644 --- a/modules/datadog-integration/README.md +++ b/modules/datadog-integration/README.md @@ -100,7 +100,7 @@ components: ## References * Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys) -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-integration) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-integration) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-lambda-forwarder/README.md b/modules/datadog-lambda-forwarder/README.md index 4146326be..7d856d77d 100644 --- a/modules/datadog-lambda-forwarder/README.md +++ b/modules/datadog-lambda-forwarder/README.md @@ -154,7 +154,7 @@ components: ## References * Datadog's [documentation about provisioning keys](https://docs.datadoghq.com/account_management/api-app-keys -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-lambda-forwarder) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-lambda-forwarder) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-monitor/README.md b/modules/datadog-monitor/README.md index 9ef9dc6bd..968cf5e09 100644 --- a/modules/datadog-monitor/README.md +++ b/modules/datadog-monitor/README.md @@ -243,7 +243,7 @@ No resources. - [datadog-integration](https://docs.cloudposse.com/components/library/aws/datadog-integration/) ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-monitor) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-monitor) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/datadog-private-location-ecs/README.md b/modules/datadog-private-location-ecs/README.md index 5e50c68c4..06b594330 100644 --- a/modules/datadog-private-location-ecs/README.md +++ b/modules/datadog-private-location-ecs/README.md @@ -134,6 +134,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecs-service) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs-service) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dms/endpoint/README.md b/modules/dms/endpoint/README.md index 588534f49..264580caa 100644 --- a/modules/dms/endpoint/README.md +++ b/modules/dms/endpoint/README.md @@ -154,7 +154,7 @@ components: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dms/modules/dms-endpoint) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-endpoint) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dms/iam/README.md b/modules/dms/iam/README.md index 5fc860077..21eb4e22b 100644 --- a/modules/dms/iam/README.md +++ b/modules/dms/iam/README.md @@ -82,7 +82,7 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dms/modules/dms-iam) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-iam) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dms/replication-instance/README.md b/modules/dms/replication-instance/README.md index 8eeb64017..42b1b31b0 100644 --- a/modules/dms/replication-instance/README.md +++ b/modules/dms/replication-instance/README.md @@ -117,7 +117,7 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dms/modules/dms-replication-instance) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-replication-instance) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dms/replication-task/README.md b/modules/dms/replication-task/README.md index 7b7c4dbbc..f7968c246 100644 --- a/modules/dms/replication-task/README.md +++ b/modules/dms/replication-task/README.md @@ -107,7 +107,7 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dms/modules/dms-replication-task) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dms/modules/dms-replication-task) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index d88cc4231..c608b8a3c 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -1,6 +1,6 @@ # Component: `dns-delegated` -This component is responsible for provisioning a DNS zone which delegates nameservers to the DNS zone in the primary DNS account. The primary DNS zone is expected to already be provisioned via [the `dns-primary` component](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dns-primary). +This component is responsible for provisioning a DNS zone which delegates nameservers to the DNS zone in the primary DNS account. The primary DNS zone is expected to already be provisioned via [the `dns-primary` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-primary). This component also provisions a wildcard ACM certificate for the given subdomain. @@ -211,7 +211,7 @@ Takeaway ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dns-delegated) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-delegated) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dns-primary/README.md b/modules/dns-primary/README.md index 46bd8f6e4..32e53470b 100644 --- a/modules/dns-primary/README.md +++ b/modules/dns-primary/README.md @@ -2,7 +2,7 @@ This component is responsible for provisioning the primary DNS zones into an AWS account. By convention, we typically provision the primary DNS zones in the `dns` account. The primary account for branded zones (e.g. `example.com`), however, would be in the `prod` account, while staging zone (e.g. `example.qa`) might be in the `staging` account. -The zones from the primary DNS zone are then expected to be delegated to other accounts via [the `dns-delegated` component](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dns-delegated). Additionally, external records can be created on the primary DNS zones via the `record_config` variable. +The zones from the primary DNS zone are then expected to be delegated to other accounts via [the `dns-delegated` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-delegated). Additionally, external records can be created on the primary DNS zones via the `record_config` variable. ## Architecture @@ -145,6 +145,6 @@ Use the [acm](https://docs.cloudposse.com/components/library/aws/acm) component ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dns-primary) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-primary) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/documentdb/README.md b/modules/documentdb/README.md index 5dee96a71..fb43cab53 100644 --- a/modules/documentdb/README.md +++ b/modules/documentdb/README.md @@ -121,7 +121,7 @@ components: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/documentdb) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/documentdb) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/dynamodb/README.md b/modules/dynamodb/README.md index e148d820b..eec46b957 100644 --- a/modules/dynamodb/README.md +++ b/modules/dynamodb/README.md @@ -116,7 +116,7 @@ No resources. ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/dynamodb) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dynamodb) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ecr/README.md b/modules/ecr/README.md index be5d2fc31..46c209cd1 100644 --- a/modules/ecr/README.md +++ b/modules/ecr/README.md @@ -1,7 +1,7 @@ # Component: `ecr` This component is responsible for provisioning repositories, lifecycle rules, and permissions for streamlined ECR usage. -This utilizes [the roles-to-principals submodule](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/account-map/modules/roles-to-principals) +This utilizes [the roles-to-principals submodule](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/account-map/modules/roles-to-principals) to assign accounts to various roles. It is also compatible with the [GitHub Actions IAM Role mixin](https://github.com/cloudposse/terraform-aws-components/blob/master/mixins/github-actions-iam-role/README-github-action-iam-role.md). @@ -137,7 +137,7 @@ components: - [Decide on ECR Strategy](https://docs.cloudposse.com/reference-architecture/design-decisions/foundational-platform/decide-on-ecr-strategy) ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecr) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecr) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ecs-service/README.md b/modules/ecs-service/README.md index 8884e4c40..d1ff00d8e 100644 --- a/modules/ecs-service/README.md +++ b/modules/ecs-service/README.md @@ -342,6 +342,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecs-service) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs-service) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/ecs/README.md b/modules/ecs/README.md index c38cb3cbc..89726f3d0 100644 --- a/modules/ecs/README.md +++ b/modules/ecs/README.md @@ -147,6 +147,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/ecs) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/ecs) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/efs/README.md b/modules/efs/README.md index f43e3ec2e..e6483a916 100644 --- a/modules/efs/README.md +++ b/modules/efs/README.md @@ -103,7 +103,7 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/efs) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/efs) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/eks/actions-runner-controller/README.md b/modules/eks/actions-runner-controller/README.md index 1dd6d8f5c..6940f7c5b 100644 --- a/modules/eks/actions-runner-controller/README.md +++ b/modules/eks/actions-runner-controller/README.md @@ -500,7 +500,7 @@ Consult [actions-runner-controller](https://github.com/actions-runner-controller ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/actions-runner-controller) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) - Cloud Posse's upstream component - [alb-controller](https://artifacthub.io/packages/helm/aws/aws-load-balancer-controller) - Helm Chart - [alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) - AWS Load Balancer Controller - [actions-runner-controller Webhook Driven Scaling](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/docs/detailed-docs.md#webhook-driven-scaling) diff --git a/modules/eks/alb-controller-ingress-group/README.md b/modules/eks/alb-controller-ingress-group/README.md index 03e98184f..f9c20227e 100644 --- a/modules/eks/alb-controller-ingress-group/README.md +++ b/modules/eks/alb-controller-ingress-group/README.md @@ -142,7 +142,7 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/alb-controller-ingress-group) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/alb-controller-ingress-group) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/eks/datadog-agent/README.md b/modules/eks/datadog-agent/README.md index fe592ec6f..136f8848d 100644 --- a/modules/eks/datadog-agent/README.md +++ b/modules/eks/datadog-agent/README.md @@ -259,4 +259,4 @@ https-checks: ## References * Datadog's [Kubernetes Agent documentation](https://docs.datadoghq.com/containers/kubernetes/) -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/datadog-agent) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/datadog-agent) - Cloud Posse's upstream component diff --git a/modules/eks/echo-server/README.md b/modules/eks/echo-server/README.md index 34efca50f..6e065874d 100644 --- a/modules/eks/echo-server/README.md +++ b/modules/eks/echo-server/README.md @@ -1,6 +1,6 @@ # Component: `eks/echo-server` -This is copied from [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/echo-server). +This is copied from [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/echo-server). This component installs the [Ealenn/Echo-Server](https://github.com/Ealenn/Echo-Server) to EKS clusters. The echo server is a server that sends it back to the client a JSON representation of all the data diff --git a/modules/eks/github-actions-runner/README.md b/modules/eks/github-actions-runner/README.md index 037a2fd4c..36bf50d53 100644 --- a/modules/eks/github-actions-runner/README.md +++ b/modules/eks/github-actions-runner/README.md @@ -465,7 +465,7 @@ implementation. ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/actions-runner-controller) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/actions-runner-controller) - Cloud Posse's upstream component - [alb-controller](https://artifacthub.io/packages/helm/aws/aws-load-balancer-controller) - Helm Chart - [alb-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) - AWS Load Balancer Controller - [actions-runner-controller Webhook Driven Scaling](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/docs/detailed-docs.md#webhook-driven-scaling) diff --git a/modules/eks/keda/README.md b/modules/eks/keda/README.md index 89b213ed5..ca8ede1fc 100644 --- a/modules/eks/keda/README.md +++ b/modules/eks/keda/README.md @@ -122,4 +122,4 @@ components: ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/keda) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/keda) - Cloud Posse's upstream component diff --git a/modules/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md index 3abf87d46..366f618d0 100644 --- a/modules/eks/redis-operator/README.md +++ b/modules/eks/redis-operator/README.md @@ -155,4 +155,4 @@ components: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/redis-operator) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/redis-operator) - Cloud Posse's upstream component diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md index 614bf730e..6ac863987 100644 --- a/modules/eks/redis/README.md +++ b/modules/eks/redis/README.md @@ -161,4 +161,4 @@ components: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/eks/redis) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/eks/redis) - Cloud Posse's upstream component diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md index b8bae75c0..73d0a0942 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -137,6 +137,6 @@ No resources. ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/elasticache-redis) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/elasticache-redis) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/elasticsearch/README.md b/modules/elasticsearch/README.md index e67d17af8..a55401b8b 100644 --- a/modules/elasticsearch/README.md +++ b/modules/elasticsearch/README.md @@ -120,6 +120,6 @@ components: ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/elasticsearch) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/elasticsearch) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/github-action-token-rotator/README.md b/modules/github-action-token-rotator/README.md index 9ddcffecd..080320c2a 100644 --- a/modules/github-action-token-rotator/README.md +++ b/modules/github-action-token-rotator/README.md @@ -88,7 +88,7 @@ No resources. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/github-action-token-rotator) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-action-token-rotator) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md index c0a5908d5..08968f5c2 100644 --- a/modules/github-oidc-provider/README.md +++ b/modules/github-oidc-provider/README.md @@ -121,7 +121,7 @@ permissions: ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/github-oidc-provider) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-oidc-provider) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/github-oidc-role/README.md b/modules/github-oidc-role/README.md index 886f880ca..7d21a02f4 100644 --- a/modules/github-oidc-role/README.md +++ b/modules/github-oidc-role/README.md @@ -234,6 +234,6 @@ There are two methods for adding custom policies to the IAM role. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/github-oidc-role) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-oidc-role) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/github-runners/README.md b/modules/github-runners/README.md index 098a58a63..933199baa 100644 --- a/modules/github-runners/README.md +++ b/modules/github-runners/README.md @@ -425,7 +425,7 @@ The `overrides` will override the `instance_type` above. ## References -* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/github-runners) - Cloud Posse's upstream component +* [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/github-runners) - Cloud Posse's upstream component * [AWS: Auto Scaling groups with multiple instance types and purchase options](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-mixed-instances-groups.html) * [InstancesDistribution](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_InstancesDistribution.html) - [MixedInstancesPolicy](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_MixedInstancesPolicy.html) diff --git a/modules/gitops/README.md b/modules/gitops/README.md index 266c8c16f..f5d189c0e 100644 --- a/modules/gitops/README.md +++ b/modules/gitops/README.md @@ -125,6 +125,6 @@ components: ## References -- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/gitops) - Cloud Posse's upstream component +- [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/gitops) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/global-accelerator-endpoint-group/README.md b/modules/global-accelerator-endpoint-group/README.md index a20a6ca10..4a7e9f647 100644 --- a/modules/global-accelerator-endpoint-group/README.md +++ b/modules/global-accelerator-endpoint-group/README.md @@ -81,7 +81,7 @@ No resources. ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/global-accelerator-endpoint-group) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/global-accelerator-endpoint-group) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/global-accelerator/README.md b/modules/global-accelerator/README.md index 2001b8ff5..e1bdc94e4 100644 --- a/modules/global-accelerator/README.md +++ b/modules/global-accelerator/README.md @@ -93,7 +93,7 @@ No resources. ## References - * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/master/modules/global-accelerator) - Cloud Posse's upstream component + * [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/global-accelerator) - Cloud Posse's upstream component [](https://cpco.io/component) diff --git a/modules/glue/catalog-database/README.md b/modules/glue/catalog-database/README.md new file mode 100644 index 000000000..5f2711f84 --- /dev/null +++ b/modules/glue/catalog-database/README.md @@ -0,0 +1,103 @@ +# Component: `glue/catalog-database` + +This component provisions Glue catalog databases. + +## Usage + +**Stack Level**: Regional + +```yaml +components: + terraform: + glue/catalog-database/example: + metadata: + component: glue/catalog-database + vars: + enabled: true + name: example + catalog_database_description: Glue catalog database example + location_uri: "s3://awsglue-datasets/examples/medicare/Medicare_Hospital_Provider.csv" + glue_iam_component_name: "glue/iam" + lakeformation_permissions_enabled: true + lakeformation_permissions: + - "ALL" +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.0 | +| [utils](#requirement\_utils) | >= 1.15.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [glue\_catalog\_database](#module\_glue\_catalog\_database) | cloudposse/glue/aws//modules/glue-catalog-database | 0.4.0 | +| [glue\_iam\_role](#module\_glue\_iam\_role) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_lakeformation_permissions.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lakeformation_permissions) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [catalog\_database\_description](#input\_catalog\_database\_description) | Glue catalog database description | `string` | `null` | no | +| [catalog\_database\_name](#input\_catalog\_database\_name) | Glue catalog database name. The acceptable characters are lowercase letters, numbers, and the underscore character | `string` | `null` | no | +| [catalog\_id](#input\_catalog\_id) | ID of the Glue Catalog to create the database in. If omitted, this defaults to the AWS Account ID | `string` | `null` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |